Skip to content

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

Merged
merged 30 commits into from
Aug 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
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
16 changes: 14 additions & 2 deletions coderd/authorize.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,21 @@ 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)
objects, err := rbac.Filter(r.Context(), api.Authorizer, roles.ID.String(), roles.Roles, action, objects)
if err != nil {
// Log the error as Filter should not be erroring.
api.Logger.Error(r.Context(), "filter failed",
slog.Error(err),
slog.F("user_id", roles.ID),
slog.F("username", roles.Username),
slog.F("route", r.URL.Path),
slog.F("action", action),
)
return nil, err
}
return objects, nil
}

// Authorize will return false if the user is not authorized to do the action.
Expand Down
34 changes: 27 additions & 7 deletions coderd/coderd_test.go
Original file line number Diff line number Diff 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 := &recordingAuthorizer{}

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

type fakeAuthorizer struct {
type recordingAuthorizer struct {
Called *authCall
AlwaysReturn error
}

func (f *fakeAuthorizer) ByRoleName(_ context.Context, subjectID string, roleNames []string, action rbac.Action, object rbac.Object) error {
f.Called = &authCall{
func (r *recordingAuthorizer) ByRoleName(_ 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 f.AlwaysReturn
return r.AlwaysReturn
}

func (f *fakeAuthorizer) reset() {
f.Called = nil
func (r *recordingAuthorizer) PrepareByRoleName(_ context.Context, subjectID string, roles []string, action rbac.Action, _ string) (rbac.PreparedAuthorized, error) {
return &fakePreparedAuthorizer{
Original: r,
SubjectID: subjectID,
Roles: roles,
Action: action,
}, nil
}

func (r *recordingAuthorizer) reset() {
r.Called = nil
}

type fakePreparedAuthorizer struct {
Original *recordingAuthorizer
SubjectID string
Roles []string
Action rbac.Action
}

func (f *fakePreparedAuthorizer) Authorize(ctx context.Context, object rbac.Object) error {
return f.Original.ByRoleName(ctx, f.SubjectID, f.Roles, f.Action, object)
}
9 changes: 8 additions & 1 deletion coderd/provisionerdaemons.go
Original file line number Diff line number Diff 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
71 changes: 48 additions & 23 deletions coderd/rbac/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,41 @@ import (

type Authorizer interface {
ByRoleName(ctx context.Context, subjectID string, roleNames []string, action Action, object Object) error
PrepareByRoleName(ctx context.Context, subjectID string, roleNames []string, action Action, objectType string) (PreparedAuthorized, error)
}

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, objects []O) ([]O, error) {
if len(objects) == 0 {
// Nothing to filter
return objects, nil
}
objectType := objects[0].RBACObject().Type

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 +62,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 +81,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 +106,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)
}
Loading