Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
Emyrk merged 30 commits intomainfromstevenmasley/rbac_update_rego
Aug 11, 2022
Merged
Show file tree
Hide file tree
Changes from17 commits
Commits
Show all changes
30 commits
Select commitHold shift + click to select a range
b3536bc
chore: Update rego to be partial execution friendly
EmyrkAug 10, 2022
75e3a12
test: Add unit test to verify partial query support
EmyrkAug 10, 2022
6cdf575
Add partial filtering and evaluation
EmyrkAug 10, 2022
f5eacd0
Benmark Partials
EmyrkAug 10, 2022
4a7c68e
Linting
EmyrkAug 10, 2022
df75be5
Spelling
EmyrkAug 10, 2022
e90ac2d
Fix recording fake authorizer
EmyrkAug 10, 2022
1e774e0
Fix partial query tests
EmyrkAug 10, 2022
38917dc
Working partial execution to queries
EmyrkAug 10, 2022
e139a1f
Cleanup
EmyrkAug 10, 2022
c44d4d1
Cleanup
EmyrkAug 10, 2022
19f3557
Add comments
EmyrkAug 10, 2022
f9dd9aa
Spelling
EmyrkAug 10, 2022
bed9f4f
Remove old Filter
EmyrkAug 10, 2022
74d90f4
Improve error message
EmyrkAug 10, 2022
fe0d05a
Comments
EmyrkAug 10, 2022
dd5c55c
Make test parallel
EmyrkAug 10, 2022
ae22f89
Fix typo
EmyrkAug 10, 2022
0266963
Drop an arg from Filter
EmyrkAug 10, 2022
af457b9
Test timed out
EmyrkAug 10, 2022
21f4f21
Test less random, rename param
EmyrkAug 11, 2022
2c87220
Add rego comments with explainations
EmyrkAug 11, 2022
1c407ab
Add more rego comments
EmyrkAug 11, 2022
9f9b2d1
Move fakeauthorizer code
EmyrkAug 11, 2022
ab55cf5
Unexport partial fields
EmyrkAug 11, 2022
abf098d
Reduce wait
EmyrkAug 11, 2022
44c7370
Linting + Typos
EmyrkAug 11, 2022
4611322
Wording
EmyrkAug 11, 2022
c8e26a8
Test runs long
EmyrkAug 11, 2022
510e94b
The single test is taking too long
EmyrkAug 11, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletionscoderd/authorize.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Seems like all of this logic could be in therbac package itself, then we wouldn't need therecordingAuthorizer code. Then functions could just callrbac.AuthorizeFilter directly.

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

You are right about the object type stuff 👍

As forrecordingAuthorizer. That is needed for my unit test that checks all the routes.recordingAuthorizer is never used outside unit tests.

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")
}
return objects, nil
}

// Authorize will return false if the user is not authorized to do the action.
Expand Down
28 changes: 18 additions & 10 deletionscoderd/coderd_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -63,7 +63,7 @@ func TestBuildInfo(t *testing.T) {
// TestAuthorizeAllEndpoints will check `authorize` is called on every endpoint registered.
func TestAuthorizeAllEndpoints(t *testing.T) {
t.Parallel()
authorizer :=&fakeAuthorizer{}
authorizer :=newRecordingAuthorizer()

ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
Expand DownExpand Up@@ -563,21 +563,29 @@ type authCall struct {
Object rbac.Object
}

type fakeAuthorizer struct {
type recordingAuthorizer struct {
*rbac.FakeAuthorizer
Called *authCall
AlwaysReturn error
}

func (f *fakeAuthorizer) ByRoleName(_ context.Context, subjectID string, roleNames []string, action rbac.Action, object rbac.Object) error {
f.Called = &authCall{
SubjectID: subjectID,
Roles: roleNames,
Action: action,
Object: object,
func newRecordingAuthorizer() *recordingAuthorizer {
r := &recordingAuthorizer{}
// Use the fake authorizer by rbac to handle prepared authorizers.
r.FakeAuthorizer = &rbac.FakeAuthorizer{
AuthFunc: func(ctx context.Context, subjectID string, roleNames []string, action rbac.Action, object rbac.Object) error {
r.Called = &authCall{
SubjectID: subjectID,
Roles: roleNames,
Action: action,
Object: object,
}
return r.AlwaysReturn
},
}
returnf.AlwaysReturn
returnr
}

func (f *fakeAuthorizer) reset() {
func (f *recordingAuthorizer) reset() {
f.Called = nil
}
9 changes: 8 additions & 1 deletioncoderd/provisionerdaemons.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,7 +50,14 @@ func (api *API) provisionerDaemons(rw http.ResponseWriter, r *http.Request) {
if daemons == nil {
daemons = []database.ProvisionerDaemon{}
}
daemons = AuthorizeFilter(api, r, rbac.ActionRead, daemons)
daemons, err = AuthorizeFilter(api, r, rbac.ActionRead, daemons)
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching provisioner daemons.",
Detail: err.Error(),
})
return
}

httpapi.Write(rw, http.StatusOK, daemons)
}
Expand Down
31 changes: 31 additions & 0 deletionscoderd/rbac/auth_fake.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
package rbac

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)
}
65 changes: 42 additions & 23 deletionscoderd/rbac/authz.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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)
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
MemberAuthor

@EmyrkEmyrkAug 10, 2022
edited
Loading

Choose a reason for hiding this comment

The 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☹️

recordingAuthorizer always returns false and save the auth calls so I can assert them in my tests.

kylecarbs reacted with thumbs up emoji
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I don't see any use case for exposing thePreparedAuthorized to consumers of authorization --- why not just expose the Filter() method here?

Copy link
MemberAuthor

Choose a reason for hiding this comment

The 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 genericFilter() method is you can pass in and return the same typed slice.

If I made it a non-generic function, you'd have to take your slice of[]Workspace, convert it to[]rbac.Object, and then convert it back to[]Workspace. I could do this all in somerbac.Authorize, but it takes a lot of slice copying I prefer to avoid.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Well, that sucks. Go generics continue to disappoint.

Emyrk reacted with confused emoji
Copy link
MemberAuthor

Choose a reason for hiding this comment

The 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()
Expand All@@ -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)

Expand All@@ -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)
}

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

iflen(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
}

returnnil
returna.Prepare(ctx, subjectID, roles, action, objectType)
}
Loading

[8]ページ先頭

©2009-2025 Movatter.jp