Skip to content

Commit 7f2de03

Browse files
committed
Merge branch 'groups' of github.com:coder/coder into groups
2 parents ff9d968 + 7544e37 commit 7f2de03

File tree

10 files changed

+80
-51
lines changed

10 files changed

+80
-51
lines changed

coderd/authorize.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212

1313
func AuthorizeFilter[O rbac.Objecter](h *HTTPAuthorizer, r *http.Request, action rbac.Action, objects []O) ([]O, error) {
1414
roles := httpmw.UserAuthorization(r)
15-
objects, err := rbac.Filter(r.Context(), h.Authorizer, roles.ID.String(), roles.Roles, roles.Scope.ToRBAC(), action, objects)
15+
objects, err := rbac.Filter(r.Context(), h.Authorizer, roles.ID.String(), roles.Roles, roles.Groups, roles.Scope.ToRBAC(), action, objects)
1616
if err != nil {
1717
// Log the error as Filter should not be erroring.
1818
h.Logger.Error(r.Context(), "filter failed",
@@ -57,7 +57,7 @@ func (api *API) Authorize(r *http.Request, action rbac.Action, object rbac.Objec
5757
// }
5858
func (h *HTTPAuthorizer) Authorize(r *http.Request, action rbac.Action, object rbac.Objecter) bool {
5959
roles := httpmw.UserAuthorization(r)
60-
err := h.Authorizer.ByRoleName(r.Context(), roles.ID.String(), roles.Roles, roles.Scope.ToRBAC(), action, object.RBACObject())
60+
err := h.Authorizer.ByRoleName(r.Context(), roles.ID.String(), roles.Roles, roles.Groups, roles.Scope.ToRBAC(), action, object.RBACObject())
6161
if err != nil {
6262
// Log the errors for debugging
6363
internalError := new(rbac.UnauthorizedError)

coderd/coderdtest/authorize.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,7 @@ func (a *AuthTester) Test(ctx context.Context, assertRoute map[string]RouteCheck
498498
type authCall struct {
499499
SubjectID string
500500
Roles []string
501+
Groups []string
501502
Scope rbac.Scope
502503
Action rbac.Action
503504
Object rbac.Object
@@ -510,24 +511,26 @@ type RecordingAuthorizer struct {
510511

511512
var _ rbac.Authorizer = (*RecordingAuthorizer)(nil)
512513

513-
func (r *RecordingAuthorizer) ByRoleName(_ context.Context, subjectID string, roleNames []string, scope rbac.Scope, action rbac.Action, object rbac.Object) error {
514+
func (r *RecordingAuthorizer) ByRoleName(_ context.Context, subjectID string, roleNames []string, groups []string, scope rbac.Scope, action rbac.Action, object rbac.Object) error {
514515
r.Called = &authCall{
515516
SubjectID: subjectID,
516517
Roles: roleNames,
518+
Groups: groups,
517519
Scope: scope,
518520
Action: action,
519521
Object: object,
520522
}
521523
return r.AlwaysReturn
522524
}
523525

524-
func (r *RecordingAuthorizer) PrepareByRoleName(_ context.Context, subjectID string, roles []string, scope rbac.Scope, action rbac.Action, _ string) (rbac.PreparedAuthorized, error) {
526+
func (r *RecordingAuthorizer) PrepareByRoleName(_ context.Context, subjectID string, roles []string, groups []string, scope rbac.Scope, action rbac.Action, _ string) (rbac.PreparedAuthorized, error) {
525527
return &fakePreparedAuthorizer{
526528
Original: r,
527529
SubjectID: subjectID,
528530
Roles: roles,
529531
Scope: scope,
530532
Action: action,
533+
Groups: groups,
531534
}, nil
532535
}
533536

@@ -539,10 +542,11 @@ type fakePreparedAuthorizer struct {
539542
Original *RecordingAuthorizer
540543
SubjectID string
541544
Roles []string
545+
Groups []string
542546
Scope rbac.Scope
543547
Action rbac.Action
544548
}
545549

546550
func (f *fakePreparedAuthorizer) Authorize(ctx context.Context, object rbac.Object) error {
547-
return f.Original.ByRoleName(ctx, f.SubjectID, f.Roles, f.Scope, f.Action, object)
551+
return f.Original.ByRoleName(ctx, f.SubjectID, f.Roles, f.Groups, f.Scope, f.Action, object)
548552
}

coderd/httpmw/apikey.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ type Authorization struct {
4141
ID uuid.UUID
4242
Username string
4343
Roles []string
44+
Groups []string
4445
Scope database.APIKeyScope
4546
}
4647

@@ -336,6 +337,7 @@ func ExtractAPIKey(db database.Store, oauth *OAuth2Configs, redirectToLogin bool
336337
Username: roles.Username,
337338
Roles: roles.Roles,
338339
Scope: key.Scope,
340+
Groups: roles.Groups,
339341
})
340342

341343
next.ServeHTTP(rw, r.WithContext(ctx))

coderd/rbac/authz.go

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import (
1313
)
1414

1515
type Authorizer interface {
16-
ByRoleName(ctx context.Context, subjectID string, roleNames []string, scope Scope, action Action, object Object) error
17-
PrepareByRoleName(ctx context.Context, subjectID string, roleNames []string, scope Scope, action Action, objectType string) (PreparedAuthorized, error)
16+
ByRoleName(ctx context.Context, subjectID string, roleNames []string, groups []string, scope Scope, action Action, object Object) error
17+
PrepareByRoleName(ctx context.Context, subjectID string, roleNames []string, groups []string, scope Scope, action Action, objectType string) (PreparedAuthorized, error)
1818
}
1919

2020
type PreparedAuthorized interface {
@@ -25,7 +25,7 @@ type PreparedAuthorized interface {
2525
// the elements the subject does not have permission for. This function slows
2626
// down if the list contains objects of multiple types. Attempt to only
2727
// filter objects of the same type for faster performance.
28-
func Filter[O Objecter](ctx context.Context, auth Authorizer, subjID string, subjRoles []string, scope Scope, action Action, objects []O) ([]O, error) {
28+
func Filter[O Objecter](ctx context.Context, auth Authorizer, subjID string, subjRoles []string, groups []string, scope Scope, action Action, objects []O) ([]O, error) {
2929
ctx, span := tracing.StartSpan(ctx, trace.WithAttributes(
3030
attribute.String("subject_id", subjID),
3131
attribute.StringSlice("subject_roles", subjRoles),
@@ -48,7 +48,7 @@ func Filter[O Objecter](ctx context.Context, auth Authorizer, subjID string, sub
4848
objectAuth, ok := prepared[object.RBACObject().Type]
4949
if !ok {
5050
var err error
51-
objectAuth, err = auth.PrepareByRoleName(ctx, subjID, subjRoles, scope, action, objectType)
51+
objectAuth, err = auth.PrepareByRoleName(ctx, subjID, subjRoles, groups, scope, action, objectType)
5252
if err != nil {
5353
return nil, xerrors.Errorf("prepare: %w", err)
5454
}
@@ -93,20 +93,21 @@ func NewAuthorizer() (*RegoAuthorizer, error) {
9393
}
9494

9595
type authSubject struct {
96-
ID string `json:"id"`
97-
Roles []Role `json:"roles"`
96+
ID string `json:"id"`
97+
Roles []Role `json:"roles"`
98+
Groups []string `json:"groups"`
9899
}
99100

100101
// ByRoleName will expand all roleNames into roles before calling Authorize().
101102
// This is the function intended to be used outside this package.
102103
// The role is fetched from the builtin map located in memory.
103-
func (a RegoAuthorizer) ByRoleName(ctx context.Context, subjectID string, roleNames []string, scope Scope, action Action, object Object) error {
104+
func (a RegoAuthorizer) ByRoleName(ctx context.Context, subjectID string, roleNames []string, groups []string, scope Scope, action Action, object Object) error {
104105
roles, err := RolesByNames(roleNames)
105106
if err != nil {
106107
return err
107108
}
108109

109-
err = a.Authorize(ctx, subjectID, roles, action, object)
110+
err = a.Authorize(ctx, subjectID, roles, groups, action, object)
110111
if err != nil {
111112
return err
112113
}
@@ -118,7 +119,7 @@ func (a RegoAuthorizer) ByRoleName(ctx context.Context, subjectID string, roleNa
118119
return err
119120
}
120121

121-
err = a.Authorize(ctx, subjectID, []Role{scopeRole}, action, object)
122+
err = a.Authorize(ctx, subjectID, []Role{scopeRole}, groups, action, object)
122123
if err != nil {
123124
return err
124125
}
@@ -129,14 +130,15 @@ func (a RegoAuthorizer) ByRoleName(ctx context.Context, subjectID string, roleNa
129130

130131
// Authorize allows passing in custom Roles.
131132
// This is really helpful for unit testing, as we can create custom roles to exercise edge cases.
132-
func (a RegoAuthorizer) Authorize(ctx context.Context, subjectID string, roles []Role, action Action, object Object) error {
133+
func (a RegoAuthorizer) Authorize(ctx context.Context, subjectID string, roles []Role, groups []string, action Action, object Object) error {
133134
ctx, span := tracing.StartSpan(ctx)
134135
defer span.End()
135136

136137
input := map[string]interface{}{
137138
"subject": authSubject{
138-
ID: subjectID,
139-
Roles: roles,
139+
ID: subjectID,
140+
Roles: roles,
141+
Groups: groups,
140142
},
141143
"object": object,
142144
"action": action,
@@ -156,19 +158,19 @@ func (a RegoAuthorizer) Authorize(ctx context.Context, subjectID string, roles [
156158

157159
// Prepare will partially execute the rego policy leaving the object fields unknown (except for the type).
158160
// This will vastly speed up performance if batch authorization on the same type of objects is needed.
159-
func (RegoAuthorizer) Prepare(ctx context.Context, subjectID string, roles []Role, scope Scope, action Action, objectType string) (*PartialAuthorizer, error) {
161+
func (RegoAuthorizer) Prepare(ctx context.Context, subjectID string, roles []Role, groups []string, scope Scope, action Action, objectType string) (*PartialAuthorizer, error) {
160162
ctx, span := tracing.StartSpan(ctx)
161163
defer span.End()
162164

163-
auth, err := newPartialAuthorizer(ctx, subjectID, roles, scope, action, objectType)
165+
auth, err := newPartialAuthorizer(ctx, subjectID, roles, groups, scope, action, objectType)
164166
if err != nil {
165167
return nil, xerrors.Errorf("new partial authorizer: %w", err)
166168
}
167169

168170
return auth, nil
169171
}
170172

171-
func (a RegoAuthorizer) PrepareByRoleName(ctx context.Context, subjectID string, roleNames []string, scope Scope, action Action, objectType string) (PreparedAuthorized, error) {
173+
func (a RegoAuthorizer) PrepareByRoleName(ctx context.Context, subjectID string, roleNames []string, groups []string, scope Scope, action Action, objectType string) (PreparedAuthorized, error) {
172174
ctx, span := tracing.StartSpan(ctx)
173175
defer span.End()
174176

@@ -177,5 +179,5 @@ func (a RegoAuthorizer) PrepareByRoleName(ctx context.Context, subjectID string,
177179
return nil, err
178180
}
179181

180-
return a.Prepare(ctx, subjectID, roles, scope, action, objectType)
182+
return a.Prepare(ctx, subjectID, roles, groups, scope, action, objectType)
181183
}

coderd/rbac/authz_internal_test.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ type subject struct {
1919
// For the unit test we want to pass in the roles directly, instead of just
2020
// by name. This allows us to test custom roles that do not exist in the product,
2121
// but test edge cases of the implementation.
22-
Roles []Role `json:"roles"`
22+
Roles []Role `json:"roles"`
23+
Groups []string `json:"groups"`
2324
}
2425

2526
type fakeObject struct {
@@ -162,7 +163,7 @@ func TestFilter(t *testing.T) {
162163
var allowedCount int
163164
for i, obj := range localObjects {
164165
obj.Type = tc.ObjectType
165-
err := auth.ByRoleName(ctx, tc.SubjectID, tc.Roles, scope, ActionRead, obj.RBACObject())
166+
err := auth.ByRoleName(ctx, tc.SubjectID, tc.Roles, []string{}, scope, ActionRead, obj.RBACObject())
166167
obj.Allowed = err == nil
167168
if err == nil {
168169
allowedCount++
@@ -171,7 +172,7 @@ func TestFilter(t *testing.T) {
171172
}
172173

173174
// Run by filter
174-
list, err := Filter(ctx, auth, tc.SubjectID, tc.Roles, scope, tc.Action, localObjects)
175+
list, err := Filter(ctx, auth, tc.SubjectID, tc.Roles, []string{}, scope, tc.Action, localObjects)
175176
require.NoError(t, err)
176177
require.Equal(t, allowedCount, len(list), "expected number of allowed")
177178
for _, obj := range list {
@@ -714,7 +715,7 @@ func testAuthorize(t *testing.T, name string, subject subject, sets ...[]authTes
714715
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
715716
t.Cleanup(cancel)
716717

717-
authError := authorizer.Authorize(ctx, subject.UserID, subject.Roles, a, c.resource)
718+
authError := authorizer.Authorize(ctx, subject.UserID, subject.Roles, subject.Groups, a, c.resource)
718719

719720
// Logging only
720721
if authError != nil {
@@ -739,7 +740,7 @@ func testAuthorize(t *testing.T, name string, subject subject, sets ...[]authTes
739740
assert.Error(t, authError, "expected unauthorized")
740741
}
741742

742-
partialAuthz, err := authorizer.Prepare(ctx, subject.UserID, subject.Roles, ScopeAll, a, c.resource.Type)
743+
partialAuthz, err := authorizer.Prepare(ctx, subject.UserID, subject.Roles, subject.Groups, ScopeAll, a, c.resource.Type)
743744
require.NoError(t, err, "make prepared authorizer")
744745

745746
// Also check the rego policy can form a valid partial query result.

coderd/rbac/builtin_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ func BenchmarkRBACFilter(b *testing.B) {
3232
benchCases := []struct {
3333
Name string
3434
Roles []string
35+
Groups []string
3536
UserID uuid.UUID
3637
Scope rbac.Scope
3738
}{
@@ -90,7 +91,7 @@ func BenchmarkRBACFilter(b *testing.B) {
9091
b.Run(c.Name, func(b *testing.B) {
9192
objects := benchmarkSetup(orgs, users, b.N)
9293
b.ResetTimer()
93-
allowed, err := rbac.Filter(context.Background(), authorizer, c.UserID.String(), c.Roles, c.Scope, rbac.ActionRead, objects)
94+
allowed, err := rbac.Filter(context.Background(), authorizer, c.UserID.String(), c.Roles, c.Groups, c.Scope, rbac.ActionRead, objects)
9495
require.NoError(b, err)
9596
var _ = allowed
9697
})
@@ -114,6 +115,7 @@ type authSubject struct {
114115
Name string
115116
UserID string
116117
Roles []string
118+
Groups []string
117119
}
118120

119121
func TestRolePermissions(t *testing.T) {
@@ -359,7 +361,7 @@ func TestRolePermissions(t *testing.T) {
359361
delete(remainingSubjs, subj.Name)
360362
msg := fmt.Sprintf("%s as %q doing %q on %q", c.Name, subj.Name, action, c.Resource.Type)
361363
// TODO: scopey
362-
err := auth.ByRoleName(context.Background(), subj.UserID, subj.Roles, rbac.ScopeAll, action, c.Resource)
364+
err := auth.ByRoleName(context.Background(), subj.UserID, subj.Roles, subj.Groups, rbac.ScopeAll, action, c.Resource)
363365
if result {
364366
assert.NoError(t, err, fmt.Sprintf("Should pass: %s", msg))
365367
} else {

coderd/rbac/object.go

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -160,8 +160,8 @@ type Object struct {
160160
// Type is "workspace", "project", "app", etc
161161
Type string `json:"type"`
162162

163-
// map[string][]Action
164-
ACLUserList map[string][]Action ` json:"acl_user_list"`
163+
ACLUserList map[string][]Action ` json:"acl_user_list"`
164+
ACLGroupList map[string][]Action ` json:"acl_group_list"`
165165
}
166166

167167
func (z Object) RBACObject() Object {
@@ -171,36 +171,53 @@ func (z Object) RBACObject() Object {
171171
// All returns an object matching all resources of the same type.
172172
func (z Object) All() Object {
173173
return Object{
174-
Owner: "",
175-
OrgID: "",
176-
Type: z.Type,
174+
Owner: "",
175+
OrgID: "",
176+
Type: z.Type,
177+
ACLUserList: map[string][]Action{},
178+
ACLGroupList: map[string][]Action{},
177179
}
178180
}
179181

180182
// InOrg adds an org OwnerID to the resource
181183
func (z Object) InOrg(orgID uuid.UUID) Object {
182184
return Object{
183-
Owner: z.Owner,
184-
OrgID: orgID.String(),
185-
Type: z.Type,
185+
Owner: z.Owner,
186+
OrgID: orgID.String(),
187+
Type: z.Type,
188+
ACLUserList: z.ACLUserList,
189+
ACLGroupList: z.ACLGroupList,
186190
}
187191
}
188192

189193
// WithOwner adds an OwnerID to the resource
190194
func (z Object) WithOwner(ownerID string) Object {
191195
return Object{
192-
Owner: ownerID,
193-
OrgID: z.OrgID,
194-
Type: z.Type,
196+
Owner: ownerID,
197+
OrgID: z.OrgID,
198+
Type: z.Type,
199+
ACLUserList: z.ACLUserList,
200+
ACLGroupList: z.ACLGroupList,
195201
}
196202
}
197203

198204
// WithACLUserList adds an ACL list to a given object
199205
func (z Object) WithACLUserList(acl map[string][]Action) Object {
200206
return Object{
201-
Owner: z.Owner,
202-
OrgID: z.OrgID,
203-
Type: z.Type,
204-
ACLUserList: acl,
207+
Owner: z.Owner,
208+
OrgID: z.OrgID,
209+
Type: z.Type,
210+
ACLUserList: acl,
211+
ACLGroupList: z.ACLGroupList,
212+
}
213+
}
214+
215+
func (z Object) WithGroups(groups map[string][]Action) Object {
216+
return Object{
217+
Owner: z.Owner,
218+
OrgID: z.OrgID,
219+
Type: z.Type,
220+
ACLUserList: z.ACLUserList,
221+
ACLGroupList: groups,
205222
}
206223
}

coderd/rbac/partial.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ func (pa *PartialAuthorizer) Authorize(ctx context.Context, object Object) error
3535
return nil
3636
}
3737

38-
func newPartialAuthorizer(ctx context.Context, subjectID string, roles []Role, scope Scope, action Action, objectType string) (*PartialAuthorizer, error) {
38+
func newPartialAuthorizer(ctx context.Context, subjectID string, roles []Role, groups []string, scope Scope, action Action, objectType string) (*PartialAuthorizer, error) {
3939
ctx, span := tracing.StartSpan(ctx)
4040
defer span.End()
4141

42-
pAuth, err := newSubPartialAuthorizer(ctx, subjectID, roles, action, objectType)
42+
pAuth, err := newSubPartialAuthorizer(ctx, subjectID, roles, groups, action, objectType)
4343
if err != nil {
4444
return nil, err
4545
}
@@ -51,7 +51,7 @@ func newPartialAuthorizer(ctx context.Context, subjectID string, roles []Role, s
5151
return nil, xerrors.Errorf("unknown scope %q", scope)
5252
}
5353

54-
scopeAuth, err = newSubPartialAuthorizer(ctx, subjectID, []Role{scopeRole}, action, objectType)
54+
scopeAuth, err = newSubPartialAuthorizer(ctx, subjectID, []Role{scopeRole}, groups, action, objectType)
5555
if err != nil {
5656
return nil, err
5757
}
@@ -78,14 +78,15 @@ type subPartialAuthorizer struct {
7878
alwaysTrue bool
7979
}
8080

81-
func newSubPartialAuthorizer(ctx context.Context, subjectID string, roles []Role, action Action, objectType string) (*subPartialAuthorizer, error) {
81+
func newSubPartialAuthorizer(ctx context.Context, subjectID string, roles []Role, groups []string, action Action, objectType string) (*subPartialAuthorizer, error) {
8282
ctx, span := tracing.StartSpan(ctx)
8383
defer span.End()
8484

8585
input := map[string]interface{}{
8686
"subject": authSubject{
87-
ID: subjectID,
88-
Roles: roles,
87+
ID: subjectID,
88+
Roles: roles,
89+
Groups: groups,
8990
},
9091
"object": map[string]string{
9192
"type": objectType,

0 commit comments

Comments
 (0)