feat: add template RBAC/groups (#4235)

This commit is contained in:
Jon Ayers
2022-10-10 15:37:06 -05:00
committed by GitHub
parent 2687e3db49
commit 3120c94c22
122 changed files with 8088 additions and 1062 deletions

View File

@ -3,7 +3,6 @@ package rbac
import (
"context"
_ "embed"
"fmt"
"sync"
"github.com/open-policy-agent/opa/rego"
@ -15,8 +14,8 @@ import (
)
type Authorizer interface {
ByRoleName(ctx context.Context, subjectID string, roleNames []string, scope Scope, action Action, object Object) error
PrepareByRoleName(ctx context.Context, subjectID string, roleNames []string, scope Scope, action Action, objectType string) (PreparedAuthorized, error)
ByRoleName(ctx context.Context, subjectID string, roleNames []string, scope Scope, groups []string, action Action, object Object) error
PrepareByRoleName(ctx context.Context, subjectID string, roleNames []string, scope Scope, groups []string, action Action, objectType string) (PreparedAuthorized, error)
}
type PreparedAuthorized interface {
@ -27,7 +26,7 @@ type PreparedAuthorized interface {
// Filter takes in a list of objects, and will filter the list removing all
// 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, scope Scope, action Action, objects []O) ([]O, error) {
func Filter[O Objecter](ctx context.Context, auth Authorizer, subjID string, subjRoles []string, scope Scope, groups []string, action Action, objects []O) ([]O, error) {
ctx, span := tracing.StartSpan(ctx, trace.WithAttributes(
attribute.String("subject_id", subjID),
attribute.StringSlice("subject_roles", subjRoles),
@ -52,7 +51,7 @@ func Filter[O Objecter](ctx context.Context, auth Authorizer, subjID string, sub
if rbacObj.Type != objectType {
return nil, xerrors.Errorf("object types must be uniform across the set (%s), found %s", objectType, rbacObj)
}
err := auth.ByRoleName(ctx, subjID, subjRoles, scope, action, o.RBACObject())
err := auth.ByRoleName(ctx, subjID, subjRoles, scope, groups, action, o.RBACObject())
if err == nil {
filtered = append(filtered, o)
}
@ -60,7 +59,7 @@ func Filter[O Objecter](ctx context.Context, auth Authorizer, subjID string, sub
return filtered, nil
}
prepared, err := auth.PrepareByRoleName(ctx, subjID, subjRoles, scope, action, objectType)
prepared, err := auth.PrepareByRoleName(ctx, subjID, subjRoles, scope, groups, action, objectType)
if err != nil {
return nil, xerrors.Errorf("prepare: %w", err)
}
@ -95,21 +94,11 @@ var (
query rego.PreparedEvalQuery
)
const (
rolesOkCheck = "role_ok"
scopeOkCheck = "scope_ok"
)
func NewAuthorizer() *RegoAuthorizer {
queryOnce.Do(func() {
var err error
query, err = rego.New(
// Bind the results to 2 variables for easy checking later.
rego.Query(
fmt.Sprintf("%s := data.authz.role_allow "+
"%s := data.authz.scope_allow",
rolesOkCheck, scopeOkCheck),
),
rego.Query("data.authz.allow"),
rego.Module("policy.rego", policy),
).PrepareForEval(context.Background())
if err != nil {
@ -120,15 +109,16 @@ func NewAuthorizer() *RegoAuthorizer {
}
type authSubject struct {
ID string `json:"id"`
Roles []Role `json:"roles"`
Scope Role `json:"scope"`
ID string `json:"id"`
Roles []Role `json:"roles"`
Groups []string `json:"groups"`
Scope Role `json:"scope"`
}
// ByRoleName will expand all roleNames into roles before calling Authorize().
// 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, scope Scope, action Action, object Object) error {
func (a RegoAuthorizer) ByRoleName(ctx context.Context, subjectID string, roleNames []string, scope Scope, groups []string, action Action, object Object) error {
roles, err := RolesByNames(roleNames)
if err != nil {
return err
@ -139,7 +129,7 @@ func (a RegoAuthorizer) ByRoleName(ctx context.Context, subjectID string, roleNa
return err
}
err = a.Authorize(ctx, subjectID, roles, scopeRole, action, object)
err = a.Authorize(ctx, subjectID, roles, scopeRole, groups, action, object)
if err != nil {
return err
}
@ -149,12 +139,16 @@ func (a RegoAuthorizer) ByRoleName(ctx context.Context, subjectID string, roleNa
// Authorize allows passing in custom Roles.
// This is really helpful for unit testing, as we can create custom roles to exercise edge cases.
func (a RegoAuthorizer) Authorize(ctx context.Context, subjectID string, roles []Role, scope Role, action Action, object Object) error {
func (a RegoAuthorizer) Authorize(ctx context.Context, subjectID string, roles []Role, scope Role, groups []string, action Action, object Object) error {
ctx, span := tracing.StartSpan(ctx)
defer span.End()
input := map[string]interface{}{
"subject": authSubject{
ID: subjectID,
Roles: roles,
Scope: scope,
ID: subjectID,
Roles: roles,
Groups: groups,
Scope: scope,
},
"object": object,
"action": action,
@ -165,37 +159,19 @@ func (a RegoAuthorizer) Authorize(ctx context.Context, subjectID string, roles [
return ForbiddenWithInternal(xerrors.Errorf("eval rego: %w", err), input, results)
}
// We expect only the 2 bindings for scopes and roles checks.
if len(results) == 1 && len(results[0].Bindings) == 2 {
roleCheck, ok := results[0].Bindings[rolesOkCheck].(bool)
if !ok || !roleCheck {
return ForbiddenWithInternal(xerrors.Errorf("policy disallows request"), input, results)
}
scopeCheck, ok := results[0].Bindings[scopeOkCheck].(bool)
if !ok || !scopeCheck {
return ForbiddenWithInternal(xerrors.Errorf("policy disallows request"), input, results)
}
// This is purely defensive programming. The two above checks already
// check for 'true' expressions. This is just a sanity check to make
// sure we don't add non-boolean expressions to our query.
// This is super cheap to do, and just adds in some extra safety for
// programmer error.
for _, exp := range results[0].Expressions {
if b, ok := exp.Value.(bool); !ok || !b {
return ForbiddenWithInternal(xerrors.Errorf("policy disallows request"), input, results)
}
}
return nil
if !results.Allowed() {
return ForbiddenWithInternal(xerrors.Errorf("policy disallows request"), input, results)
}
return ForbiddenWithInternal(xerrors.Errorf("policy disallows request"), 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, scope Role, action Action, objectType string) (*PartialAuthorizer, error) {
auth, err := newPartialAuthorizer(ctx, subjectID, roles, scope, action, objectType)
func (RegoAuthorizer) Prepare(ctx context.Context, subjectID string, roles []Role, scope Role, groups []string, action Action, objectType string) (*PartialAuthorizer, error) {
ctx, span := tracing.StartSpan(ctx)
defer span.End()
auth, err := newPartialAuthorizer(ctx, subjectID, roles, scope, groups, action, objectType)
if err != nil {
return nil, xerrors.Errorf("new partial authorizer: %w", err)
}
@ -203,7 +179,10 @@ func (RegoAuthorizer) Prepare(ctx context.Context, subjectID string, roles []Rol
return auth, nil
}
func (a RegoAuthorizer) PrepareByRoleName(ctx context.Context, subjectID string, roleNames []string, scope Scope, action Action, objectType string) (PreparedAuthorized, error) {
func (a RegoAuthorizer) PrepareByRoleName(ctx context.Context, subjectID string, roleNames []string, scope Scope, groups []string, action Action, objectType string) (PreparedAuthorized, error) {
ctx, span := tracing.StartSpan(ctx)
defer span.End()
roles, err := RolesByNames(roleNames)
if err != nil {
return nil, err
@ -214,5 +193,5 @@ func (a RegoAuthorizer) PrepareByRoleName(ctx context.Context, subjectID string,
return nil, err
}
return a.Prepare(ctx, subjectID, roles, scopeRole, action, objectType)
return a.Prepare(ctx, subjectID, roles, scopeRole, groups, action, objectType)
}

View File

@ -19,8 +19,9 @@ type subject struct {
// For the unit test we want to pass in the roles directly, instead of just
// by name. This allows us to test custom roles that do not exist in the product,
// but test edge cases of the implementation.
Roles []Role `json:"roles"`
Scope Role `json:"scope"`
Roles []Role `json:"roles"`
Groups []string `json:"groups"`
Scope Role `json:"scope"`
}
type fakeObject struct {
@ -41,7 +42,8 @@ func (w fakeObject) RBACObject() Object {
func TestFilterError(t *testing.T) {
t.Parallel()
auth := NewAuthorizer()
_, err := Filter(context.Background(), auth, uuid.NewString(), []string{}, ScopeAll, ActionRead, []Object{ResourceUser, ResourceWorkspace})
_, err := Filter(context.Background(), auth, uuid.NewString(), []string{}, ScopeAll, []string{}, ActionRead, []Object{ResourceUser, ResourceWorkspace})
require.ErrorContains(t, err, "object types must be uniform")
}
@ -169,7 +171,7 @@ func TestFilter(t *testing.T) {
var allowedCount int
for i, obj := range localObjects {
obj.Type = tc.ObjectType
err := auth.ByRoleName(ctx, tc.SubjectID, tc.Roles, scope, ActionRead, obj.RBACObject())
err := auth.ByRoleName(ctx, tc.SubjectID, tc.Roles, scope, []string{}, ActionRead, obj.RBACObject())
obj.Allowed = err == nil
if err == nil {
allowedCount++
@ -178,7 +180,7 @@ func TestFilter(t *testing.T) {
}
// Run by filter
list, err := Filter(ctx, auth, tc.SubjectID, tc.Roles, scope, tc.Action, localObjects)
list, err := Filter(ctx, auth, tc.SubjectID, tc.Roles, scope, []string{}, tc.Action, localObjects)
require.NoError(t, err)
require.Equal(t, allowedCount, len(list), "expected number of allowed")
for _, obj := range list {
@ -193,15 +195,82 @@ func TestAuthorizeDomain(t *testing.T) {
t.Parallel()
defOrg := uuid.New()
unuseID := uuid.New()
allUsersGroup := "Everyone"
user := subject{
UserID: "me",
Scope: must(ScopeRole(ScopeAll)),
Groups: []string{allUsersGroup},
Roles: []Role{
must(RoleByName(RoleMember())),
must(RoleByName(RoleOrgMember(defOrg))),
},
}
testAuthorize(t, "UserACLList", user, []authTestCase{
{
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(unuseID).WithACLUserList(map[string][]Action{
user.UserID: allActions(),
}),
actions: allActions(),
allow: true,
},
{
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(unuseID).WithACLUserList(map[string][]Action{
user.UserID: {WildcardSymbol},
}),
actions: allActions(),
allow: true,
},
{
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(unuseID).WithACLUserList(map[string][]Action{
user.UserID: {ActionRead, ActionUpdate},
}),
actions: []Action{ActionCreate, ActionDelete},
allow: false,
},
{
// By default users cannot update templates
resource: ResourceTemplate.InOrg(defOrg).WithACLUserList(map[string][]Action{
user.UserID: {ActionUpdate},
}),
actions: []Action{ActionUpdate},
allow: true,
},
})
testAuthorize(t, "GroupACLList", user, []authTestCase{
{
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(defOrg).WithGroupACL(map[string][]Action{
allUsersGroup: allActions(),
}),
actions: allActions(),
allow: true,
},
{
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(defOrg).WithGroupACL(map[string][]Action{
allUsersGroup: {WildcardSymbol},
}),
actions: allActions(),
allow: true,
},
{
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(defOrg).WithGroupACL(map[string][]Action{
allUsersGroup: {ActionRead, ActionUpdate},
}),
actions: []Action{ActionCreate, ActionDelete},
allow: false,
},
{
// By default users cannot update templates
resource: ResourceTemplate.InOrg(defOrg).WithGroupACL(map[string][]Action{
allUsersGroup: {ActionUpdate},
}),
actions: []Action{ActionUpdate},
allow: true,
},
})
testAuthorize(t, "Member", user, []authTestCase{
// Org + me
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.UserID), actions: allActions(), allow: true},
@ -743,9 +812,6 @@ func testAuthorize(t *testing.T, name string, subject subject, sets ...[]authTes
for _, cases := range sets {
for i, c := range cases {
c := c
if c.resource.Type != "application_connect" {
continue
}
caseName := fmt.Sprintf("%s/%d", name, i)
t.Run(caseName, func(t *testing.T) {
t.Parallel()
@ -753,23 +819,21 @@ func testAuthorize(t *testing.T, name string, subject subject, sets ...[]authTes
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
t.Cleanup(cancel)
authError := authorizer.Authorize(ctx, subject.UserID, subject.Roles, subject.Scope, a, c.resource)
authError := authorizer.Authorize(ctx, subject.UserID, subject.Roles, subject.Scope, subject.Groups, a, c.resource)
d, _ := json.Marshal(map[string]interface{}{
"subject": subject,
"object": c.resource,
"action": a,
})
// Logging only
t.Logf("input: %s", string(d))
if authError != nil {
var uerr *UnauthorizedError
xerrors.As(authError, &uerr)
d, _ := json.Marshal(uerr.Input())
t.Logf("input: %s", string(d))
t.Logf("internal error: %+v", uerr.Internal().Error())
t.Logf("output: %+v", uerr.Output())
} else {
d, _ := json.Marshal(map[string]interface{}{
"subject": subject,
"object": c.resource,
"action": a,
})
t.Log(string(d))
}
if c.allow {
@ -778,19 +842,17 @@ func testAuthorize(t *testing.T, name string, subject subject, sets ...[]authTes
assert.Error(t, authError, "expected unauthorized")
}
partialAuthz, err := authorizer.Prepare(ctx, subject.UserID, subject.Roles, subject.Scope, a, c.resource.Type)
partialAuthz, err := authorizer.Prepare(ctx, subject.UserID, subject.Roles, subject.Scope, subject.Groups, a, c.resource.Type)
require.NoError(t, err, "make prepared authorizer")
// Ensure the partial can compile to a SQL clause.
// This does not guarantee that the clause is valid SQL.
_, err = Compile(partialAuthz.partialQueries)
_, err = Compile(partialAuthz)
require.NoError(t, err, "compile prepared authorizer")
// Also check the rego policy can form a valid partial query result.
// This ensures we can convert the queries into SQL WHERE clauses in the future.
// If this function returns 'Support' sections, then we cannot convert the query into SQL.
d, _ := json.Marshal(partialAuthz.input)
t.Logf("input: %s", string(d))
for _, q := range partialAuthz.partialQueries.Queries {
t.Logf("query: %+v", q.String())
}

View File

@ -63,8 +63,8 @@ var (
return Role{
Name: owner,
DisplayName: "Owner",
Site: permissions(map[Object][]Action{
ResourceWildcard: {WildcardSymbol},
Site: permissions(map[string][]Action{
ResourceWildcard.Type: {WildcardSymbol},
}),
}
},
@ -74,15 +74,15 @@ var (
return Role{
Name: member,
DisplayName: "",
Site: permissions(map[Object][]Action{
Site: permissions(map[string][]Action{
// All users can read all other users and know they exist.
ResourceUser: {ActionRead},
ResourceRoleAssignment: {ActionRead},
ResourceUser.Type: {ActionRead},
ResourceRoleAssignment.Type: {ActionRead},
// All users can see the provisioner daemons.
ResourceProvisionerDaemon: {ActionRead},
ResourceProvisionerDaemon.Type: {ActionRead},
}),
User: permissions(map[Object][]Action{
ResourceWildcard: {WildcardSymbol},
User: permissions(map[string][]Action{
ResourceWildcard.Type: {WildcardSymbol},
}),
}
},
@ -94,11 +94,11 @@ var (
return Role{
Name: auditor,
DisplayName: "Auditor",
Site: permissions(map[Object][]Action{
Site: permissions(map[string][]Action{
// Should be able to read all template details, even in orgs they
// are not in.
ResourceTemplate: {ActionRead},
ResourceAuditLog: {ActionRead},
ResourceTemplate.Type: {ActionRead},
ResourceAuditLog.Type: {ActionRead},
}),
}
},
@ -107,13 +107,13 @@ var (
return Role{
Name: templateAdmin,
DisplayName: "Template Admin",
Site: permissions(map[Object][]Action{
ResourceTemplate: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
Site: permissions(map[string][]Action{
ResourceTemplate.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
// CRUD all files, even those they did not upload.
ResourceFile: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
ResourceWorkspace: {ActionRead},
ResourceFile.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
ResourceWorkspace.Type: {ActionRead},
// CRUD to provisioner daemons for now.
ResourceProvisionerDaemon: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
ResourceProvisionerDaemon.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
}),
}
},
@ -122,11 +122,11 @@ var (
return Role{
Name: userAdmin,
DisplayName: "User Admin",
Site: permissions(map[Object][]Action{
ResourceRoleAssignment: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
ResourceUser: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
Site: permissions(map[string][]Action{
ResourceRoleAssignment.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
ResourceUser.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
// Full perms to manage org members
ResourceOrganizationMember: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
ResourceOrganizationMember.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
}),
}
},
@ -168,13 +168,12 @@ var (
Action: ActionRead,
},
{
// All org members can read templates in the org
ResourceType: ResourceTemplate.Type,
// Can read available roles.
ResourceType: ResourceOrgRoleAssignment.Type,
Action: ActionRead,
},
{
// Can read available roles.
ResourceType: ResourceOrgRoleAssignment.Type,
ResourceType: ResourceGroup.Type,
Action: ActionRead,
},
},
@ -390,14 +389,14 @@ func roleSplit(role string) (name string, orgID string, err error) {
// permissions is just a helper function to make building roles that list out resources
// and actions a bit easier.
func permissions(perms map[Object][]Action) []Permission {
func permissions(perms map[string][]Action) []Permission {
list := make([]Permission, 0, len(perms))
for k, actions := range perms {
for _, act := range actions {
act := act
list = append(list, Permission{
Negate: false,
ResourceType: k.Type,
ResourceType: k,
Action: act,
})
}

View File

@ -32,6 +32,7 @@ func BenchmarkRBACFilter(b *testing.B) {
benchCases := []struct {
Name string
Roles []string
Groups []string
UserID uuid.UUID
Scope rbac.Scope
}{
@ -87,7 +88,7 @@ func BenchmarkRBACFilter(b *testing.B) {
b.Run(c.Name, func(b *testing.B) {
objects := benchmarkSetup(orgs, users, b.N)
b.ResetTimer()
allowed, err := rbac.Filter(context.Background(), authorizer, c.UserID.String(), c.Roles, c.Scope, rbac.ActionRead, objects)
allowed, err := rbac.Filter(context.Background(), authorizer, c.UserID.String(), c.Roles, c.Scope, c.Groups, rbac.ActionRead, objects)
require.NoError(b, err)
var _ = allowed
})
@ -96,11 +97,17 @@ func BenchmarkRBACFilter(b *testing.B) {
func benchmarkSetup(orgs []uuid.UUID, users []uuid.UUID, size int) []rbac.Object {
// Create a "random" but deterministic set of objects.
aclList := map[string][]rbac.Action{
uuid.NewString(): {rbac.ActionRead, rbac.ActionUpdate},
uuid.NewString(): {rbac.ActionCreate},
}
objectList := make([]rbac.Object, size)
for i := range objectList {
objectList[i] = rbac.ResourceWorkspace.
InOrg(orgs[i%len(orgs)]).
WithOwner(users[i%len(users)].String())
WithOwner(users[i%len(users)].String()).
WithACLUserList(aclList).
WithGroupACL(aclList)
}
return objectList
@ -111,6 +118,7 @@ type authSubject struct {
Name string
UserID string
Roles []string
Groups []string
}
func TestRolePermissions(t *testing.T) {
@ -227,8 +235,8 @@ func TestRolePermissions(t *testing.T) {
Actions: []rbac.Action{rbac.ActionRead},
Resource: rbac.ResourceTemplate.InOrg(orgID),
AuthorizeMap: map[bool][]authSubject{
true: {owner, orgMemberMe, orgAdmin, templateAdmin},
false: {memberMe, otherOrgAdmin, otherOrgMember, userAdmin},
true: {owner, orgAdmin, templateAdmin},
false: {memberMe, otherOrgAdmin, otherOrgMember, userAdmin, orgMemberMe},
},
},
{
@ -242,7 +250,7 @@ func TestRolePermissions(t *testing.T) {
},
{
Name: "MyFile",
Actions: []rbac.Action{rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
Resource: rbac.ResourceFile.WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]authSubject{
true: {owner, memberMe, orgMemberMe, templateAdmin},
@ -348,6 +356,19 @@ func TestRolePermissions(t *testing.T) {
false: {memberMe, otherOrgAdmin, otherOrgMember, templateAdmin},
},
},
{
Name: "AllUsersGroupACL",
Actions: []rbac.Action{rbac.ActionRead},
Resource: rbac.ResourceTemplate.InOrg(orgID).WithGroupACL(
map[string][]rbac.Action{
orgID.String(): {rbac.ActionRead},
}),
AuthorizeMap: map[bool][]authSubject{
true: {owner, orgAdmin, orgMemberMe, templateAdmin},
false: {memberMe, otherOrgAdmin, otherOrgMember, userAdmin},
},
},
}
for _, c := range testCases {
@ -365,7 +386,7 @@ func TestRolePermissions(t *testing.T) {
delete(remainingSubjs, subj.Name)
msg := fmt.Sprintf("%s as %q doing %q on %q", c.Name, subj.Name, action, c.Resource.Type)
// TODO: scopey
err := auth.ByRoleName(context.Background(), subj.UserID, subj.Roles, rbac.ScopeAll, action, c.Resource)
err := auth.ByRoleName(context.Background(), subj.UserID, subj.Roles, rbac.ScopeAll, subj.Groups, action, c.Resource)
if result {
assert.NoError(t, err, fmt.Sprintf("Should pass: %s", msg))
} else {

View File

@ -54,6 +54,14 @@ var (
Type: "template",
}
// ResourceGroup CRUD. Org admins only.
// create/delete = Make or delete a new group.
// update = Update the name or members of a group.
// read = Read groups and their members.
ResourceGroup = Object{
Type: "group",
}
ResourceFile = Object{
Type: "file",
}
@ -152,7 +160,9 @@ type Object struct {
// Type is "workspace", "project", "app", etc
Type string `json:"type"`
// TODO: SharedUsers?
ACLUserList map[string][]Action ` json:"acl_user_list"`
ACLGroupList map[string][]Action ` json:"acl_group_list"`
}
func (z Object) RBACObject() Object {
@ -162,26 +172,53 @@ func (z Object) RBACObject() Object {
// All returns an object matching all resources of the same type.
func (z Object) All() Object {
return Object{
Owner: "",
OrgID: "",
Type: z.Type,
Owner: "",
OrgID: "",
Type: z.Type,
ACLUserList: map[string][]Action{},
ACLGroupList: map[string][]Action{},
}
}
// InOrg adds an org OwnerID to the resource
func (z Object) InOrg(orgID uuid.UUID) Object {
return Object{
Owner: z.Owner,
OrgID: orgID.String(),
Type: z.Type,
Owner: z.Owner,
OrgID: orgID.String(),
Type: z.Type,
ACLUserList: z.ACLUserList,
ACLGroupList: z.ACLGroupList,
}
}
// WithOwner adds an OwnerID to the resource
func (z Object) WithOwner(ownerID string) Object {
return Object{
Owner: ownerID,
OrgID: z.OrgID,
Type: z.Type,
Owner: ownerID,
OrgID: z.OrgID,
Type: z.Type,
ACLUserList: z.ACLUserList,
ACLGroupList: z.ACLGroupList,
}
}
// WithACLUserList adds an ACL list to a given object
func (z Object) WithACLUserList(acl map[string][]Action) Object {
return Object{
Owner: z.Owner,
OrgID: z.OrgID,
Type: z.Type,
ACLUserList: acl,
ACLGroupList: z.ACLGroupList,
}
}
func (z Object) WithGroupACL(groups map[string][]Action) Object {
return Object{
Owner: z.Owner,
OrgID: z.OrgID,
Type: z.Type,
ACLUserList: z.ACLUserList,
ACLGroupList: groups,
}
}

View File

@ -29,7 +29,7 @@ type PartialAuthorizer struct {
var _ PreparedAuthorized = (*PartialAuthorizer)(nil)
func (pa *PartialAuthorizer) Compile() (AuthorizeFilter, error) {
filter, err := Compile(pa.partialQueries)
filter, err := Compile(pa)
if err != nil {
return nil, xerrors.Errorf("compile: %w", err)
}
@ -99,7 +99,7 @@ EachQueryLoop:
// inspect this any further. But just in case, we will verify each expression
// did resolve to 'true'. This is purely defensive programming.
for _, exp := range results[0].Expressions {
if exp.String() != "true" {
if v, ok := exp.Value.(bool); !ok || !v {
continue EachQueryLoop
}
}
@ -110,15 +110,16 @@ EachQueryLoop:
return ForbiddenWithInternal(xerrors.Errorf("policy disallows request"), pa.input, nil)
}
func newPartialAuthorizer(ctx context.Context, subjectID string, roles []Role, scope Role, action Action, objectType string) (*PartialAuthorizer, error) {
func newPartialAuthorizer(ctx context.Context, subjectID string, roles []Role, scope Role, groups []string, action Action, objectType string) (*PartialAuthorizer, error) {
ctx, span := tracing.StartSpan(ctx)
defer span.End()
input := map[string]interface{}{
"subject": authSubject{
ID: subjectID,
Roles: roles,
Scope: scope,
ID: subjectID,
Roles: roles,
Scope: scope,
Groups: groups,
},
"object": map[string]string{
"type": objectType,
@ -129,11 +130,13 @@ func newPartialAuthorizer(ctx context.Context, subjectID string, roles []Role, s
// Run the rego policy with a few unknown fields. This should simplify our
// policy to a set of queries.
partialQueries, err := rego.New(
rego.Query("data.authz.role_allow = true data.authz.scope_allow = true"),
rego.Query("data.authz.allow = true"),
rego.Module("policy.rego", policy),
rego.Unknowns([]string{
"input.object.owner",
"input.object.org_owner",
"input.object.acl_user_list",
"input.object.acl_group_list",
}),
rego.Input(input),
).Partial(ctx)

View File

@ -2,8 +2,8 @@ package authz
import future.keywords
# A great playground: https://play.openpolicyagent.org/
# Helpful cli commands to debug.
# opa eval --format=pretty 'data.authz.role_allow data.authz.scope_allow' -d policy.rego -i input.json
# opa eval --partial --format=pretty 'data.authz.role_allow = true data.authz.scope_allow = true' -d policy.rego --unknowns input.object.owner --unknowns input.object.org_owner -i input.json
# opa eval --format=pretty 'data.authz.allow' -d policy.rego -i input.json
# opa eval --partial --format=pretty 'data.authz.allow' -d policy.rego --unknowns input.object.owner --unknowns input.object.org_owner --unknowns input.object.acl_user_list --unknowns input.object.acl_group_list -i input.json
#
# This policy is specifically constructed to compress to a set of queries if the
@ -119,9 +119,13 @@ org_mem := true {
input.object.org_owner in org_members
}
org_ok {
org_mem
}
# If the object has no organization, then the user is also considered part of
# the non-existent org.
org_mem := true {
org_ok {
input.object.org_owner == ""
}
@ -156,7 +160,6 @@ user_allow(roles) := num {
# Allow query:
# data.authz.role_allow = true data.authz.scope_allow = true
default role_allow = false
role_allow {
site = 1
}
@ -171,12 +174,10 @@ role_allow {
not org = -1
# If we are not a member of an org, and the object has an org, then we are
# not authorized. This is an "implied -1" for not being in the org.
org_mem
org_ok
user = 1
}
default scope_allow = false
scope_allow {
scope_site = 1
}
@ -191,6 +192,48 @@ scope_allow {
not scope_org = -1
# If we are not a member of an org, and the object has an org, then we are
# not authorized. This is an "implied -1" for not being in the org.
org_mem
org_ok
scope_user = 1
}
# ACL for users
acl_allow {
# Should you have to be a member of the org too?
perms := input.object.acl_user_list[input.subject.id]
# Either the input action or wildcard
[input.action, "*"][_] in perms
}
# ACL for groups
acl_allow {
# If there is no organization owner, the object cannot be owned by an
# org_scoped team.
org_mem
group := input.subject.groups[_]
perms := input.object.acl_group_list[group]
# Either the input action or wildcard
[input.action, "*"][_] in perms
}
# ACL for 'all_users' special group
acl_allow {
org_mem
perms := input.object.acl_group_list[input.object.org_owner]
[input.action, "*"][_] in perms
}
###############
# Final Allow
# The role or the ACL must allow the action. Scopes can be used to limit,
# so scope_allow must always be true.
allow {
role_allow
scope_allow
}
# ACL list must also have the scope_allow to pass
allow {
acl_allow
scope_allow
}

View File

@ -1,13 +1,13 @@
package rbac
import (
"context"
"fmt"
"regexp"
"strconv"
"strings"
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/rego"
"golang.org/x/xerrors"
)
@ -16,6 +16,9 @@ type TermType string
const (
VarTypeJsonbTextArray TermType = "jsonb-text-array"
VarTypeText TermType = "text"
VarTypeBoolean TermType = "boolean"
// VarTypeSkip means this variable does not exist to use.
VarTypeSkip TermType = "skip"
)
type SQLColumn struct {
@ -79,19 +82,54 @@ func DefaultConfig() SQLConfig {
}
}
func NoACLConfig() SQLConfig {
return SQLConfig{
Variables: []SQLColumn{
{
RegoMatch: regexp.MustCompile(`^input\.object\.acl_group_list\.?(.*)$`),
ColumnSelect: "",
Type: VarTypeSkip,
},
{
RegoMatch: regexp.MustCompile(`^input\.object\.acl_user_list\.?(.*)$`),
ColumnSelect: "",
Type: VarTypeSkip,
},
{
RegoMatch: regexp.MustCompile(`^input\.object\.org_owner$`),
ColumnSelect: "organization_id :: text",
Type: VarTypeText,
},
{
RegoMatch: regexp.MustCompile(`^input\.object\.owner$`),
ColumnSelect: "owner_id :: text",
Type: VarTypeText,
},
},
}
}
type AuthorizeFilter interface {
// RegoString is used in debugging to see the original rego expression.
RegoString() string
// SQLString returns the SQL expression that can be used in a WHERE clause.
SQLString(cfg SQLConfig) string
Expression
// Eval is required for the fake in memory database to work. The in memory
// database can use this function to filter the results.
Eval(object Object) bool
}
// expressionTop handles Eval(object Object) for in memory expressions
type expressionTop struct {
Expression
Auth *PartialAuthorizer
}
func (e expressionTop) Eval(object Object) bool {
return e.Auth.Authorize(context.Background(), object) == nil
}
// Compile will convert a rego query AST into our custom types. The output is
// an AST that can be used to generate SQL.
func Compile(partialQueries *rego.PartialQueries) (Expression, error) {
func Compile(pa *PartialAuthorizer) (AuthorizeFilter, error) {
partialQueries := pa.partialQueries
if len(partialQueries.Support) > 0 {
return nil, xerrors.Errorf("cannot convert support rules, expect 0 found %d", len(partialQueries.Support))
}
@ -128,11 +166,15 @@ func Compile(partialQueries *rego.PartialQueries) (Expression, error) {
}
builder.WriteString(partialQueries.Queries[i].String())
}
return expOr{
exp := expOr{
base: base{
Rego: builder.String(),
},
Expressions: result,
}
return expressionTop{
Expression: &exp,
Auth: pa,
}, nil
}
@ -218,21 +260,22 @@ func processTerms(expected int, terms []*ast.Term) ([]Term, error) {
}
func processTerm(term *ast.Term) (Term, error) {
base := base{Rego: term.String()}
termBase := base{Rego: term.String()}
switch v := term.Value.(type) {
case ast.Boolean:
return &termBoolean{
base: base,
base: termBase,
Value: bool(v),
}, nil
case ast.Ref:
obj := &termObject{
base: base,
Variables: []termVariable{},
base: termBase,
Path: []Term{},
}
var idx int
// A ref is a set of terms. If the first term is a var, then the
// following terms are the path to the value.
isRef := true
var builder strings.Builder
for _, term := range v {
if idx == 0 {
@ -241,15 +284,37 @@ func processTerm(term *ast.Term) (Term, error) {
}
}
if _, ok := term.Value.(ast.Ref); ok {
_, newRef := term.Value.(ast.Ref)
if newRef ||
// This is an unfortunate hack. To fix this, we need to rewrite
// our SQL config as a path ([]string{"input", "object", "acl_group"}).
// In the rego AST, there is no difference between selecting
// a field by a variable, and selecting a field by a literal (string).
// This was a misunderstanding.
// Example (these are equivalent by AST):
// input.object.acl_group_list['4d30d4a8-b87d-45ac-b0d4-51b2e68e7e75']
// input.object.acl_group_list.organization_id
//
// This is not equivalent
// input.object.acl_group_list[input.object.organization_id]
//
// If this becomes even more hairy, we should fix the sql config.
builder.String() == "input.object.acl_group_list" ||
builder.String() == "input.object.acl_user_list" {
if !newRef {
isRef = false
}
// New obj
obj.Variables = append(obj.Variables, termVariable{
base: base,
obj.Path = append(obj.Path, termVariable{
base: base{
Rego: builder.String(),
},
Name: builder.String(),
})
builder.Reset()
idx = 0
}
if builder.Len() != 0 {
builder.WriteString(".")
}
@ -257,20 +322,31 @@ func processTerm(term *ast.Term) (Term, error) {
idx++
}
obj.Variables = append(obj.Variables, termVariable{
base: base,
Name: builder.String(),
})
if isRef {
obj.Path = append(obj.Path, termVariable{
base: base{
Rego: builder.String(),
},
Name: builder.String(),
})
} else {
obj.Path = append(obj.Path, termString{
base: base{
Rego: fmt.Sprintf("%q", builder.String()),
},
Value: builder.String(),
})
}
return obj, nil
case ast.Var:
return &termVariable{
Name: trimQuotes(v.String()),
base: base,
base: termBase,
}, nil
case ast.String:
return &termString{
Value: trimQuotes(v.String()),
base: base,
base: termBase,
}, nil
case ast.Set:
slice := v.Slice()
@ -285,7 +361,7 @@ func processTerm(term *ast.Term) (Term, error) {
return &termSet{
Value: set,
base: base,
base: termBase,
}, nil
default:
return nil, xerrors.Errorf("invalid term: %T not supported, %q", v, term.String())
@ -306,7 +382,10 @@ func (b base) RegoString() string {
//
// Eg: neq(input.object.org_owner, "") AND input.object.org_owner == "foo"
type Expression interface {
AuthorizeFilter
// RegoString is used in debugging to see the original rego expression.
RegoString() string
// SQLString returns the SQL expression that can be used in a WHERE clause.
SQLString(cfg SQLConfig) string
}
type expAnd struct {
@ -326,15 +405,6 @@ func (t expAnd) SQLString(cfg SQLConfig) string {
return "(" + strings.Join(exprs, " AND ") + ")"
}
func (t expAnd) Eval(object Object) bool {
for _, expr := range t.Expressions {
if !expr.Eval(object) {
return false
}
}
return true
}
type expOr struct {
base
Expressions []Expression
@ -352,15 +422,6 @@ func (t expOr) SQLString(cfg SQLConfig) string {
return "(" + strings.Join(exprs, " OR ") + ")"
}
func (t expOr) Eval(object Object) bool {
for _, expr := range t.Expressions {
if expr.Eval(object) {
return true
}
}
return false
}
// Operator joins terms together to form an expression.
// Operators are also expressions.
//
@ -384,14 +445,6 @@ func (t opEqual) SQLString(cfg SQLConfig) string {
return fmt.Sprintf("%s %s %s", t.Terms[0].SQLString(cfg), op, t.Terms[1].SQLString(cfg))
}
func (t opEqual) Eval(object Object) bool {
a, b := t.Terms[0].EvalTerm(object), t.Terms[1].EvalTerm(object)
if t.Not {
return a != b
}
return a == b
}
// opInternalMember2 is checking if the first term is a member of the second term.
// The second term is a set or list.
type opInternalMember2 struct {
@ -400,20 +453,6 @@ type opInternalMember2 struct {
Haystack Term
}
func (t opInternalMember2) Eval(object Object) bool {
a, b := t.Needle.EvalTerm(object), t.Haystack.EvalTerm(object)
bset, ok := b.([]interface{})
if !ok {
return false
}
for _, elem := range bset {
if a == elem {
return true
}
}
return false
}
func (t opInternalMember2) SQLString(cfg SQLConfig) string {
if haystack, ok := t.Haystack.(*termObject); ok {
// This is a special case where the haystack is a jsonb array.
@ -425,9 +464,14 @@ func (t opInternalMember2) SQLString(cfg SQLConfig) string {
// having to add more "if" branches here.
// But until we need more cases, our basic type system is ok, and
// this is the only case we need to handle.
if haystack.SQLType(cfg) == VarTypeJsonbTextArray {
sqlType := haystack.SQLType(cfg)
if sqlType == VarTypeJsonbTextArray {
return fmt.Sprintf("%s ? %s", haystack.SQLString(cfg), t.Needle.SQLString(cfg))
}
if sqlType == VarTypeSkip {
return "true"
}
}
return fmt.Sprintf("%s = ANY(%s)", t.Needle.SQLString(cfg), t.Haystack.SQLString(cfg))
@ -440,9 +484,7 @@ func (t opInternalMember2) SQLString(cfg SQLConfig) string {
type Term interface {
RegoString() string
SQLString(cfg SQLConfig) string
// Eval will evaluate the term
// Terms can eval to any type. The operator/expression will type check.
EvalTerm(object Object) interface{}
SQLType(cfg SQLConfig) TermType
}
type termString struct {
@ -450,10 +492,6 @@ type termString struct {
Value string
}
func (t termString) EvalTerm(_ Object) interface{} {
return t.Value
}
func (t termString) SQLString(_ SQLConfig) string {
return "'" + t.Value + "'"
}
@ -471,14 +509,7 @@ func (termString) SQLType(_ SQLConfig) TermType {
// term type.
type termObject struct {
base
Variables []termVariable
}
func (t termObject) EvalTerm(obj Object) interface{} {
if len(t.Variables) == 0 {
return t.Variables[0].EvalTerm(obj)
}
panic("no nested structures are supported yet")
Path []Term
}
func (t termObject) SQLType(cfg SQLConfig) TermType {
@ -486,30 +517,30 @@ func (t termObject) SQLType(cfg SQLConfig) TermType {
// is the resulting type. This is correct for our use case.
// Solving this more generally requires a full type system, which is
// excessive for our mostly static policy.
return t.Variables[0].SQLType(cfg)
return t.Path[0].SQLType(cfg)
}
func (t termObject) SQLString(cfg SQLConfig) string {
if len(t.Variables) == 1 {
return t.Variables[0].SQLString(cfg)
if len(t.Path) == 1 {
return t.Path[0].SQLString(cfg)
}
// Combine the last 2 variables into 1 variable.
end := t.Variables[len(t.Variables)-1]
before := t.Variables[len(t.Variables)-2]
end := t.Path[len(t.Path)-1]
before := t.Path[len(t.Path)-2]
// Recursively solve the SQLString by removing the last nested reference.
// This continues until we have a single variable.
return termObject{
base: t.base,
Variables: append(
t.Variables[:len(t.Variables)-2],
Path: append(
t.Path[:len(t.Path)-2],
termVariable{
base: base{
Rego: before.base.Rego + "[" + end.base.Rego + "]",
Rego: before.RegoString() + "[" + end.RegoString() + "]",
},
// Convert the end to SQL string. We evaluate each term
// one at a time.
Name: before.Name + "." + end.SQLString(cfg),
Name: before.RegoString() + "." + end.SQLString(cfg),
},
),
}.SQLString(cfg)
@ -520,19 +551,6 @@ type termVariable struct {
Name string
}
func (t termVariable) EvalTerm(obj Object) interface{} {
switch t.Name {
case "input.object.org_owner":
return obj.OrgID
case "input.object.owner":
return obj.Owner
case "input.object.type":
return obj.Type
default:
return fmt.Sprintf("'Unknown variable %s'", t.Name)
}
}
func (t termVariable) SQLType(cfg SQLConfig) TermType {
if col := t.ColumnConfig(cfg); col != nil {
return col.Type
@ -576,13 +594,15 @@ type termSet struct {
Value []Term
}
func (t termSet) EvalTerm(obj Object) interface{} {
set := make([]interface{}, 0, len(t.Value))
for _, term := range t.Value {
set = append(set, term.EvalTerm(obj))
func (t termSet) SQLType(cfg SQLConfig) TermType {
if len(t.Value) == 0 {
return VarTypeText
}
return set
// Without a full type system, let's just assume the type of the first var
// is the resulting type. This is correct for our use case.
// Solving this more generally requires a full type system, which is
// excessive for our mostly static policy.
return t.Value[0].SQLType(cfg)
}
func (t termSet) SQLString(cfg SQLConfig) string {
@ -599,11 +619,11 @@ type termBoolean struct {
Value bool
}
func (t termBoolean) Eval(_ Object) bool {
return t.Value
func (termBoolean) SQLType(SQLConfig) TermType {
return VarTypeBoolean
}
func (t termBoolean) EvalTerm(_ Object) interface{} {
func (t termBoolean) Eval(_ Object) bool {
return t.Value
}

View File

@ -1,6 +1,7 @@
package rbac
import (
"context"
"testing"
"github.com/open-policy-agent/opa/ast"
@ -11,17 +12,10 @@ import (
func TestCompileQuery(t *testing.T) {
t.Parallel()
opts := ast.ParserOptions{
AllFutureKeywords: true,
}
t.Run("EmptyQuery", func(t *testing.T) {
t.Parallel()
expression, err := Compile(&rego.PartialQueries{
Queries: []ast.Body{
must(ast.ParseBody("")),
},
Support: []*ast.Module{},
})
expression, err := Compile(partialQueries(t, ""))
require.NoError(t, err, "compile empty")
require.Equal(t, "true", expression.RegoString(), "empty query is rego 'true'")
@ -30,12 +24,7 @@ func TestCompileQuery(t *testing.T) {
t.Run("TrueQuery", func(t *testing.T) {
t.Parallel()
expression, err := Compile(&rego.PartialQueries{
Queries: []ast.Body{
must(ast.ParseBody("true")),
},
Support: []*ast.Module{},
})
expression, err := Compile(partialQueries(t, "true"))
require.NoError(t, err, "compile")
require.Equal(t, "true", expression.RegoString(), "true query is rego 'true'")
@ -44,49 +33,118 @@ func TestCompileQuery(t *testing.T) {
t.Run("ACLIn", func(t *testing.T) {
t.Parallel()
expression, err := Compile(&rego.PartialQueries{
Queries: []ast.Body{
ast.MustParseBodyWithOpts(`"*" in input.object.acl_group_list.allUsers`, opts),
},
Support: []*ast.Module{},
})
expression, err := Compile(partialQueries(t, `"*" in input.object.acl_group_list.allUsers`))
require.NoError(t, err, "compile")
require.Equal(t, `internal.member_2("*", input.object.acl_group_list.allUsers)`, expression.RegoString(), "convert to internal_member")
require.Equal(t, `group_acl->allUsers ? '*'`, expression.SQLString(DefaultConfig()), "jsonb in")
require.Equal(t, `group_acl->'allUsers' ? '*'`, expression.SQLString(DefaultConfig()), "jsonb in")
})
t.Run("Complex", func(t *testing.T) {
t.Parallel()
expression, err := Compile(&rego.PartialQueries{
Queries: []ast.Body{
ast.MustParseBodyWithOpts(`input.object.org_owner != ""`, opts),
ast.MustParseBodyWithOpts(`input.object.org_owner in {"a", "b", "c"}`, opts),
ast.MustParseBodyWithOpts(`input.object.org_owner != ""`, opts),
ast.MustParseBodyWithOpts(`"read" in input.object.acl_group_list.allUsers`, opts),
ast.MustParseBodyWithOpts(`"read" in input.object.acl_user_list.me`, opts),
},
Support: []*ast.Module{},
})
expression, err := Compile(partialQueries(t,
`input.object.org_owner != ""`,
`input.object.org_owner in {"a", "b", "c"}`,
`input.object.org_owner != ""`,
`"read" in input.object.acl_group_list.allUsers`,
`"read" in input.object.acl_user_list.me`,
))
require.NoError(t, err, "compile")
require.Equal(t, `(organization_id :: text != '' OR `+
`organization_id :: text = ANY(ARRAY ['a','b','c']) OR `+
`organization_id :: text != '' OR `+
`group_acl->allUsers ? 'read' OR `+
`user_acl->me ? 'read')`,
`group_acl->'allUsers' ? 'read' OR `+
`user_acl->'me' ? 'read')`,
expression.SQLString(DefaultConfig()), "complex")
})
t.Run("SetDereference", func(t *testing.T) {
t.Parallel()
expression, err := Compile(&rego.PartialQueries{
Queries: []ast.Body{
ast.MustParseBodyWithOpts(`"*" in input.object.acl_group_list[input.object.org_owner]`, opts),
},
Support: []*ast.Module{},
})
expression, err := Compile(partialQueries(t,
`"*" in input.object.acl_group_list[input.object.org_owner]`,
))
require.NoError(t, err, "compile")
require.Equal(t, `group_acl->organization_id :: text ? '*'`,
expression.SQLString(DefaultConfig()), "set dereference")
})
t.Run("JsonbLiteralDereference", func(t *testing.T) {
t.Parallel()
expression, err := Compile(partialQueries(t,
`"*" in input.object.acl_group_list["4d30d4a8-b87d-45ac-b0d4-51b2e68e7e75"]`,
))
require.NoError(t, err, "compile")
require.Equal(t, `group_acl->'4d30d4a8-b87d-45ac-b0d4-51b2e68e7e75' ? '*'`,
expression.SQLString(DefaultConfig()), "literal dereference")
})
t.Run("NoACLColumns", func(t *testing.T) {
t.Parallel()
expression, err := Compile(partialQueries(t,
`"*" in input.object.acl_group_list["4d30d4a8-b87d-45ac-b0d4-51b2e68e7e75"]`,
))
require.NoError(t, err, "compile")
require.Equal(t, `true`,
expression.SQLString(NoACLConfig()), "literal dereference")
})
}
func TestEvalQuery(t *testing.T) {
t.Parallel()
t.Run("GroupACL", func(t *testing.T) {
t.Parallel()
expression, err := Compile(partialQueries(t,
`"read" in input.object.acl_group_list["4d30d4a8-b87d-45ac-b0d4-51b2e68e7e75"]`,
))
require.NoError(t, err, "compile")
result := expression.Eval(Object{
Owner: "not-me",
OrgID: "random",
Type: "workspace",
ACLUserList: map[string][]Action{},
ACLGroupList: map[string][]Action{
"4d30d4a8-b87d-45ac-b0d4-51b2e68e7e75": {"read"},
},
})
require.True(t, result, "eval")
})
}
func partialQueries(t *testing.T, queries ...string) *PartialAuthorizer {
opts := ast.ParserOptions{
AllFutureKeywords: true,
}
astQueries := make([]ast.Body, 0, len(queries))
for _, q := range queries {
astQueries = append(astQueries, ast.MustParseBodyWithOpts(q, opts))
}
prepareQueries := make([]rego.PreparedEvalQuery, 0, len(queries))
for _, q := range astQueries {
var prepped rego.PreparedEvalQuery
var err error
if q.String() == "" {
prepped, err = rego.New(
rego.Query("true"),
).PrepareForEval(context.Background())
} else {
prepped, err = rego.New(
rego.ParsedQuery(q),
).PrepareForEval(context.Background())
}
require.NoError(t, err, "prepare query")
prepareQueries = append(prepareQueries, prepped)
}
return &PartialAuthorizer{
partialQueries: &rego.PartialQueries{
Queries: astQueries,
Support: []*ast.Module{},
},
preparedQueries: prepareQueries,
input: nil,
alwaysTrue: false,
}
}

View File

@ -19,8 +19,8 @@ var builtinScopes map[Scope]Role = map[Scope]Role{
ScopeAll: {
Name: fmt.Sprintf("Scope_%s", ScopeAll),
DisplayName: "All operations",
Site: permissions(map[Object][]Action{
ResourceWildcard: {WildcardSymbol},
Site: permissions(map[string][]Action{
ResourceWildcard.Type: {WildcardSymbol},
}),
Org: map[string][]Permission{},
User: []Permission{},
@ -29,8 +29,8 @@ var builtinScopes map[Scope]Role = map[Scope]Role{
ScopeApplicationConnect: {
Name: fmt.Sprintf("Scope_%s", ScopeApplicationConnect),
DisplayName: "Ability to connect to applications",
Site: permissions(map[Object][]Action{
ResourceWorkspaceApplicationConnect: {ActionCreate},
Site: permissions(map[string][]Action{
ResourceWorkspaceApplicationConnect.Type: {ActionCreate},
}),
Org: map[string][]Permission{},
User: []Permission{},