Skip to content

feat: Dbauthz is now default, remove out of experimental #6650

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 32 commits into from
Mar 21, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
ac597a5
feat: dbauthz always on, out of experimental
Emyrk Mar 16, 2023
7118bf0
Add ability to do rbac checks in unit tests
Emyrk Mar 17, 2023
c555c57
Remove AuthorizeAllEndpoints
Emyrk Mar 17, 2023
c6210c4
Remove some duplicate rbac checks
Emyrk Mar 17, 2023
d48a5c3
Remove rest of duplicate rbac checks
Emyrk Mar 17, 2023
7672f6e
Adding unit tests for rbac checks
Emyrk Mar 17, 2023
263801b
Add method to unit test rbac
Emyrk Mar 17, 2023
893198a
Add comment
Emyrk Mar 17, 2023
6b2c3f9
Add comments
Emyrk Mar 17, 2023
84ba18d
Add comment
Emyrk Mar 17, 2023
38bec6d
Merge remote-tracking branch 'origin/main' into stevenmasley/dbauthz_on
Emyrk Mar 17, 2023
a5ff7fc
Make gen
Emyrk Mar 17, 2023
bb788a4
Make golden files
Emyrk Mar 17, 2023
c0f6ff0
linting
Emyrk Mar 20, 2023
386a967
Merge remote-tracking branch 'origin/main' into stevenmasley/dbauthz_on
Emyrk Mar 20, 2023
f7842ee
linting
Emyrk Mar 20, 2023
b6130e3
linting
Emyrk Mar 20, 2023
8b744ac
Merge lost a config section
Emyrk Mar 20, 2023
55f01c5
Linting
Emyrk Mar 20, 2023
4965c10
Make gen
Emyrk Mar 20, 2023
d038934
Make gen
Emyrk Mar 20, 2023
55b1308
remove experiment enum
Emyrk Mar 20, 2023
f7923df
Make gen
Emyrk Mar 20, 2023
549a34d
Linting
Emyrk Mar 20, 2023
2ae9eac
Correct unit test
Emyrk Mar 20, 2023
faf0714
Merge remote-tracking branch 'origin/main' into stevenmasley/dbauthz_on
Emyrk Mar 20, 2023
899cc69
Override a copy of the error
Emyrk Mar 20, 2023
4fcc69e
have nicer status codes
Emyrk Mar 20, 2023
4ab765f
Test be parallel
Emyrk Mar 20, 2023
9b69f39
This test takes way too long
Emyrk Mar 20, 2023
974d915
Paginated users was taking too long
Emyrk Mar 20, 2023
0934326
Subtest context is shared :(
Emyrk Mar 20, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add ability to do rbac checks in unit tests
  • Loading branch information
Emyrk committed Mar 17, 2023
commit 7118bf09a332faf477b77de011950f163cd50b65
6 changes: 3 additions & 3 deletions coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,9 @@ func New(options *Options) *API {
options = &Options{}
}

if options.Authorizer == nil {
options.Authorizer = rbac.NewCachingAuthorizer(options.PrometheusRegistry)
}
options.Database = dbauthz.New(
options.Database,
options.Authorizer,
Expand Down Expand Up @@ -204,9 +207,6 @@ func New(options *Options) *API {
if options.PrometheusRegistry == nil {
options.PrometheusRegistry = prometheus.NewRegistry()
}
if options.Authorizer == nil {
options.Authorizer = rbac.NewCachingAuthorizer(options.PrometheusRegistry)
}
if options.TailnetCoordinator == nil {
options.TailnetCoordinator = tailnet.NewCoordinator()
}
Expand Down
121 changes: 117 additions & 4 deletions coderd/coderdtest/authorize.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,92 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"

"github.com/coder/coder/cryptorand"

"github.com/coder/coder/coderd"
"github.com/coder/coder/coderd/database/dbauthz"
"github.com/coder/coder/coderd/rbac"
"github.com/coder/coder/coderd/rbac/regosql"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/cryptorand"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
)

type CoderSDKObject interface {
codersdk.User
}

func RBACObject[C CoderSDKObject](o C) rbac.Object {
switch ro := any(o).(type) {
case codersdk.User:
return rbac.ResourceUser.WithID(ro.ID)
default:
panic("unknown object type")
}
}

// RBACAsserter is a helper for asserting that the correct RBAC checks are
// performed. This struct is tied to a given user, and only authorizes calls
// for this user are checked.
type RBACAsserter struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really nice to have integrated into coderdtest.

Subject rbac.Subject

Recorder *RecordingAuthorizer
}

func (a RBACAsserter) AllCalls() []rbac.AuthCall {
return a.Recorder.AllCalls(&a.Subject)
}

// AssertChecked will assert a given rbac check was performed. It does not care
// about order of checks, or any other checks. This is useful when you do not
// care about asserting every check that was performed.
func (a RBACAsserter) AssertChecked(t *testing.T, action rbac.Action, objects ...rbac.Object) {
pairs := make([]ActionObjectPair, 0, len(objects))
for _, obj := range objects {
pairs = append(pairs, a.Recorder.Pair(action, obj))
}
a.Recorder.AssertOutOfOrder(t, a.Subject, pairs...)
}

// AssertInOrder must be called in the correct order of authz checks. If the objects
// or actions are not in the correct order, the test will fail.
func (a RBACAsserter) AssertInOrder(t *testing.T, action rbac.Action, objects ...rbac.Object) {
pairs := make([]ActionObjectPair, 0, len(objects))
for _, obj := range objects {
pairs = append(pairs, a.Recorder.Pair(action, obj))
}
a.Recorder.AssertActor(t, a.Subject, pairs...)
}

func AssertRBAC(t *testing.T, api *coderd.API, client *codersdk.Client) RBACAsserter {
recorder, ok := api.Authorizer.(*RecordingAuthorizer)
if !ok {
t.Fatal("expected RecordingAuthorizer")
}

// We use the database directly to not cause additional auth checks on behalf
// of the user. This does add authz checks on behalf of the system user, but
// it is hard to avoid that.
ctx := dbauthz.AsSystemRestricted(context.Background())
token := client.SessionToken()
parts := strings.Split(token, "-")
key, err := api.Database.GetAPIKeyByID(ctx, parts[0])
require.NoError(t, err, "fetch client api key")

roles, err := api.Database.GetAuthorizationUserRoles(ctx, key.UserID)
require.NoError(t, err, "fetch user roles")

return RBACAsserter{
Subject: rbac.Subject{
ID: key.UserID.String(),
Roles: rbac.RoleNames(roles.Roles),
Groups: roles.Groups,
Scope: rbac.ScopeName(key.Scope),
},
Recorder: recorder,
}
}

func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
// Some quick reused objects
workspaceRBACObj := rbac.ResourceWorkspace.WithID(a.Workspace.ID).InOrg(a.Organization.ID).WithOwner(a.Workspace.OwnerID.String())
Expand Down Expand Up @@ -598,11 +674,48 @@ func (r *RecordingAuthorizer) AllAsserted() error {
return nil
}

// AllCalls is useful for debugging.
func (r *RecordingAuthorizer) AllCalls(actor *rbac.Subject) []rbac.AuthCall {
r.RLock()
defer r.RUnlock()

called := make([]rbac.AuthCall, 0, len(r.Called))
for _, c := range r.Called {
if actor != nil && !c.Actor.Equal(*actor) {
continue
}
called = append(called, c.AuthCall)
}
return called
}

// AssertOutOfOrder asserts that the given actor performed the given action
// on the given objects. It does not care about the order of the calls.
// When marking authz calls as asserted, it will mark the first matching
// calls first.
func (r *RecordingAuthorizer) AssertOutOfOrder(t *testing.T, actor rbac.Subject, did ...ActionObjectPair) {
r.Lock()
defer r.Unlock()

for _, do := range did {
found := false
// Find the first non-asserted call that matches the actor, action, and object.
for i, call := range r.Called {
if !call.asserted && call.Actor.Equal(actor) && call.Action == do.Action && call.Object.Equal(do.Object) {
r.Called[i].asserted = true
found = true
break
}
}
require.True(t, found, "assertion missing: %s %s %s", actor, do.Action, do.Object)
}
}

// AssertActor asserts in order. If the order of authz calls does not match,
// this will fail.
func (r *RecordingAuthorizer) AssertActor(t *testing.T, actor rbac.Subject, did ...ActionObjectPair) {
r.RLock()
defer r.RUnlock()
r.Lock()
defer r.Unlock()
ptr := 0
for i, call := range r.Called {
if ptr == len(did) {
Expand Down
37 changes: 37 additions & 0 deletions coderd/coderdtest/authorize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package coderdtest_test

import (
"context"
"math/rand"
"os"
"strings"
"testing"
Expand Down Expand Up @@ -81,6 +82,42 @@ func TestAuthzRecorder(t *testing.T) {
rec.AssertActor(t, a, aPairs...)
require.NoError(t, rec.AllAsserted(), "all assertions should have been made")
})

t.Run("AuthorizeOutOfOrder", func(t *testing.T) {
t.Parallel()

rec := &coderdtest.RecordingAuthorizer{
Wrapped: &coderdtest.FakeAuthorizer{},
}
sub := coderdtest.RandomRBACSubject()
pairs := fuzzAuthz(t, sub, rec, 10)
rand.Shuffle(len(pairs), func(i, j int) {
pairs[i], pairs[j] = pairs[j], pairs[i]
})

rec.AssertOutOfOrder(t, sub, pairs...)
require.NoError(t, rec.AllAsserted(), "all assertions should have been made")
})

t.Run("AllCalls", func(t *testing.T) {
t.Parallel()

rec := &coderdtest.RecordingAuthorizer{
Wrapped: &coderdtest.FakeAuthorizer{},
}
sub := coderdtest.RandomRBACSubject()
calls := rec.AllCalls(&sub)
pairs := make([]coderdtest.ActionObjectPair, 0, len(calls))
for _, call := range calls {
pairs = append(pairs, coderdtest.ActionObjectPair{
Action: call.Action,
Object: call.Object,
})
}

rec.AssertActor(t, sub, pairs...)
require.NoError(t, rec.AllAsserted(), "all assertions should have been made")
})
}

// fuzzAuthzPrep has same action and object types for all calls.
Expand Down
9 changes: 5 additions & 4 deletions coderd/coderdtest/coderdtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,17 +180,18 @@ func NewOptions(t *testing.T, options *Options) (func(http.Handler), context.Can
close(options.AutobuildStats)
})
}
if options.Database == nil {
options.Database, options.Pubsub = dbtestutil.NewDB(t)
}
options.Database = dbauthz.New(options.Database, options.Authorizer, slogtest.Make(t, nil).Leveled(slog.LevelDebug))

if options.Authorizer == nil {
options.Authorizer = &RecordingAuthorizer{
Wrapped: rbac.NewCachingAuthorizer(prometheus.NewRegistry()),
}
}

if options.Database == nil {
options.Database, options.Pubsub = dbtestutil.NewDB(t)
options.Database = dbauthz.New(options.Database, options.Authorizer, slogtest.Make(t, nil).Leveled(slog.LevelDebug))
}

if options.DeploymentValues == nil {
options.DeploymentValues = DeploymentValues(t)
}
Expand Down