Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 7118bf0

Browse files
committedMar 17, 2023
Add ability to do rbac checks in unit tests
1 parent ac597a5 commit 7118bf0

File tree

4 files changed

+162
-11
lines changed

4 files changed

+162
-11
lines changed
 

‎coderd/coderd.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ func New(options *Options) *API {
164164
options = &Options{}
165165
}
166166

167+
if options.Authorizer == nil {
168+
options.Authorizer = rbac.NewCachingAuthorizer(options.PrometheusRegistry)
169+
}
167170
options.Database = dbauthz.New(
168171
options.Database,
169172
options.Authorizer,
@@ -204,9 +207,6 @@ func New(options *Options) *API {
204207
if options.PrometheusRegistry == nil {
205208
options.PrometheusRegistry = prometheus.NewRegistry()
206209
}
207-
if options.Authorizer == nil {
208-
options.Authorizer = rbac.NewCachingAuthorizer(options.PrometheusRegistry)
209-
}
210210
if options.TailnetCoordinator == nil {
211211
options.TailnetCoordinator = tailnet.NewCoordinator()
212212
}

‎coderd/coderdtest/authorize.go

Lines changed: 117 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,16 +19,92 @@ import (
1919
"github.com/stretchr/testify/require"
2020
"golang.org/x/xerrors"
2121

22-
"github.com/coder/coder/cryptorand"
23-
2422
"github.com/coder/coder/coderd"
23+
"github.com/coder/coder/coderd/database/dbauthz"
2524
"github.com/coder/coder/coderd/rbac"
2625
"github.com/coder/coder/coderd/rbac/regosql"
2726
"github.com/coder/coder/codersdk"
27+
"github.com/coder/coder/cryptorand"
2828
"github.com/coder/coder/provisioner/echo"
2929
"github.com/coder/coder/provisionersdk/proto"
3030
)
3131

32+
type CoderSDKObject interface {
33+
codersdk.User
34+
}
35+
36+
func RBACObject[C CoderSDKObject](o C) rbac.Object {
37+
switch ro := any(o).(type) {
38+
case codersdk.User:
39+
return rbac.ResourceUser.WithID(ro.ID)
40+
default:
41+
panic("unknown object type")
42+
}
43+
}
44+
45+
// RBACAsserter is a helper for asserting that the correct RBAC checks are
46+
// performed. This struct is tied to a given user, and only authorizes calls
47+
// for this user are checked.
48+
type RBACAsserter struct {
49+
Subject rbac.Subject
50+
51+
Recorder *RecordingAuthorizer
52+
}
53+
54+
func (a RBACAsserter) AllCalls() []rbac.AuthCall {
55+
return a.Recorder.AllCalls(&a.Subject)
56+
}
57+
58+
// AssertChecked will assert a given rbac check was performed. It does not care
59+
// about order of checks, or any other checks. This is useful when you do not
60+
// care about asserting every check that was performed.
61+
func (a RBACAsserter) AssertChecked(t *testing.T, action rbac.Action, objects ...rbac.Object) {
62+
pairs := make([]ActionObjectPair, 0, len(objects))
63+
for _, obj := range objects {
64+
pairs = append(pairs, a.Recorder.Pair(action, obj))
65+
}
66+
a.Recorder.AssertOutOfOrder(t, a.Subject, pairs...)
67+
}
68+
69+
// AssertInOrder must be called in the correct order of authz checks. If the objects
70+
// or actions are not in the correct order, the test will fail.
71+
func (a RBACAsserter) AssertInOrder(t *testing.T, action rbac.Action, objects ...rbac.Object) {
72+
pairs := make([]ActionObjectPair, 0, len(objects))
73+
for _, obj := range objects {
74+
pairs = append(pairs, a.Recorder.Pair(action, obj))
75+
}
76+
a.Recorder.AssertActor(t, a.Subject, pairs...)
77+
}
78+
79+
func AssertRBAC(t *testing.T, api *coderd.API, client *codersdk.Client) RBACAsserter {
80+
recorder, ok := api.Authorizer.(*RecordingAuthorizer)
81+
if !ok {
82+
t.Fatal("expected RecordingAuthorizer")
83+
}
84+
85+
// We use the database directly to not cause additional auth checks on behalf
86+
// of the user. This does add authz checks on behalf of the system user, but
87+
// it is hard to avoid that.
88+
ctx := dbauthz.AsSystemRestricted(context.Background())
89+
token := client.SessionToken()
90+
parts := strings.Split(token, "-")
91+
key, err := api.Database.GetAPIKeyByID(ctx, parts[0])
92+
require.NoError(t, err, "fetch client api key")
93+
94+
roles, err := api.Database.GetAuthorizationUserRoles(ctx, key.UserID)
95+
require.NoError(t, err, "fetch user roles")
96+
97+
return RBACAsserter{
98+
Subject: rbac.Subject{
99+
ID: key.UserID.String(),
100+
Roles: rbac.RoleNames(roles.Roles),
101+
Groups: roles.Groups,
102+
Scope: rbac.ScopeName(key.Scope),
103+
},
104+
Recorder: recorder,
105+
}
106+
}
107+
32108
func AGPLRoutes(a *AuthTester) (map[string]string, map[string]RouteCheck) {
33109
// Some quick reused objects
34110
workspaceRBACObj := rbac.ResourceWorkspace.WithID(a.Workspace.ID).InOrg(a.Organization.ID).WithOwner(a.Workspace.OwnerID.String())
@@ -598,11 +674,48 @@ func (r *RecordingAuthorizer) AllAsserted() error {
598674
return nil
599675
}
600676

677+
// AllCalls is useful for debugging.
678+
func (r *RecordingAuthorizer) AllCalls(actor *rbac.Subject) []rbac.AuthCall {
679+
r.RLock()
680+
defer r.RUnlock()
681+
682+
called := make([]rbac.AuthCall, 0, len(r.Called))
683+
for _, c := range r.Called {
684+
if actor != nil && !c.Actor.Equal(*actor) {
685+
continue
686+
}
687+
called = append(called, c.AuthCall)
688+
}
689+
return called
690+
}
691+
692+
// AssertOutOfOrder asserts that the given actor performed the given action
693+
// on the given objects. It does not care about the order of the calls.
694+
// When marking authz calls as asserted, it will mark the first matching
695+
// calls first.
696+
func (r *RecordingAuthorizer) AssertOutOfOrder(t *testing.T, actor rbac.Subject, did ...ActionObjectPair) {
697+
r.Lock()
698+
defer r.Unlock()
699+
700+
for _, do := range did {
701+
found := false
702+
// Find the first non-asserted call that matches the actor, action, and object.
703+
for i, call := range r.Called {
704+
if !call.asserted && call.Actor.Equal(actor) && call.Action == do.Action && call.Object.Equal(do.Object) {
705+
r.Called[i].asserted = true
706+
found = true
707+
break
708+
}
709+
}
710+
require.True(t, found, "assertion missing: %s %s %s", actor, do.Action, do.Object)
711+
}
712+
}
713+
601714
// AssertActor asserts in order. If the order of authz calls does not match,
602715
// this will fail.
603716
func (r *RecordingAuthorizer) AssertActor(t *testing.T, actor rbac.Subject, did ...ActionObjectPair) {
604-
r.RLock()
605-
defer r.RUnlock()
717+
r.Lock()
718+
defer r.Unlock()
606719
ptr := 0
607720
for i, call := range r.Called {
608721
if ptr == len(did) {

‎coderd/coderdtest/authorize_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package coderdtest_test
22

33
import (
44
"context"
5+
"math/rand"
56
"os"
67
"strings"
78
"testing"
@@ -81,6 +82,42 @@ func TestAuthzRecorder(t *testing.T) {
8182
rec.AssertActor(t, a, aPairs...)
8283
require.NoError(t, rec.AllAsserted(), "all assertions should have been made")
8384
})
85+
86+
t.Run("AuthorizeOutOfOrder", func(t *testing.T) {
87+
t.Parallel()
88+
89+
rec := &coderdtest.RecordingAuthorizer{
90+
Wrapped: &coderdtest.FakeAuthorizer{},
91+
}
92+
sub := coderdtest.RandomRBACSubject()
93+
pairs := fuzzAuthz(t, sub, rec, 10)
94+
rand.Shuffle(len(pairs), func(i, j int) {
95+
pairs[i], pairs[j] = pairs[j], pairs[i]
96+
})
97+
98+
rec.AssertOutOfOrder(t, sub, pairs...)
99+
require.NoError(t, rec.AllAsserted(), "all assertions should have been made")
100+
})
101+
102+
t.Run("AllCalls", func(t *testing.T) {
103+
t.Parallel()
104+
105+
rec := &coderdtest.RecordingAuthorizer{
106+
Wrapped: &coderdtest.FakeAuthorizer{},
107+
}
108+
sub := coderdtest.RandomRBACSubject()
109+
calls := rec.AllCalls(&sub)
110+
pairs := make([]coderdtest.ActionObjectPair, 0, len(calls))
111+
for _, call := range calls {
112+
pairs = append(pairs, coderdtest.ActionObjectPair{
113+
Action: call.Action,
114+
Object: call.Object,
115+
})
116+
}
117+
118+
rec.AssertActor(t, sub, pairs...)
119+
require.NoError(t, rec.AllAsserted(), "all assertions should have been made")
120+
})
84121
}
85122

86123
// fuzzAuthzPrep has same action and object types for all calls.

‎coderd/coderdtest/coderdtest.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,17 +180,18 @@ func NewOptions(t *testing.T, options *Options) (func(http.Handler), context.Can
180180
close(options.AutobuildStats)
181181
})
182182
}
183-
if options.Database == nil {
184-
options.Database, options.Pubsub = dbtestutil.NewDB(t)
185-
}
186-
options.Database = dbauthz.New(options.Database, options.Authorizer, slogtest.Make(t, nil).Leveled(slog.LevelDebug))
187183

188184
if options.Authorizer == nil {
189185
options.Authorizer = &RecordingAuthorizer{
190186
Wrapped: rbac.NewCachingAuthorizer(prometheus.NewRegistry()),
191187
}
192188
}
193189

190+
if options.Database == nil {
191+
options.Database, options.Pubsub = dbtestutil.NewDB(t)
192+
options.Database = dbauthz.New(options.Database, options.Authorizer, slogtest.Make(t, nil).Leveled(slog.LevelDebug))
193+
}
194+
194195
if options.DeploymentValues == nil {
195196
options.DeploymentValues = DeploymentValues(t)
196197
}

0 commit comments

Comments
 (0)
Failed to load comments.