-
Notifications
You must be signed in to change notification settings - Fork 901
chore: Update rego to be partial execution friendly #3449
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 17 commits
b3536bc
75e3a12
6cdf575
f5eacd0
4a7c68e
df75be5
e90ac2d
1e774e0
38917dc
e139a1f
c44d4d1
19f3557
f9dd9aa
bed9f4f
74d90f4
fe0d05a
dd5c55c
ae22f89
0266963
af457b9
21f4f21
2c87220
1c407ab
9f9b2d1
ab55cf5
abf098d
44c7370
4611322
c8e26a8
510e94b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,9 +10,27 @@ import ( | |
"github.com/coder/coder/coderd/rbac" | ||
) | ||
|
||
func AuthorizeFilter[O rbac.Objecter](api *API, r *http.Request, action rbac.Action, objects []O) []O { | ||
func AuthorizeFilter[O rbac.Objecter](api *API, r *http.Request, action rbac.Action, objects []O) ([]O, error) { | ||
roles := httpmw.AuthorizationUserRoles(r) | ||
return rbac.Filter(r.Context(), api.Authorizer, roles.ID.String(), roles.Roles, action, objects) | ||
|
||
if len(objects) == 0 { | ||
return objects, nil | ||
} | ||
objecType := objects[0].RBACObject().Type | ||
objects, err := rbac.Filter(r.Context(), api.Authorizer, roles.ID.String(), roles.Roles, action, objecType, objects) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems like all of this logic could be in the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are right about the object type stuff 👍 As for |
||
if err != nil { | ||
api.Logger.Error(r.Context(), "filter failed", | ||
slog.Error(err), | ||
slog.F("object_type", objecType), | ||
slog.F("user_id", roles.ID), | ||
slog.F("username", roles.Username), | ||
slog.F("route", r.URL.Path), | ||
slog.F("action", action), | ||
) | ||
// Hide the underlying error in case it has sensitive information | ||
return nil, xerrors.Errorf("failed to filter requested objects") | ||
Emyrk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
return objects, nil | ||
} | ||
|
||
// Authorize will return false if the user is not authorized to do the action. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package rbac | ||
Emyrk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
import "context" | ||
|
||
type FakeAuthorizer struct { | ||
AuthFunc func(ctx context.Context, subjectID string, roleNames []string, action Action, object Object) error | ||
} | ||
|
||
func (f FakeAuthorizer) ByRoleName(ctx context.Context, subjectID string, roleNames []string, action Action, object Object) error { | ||
return f.AuthFunc(ctx, subjectID, roleNames, action, object) | ||
} | ||
|
||
func (f FakeAuthorizer) PrepareByRoleName(_ context.Context, subjectID string, roles []string, action Action, _ string) (PreparedAuthorized, error) { | ||
return &fakePreparedAuthorizer{ | ||
Original: f, | ||
SubjectID: subjectID, | ||
Roles: roles, | ||
Action: action, | ||
}, nil | ||
} | ||
|
||
type fakePreparedAuthorizer struct { | ||
Original Authorizer | ||
SubjectID string | ||
Roles []string | ||
Action Action | ||
} | ||
|
||
func (f fakePreparedAuthorizer) Authorize(ctx context.Context, object Object) error { | ||
return f.Original.ByRoleName(ctx, f.SubjectID, f.Roles, f.Action, object) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -11,24 +11,35 @@ import ( | |
|
||
type Authorizer interface { | ||
ByRoleName(ctx context.Context, subjectID string, roleNames []string, action Action, object Object) error | ||
PrepareByRoleName(ctx context.Context, subjectID string, roles []string, action Action, objectType string) (PreparedAuthorized, error) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function is only used internally, so seems like it might not need to be exported like this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I need it for my Fake authorizer to work for some unit tests. I can't fake it without an interface
Emyrk marked this conversation as resolved.
Show resolved
Hide resolved
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see any use case for exposing the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wish I could, but you can't make generic methods on structs. The nice thing about the generic If I made it a non-generic function, you'd have to take your slice of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, that sucks. Go generics continue to disappoint. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yea, it's really dumb because I don't see why it's not possible. Like I would like to know the objection to it. |
||
} | ||
|
||
type PreparedAuthorized interface { | ||
Authorize(ctx context.Context, object Object) error | ||
} | ||
|
||
// Filter takes in a list of objects, and will filter the list removing all | ||
// the elements the subject does not have permission for. | ||
// Filter does not allocate a new slice, and will use the existing one | ||
// passed in. This can cause memory leaks if the slice is held for a prolonged | ||
// period of time. | ||
func Filter[O Objecter](ctx context.Context, auth Authorizer, subjID string, subjRoles []string, action Action, objects []O) []O { | ||
// the elements the subject does not have permission for. All objects must be | ||
// of the same type. | ||
func Filter[O Objecter](ctx context.Context, auth Authorizer, subjID string, subjRoles []string, action Action, objectType string, objects []O) ([]O, error) { | ||
filtered := make([]O, 0) | ||
prepared, err := auth.PrepareByRoleName(ctx, subjID, subjRoles, action, objectType) | ||
if err != nil { | ||
return nil, xerrors.Errorf("prepare: %w", err) | ||
} | ||
|
||
for i := range objects { | ||
object := objects[i] | ||
err := auth.ByRoleName(ctx, subjID, subjRoles, action, object.RBACObject()) | ||
rbacObj := object.RBACObject() | ||
if rbacObj.Type != objectType { | ||
return nil, xerrors.Errorf("object types must be uniform across the set (%s), found %s", objectType, object.RBACObject().Type) | ||
} | ||
err := prepared.Authorize(ctx, rbacObj) | ||
if err == nil { | ||
filtered = append(filtered, object) | ||
} | ||
} | ||
return filtered | ||
return filtered, nil | ||
} | ||
|
||
// RegoAuthorizer will use a prepared rego query for performing authorize() | ||
|
@@ -45,7 +56,7 @@ func NewAuthorizer() (*RegoAuthorizer, error) { | |
query, err := rego.New( | ||
// allowed is the `allow` field from the prepared query. This is the field to check if authorization is | ||
// granted. | ||
rego.Query("allowed = data.authz.allow"), | ||
rego.Query("data.authz.allow"), | ||
rego.Module("policy.rego", policy), | ||
).PrepareForEval(ctx) | ||
|
||
|
@@ -64,14 +75,11 @@ type authSubject struct { | |
// This is the function intended to be used outside this package. | ||
// The role is fetched from the builtin map located in memory. | ||
func (a RegoAuthorizer) ByRoleName(ctx context.Context, subjectID string, roleNames []string, action Action, object Object) error { | ||
roles := make([]Role, 0, len(roleNames)) | ||
for _, n := range roleNames { | ||
r, err := RoleByName(n) | ||
if err != nil { | ||
return xerrors.Errorf("get role permissions: %w", err) | ||
} | ||
roles = append(roles, r) | ||
roles, err := RolesByNames(roleNames) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return a.Authorize(ctx, subjectID, roles, action, object) | ||
} | ||
|
||
|
@@ -92,18 +100,29 @@ func (a RegoAuthorizer) Authorize(ctx context.Context, subjectID string, roles [ | |
return ForbiddenWithInternal(xerrors.Errorf("eval rego: %w", err), input, results) | ||
} | ||
|
||
if len(results) != 1 { | ||
return ForbiddenWithInternal(xerrors.Errorf("expect only 1 result, got %d", len(results)), input, results) | ||
if !results.Allowed() { | ||
return ForbiddenWithInternal(xerrors.Errorf("policy disallows request"), input, results) | ||
} | ||
|
||
allowedResult, ok := (results[0].Bindings["allowed"]).(bool) | ||
if !ok { | ||
return ForbiddenWithInternal(xerrors.Errorf("expected allowed to be a bool but got %T", allowedResult), input, results) | ||
return nil | ||
} | ||
|
||
// Prepare will partially execute the rego policy leaving the object fields unknown (except for the type). | ||
// This will vastly speed up performance if batch authorization on the same type of objects is needed. | ||
func (RegoAuthorizer) Prepare(ctx context.Context, subjectID string, roles []Role, action Action, objectType string) (*PartialAuthorizer, error) { | ||
auth, err := newPartialAuthorizer(ctx, subjectID, roles, action, objectType) | ||
if err != nil { | ||
return nil, xerrors.Errorf("new partial authorizer: %w", err) | ||
} | ||
|
||
if !allowedResult { | ||
return ForbiddenWithInternal(xerrors.Errorf("policy disallows request"), input, results) | ||
return auth, nil | ||
} | ||
|
||
func (a RegoAuthorizer) PrepareByRoleName(ctx context.Context, subjectID string, roleNames []string, action Action, objectType string) (PreparedAuthorized, error) { | ||
roles, err := RolesByNames(roleNames) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return nil | ||
return a.Prepare(ctx, subjectID, roles, action, objectType) | ||
} |
Uh oh!
There was an error while loading. Please reload this page.