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 1 commit
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
Prev Previous commit
Next Next commit
Linting
  • Loading branch information
Emyrk committed Aug 10, 2022
commit 4a7c68e1982af6f9da7d8204920dfa49ea0ece0e
28 changes: 18 additions & 10 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 := newRecordingAuthorizer()

ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
Expand Down Expand 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
},
}
return f.AlwaysReturn
return r
}

func (f *fakeAuthorizer) reset() {
func (f *recordingAuthorizer) reset() {
f.Called = nil
}
31 changes: 31 additions & 0 deletions coderd/rbac/auth_fake.go
Original file line number Diff line number Diff 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)
}
22 changes: 5 additions & 17 deletions coderd/rbac/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ 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, err := a.rolesByNames(roleNames)
roles, err := RolesByNames(roleNames)
if err != nil {
return err
}
Expand Down Expand Up @@ -119,15 +119,15 @@ func (a RegoAuthorizer) Authorize(ctx context.Context, subjectID string, roles [
}

func (a RegoAuthorizer) PrepareByRoleName(ctx context.Context, subjectID string, roleNames []string, action Action, objectType string) (PreparedAuthorized, error) {
roles, err := a.rolesByNames(roleNames)
roles, err := RolesByNames(roleNames)
if err != nil {
return nil, err
}

return a.Prepare(ctx, subjectID, roles, action, objectType)
}

func (a RegoAuthorizer) Prepare(ctx context.Context, subjectID string, roles []Role, action Action, objectType string) (*partialAuthorizer, error) {
func (RegoAuthorizer) Prepare(ctx context.Context, subjectID string, roles []Role, action Action, objectType string) (*PartialAuthorizer, error) {
input := map[string]interface{}{
"subject": authSubject{
ID: subjectID,
Expand All @@ -139,7 +139,7 @@ func (a RegoAuthorizer) Prepare(ctx context.Context, subjectID string, roles []R
"action": action,
}

rego := rego.New(
regoPolicy := rego.New(
rego.Query("data.authz.allow"),
rego.Module("policy.rego", policy),
rego.Unknowns([]string{
Expand All @@ -149,26 +149,14 @@ func (a RegoAuthorizer) Prepare(ctx context.Context, subjectID string, roles []R
rego.Input(input),
)

auth, err := newPartialAuthorizer(ctx, rego, input)
auth, err := newPartialAuthorizer(ctx, regoPolicy, input)
if err != nil {
return nil, xerrors.Errorf("new partial authorizer: %w", err)
}

return auth, nil
}

func (a RegoAuthorizer) rolesByNames(roleNames []string) ([]Role, error) {
roles := make([]Role, 0, len(roleNames))
for _, n := range roleNames {
r, err := RoleByName(n)
if err != nil {
return nil, xerrors.Errorf("get role permissions: %w", err)
}
roles = append(roles, r)
}
return roles, nil
}

// CheckPartial will not authorize the request. This function is to be used for unit testing to verify the rego policy
// can be converted into ONLY queries. This ensures we can convert the queries into SQL WHERE clauses in the future.
// If this function returns an error, then there is a set of inputs that also returns support rules, which cannot
Expand Down
4 changes: 2 additions & 2 deletions coderd/rbac/authz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ func TestFilter(t *testing.T) {
c := c
t.Run(c.Name, func(t *testing.T) {
t.Parallel()
authorizer := fakeAuthorizer{
authorizer := rbac.FakeAuthorizer{
AuthFunc: func(_ context.Context, _ string, _ []string, _ rbac.Action, object rbac.Object) error {
return c.Auth(object)
},
Expand Down Expand Up @@ -573,7 +573,7 @@ func testAuthorize(t *testing.T, name string, subject subject, sets ...[]authTes
t.Run(name, func(t *testing.T) {
for _, a := range c.actions {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
t.Cleanup(cancel)
authError := authorizer.Authorize(ctx, subject.UserID, subject.Roles, a, c.resource)
if c.allow {
if authError != nil {
Expand Down
12 changes: 12 additions & 0 deletions coderd/rbac/builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,18 @@ func RoleByName(name string) (Role, error) {
return role, nil
}

func RolesByNames(roleNames []string) ([]Role, error) {
roles := make([]Role, 0, len(roleNames))
for _, n := range roleNames {
r, err := RoleByName(n)
if err != nil {
return nil, xerrors.Errorf("get role permissions: %w", err)
}
roles = append(roles, r)
}
return roles, nil
}

func IsOrgRole(roleName string) (string, bool) {
_, orgID, err := roleSplit(roleName)
if err == nil && orgID != "" {
Expand Down
35 changes: 0 additions & 35 deletions coderd/rbac/fake_test.go

This file was deleted.

8 changes: 4 additions & 4 deletions coderd/rbac/partial.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,28 @@ import (
"github.com/open-policy-agent/opa/rego"
)

type partialAuthorizer struct {
type PartialAuthorizer struct {
// PartialRego is mainly used for unit testing. It is the rego source policy.
PartialRego *rego.Rego
PartialResult rego.PartialResult
Input map[string]interface{}
}

func newPartialAuthorizer(ctx context.Context, partialRego *rego.Rego, input map[string]interface{}) (*partialAuthorizer, error) {
func newPartialAuthorizer(ctx context.Context, partialRego *rego.Rego, input map[string]interface{}) (*PartialAuthorizer, error) {
pResult, err := partialRego.PartialResult(ctx)
if err != nil {
return nil, xerrors.Errorf("partial results: %w", err)
}

return &partialAuthorizer{
return &PartialAuthorizer{
PartialRego: partialRego,
PartialResult: pResult,
Input: input,
}, nil
}

// Authorize authorizes a single object
func (a partialAuthorizer) Authorize(ctx context.Context, object Object) error {
func (a PartialAuthorizer) Authorize(ctx context.Context, object Object) error {
results, err := a.PartialResult.Rego(rego.Input(
map[string]interface{}{
"object": object,
Expand Down