Skip to content

Commit ebaae75

Browse files
authored
test: Unit test to assert role capabilities (#1781)
* test: Unit test to assert role permissions This unit test allows for asserting which roles can perform actions on various objects. This is much easier than making unit tests to hit the api.
1 parent 1222787 commit ebaae75

File tree

1 file changed

+241
-1
lines changed

1 file changed

+241
-1
lines changed

coderd/rbac/builtin_test.go

+241-1
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,256 @@
11
package rbac_test
22

33
import (
4+
"context"
45
"fmt"
56
"testing"
67

78
"github.com/google/uuid"
8-
9+
"github.com/stretchr/testify/assert"
910
"github.com/stretchr/testify/require"
1011

1112
"github.com/coder/coder/coderd/rbac"
1213
)
1314

15+
type authSubject struct {
16+
// Name is helpful for test assertions
17+
Name string
18+
UserID string
19+
Roles []string
20+
}
21+
22+
func TestRolePermissions(t *testing.T) {
23+
t.Parallel()
24+
25+
auth, err := rbac.NewAuthorizer()
26+
require.NoError(t, err, "new rego authorizer")
27+
28+
// currentUser is anything that references "me", "mine", or "my".
29+
currentUser := uuid.New()
30+
adminID := uuid.New()
31+
orgID := uuid.New()
32+
otherOrg := uuid.New()
33+
34+
// Subjects to user
35+
memberMe := authSubject{Name: "member_me", UserID: currentUser.String(), Roles: []string{rbac.RoleMember()}}
36+
orgMemberMe := authSubject{Name: "org_member_me", UserID: currentUser.String(), Roles: []string{rbac.RoleMember(), rbac.RoleOrgMember(orgID)}}
37+
38+
admin := authSubject{Name: "admin", UserID: adminID.String(), Roles: []string{rbac.RoleMember(), rbac.RoleAdmin()}}
39+
orgAdmin := authSubject{Name: "org_admin", UserID: adminID.String(), Roles: []string{rbac.RoleMember(), rbac.RoleOrgMember(orgID), rbac.RoleOrgAdmin(orgID)}}
40+
41+
otherOrgMember := authSubject{Name: "org_member_other", UserID: uuid.NewString(), Roles: []string{rbac.RoleMember(), rbac.RoleOrgMember(otherOrg)}}
42+
otherOrgAdmin := authSubject{Name: "org_admin_other", UserID: uuid.NewString(), Roles: []string{rbac.RoleMember(), rbac.RoleOrgMember(otherOrg), rbac.RoleOrgAdmin(otherOrg)}}
43+
44+
// requiredSubjects are required to be asserted in each test case. This is
45+
// to make sure one is not forgotten.
46+
requiredSubjects := []authSubject{memberMe, admin, orgMemberMe, orgAdmin, otherOrgAdmin, otherOrgMember}
47+
48+
testCases := []struct {
49+
// Name the test case to better locate the failing test case.
50+
Name string
51+
Resource rbac.Object
52+
Actions []rbac.Action
53+
// AuthorizeMap must cover all subjects in 'requiredSubjects'.
54+
// This map will run an Authorize() check with the resource, action,
55+
// and subjects. The subjects are split into 2 categories, "true" and
56+
// "false".
57+
// true: Subjects who Authorize should return no error
58+
// false: Subjects who Authorize should return forbidden.
59+
AuthorizeMap map[bool][]authSubject
60+
}{
61+
{
62+
Name: "MyUser",
63+
Actions: []rbac.Action{rbac.ActionRead},
64+
Resource: rbac.ResourceUser.WithID(currentUser.String()),
65+
AuthorizeMap: map[bool][]authSubject{
66+
true: {admin, memberMe, orgMemberMe, orgAdmin, otherOrgMember, otherOrgAdmin},
67+
false: {},
68+
},
69+
},
70+
{
71+
Name: "AUser",
72+
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
73+
Resource: rbac.ResourceUser,
74+
AuthorizeMap: map[bool][]authSubject{
75+
true: {admin},
76+
false: {memberMe, orgMemberMe, orgAdmin, otherOrgMember, otherOrgAdmin},
77+
},
78+
},
79+
{
80+
Name: "MyWorkspaceInOrg",
81+
// When creating the WithID won't be set, but it does not change the result.
82+
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
83+
Resource: rbac.ResourceWorkspace.InOrg(orgID).WithOwner(currentUser.String()).WithID(uuid.NewString()),
84+
AuthorizeMap: map[bool][]authSubject{
85+
true: {admin, orgMemberMe, orgAdmin},
86+
false: {memberMe, otherOrgAdmin, otherOrgMember},
87+
},
88+
},
89+
{
90+
Name: "Templates",
91+
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
92+
Resource: rbac.ResourceTemplate.InOrg(orgID).WithID(uuid.NewString()),
93+
AuthorizeMap: map[bool][]authSubject{
94+
true: {admin, orgAdmin},
95+
false: {memberMe, orgMemberMe, otherOrgAdmin, otherOrgMember},
96+
},
97+
},
98+
{
99+
Name: "ReadTemplates",
100+
Actions: []rbac.Action{rbac.ActionRead},
101+
Resource: rbac.ResourceTemplate.InOrg(orgID).WithID(uuid.NewString()),
102+
AuthorizeMap: map[bool][]authSubject{
103+
true: {admin, orgMemberMe, orgAdmin},
104+
false: {memberMe, otherOrgAdmin, otherOrgMember},
105+
},
106+
},
107+
{
108+
Name: "Files",
109+
Actions: []rbac.Action{rbac.ActionCreate},
110+
Resource: rbac.ResourceFile,
111+
AuthorizeMap: map[bool][]authSubject{
112+
true: {admin},
113+
false: {orgMemberMe, orgAdmin, memberMe, otherOrgAdmin, otherOrgMember},
114+
},
115+
},
116+
{
117+
Name: "MyFile",
118+
Actions: []rbac.Action{rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
119+
Resource: rbac.ResourceFile.WithID(uuid.NewString()).WithOwner(currentUser.String()),
120+
AuthorizeMap: map[bool][]authSubject{
121+
true: {admin, memberMe, orgMemberMe},
122+
false: {orgAdmin, otherOrgAdmin, otherOrgMember},
123+
},
124+
},
125+
{
126+
Name: "CreateOrganizations",
127+
Actions: []rbac.Action{rbac.ActionCreate},
128+
Resource: rbac.ResourceOrganization,
129+
AuthorizeMap: map[bool][]authSubject{
130+
true: {admin},
131+
false: {orgAdmin, otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe},
132+
},
133+
},
134+
{
135+
Name: "Organizations",
136+
Actions: []rbac.Action{rbac.ActionUpdate, rbac.ActionDelete},
137+
Resource: rbac.ResourceOrganization.InOrg(orgID).WithID(orgID.String()),
138+
AuthorizeMap: map[bool][]authSubject{
139+
true: {admin, orgAdmin},
140+
false: {otherOrgAdmin, otherOrgMember, memberMe, orgMemberMe},
141+
},
142+
},
143+
{
144+
Name: "ReadOrganizations",
145+
Actions: []rbac.Action{rbac.ActionRead},
146+
Resource: rbac.ResourceOrganization.InOrg(orgID).WithID(orgID.String()),
147+
AuthorizeMap: map[bool][]authSubject{
148+
true: {admin, orgAdmin, orgMemberMe},
149+
false: {otherOrgAdmin, otherOrgMember, memberMe},
150+
},
151+
},
152+
{
153+
Name: "RoleAssignment",
154+
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
155+
Resource: rbac.ResourceRoleAssignment,
156+
AuthorizeMap: map[bool][]authSubject{
157+
true: {admin},
158+
false: {orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe},
159+
},
160+
},
161+
{
162+
Name: "ReadRoleAssignment",
163+
Actions: []rbac.Action{rbac.ActionRead},
164+
Resource: rbac.ResourceRoleAssignment,
165+
AuthorizeMap: map[bool][]authSubject{
166+
true: {admin, orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe},
167+
false: {},
168+
},
169+
},
170+
{
171+
Name: "OrgRoleAssignment",
172+
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
173+
Resource: rbac.ResourceOrgRoleAssignment.InOrg(orgID),
174+
AuthorizeMap: map[bool][]authSubject{
175+
true: {admin, orgAdmin},
176+
false: {orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe},
177+
},
178+
},
179+
{
180+
Name: "ReadOrgRoleAssignment",
181+
Actions: []rbac.Action{rbac.ActionRead},
182+
Resource: rbac.ResourceOrgRoleAssignment.InOrg(orgID),
183+
AuthorizeMap: map[bool][]authSubject{
184+
true: {admin, orgAdmin, orgMemberMe},
185+
false: {otherOrgAdmin, otherOrgMember, memberMe},
186+
},
187+
},
188+
{
189+
Name: "APIKey",
190+
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
191+
Resource: rbac.ResourceAPIKey.WithOwner(currentUser.String()).WithID(uuid.NewString()),
192+
AuthorizeMap: map[bool][]authSubject{
193+
true: {admin, orgMemberMe, memberMe},
194+
false: {orgAdmin, otherOrgAdmin, otherOrgMember},
195+
},
196+
},
197+
{
198+
Name: "UserData",
199+
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
200+
Resource: rbac.ResourceUserData.WithOwner(currentUser.String()).WithID(currentUser.String()),
201+
AuthorizeMap: map[bool][]authSubject{
202+
true: {admin, orgMemberMe, memberMe},
203+
false: {orgAdmin, otherOrgAdmin, otherOrgMember},
204+
},
205+
},
206+
{
207+
Name: "ManageOrgMember",
208+
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
209+
Resource: rbac.ResourceOrganizationMember.InOrg(orgID).WithID(uuid.NewString()),
210+
AuthorizeMap: map[bool][]authSubject{
211+
true: {admin, orgAdmin},
212+
false: {orgMemberMe, memberMe, otherOrgAdmin, otherOrgMember},
213+
},
214+
},
215+
{
216+
Name: "ReadOrgMember",
217+
Actions: []rbac.Action{rbac.ActionRead},
218+
Resource: rbac.ResourceOrganizationMember.InOrg(orgID).WithID(uuid.NewString()),
219+
AuthorizeMap: map[bool][]authSubject{
220+
true: {admin, orgAdmin, orgMemberMe},
221+
false: {memberMe, otherOrgAdmin, otherOrgMember},
222+
},
223+
},
224+
}
225+
226+
for _, c := range testCases {
227+
c := c
228+
t.Run(c.Name, func(t *testing.T) {
229+
t.Parallel()
230+
remainingSubjs := make(map[string]struct{})
231+
for _, subj := range requiredSubjects {
232+
remainingSubjs[subj.Name] = struct{}{}
233+
}
234+
235+
for _, action := range c.Actions {
236+
for result, subjs := range c.AuthorizeMap {
237+
for _, subj := range subjs {
238+
delete(remainingSubjs, subj.Name)
239+
msg := fmt.Sprintf("%s as %q doing %q on %q", c.Name, subj.Name, action, c.Resource.Type)
240+
err := auth.ByRoleName(context.Background(), subj.UserID, subj.Roles, action, c.Resource)
241+
if result {
242+
assert.NoError(t, err, fmt.Sprintf("Should pass: %s", msg))
243+
} else {
244+
assert.ErrorContains(t, err, "forbidden", fmt.Sprintf("Should fail: %s", msg))
245+
}
246+
}
247+
}
248+
}
249+
require.Empty(t, remainingSubjs, "test should cover all subjects")
250+
})
251+
}
252+
}
253+
14254
func TestIsOrgRole(t *testing.T) {
15255
t.Parallel()
16256
randomUUID := uuid.New()

0 commit comments

Comments
 (0)