Skip to content

Commit 3ae42f4

Browse files
authored
chore: Update rego to be partial execution friendly (#3449)
- Improves performance of batch authorization calls - Enables possibility to convert rego auth calls into SQL WHERE clauses
1 parent 4a17e0d commit 3ae42f4

16 files changed

+1124
-818
lines changed

coderd/authorize.go

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,21 @@ import (
1010
"github.com/coder/coder/coderd/rbac"
1111
)
1212

13-
func AuthorizeFilter[O rbac.Objecter](api *API, r *http.Request, action rbac.Action, objects []O) []O {
13+
func AuthorizeFilter[O rbac.Objecter](api *API, r *http.Request, action rbac.Action, objects []O) ([]O, error) {
1414
roles := httpmw.AuthorizationUserRoles(r)
15-
return rbac.Filter(r.Context(), api.Authorizer, roles.ID.String(), roles.Roles, action, objects)
15+
objects, err := rbac.Filter(r.Context(), api.Authorizer, roles.ID.String(), roles.Roles, action, objects)
16+
if err != nil {
17+
// Log the error as Filter should not be erroring.
18+
api.Logger.Error(r.Context(), "filter failed",
19+
slog.Error(err),
20+
slog.F("user_id", roles.ID),
21+
slog.F("username", roles.Username),
22+
slog.F("route", r.URL.Path),
23+
slog.F("action", action),
24+
)
25+
return nil, err
26+
}
27+
return objects, nil
1628
}
1729

1830
// Authorize will return false if the user is not authorized to do the action.

coderd/coderd_test.go

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func TestBuildInfo(t *testing.T) {
6363
// TestAuthorizeAllEndpoints will check `authorize` is called on every endpoint registered.
6464
func TestAuthorizeAllEndpoints(t *testing.T) {
6565
t.Parallel()
66-
authorizer := &fakeAuthorizer{}
66+
authorizer := &recordingAuthorizer{}
6767

6868
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
6969
defer cancel()
@@ -563,21 +563,41 @@ type authCall struct {
563563
Object rbac.Object
564564
}
565565

566-
type fakeAuthorizer struct {
566+
type recordingAuthorizer struct {
567567
Called *authCall
568568
AlwaysReturn error
569569
}
570570

571-
func (f *fakeAuthorizer) ByRoleName(_ context.Context, subjectID string, roleNames []string, action rbac.Action, object rbac.Object) error {
572-
f.Called = &authCall{
571+
func (r *recordingAuthorizer) ByRoleName(_ context.Context, subjectID string, roleNames []string, action rbac.Action, object rbac.Object) error {
572+
r.Called = &authCall{
573573
SubjectID: subjectID,
574574
Roles: roleNames,
575575
Action: action,
576576
Object: object,
577577
}
578-
return f.AlwaysReturn
578+
return r.AlwaysReturn
579579
}
580580

581-
func (f *fakeAuthorizer) reset() {
582-
f.Called = nil
581+
func (r *recordingAuthorizer) PrepareByRoleName(_ context.Context, subjectID string, roles []string, action rbac.Action, _ string) (rbac.PreparedAuthorized, error) {
582+
return &fakePreparedAuthorizer{
583+
Original: r,
584+
SubjectID: subjectID,
585+
Roles: roles,
586+
Action: action,
587+
}, nil
588+
}
589+
590+
func (r *recordingAuthorizer) reset() {
591+
r.Called = nil
592+
}
593+
594+
type fakePreparedAuthorizer struct {
595+
Original *recordingAuthorizer
596+
SubjectID string
597+
Roles []string
598+
Action rbac.Action
599+
}
600+
601+
func (f *fakePreparedAuthorizer) Authorize(ctx context.Context, object rbac.Object) error {
602+
return f.Original.ByRoleName(ctx, f.SubjectID, f.Roles, f.Action, object)
583603
}

coderd/provisionerdaemons.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,14 @@ func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) {
5050
if daemons == nil {
5151
daemons = []database.ProvisionerDaemon{}
5252
}
53-
daemons = AuthorizeFilter(api, r, rbac.ActionRead, daemons)
53+
daemons, err = AuthorizeFilter(api, r, rbac.ActionRead, daemons)
54+
if err != nil {
55+
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
56+
Message: "Internal error fetching provisioner daemons.",
57+
Detail: err.Error(),
58+
})
59+
return
60+
}
5461

5562
httpapi.Write(rw, http.StatusOK, daemons)
5663
}

coderd/rbac/authz.go

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -11,24 +11,41 @@ import (
1111

1212
type Authorizer interface {
1313
ByRoleName(ctx context.Context, subjectID string, roleNames []string, action Action, object Object) error
14+
PrepareByRoleName(ctx context.Context, subjectID string, roleNames []string, action Action, objectType string) (PreparedAuthorized, error)
15+
}
16+
17+
type PreparedAuthorized interface {
18+
Authorize(ctx context.Context, object Object) error
1419
}
1520

1621
// Filter takes in a list of objects, and will filter the list removing all
17-
// the elements the subject does not have permission for.
18-
// Filter does not allocate a new slice, and will use the existing one
19-
// passed in. This can cause memory leaks if the slice is held for a prolonged
20-
// period of time.
21-
func Filter[O Objecter](ctx context.Context, auth Authorizer, subjID string, subjRoles []string, action Action, objects []O) []O {
22+
// the elements the subject does not have permission for. All objects must be
23+
// of the same type.
24+
func Filter[O Objecter](ctx context.Context, auth Authorizer, subjID string, subjRoles []string, action Action, objects []O) ([]O, error) {
25+
if len(objects) == 0 {
26+
// Nothing to filter
27+
return objects, nil
28+
}
29+
objectType := objects[0].RBACObject().Type
30+
2231
filtered := make([]O, 0)
32+
prepared, err := auth.PrepareByRoleName(ctx, subjID, subjRoles, action, objectType)
33+
if err != nil {
34+
return nil, xerrors.Errorf("prepare: %w", err)
35+
}
2336

2437
for i := range objects {
2538
object := objects[i]
26-
err := auth.ByRoleName(ctx, subjID, subjRoles, action, object.RBACObject())
39+
rbacObj := object.RBACObject()
40+
if rbacObj.Type != objectType {
41+
return nil, xerrors.Errorf("object types must be uniform across the set (%s), found %s", objectType, object.RBACObject().Type)
42+
}
43+
err := prepared.Authorize(ctx, rbacObj)
2744
if err == nil {
2845
filtered = append(filtered, object)
2946
}
3047
}
31-
return filtered
48+
return filtered, nil
3249
}
3350

3451
// RegoAuthorizer will use a prepared rego query for performing authorize()
@@ -45,7 +62,7 @@ func NewAuthorizer() (*RegoAuthorizer, error) {
4562
query, err := rego.New(
4663
// allowed is the `allow` field from the prepared query. This is the field to check if authorization is
4764
// granted.
48-
rego.Query("allowed = data.authz.allow"),
65+
rego.Query("data.authz.allow"),
4966
rego.Module("policy.rego", policy),
5067
).PrepareForEval(ctx)
5168

@@ -64,14 +81,11 @@ type authSubject struct {
6481
// This is the function intended to be used outside this package.
6582
// The role is fetched from the builtin map located in memory.
6683
func (a RegoAuthorizer) ByRoleName(ctx context.Context, subjectID string, roleNames []string, action Action, object Object) error {
67-
roles := make([]Role, 0, len(roleNames))
68-
for _, n := range roleNames {
69-
r, err := RoleByName(n)
70-
if err != nil {
71-
return xerrors.Errorf("get role permissions: %w", err)
72-
}
73-
roles = append(roles, r)
84+
roles, err := RolesByNames(roleNames)
85+
if err != nil {
86+
return err
7487
}
88+
7589
return a.Authorize(ctx, subjectID, roles, action, object)
7690
}
7791

@@ -92,18 +106,29 @@ func (a RegoAuthorizer) Authorize(ctx context.Context, subjectID string, roles [
92106
return ForbiddenWithInternal(xerrors.Errorf("eval rego: %w", err), input, results)
93107
}
94108

95-
if len(results) != 1 {
96-
return ForbiddenWithInternal(xerrors.Errorf("expect only 1 result, got %d", len(results)), input, results)
109+
if !results.Allowed() {
110+
return ForbiddenWithInternal(xerrors.Errorf("policy disallows request"), input, results)
97111
}
98112

99-
allowedResult, ok := (results[0].Bindings["allowed"]).(bool)
100-
if !ok {
101-
return ForbiddenWithInternal(xerrors.Errorf("expected allowed to be a bool but got %T", allowedResult), input, results)
113+
return nil
114+
}
115+
116+
// Prepare will partially execute the rego policy leaving the object fields unknown (except for the type).
117+
// This will vastly speed up performance if batch authorization on the same type of objects is needed.
118+
func (RegoAuthorizer) Prepare(ctx context.Context, subjectID string, roles []Role, action Action, objectType string) (*PartialAuthorizer, error) {
119+
auth, err := newPartialAuthorizer(ctx, subjectID, roles, action, objectType)
120+
if err != nil {
121+
return nil, xerrors.Errorf("new partial authorizer: %w", err)
102122
}
103123

104-
if !allowedResult {
105-
return ForbiddenWithInternal(xerrors.Errorf("policy disallows request"), input, results)
124+
return auth, nil
125+
}
126+
127+
func (a RegoAuthorizer) PrepareByRoleName(ctx context.Context, subjectID string, roleNames []string, action Action, objectType string) (PreparedAuthorized, error) {
128+
roles, err := RolesByNames(roleNames)
129+
if err != nil {
130+
return nil, err
106131
}
107132

108-
return nil
133+
return a.Prepare(ctx, subjectID, roles, action, objectType)
109134
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy