@@ -19,16 +19,92 @@ import (
19
19
"github.com/stretchr/testify/require"
20
20
"golang.org/x/xerrors"
21
21
22
- "github.com/coder/coder/cryptorand"
23
-
24
22
"github.com/coder/coder/coderd"
23
+ "github.com/coder/coder/coderd/database/dbauthz"
25
24
"github.com/coder/coder/coderd/rbac"
26
25
"github.com/coder/coder/coderd/rbac/regosql"
27
26
"github.com/coder/coder/codersdk"
27
+ "github.com/coder/coder/cryptorand"
28
28
"github.com/coder/coder/provisioner/echo"
29
29
"github.com/coder/coder/provisionersdk/proto"
30
30
)
31
31
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
+
32
108
func AGPLRoutes (a * AuthTester ) (map [string ]string , map [string ]RouteCheck ) {
33
109
// Some quick reused objects
34
110
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 {
598
674
return nil
599
675
}
600
676
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
+
601
714
// AssertActor asserts in order. If the order of authz calls does not match,
602
715
// this will fail.
603
716
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 ()
606
719
ptr := 0
607
720
for i , call := range r .Called {
608
721
if ptr == len (did ) {
0 commit comments