chore: push rbac actions to policy package (#13274)

Just moved `rbac.Action` -> `policy.Action`. This is for the stacked PR to not have circular dependencies when doing autogen. Without this, the autogen can produce broken golang code, which prevents the autogen from compiling.

So just avoiding circular dependencies. Doing this in it's own PR to reduce LoC diffs in the primary PR, since this has 0 functional changes.
This commit is contained in:
Steven Masley
2024-05-15 09:46:35 -05:00
committed by GitHub
parent f14927955d
commit cb6b5e8fbd
52 changed files with 971 additions and 925 deletions
+8 -10
View File
@@ -3,12 +3,14 @@ package rbac
import (
"github.com/open-policy-agent/opa/ast"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/rbac/policy"
)
// regoInputValue returns a rego input value for the given subject, action, and
// object. This rego input is already parsed and can be used directly in a
// rego query.
func regoInputValue(subject Subject, action Action, object Object) (ast.Value, error) {
func regoInputValue(subject Subject, action policy.Action, object Object) (ast.Value, error) {
regoSubj, err := subject.regoValue()
if err != nil {
return nil, xerrors.Errorf("subject: %w", err)
@@ -34,7 +36,7 @@ func regoInputValue(subject Subject, action Action, object Object) (ast.Value, e
// regoPartialInputValue is the same as regoInputValue but only includes the
// object type. This is for partial evaluations.
func regoPartialInputValue(subject Subject, action Action, objectType string) (ast.Value, error) {
func regoPartialInputValue(subject Subject, action policy.Action, objectType string) (ast.Value, error) {
regoSubj, err := subject.regoValue()
if err != nil {
return nil, xerrors.Errorf("subject: %w", err)
@@ -103,11 +105,11 @@ func (s Subject) regoValue() (ast.Value, error) {
func (z Object) regoValue() ast.Value {
userACL := ast.NewObject()
for k, v := range z.ACLUserList {
userACL.Insert(ast.StringTerm(k), ast.NewTerm(regoSlice(v)))
userACL.Insert(ast.StringTerm(k), ast.NewTerm(regoSliceString(v...)))
}
grpACL := ast.NewObject()
for k, v := range z.ACLGroupList {
grpACL.Insert(ast.StringTerm(k), ast.NewTerm(regoSlice(v)))
grpACL.Insert(ast.StringTerm(k), ast.NewTerm(regoSliceString(v...)))
}
return ast.NewObject(
[2]*ast.Term{
@@ -200,10 +202,6 @@ func (perm Permission) regoValue() ast.Value {
)
}
func (act Action) regoValue() ast.Value {
return ast.StringTerm(string(act)).Value
}
type regoValue interface {
regoValue() ast.Value
}
@@ -218,10 +216,10 @@ func regoSlice[T regoValue](slice []T) *ast.Array {
return ast.NewArray(terms...)
}
func regoSliceString(slice ...string) *ast.Array {
func regoSliceString[T ~string](slice ...T) *ast.Array {
terms := make([]*ast.Term, len(slice))
for i, v := range slice {
terms[i] = ast.StringTerm(v)
terms[i] = ast.StringTerm(string(v))
}
return ast.NewArray(terms...)
}
+19 -28
View File
@@ -19,30 +19,21 @@ import (
"go.opentelemetry.io/otel/trace"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/coderd/rbac/regosql"
"github.com/coder/coder/v2/coderd/rbac/regosql/sqltypes"
"github.com/coder/coder/v2/coderd/tracing"
"github.com/coder/coder/v2/coderd/util/slice"
)
// Action represents the allowed actions to be done on an object.
type Action string
const (
ActionCreate Action = "create"
ActionRead Action = "read"
ActionUpdate Action = "update"
ActionDelete Action = "delete"
)
// AllActions is a helper function to return all the possible actions types.
func AllActions() []Action {
return []Action{ActionCreate, ActionRead, ActionUpdate, ActionDelete}
func AllActions() []policy.Action {
return []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}
}
type AuthCall struct {
Actor Subject
Action Action
Action policy.Action
Object Object
}
@@ -52,7 +43,7 @@ type AuthCall struct {
//
// Note that this ignores some fields such as the permissions within a given
// role, as this assumes all roles are static to a given role name.
func hashAuthorizeCall(actor Subject, action Action, object Object) [32]byte {
func hashAuthorizeCall(actor Subject, action policy.Action, object Object) [32]byte {
var hashOut [32]byte
hash := sha256.New()
@@ -139,8 +130,8 @@ type Authorizer interface {
// Authorize will authorize the given subject to perform the given action
// on the given object. Authorize is pure and deterministic with respect to
// its arguments and the surrounding object.
Authorize(ctx context.Context, subject Subject, action Action, object Object) error
Prepare(ctx context.Context, subject Subject, action Action, objectType string) (PreparedAuthorized, error)
Authorize(ctx context.Context, subject Subject, action policy.Action, object Object) error
Prepare(ctx context.Context, subject Subject, action policy.Action, objectType string) (PreparedAuthorized, error)
}
type PreparedAuthorized interface {
@@ -154,7 +145,7 @@ type PreparedAuthorized interface {
//
// Ideally the 'CompileToSQL' is used instead for large sets. This cost scales
// linearly with the number of objects passed in.
func Filter[O Objecter](ctx context.Context, auth Authorizer, subject Subject, action Action, objects []O) ([]O, error) {
func Filter[O Objecter](ctx context.Context, auth Authorizer, subject Subject, action policy.Action, objects []O) ([]O, error) {
if len(objects) == 0 {
// Nothing to filter
return objects, nil
@@ -236,7 +227,7 @@ var (
// Load the policy from policy.rego in this directory.
//
//go:embed policy.rego
policy string
regoPolicy string
queryOnce sync.Once
query rego.PreparedEvalQuery
partialQuery rego.PreparedPartialQuery
@@ -254,7 +245,7 @@ func NewAuthorizer(registry prometheus.Registerer) *RegoAuthorizer {
var err error
query, err = rego.New(
rego.Query("data.authz.allow"),
rego.Module("policy.rego", policy),
rego.Module("policy.rego", regoPolicy),
).PrepareForEval(context.Background())
if err != nil {
panic(xerrors.Errorf("compile rego: %w", err))
@@ -269,7 +260,7 @@ func NewAuthorizer(registry prometheus.Registerer) *RegoAuthorizer {
"input.object.acl_group_list",
}),
rego.Query("data.authz.allow = true"),
rego.Module("policy.rego", policy),
rego.Module("policy.rego", regoPolicy),
).PrepareForPartial(context.Background())
if err != nil {
panic(xerrors.Errorf("compile partial rego: %w", err))
@@ -334,7 +325,7 @@ type authSubject struct {
// It returns `nil` if the subject is authorized to perform the action on
// the object.
// If an error is returned, the authorization is denied.
func (a RegoAuthorizer) Authorize(ctx context.Context, subject Subject, action Action, object Object) error {
func (a RegoAuthorizer) Authorize(ctx context.Context, subject Subject, action policy.Action, object Object) error {
start := time.Now()
ctx, span := tracing.StartSpan(ctx,
trace.WithTimestamp(start), // Reuse the time.Now for metric and trace
@@ -365,7 +356,7 @@ func (a RegoAuthorizer) Authorize(ctx context.Context, subject Subject, action A
// It is a different function so the exported one can add tracing + metrics.
// That code tends to clutter up the actual logic, so it's separated out.
// nolint:revive
func (a RegoAuthorizer) authorize(ctx context.Context, subject Subject, action Action, object Object) error {
func (a RegoAuthorizer) authorize(ctx context.Context, subject Subject, action policy.Action, object Object) error {
if subject.Roles == nil {
return xerrors.Errorf("subject must have roles")
}
@@ -392,7 +383,7 @@ func (a RegoAuthorizer) authorize(ctx context.Context, subject Subject, action A
// 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 (a RegoAuthorizer) Prepare(ctx context.Context, subject Subject, action Action, objectType string) (PreparedAuthorized, error) {
func (a RegoAuthorizer) Prepare(ctx context.Context, subject Subject, action policy.Action, objectType string) (PreparedAuthorized, error) {
start := time.Now()
ctx, span := tracing.StartSpan(ctx,
trace.WithTimestamp(start),
@@ -428,7 +419,7 @@ type PartialAuthorizer struct {
// input is used purely for debugging and logging.
subjectInput Subject
subjectAction Action
subjectAction policy.Action
subjectResourceType Object
// preparedQueries are the compiled set of queries after partial evaluation.
@@ -537,7 +528,7 @@ EachQueryLoop:
pa.subjectInput, pa.subjectAction, pa.subjectResourceType, nil)
}
func (a RegoAuthorizer) newPartialAuthorizer(ctx context.Context, subject Subject, action Action, objectType string) (*PartialAuthorizer, error) {
func (a RegoAuthorizer) newPartialAuthorizer(ctx context.Context, subject Subject, action policy.Action, objectType string) (*PartialAuthorizer, error) {
if subject.Roles == nil {
return nil, xerrors.Errorf("subject must have roles")
}
@@ -676,7 +667,7 @@ func Cacher(authz Authorizer) Authorizer {
}
}
func (c *authCache) Authorize(ctx context.Context, subject Subject, action Action, object Object) error {
func (c *authCache) Authorize(ctx context.Context, subject Subject, action policy.Action, object Object) error {
authorizeCacheKey := hashAuthorizeCall(subject, action, object)
var err error
@@ -697,13 +688,13 @@ func (c *authCache) Authorize(ctx context.Context, subject Subject, action Actio
// Prepare returns the underlying PreparedAuthorized. The cache does not apply
// to prepared authorizations. These should be using a SQL filter, and
// therefore the cache is not needed.
func (c *authCache) Prepare(ctx context.Context, subject Subject, action Action, objectType string) (PreparedAuthorized, error) {
func (c *authCache) Prepare(ctx context.Context, subject Subject, action policy.Action, objectType string) (PreparedAuthorized, error) {
return c.authz.Prepare(ctx, subject, action, objectType)
}
// rbacTraceAttributes are the attributes that are added to all spans created by
// the rbac package. These attributes should help to debug slow spans.
func rbacTraceAttributes(actor Subject, action Action, objectType string, extra ...attribute.KeyValue) trace.SpanStartOption {
func rbacTraceAttributes(actor Subject, action policy.Action, objectType string, extra ...attribute.KeyValue) trace.SpanStartOption {
return trace.WithAttributes(
append(extra,
attribute.StringSlice("subject_roles", actor.SafeRoleNames()),
+65 -64
View File
@@ -13,6 +13,7 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/coderd/rbac/regosql"
"github.com/coder/coder/v2/testutil"
)
@@ -59,7 +60,7 @@ func TestFilterError(t *testing.T) {
Scope: ScopeAll,
}
_, err := Filter(context.Background(), auth, subject, ActionRead, []Object{ResourceUser, ResourceWorkspace})
_, err := Filter(context.Background(), auth, subject, policy.ActionRead, []Object{ResourceUser, ResourceWorkspace})
require.ErrorContains(t, err, "object types must be uniform")
})
@@ -67,7 +68,7 @@ func TestFilterError(t *testing.T) {
t.Parallel()
auth := &MockAuthorizer{
AuthorizeFunc: func(ctx context.Context, subject Subject, action Action, object Object) error {
AuthorizeFunc: func(ctx context.Context, subject Subject, action policy.Action, object Object) error {
// Authorize func always returns nil, unless the context is canceled.
return ctx.Err()
},
@@ -97,7 +98,7 @@ func TestFilterError(t *testing.T) {
ResourceUser,
}
_, err := Filter(ctx, auth, subject, ActionRead, objects)
_, err := Filter(ctx, auth, subject, policy.ActionRead, objects)
require.ErrorIs(t, err, context.Canceled)
})
@@ -117,7 +118,7 @@ func TestFilterError(t *testing.T) {
bomb: cancel,
}
_, err := Filter(ctx, auth, subject, ActionRead, objects)
_, err := Filter(ctx, auth, subject, policy.ActionRead, objects)
require.ErrorIs(t, err, context.Canceled)
})
})
@@ -150,7 +151,7 @@ func TestFilter(t *testing.T) {
testCases := []struct {
Name string
Actor Subject
Action Action
Action policy.Action
ObjectType string
}{
{
@@ -160,7 +161,7 @@ func TestFilter(t *testing.T) {
Roles: RoleNames{},
},
ObjectType: ResourceWorkspace.Type,
Action: ActionRead,
Action: policy.ActionRead,
},
{
Name: "Admin",
@@ -169,7 +170,7 @@ func TestFilter(t *testing.T) {
Roles: RoleNames{RoleOrgMember(orgIDs[0]), "auditor", RoleOwner(), RoleMember()},
},
ObjectType: ResourceWorkspace.Type,
Action: ActionRead,
Action: policy.ActionRead,
},
{
Name: "OrgAdmin",
@@ -178,7 +179,7 @@ func TestFilter(t *testing.T) {
Roles: RoleNames{RoleOrgMember(orgIDs[0]), RoleOrgAdmin(orgIDs[0]), RoleMember()},
},
ObjectType: ResourceWorkspace.Type,
Action: ActionRead,
Action: policy.ActionRead,
},
{
Name: "OrgMember",
@@ -187,7 +188,7 @@ func TestFilter(t *testing.T) {
Roles: RoleNames{RoleOrgMember(orgIDs[0]), RoleOrgMember(orgIDs[1]), RoleMember()},
},
ObjectType: ResourceWorkspace.Type,
Action: ActionRead,
Action: policy.ActionRead,
},
{
Name: "ManyRoles",
@@ -203,7 +204,7 @@ func TestFilter(t *testing.T) {
},
},
ObjectType: ResourceWorkspace.Type,
Action: ActionRead,
Action: policy.ActionRead,
},
{
Name: "SiteMember",
@@ -212,7 +213,7 @@ func TestFilter(t *testing.T) {
Roles: RoleNames{RoleMember()},
},
ObjectType: ResourceUser.Type,
Action: ActionRead,
Action: policy.ActionRead,
},
{
Name: "ReadOrgs",
@@ -227,7 +228,7 @@ func TestFilter(t *testing.T) {
},
},
ObjectType: ResourceOrganization.Type,
Action: ActionRead,
Action: policy.ActionRead,
},
{
Name: "ScopeApplicationConnect",
@@ -236,7 +237,7 @@ func TestFilter(t *testing.T) {
Roles: RoleNames{RoleOrgMember(orgIDs[0]), "auditor", RoleOwner(), RoleMember()},
},
ObjectType: ResourceWorkspace.Type,
Action: ActionRead,
Action: policy.ActionRead,
},
}
@@ -263,7 +264,7 @@ func TestFilter(t *testing.T) {
var allowedCount int
for i, obj := range localObjects {
obj.Type = tc.ObjectType
err := auth.Authorize(ctx, actor, ActionRead, obj.RBACObject())
err := auth.Authorize(ctx, actor, policy.ActionRead, obj.RBACObject())
obj.Allowed = err == nil
if err == nil {
allowedCount++
@@ -301,64 +302,64 @@ func TestAuthorizeDomain(t *testing.T) {
testAuthorize(t, "UserACLList", user, []authTestCase{
{
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(unuseID).WithACLUserList(map[string][]Action{
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(unuseID).WithACLUserList(map[string][]policy.Action{
user.ID: AllActions(),
}),
actions: AllActions(),
allow: true,
},
{
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(unuseID).WithACLUserList(map[string][]Action{
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(unuseID).WithACLUserList(map[string][]policy.Action{
user.ID: {WildcardSymbol},
}),
actions: AllActions(),
allow: true,
},
{
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(unuseID).WithACLUserList(map[string][]Action{
user.ID: {ActionRead, ActionUpdate},
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(unuseID).WithACLUserList(map[string][]policy.Action{
user.ID: {policy.ActionRead, policy.ActionUpdate},
}),
actions: []Action{ActionCreate, ActionDelete},
actions: []policy.Action{policy.ActionCreate, policy.ActionDelete},
allow: false,
},
{
// By default users cannot update templates
resource: ResourceTemplate.InOrg(defOrg).WithACLUserList(map[string][]Action{
user.ID: {ActionUpdate},
resource: ResourceTemplate.InOrg(defOrg).WithACLUserList(map[string][]policy.Action{
user.ID: {policy.ActionUpdate},
}),
actions: []Action{ActionUpdate},
actions: []policy.Action{policy.ActionUpdate},
allow: true,
},
})
testAuthorize(t, "GroupACLList", user, []authTestCase{
{
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(defOrg).WithGroupACL(map[string][]Action{
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(defOrg).WithGroupACL(map[string][]policy.Action{
allUsersGroup: AllActions(),
}),
actions: AllActions(),
allow: true,
},
{
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(defOrg).WithGroupACL(map[string][]Action{
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(defOrg).WithGroupACL(map[string][]policy.Action{
allUsersGroup: {WildcardSymbol},
}),
actions: AllActions(),
allow: true,
},
{
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(defOrg).WithGroupACL(map[string][]Action{
allUsersGroup: {ActionRead, ActionUpdate},
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(defOrg).WithGroupACL(map[string][]policy.Action{
allUsersGroup: {policy.ActionRead, policy.ActionUpdate},
}),
actions: []Action{ActionCreate, ActionDelete},
actions: []policy.Action{policy.ActionCreate, policy.ActionDelete},
allow: false,
},
{
// By default users cannot update templates
resource: ResourceTemplate.InOrg(defOrg).WithGroupACL(map[string][]Action{
allUsersGroup: {ActionUpdate},
resource: ResourceTemplate.InOrg(defOrg).WithGroupACL(map[string][]policy.Action{
allUsersGroup: {policy.ActionUpdate},
}),
actions: []Action{ActionUpdate},
actions: []policy.Action{policy.ActionUpdate},
allow: true,
},
})
@@ -509,7 +510,7 @@ func TestAuthorizeDomain(t *testing.T) {
testAuthorize(t, "ApplicationToken", user,
// Create (connect) Actions
cases(func(c authTestCase) authTestCase {
c.actions = []Action{ActionCreate}
c.actions = []policy.Action{policy.ActionCreate}
return c
}, []authTestCase{
// Org + me
@@ -537,7 +538,7 @@ func TestAuthorizeDomain(t *testing.T) {
}),
// Not create actions
cases(func(c authTestCase) authTestCase {
c.actions = []Action{ActionRead, ActionUpdate, ActionDelete}
c.actions = []policy.Action{policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}
c.allow = false
return c
}, []authTestCase{
@@ -566,7 +567,7 @@ func TestAuthorizeDomain(t *testing.T) {
}),
// Other Objects
cases(func(c authTestCase) authTestCase {
c.actions = []Action{ActionCreate, ActionRead, ActionUpdate, ActionDelete}
c.actions = []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}
c.allow = false
return c
}, []authTestCase{
@@ -607,14 +608,14 @@ func TestAuthorizeDomain(t *testing.T) {
defOrg.String(): {{
Negate: false,
ResourceType: "*",
Action: ActionRead,
Action: policy.ActionRead,
}},
},
User: []Permission{
{
Negate: false,
ResourceType: "*",
Action: ActionRead,
Action: policy.ActionRead,
},
},
},
@@ -623,7 +624,7 @@ func TestAuthorizeDomain(t *testing.T) {
testAuthorize(t, "ReadOnly", user,
cases(func(c authTestCase) authTestCase {
c.actions = []Action{ActionRead}
c.actions = []policy.Action{policy.ActionRead}
return c
}, []authTestCase{
// Read
@@ -653,7 +654,7 @@ func TestAuthorizeDomain(t *testing.T) {
// Pass non-read actions
cases(func(c authTestCase) authTestCase {
c.actions = []Action{ActionCreate, ActionUpdate, ActionDelete}
c.actions = []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}
c.allow = false
return c
}, []authTestCase{
@@ -822,7 +823,7 @@ func TestAuthorizeScope(t *testing.T) {
testAuthorize(t, "Admin_ScopeApplicationConnect", user,
cases(func(c authTestCase) authTestCase {
c.actions = []Action{ActionRead, ActionUpdate, ActionDelete}
c.actions = []policy.Action{policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}
return c
}, []authTestCase{
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), allow: false},
@@ -839,9 +840,9 @@ func TestAuthorizeScope(t *testing.T) {
}),
// Allowed by scope:
[]authTestCase{
{resource: ResourceWorkspaceApplicationConnect.InOrg(defOrg).WithOwner("not-me"), actions: []Action{ActionCreate}, allow: true},
{resource: ResourceWorkspaceApplicationConnect.InOrg(defOrg).WithOwner(user.ID), actions: []Action{ActionCreate}, allow: true},
{resource: ResourceWorkspaceApplicationConnect.InOrg(unusedID).WithOwner("not-me"), actions: []Action{ActionCreate}, allow: true},
{resource: ResourceWorkspaceApplicationConnect.InOrg(defOrg).WithOwner("not-me"), actions: []policy.Action{policy.ActionCreate}, allow: true},
{resource: ResourceWorkspaceApplicationConnect.InOrg(defOrg).WithOwner(user.ID), actions: []policy.Action{policy.ActionCreate}, allow: true},
{resource: ResourceWorkspaceApplicationConnect.InOrg(unusedID).WithOwner("not-me"), actions: []policy.Action{policy.ActionCreate}, allow: true},
},
)
@@ -856,7 +857,7 @@ func TestAuthorizeScope(t *testing.T) {
testAuthorize(t, "User_ScopeApplicationConnect", user,
cases(func(c authTestCase) authTestCase {
c.actions = []Action{ActionRead, ActionUpdate, ActionDelete}
c.actions = []policy.Action{policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}
c.allow = false
return c
}, []authTestCase{
@@ -874,9 +875,9 @@ func TestAuthorizeScope(t *testing.T) {
}),
// Allowed by scope:
[]authTestCase{
{resource: ResourceWorkspaceApplicationConnect.InOrg(defOrg).WithOwner(user.ID), actions: []Action{ActionCreate}, allow: true},
{resource: ResourceWorkspaceApplicationConnect.InOrg(defOrg).WithOwner("not-me"), actions: []Action{ActionCreate}, allow: false},
{resource: ResourceWorkspaceApplicationConnect.InOrg(unusedID).WithOwner("not-me"), actions: []Action{ActionCreate}, allow: false},
{resource: ResourceWorkspaceApplicationConnect.InOrg(defOrg).WithOwner(user.ID), actions: []policy.Action{policy.ActionCreate}, allow: true},
{resource: ResourceWorkspaceApplicationConnect.InOrg(defOrg).WithOwner("not-me"), actions: []policy.Action{policy.ActionCreate}, allow: false},
{resource: ResourceWorkspaceApplicationConnect.InOrg(unusedID).WithOwner("not-me"), actions: []policy.Action{policy.ActionCreate}, allow: false},
},
)
@@ -891,9 +892,9 @@ func TestAuthorizeScope(t *testing.T) {
Role: Role{
Name: "workspace_agent",
DisplayName: "Workspace Agent",
Site: Permissions(map[string][]Action{
Site: Permissions(map[string][]policy.Action{
// Only read access for workspaces.
ResourceWorkspace.Type: {ActionRead},
ResourceWorkspace.Type: {policy.ActionRead},
}),
Org: map[string][]Permission{},
User: []Permission{},
@@ -905,7 +906,7 @@ func TestAuthorizeScope(t *testing.T) {
testAuthorize(t, "User_WorkspaceAgent", user,
// Test cases without ID
cases(func(c authTestCase) authTestCase {
c.actions = []Action{ActionCreate, ActionUpdate, ActionDelete}
c.actions = []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}
c.allow = false
return c
}, []authTestCase{
@@ -924,7 +925,7 @@ func TestAuthorizeScope(t *testing.T) {
// Test all cases with the workspace id
cases(func(c authTestCase) authTestCase {
c.actions = []Action{ActionCreate, ActionUpdate, ActionDelete}
c.actions = []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}
c.allow = false
c.resource.WithID(workspaceID)
return c
@@ -943,7 +944,7 @@ func TestAuthorizeScope(t *testing.T) {
}),
// Test cases with random ids. These should always fail from the scope.
cases(func(c authTestCase) authTestCase {
c.actions = []Action{ActionRead, ActionCreate, ActionUpdate, ActionDelete}
c.actions = []policy.Action{policy.ActionRead, policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete}
c.allow = false
c.resource.WithID(uuid.New())
return c
@@ -962,10 +963,10 @@ func TestAuthorizeScope(t *testing.T) {
}),
// Allowed by scope:
[]authTestCase{
{resource: ResourceWorkspace.WithID(workspaceID).InOrg(defOrg).WithOwner(user.ID), actions: []Action{ActionRead}, allow: true},
{resource: ResourceWorkspace.WithID(workspaceID).InOrg(defOrg).WithOwner(user.ID), actions: []policy.Action{policy.ActionRead}, allow: true},
// The scope will return true, but the user perms return false for resources not owned by the user.
{resource: ResourceWorkspace.WithID(workspaceID).InOrg(defOrg).WithOwner("not-me"), actions: []Action{ActionRead}, allow: false},
{resource: ResourceWorkspace.WithID(workspaceID).InOrg(unusedID).WithOwner("not-me"), actions: []Action{ActionRead}, allow: false},
{resource: ResourceWorkspace.WithID(workspaceID).InOrg(defOrg).WithOwner("not-me"), actions: []policy.Action{policy.ActionRead}, allow: false},
{resource: ResourceWorkspace.WithID(workspaceID).InOrg(unusedID).WithOwner("not-me"), actions: []policy.Action{policy.ActionRead}, allow: false},
},
)
@@ -980,9 +981,9 @@ func TestAuthorizeScope(t *testing.T) {
Role: Role{
Name: "create_workspace",
DisplayName: "Create Workspace",
Site: Permissions(map[string][]Action{
Site: Permissions(map[string][]policy.Action{
// Only read access for workspaces.
ResourceWorkspace.Type: {ActionCreate},
ResourceWorkspace.Type: {policy.ActionCreate},
}),
Org: map[string][]Permission{},
User: []Permission{},
@@ -995,7 +996,7 @@ func TestAuthorizeScope(t *testing.T) {
testAuthorize(t, "CreatWorkspaceScope", user,
// All these cases will fail because a resource ID is set.
cases(func(c authTestCase) authTestCase {
c.actions = []Action{ActionCreate, ActionRead, ActionUpdate, ActionDelete}
c.actions = []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}
c.allow = false
c.resource.ID = uuid.NewString()
return c
@@ -1015,10 +1016,10 @@ func TestAuthorizeScope(t *testing.T) {
// Test create allowed by scope:
[]authTestCase{
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: []Action{ActionCreate}, allow: true},
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: []policy.Action{policy.ActionCreate}, allow: true},
// The scope will return true, but the user perms return false for resources not owned by the user.
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: []Action{ActionCreate}, allow: false},
{resource: ResourceWorkspace.InOrg(unusedID).WithOwner("not-me"), actions: []Action{ActionCreate}, allow: false},
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: []policy.Action{policy.ActionCreate}, allow: false},
{resource: ResourceWorkspace.InOrg(unusedID).WithOwner("not-me"), actions: []policy.Action{policy.ActionCreate}, allow: false},
},
)
}
@@ -1036,7 +1037,7 @@ func cases(opt func(c authTestCase) authTestCase, cases []authTestCase) []authTe
type authTestCase struct {
resource Object
actions []Action
actions []policy.Action
allow bool
}
@@ -1127,16 +1128,16 @@ func must[T any](value T, err error) T {
}
type MockAuthorizer struct {
AuthorizeFunc func(context.Context, Subject, Action, Object) error
AuthorizeFunc func(context.Context, Subject, policy.Action, Object) error
}
var _ Authorizer = (*MockAuthorizer)(nil)
func (d *MockAuthorizer) Authorize(ctx context.Context, s Subject, a Action, o Object) error {
func (d *MockAuthorizer) Authorize(ctx context.Context, s Subject, a policy.Action, o Object) error {
return d.AuthorizeFunc(ctx, s, a, o)
}
func (d *MockAuthorizer) Prepare(_ context.Context, subject Subject, action Action, _ string) (PreparedAuthorized, error) {
func (d *MockAuthorizer) Prepare(_ context.Context, subject Subject, action policy.Action, _ string) (PreparedAuthorized, error) {
return &mockPreparedAuthorizer{
Original: d,
Subject: subject,
@@ -1152,7 +1153,7 @@ type mockPreparedAuthorizer struct {
sync.RWMutex
Original *MockAuthorizer
Subject Subject
Action Action
Action policy.Action
}
func (f *mockPreparedAuthorizer) Authorize(ctx context.Context, object Object) error {
+19 -18
View File
@@ -12,6 +12,7 @@ import (
"github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/testutil"
)
@@ -167,7 +168,7 @@ func BenchmarkRBACAuthorize(b *testing.B) {
objects := benchmarkSetup(orgs, users, b.N)
b.ResetTimer()
for i := 0; i < b.N; i++ {
allowed := authorizer.Authorize(context.Background(), c.Actor, rbac.ActionRead, objects[b.N%len(objects)])
allowed := authorizer.Authorize(context.Background(), c.Actor, policy.ActionRead, objects[b.N%len(objects)])
_ = allowed
}
})
@@ -191,30 +192,30 @@ func BenchmarkRBACAuthorizeGroups(b *testing.B) {
// Same benchmark cases, but this time groups will be used to match.
// Some '*' permissions will still match, but using a fake action reduces
// the chance.
neverMatchAction := rbac.Action("never-match-action")
neverMatchAction := policy.Action("never-match-action")
for _, c := range benchCases {
b.Run(c.Name+"GroupACL", func(b *testing.B) {
userGroupAllow := uuid.NewString()
c.Actor.Groups = append(c.Actor.Groups, userGroupAllow)
c.Actor.Scope = rbac.ScopeAll
objects := benchmarkSetup(orgs, users, b.N, func(object rbac.Object) rbac.Object {
m := map[string][]rbac.Action{
m := map[string][]policy.Action{
// Add the user's group
// Noise
uuid.NewString(): {rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
uuid.NewString(): {rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate},
uuid.NewString(): {rbac.ActionCreate, rbac.ActionRead},
uuid.NewString(): {rbac.ActionCreate},
uuid.NewString(): {rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
uuid.NewString(): {rbac.ActionRead, rbac.ActionUpdate},
uuid.NewString(): {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
uuid.NewString(): {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate},
uuid.NewString(): {policy.ActionCreate, policy.ActionRead},
uuid.NewString(): {policy.ActionCreate},
uuid.NewString(): {policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
uuid.NewString(): {policy.ActionRead, policy.ActionUpdate},
}
for _, g := range c.Actor.Groups {
// Every group the user is in will be added, but it will not match the perms. This makes the
// authorizer look at many groups before finding the one that matches.
m[g] = []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete}
m[g] = []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}
}
// This is the only group that will give permission.
m[userGroupAllow] = []rbac.Action{neverMatchAction}
m[userGroupAllow] = []policy.Action{neverMatchAction}
return object.WithGroupACL(m)
})
b.ResetTimer()
@@ -244,7 +245,7 @@ func BenchmarkRBACFilter(b *testing.B) {
b.Run("PrepareOnly-"+c.Name, func(b *testing.B) {
obType := rbac.ResourceWorkspace.Type
for i := 0; i < b.N; i++ {
_, err := authorizer.Prepare(context.Background(), c.Actor, rbac.ActionRead, obType)
_, err := authorizer.Prepare(context.Background(), c.Actor, policy.ActionRead, obType)
require.NoError(b, err)
}
})
@@ -254,7 +255,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.Actor, rbac.ActionRead, objects)
allowed, err := rbac.Filter(context.Background(), authorizer, c.Actor, policy.ActionRead, objects)
require.NoError(b, err)
_ = allowed
})
@@ -263,9 +264,9 @@ func BenchmarkRBACFilter(b *testing.B) {
func benchmarkSetup(orgs []uuid.UUID, users []uuid.UUID, size int, opts ...func(object rbac.Object) rbac.Object) []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},
aclList := map[string][]policy.Action{
uuid.NewString(): {policy.ActionRead, policy.ActionUpdate},
uuid.NewString(): {policy.ActionCreate},
}
objectList := make([]rbac.Object, size)
for i := range objectList {
@@ -297,7 +298,7 @@ func BenchmarkCacher(b *testing.B) {
var (
subj rbac.Subject
obj rbac.Object
action rbac.Action
action policy.Action
)
for i := 0; i < b.N; i++ {
if i%rat == 0 {
@@ -359,7 +360,7 @@ func TestCacher(t *testing.T) {
var (
ctx = testutil.Context(t, testutil.WaitShort)
authOut = make(chan error, 1) // buffered to not block
authorizeFunc = func(ctx context.Context, subject rbac.Subject, action rbac.Action, object rbac.Object) error {
authorizeFunc = func(ctx context.Context, subject rbac.Subject, action policy.Action, object rbac.Object) error {
// Just return what you're told.
return testutil.RequireRecvCtx(ctx, t, authOut)
}
+3 -2
View File
@@ -11,6 +11,7 @@ import (
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/httpapi/httpapiconstraints"
"github.com/coder/coder/v2/coderd/rbac/policy"
)
const (
@@ -28,7 +29,7 @@ type UnauthorizedError struct {
// These fields are for debugging purposes.
subject Subject
action Action
action policy.Action
// Note only the object type is set for partial execution.
object Object
@@ -52,7 +53,7 @@ func IsUnauthorizedError(err error) bool {
// ForbiddenWithInternal creates a new error that will return a simple
// "forbidden" to the client, logging internally the more detailed message
// provided.
func ForbiddenWithInternal(internal error, subject Subject, action Action, object Object, output rego.ResultSet) *UnauthorizedError {
func ForbiddenWithInternal(internal error, subject Subject, action policy.Action, object Object, output rego.ResultSet) *UnauthorizedError {
return &UnauthorizedError{
internal: internal,
subject: subject,
+9 -7
View File
@@ -2,6 +2,8 @@ package rbac
import (
"github.com/google/uuid"
"github.com/coder/coder/v2/coderd/rbac/policy"
)
const WildcardSymbol = "*"
@@ -250,8 +252,8 @@ type Object struct {
// Type is "workspace", "project", "app", etc
Type string `json:"type"`
ACLUserList map[string][]Action ` json:"acl_user_list"`
ACLGroupList map[string][]Action ` json:"acl_group_list"`
ACLUserList map[string][]policy.Action ` json:"acl_user_list"`
ACLGroupList map[string][]policy.Action ` json:"acl_group_list"`
}
func (z Object) Equal(b Object) bool {
@@ -279,7 +281,7 @@ func (z Object) Equal(b Object) bool {
return true
}
func equalACLLists(a, b map[string][]Action) bool {
func equalACLLists(a, b map[string][]policy.Action) bool {
if len(a) != len(b) {
return false
}
@@ -307,8 +309,8 @@ func (z Object) All() Object {
Owner: "",
OrgID: "",
Type: z.Type,
ACLUserList: map[string][]Action{},
ACLGroupList: map[string][]Action{},
ACLUserList: map[string][]policy.Action{},
ACLGroupList: map[string][]policy.Action{},
}
}
@@ -359,7 +361,7 @@ func (z Object) WithOwner(ownerID string) Object {
}
// WithACLUserList adds an ACL list to a given object
func (z Object) WithACLUserList(acl map[string][]Action) Object {
func (z Object) WithACLUserList(acl map[string][]policy.Action) Object {
return Object{
ID: z.ID,
Owner: z.Owner,
@@ -370,7 +372,7 @@ func (z Object) WithACLUserList(acl map[string][]Action) Object {
}
}
func (z Object) WithGroupACL(groups map[string][]Action) Object {
func (z Object) WithGroupACL(groups map[string][]policy.Action) Object {
return Object{
ID: z.ID,
Owner: z.Owner,
+28 -27
View File
@@ -4,6 +4,7 @@ import (
"testing"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
"github.com/coder/coder/v2/coderd/util/slice"
)
@@ -24,8 +25,8 @@ func TestObjectEqual(t *testing.T) {
{
Name: "NilVs0",
A: rbac.Object{
ACLGroupList: map[string][]rbac.Action{},
ACLUserList: map[string][]rbac.Action{},
ACLGroupList: map[string][]policy.Action{},
ACLUserList: map[string][]policy.Action{},
},
B: rbac.Object{},
Expected: true,
@@ -37,16 +38,16 @@ func TestObjectEqual(t *testing.T) {
Owner: "owner",
OrgID: "orgID",
Type: "type",
ACLUserList: map[string][]rbac.Action{},
ACLGroupList: map[string][]rbac.Action{},
ACLUserList: map[string][]policy.Action{},
ACLGroupList: map[string][]policy.Action{},
},
B: rbac.Object{
ID: "id",
Owner: "owner",
OrgID: "orgID",
Type: "type",
ACLUserList: map[string][]rbac.Action{},
ACLGroupList: map[string][]rbac.Action{},
ACLUserList: map[string][]policy.Action{},
ACLGroupList: map[string][]policy.Action{},
},
Expected: true,
},
@@ -93,13 +94,13 @@ func TestObjectEqual(t *testing.T) {
{
Name: "DifferentACLUserList",
A: rbac.Object{
ACLUserList: map[string][]rbac.Action{
"user1": {rbac.ActionRead},
ACLUserList: map[string][]policy.Action{
"user1": {policy.ActionRead},
},
},
B: rbac.Object{
ACLUserList: map[string][]rbac.Action{
"user2": {rbac.ActionRead},
ACLUserList: map[string][]policy.Action{
"user2": {policy.ActionRead},
},
},
Expected: false,
@@ -107,13 +108,13 @@ func TestObjectEqual(t *testing.T) {
{
Name: "ACLUserDiff#Actions",
A: rbac.Object{
ACLUserList: map[string][]rbac.Action{
"user1": {rbac.ActionRead},
ACLUserList: map[string][]policy.Action{
"user1": {policy.ActionRead},
},
},
B: rbac.Object{
ACLUserList: map[string][]rbac.Action{
"user1": {rbac.ActionRead, rbac.ActionUpdate},
ACLUserList: map[string][]policy.Action{
"user1": {policy.ActionRead, policy.ActionUpdate},
},
},
Expected: false,
@@ -121,13 +122,13 @@ func TestObjectEqual(t *testing.T) {
{
Name: "ACLUserDiffAction",
A: rbac.Object{
ACLUserList: map[string][]rbac.Action{
"user1": {rbac.ActionRead},
ACLUserList: map[string][]policy.Action{
"user1": {policy.ActionRead},
},
},
B: rbac.Object{
ACLUserList: map[string][]rbac.Action{
"user1": {rbac.ActionUpdate},
ACLUserList: map[string][]policy.Action{
"user1": {policy.ActionUpdate},
},
},
Expected: false,
@@ -135,14 +136,14 @@ func TestObjectEqual(t *testing.T) {
{
Name: "ACLUserDiff#Users",
A: rbac.Object{
ACLUserList: map[string][]rbac.Action{
"user1": {rbac.ActionRead},
ACLUserList: map[string][]policy.Action{
"user1": {policy.ActionRead},
},
},
B: rbac.Object{
ACLUserList: map[string][]rbac.Action{
"user1": {rbac.ActionRead},
"user2": {rbac.ActionRead},
ACLUserList: map[string][]policy.Action{
"user1": {policy.ActionRead},
"user2": {policy.ActionRead},
},
},
Expected: false,
@@ -150,13 +151,13 @@ func TestObjectEqual(t *testing.T) {
{
Name: "DifferentACLGroupList",
A: rbac.Object{
ACLGroupList: map[string][]rbac.Action{
"group1": {rbac.ActionRead},
ACLGroupList: map[string][]policy.Action{
"group1": {policy.ActionRead},
},
},
B: rbac.Object{
ACLGroupList: map[string][]rbac.Action{
"group2": {rbac.ActionRead},
ACLGroupList: map[string][]policy.Action{
"group2": {policy.ActionRead},
},
},
Expected: false,
+11
View File
@@ -0,0 +1,11 @@
package policy
// Action represents the allowed actions to be done on an object.
type Action string
const (
ActionCreate Action = "create"
ActionRead Action = "read"
ActionUpdate Action = "update"
ActionDelete Action = "delete"
)
+44 -42
View File
@@ -8,6 +8,8 @@ import (
"github.com/open-policy-agent/opa/ast"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/rbac/policy"
)
const (
@@ -144,22 +146,22 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
memberRole := Role{
Name: member,
DisplayName: "Member",
Site: Permissions(map[string][]Action{
ResourceRoleAssignment.Type: {ActionRead},
Site: Permissions(map[string][]policy.Action{
ResourceRoleAssignment.Type: {policy.ActionRead},
// All users can see the provisioner daemons.
ResourceProvisionerDaemon.Type: {ActionRead},
ResourceProvisionerDaemon.Type: {policy.ActionRead},
// All users can see OAuth2 provider applications.
ResourceOAuth2ProviderApp.Type: {ActionRead},
ResourceOAuth2ProviderApp.Type: {policy.ActionRead},
}),
Org: map[string][]Permission{},
User: append(allPermsExcept(ResourceWorkspaceDormant, ResourceUser, ResourceOrganizationMember),
Permissions(map[string][]Action{
Permissions(map[string][]policy.Action{
// Users cannot do create/update/delete on themselves, but they
// can read their own details.
ResourceUser.Type: {ActionRead},
ResourceUserWorkspaceBuildParameters.Type: {ActionRead},
ResourceUser.Type: {policy.ActionRead},
ResourceUserWorkspaceBuildParameters.Type: {policy.ActionRead},
// Users can create provisioner daemons scoped to themselves.
ResourceProvisionerDaemon.Type: {ActionCreate, ActionRead, ActionUpdate},
ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate},
})...,
),
}.withCachedRegoValue()
@@ -167,19 +169,19 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
auditorRole := Role{
Name: auditor,
DisplayName: "Auditor",
Site: Permissions(map[string][]Action{
Site: Permissions(map[string][]policy.Action{
// Should be able to read all template details, even in orgs they
// are not in.
ResourceTemplate.Type: {ActionRead},
ResourceTemplateInsights.Type: {ActionRead},
ResourceAuditLog.Type: {ActionRead},
ResourceUser.Type: {ActionRead},
ResourceGroup.Type: {ActionRead},
ResourceTemplate.Type: {policy.ActionRead},
ResourceTemplateInsights.Type: {policy.ActionRead},
ResourceAuditLog.Type: {policy.ActionRead},
ResourceUser.Type: {policy.ActionRead},
ResourceGroup.Type: {policy.ActionRead},
// Allow auditors to query deployment stats and insights.
ResourceDeploymentStats.Type: {ActionRead},
ResourceDeploymentValues.Type: {ActionRead},
ResourceDeploymentStats.Type: {policy.ActionRead},
ResourceDeploymentValues.Type: {policy.ActionRead},
// Org roles are not really used yet, so grant the perm at the site level.
ResourceOrganizationMember.Type: {ActionRead},
ResourceOrganizationMember.Type: {policy.ActionRead},
}),
Org: map[string][]Permission{},
User: []Permission{},
@@ -188,21 +190,21 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
templateAdminRole := Role{
Name: templateAdmin,
DisplayName: "Template Admin",
Site: Permissions(map[string][]Action{
ResourceTemplate.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
Site: Permissions(map[string][]policy.Action{
ResourceTemplate.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
// CRUD all files, even those they did not upload.
ResourceFile.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
ResourceWorkspace.Type: {ActionRead},
ResourceFile.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
ResourceWorkspace.Type: {policy.ActionRead},
// CRUD to provisioner daemons for now.
ResourceProvisionerDaemon.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
ResourceProvisionerDaemon.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
// Needs to read all organizations since
ResourceOrganization.Type: {ActionRead},
ResourceUser.Type: {ActionRead},
ResourceGroup.Type: {ActionRead},
ResourceOrganization.Type: {policy.ActionRead},
ResourceUser.Type: {policy.ActionRead},
ResourceGroup.Type: {policy.ActionRead},
// Org roles are not really used yet, so grant the perm at the site level.
ResourceOrganizationMember.Type: {ActionRead},
ResourceOrganizationMember.Type: {policy.ActionRead},
// Template admins can read all template insights data
ResourceTemplateInsights.Type: {ActionRead},
ResourceTemplateInsights.Type: {policy.ActionRead},
}),
Org: map[string][]Permission{},
User: []Permission{},
@@ -211,14 +213,14 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
userAdminRole := Role{
Name: userAdmin,
DisplayName: "User Admin",
Site: Permissions(map[string][]Action{
ResourceRoleAssignment.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
ResourceUser.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
ResourceUserData.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
ResourceUserWorkspaceBuildParameters.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
Site: Permissions(map[string][]policy.Action{
ResourceRoleAssignment.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
ResourceUser.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
ResourceUserData.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
ResourceUserWorkspaceBuildParameters.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
// Full perms to manage org members
ResourceOrganizationMember.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
ResourceGroup.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
ResourceOrganizationMember.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
ResourceGroup.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
}),
Org: map[string][]Permission{},
User: []Permission{},
@@ -277,19 +279,19 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
{
// All org members can read the organization
ResourceType: ResourceOrganization.Type,
Action: ActionRead,
Action: policy.ActionRead,
},
{
// Can read available roles.
ResourceType: ResourceOrgRoleAssignment.Type,
Action: ActionRead,
Action: policy.ActionRead,
},
},
},
User: []Permission{
{
ResourceType: ResourceOrganizationMember.Type,
Action: ActionRead,
Action: policy.ActionRead,
},
},
}
@@ -349,9 +351,9 @@ type ExpandableRoles interface {
// Permission is the format passed into the rego.
type Permission struct {
// Negate makes this a negative permission
Negate bool `json:"negate"`
ResourceType string `json:"resource_type"`
Action Action `json:"action"`
Negate bool `json:"negate"`
ResourceType string `json:"resource_type"`
Action policy.Action `json:"action"`
}
// Role is a set of permissions at multiple levels:
@@ -521,7 +523,7 @@ func SiteRoles() []Role {
// ChangeRoleSet is a helper function that finds the difference of 2 sets of
// roles. When setting a user's new roles, it is equivalent to adding and
// removing roles. This set determines the changes, so that the appropriate
// RBAC checks can be applied using "ActionCreate" and "ActionDelete" for
// RBAC checks can be applied using "policy.ActionCreate" and "policy.ActionDelete" for
// "added" and "removed" roles respectively.
func ChangeRoleSet(from []string, to []string) (added []string, removed []string) {
has := make(map[string]struct{})
@@ -579,7 +581,7 @@ 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[string][]Action) []Permission {
func Permissions(perms map[string][]policy.Action) []Permission {
list := make([]Permission, 0, len(perms))
for k, actions := range perms {
for _, act := range actions {
+19 -17
View File
@@ -6,6 +6,8 @@ import (
"github.com/google/uuid"
"github.com/open-policy-agent/opa/ast"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/rbac/policy"
)
// BenchmarkRBACValueAllocation benchmarks the cost of allocating a rego input
@@ -27,13 +29,13 @@ func BenchmarkRBACValueAllocation(b *testing.B) {
WithID(uuid.New()).
InOrg(uuid.New()).
WithOwner(uuid.NewString()).
WithGroupACL(map[string][]Action{
uuid.NewString(): {ActionRead, ActionCreate},
uuid.NewString(): {ActionRead, ActionCreate},
uuid.NewString(): {ActionRead, ActionCreate},
}).WithACLUserList(map[string][]Action{
uuid.NewString(): {ActionRead, ActionCreate},
uuid.NewString(): {ActionRead, ActionCreate},
WithGroupACL(map[string][]policy.Action{
uuid.NewString(): {policy.ActionRead, policy.ActionCreate},
uuid.NewString(): {policy.ActionRead, policy.ActionCreate},
uuid.NewString(): {policy.ActionRead, policy.ActionCreate},
}).WithACLUserList(map[string][]policy.Action{
uuid.NewString(): {policy.ActionRead, policy.ActionCreate},
uuid.NewString(): {policy.ActionRead, policy.ActionCreate},
})
jsonSubject := authSubject{
@@ -45,7 +47,7 @@ func BenchmarkRBACValueAllocation(b *testing.B) {
b.Run("ManualRegoValue", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := regoInputValue(actor, ActionRead, obj)
_, err := regoInputValue(actor, policy.ActionRead, obj)
require.NoError(b, err)
}
})
@@ -53,7 +55,7 @@ func BenchmarkRBACValueAllocation(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := ast.InterfaceToValue(map[string]interface{}{
"subject": jsonSubject,
"action": ActionRead,
"action": policy.ActionRead,
"object": obj,
})
require.NoError(b, err)
@@ -90,16 +92,16 @@ func TestRegoInputValue(t *testing.T) {
WithID(uuid.New()).
InOrg(uuid.New()).
WithOwner(uuid.NewString()).
WithGroupACL(map[string][]Action{
uuid.NewString(): {ActionRead, ActionCreate},
uuid.NewString(): {ActionRead, ActionCreate},
uuid.NewString(): {ActionRead, ActionCreate},
}).WithACLUserList(map[string][]Action{
uuid.NewString(): {ActionRead, ActionCreate},
uuid.NewString(): {ActionRead, ActionCreate},
WithGroupACL(map[string][]policy.Action{
uuid.NewString(): {policy.ActionRead, policy.ActionCreate},
uuid.NewString(): {policy.ActionRead, policy.ActionCreate},
uuid.NewString(): {policy.ActionRead, policy.ActionCreate},
}).WithACLUserList(map[string][]policy.Action{
uuid.NewString(): {policy.ActionRead, policy.ActionCreate},
uuid.NewString(): {policy.ActionRead, policy.ActionCreate},
})
action := ActionRead
action := policy.ActionRead
t.Run("InputValue", func(t *testing.T) {
t.Parallel()
+29 -28
View File
@@ -11,6 +11,7 @@ import (
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/rbac/policy"
)
type authSubject struct {
@@ -35,7 +36,7 @@ func TestOwnerExec(t *testing.T) {
auth := rbac.NewCachingAuthorizer(prometheus.NewRegistry())
// Exec a random workspace
err := auth.Authorize(context.Background(), owner, rbac.ActionCreate,
err := auth.Authorize(context.Background(), owner, policy.ActionCreate,
rbac.ResourceWorkspaceExecution.WithID(uuid.New()).InOrg(uuid.New()).WithOwner(uuid.NewString()))
require.ErrorAsf(t, err, &rbac.UnauthorizedError{}, "expected unauthorized error")
})
@@ -49,7 +50,7 @@ func TestOwnerExec(t *testing.T) {
auth := rbac.NewCachingAuthorizer(prometheus.NewRegistry())
// Exec a random workspace
err := auth.Authorize(context.Background(), owner, rbac.ActionCreate,
err := auth.Authorize(context.Background(), owner, policy.ActionCreate,
rbac.ResourceWorkspaceExecution.WithID(uuid.New()).InOrg(uuid.New()).WithOwner(uuid.NewString()))
require.NoError(t, err, "expected owner can")
})
@@ -94,7 +95,7 @@ func TestRolePermissions(t *testing.T) {
// Name the test case to better locate the failing test case.
Name string
Resource rbac.Object
Actions []rbac.Action
Actions []policy.Action
// AuthorizeMap must cover all subjects in 'requiredSubjects'.
// This map will run an Authorize() check with the resource, action,
// and subjects. The subjects are split into 2 categories, "true" and
@@ -105,7 +106,7 @@ func TestRolePermissions(t *testing.T) {
}{
{
Name: "MyUser",
Actions: []rbac.Action{rbac.ActionRead},
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceUserObject(currentUser),
AuthorizeMap: map[bool][]authSubject{
true: {orgMemberMe, owner, memberMe, templateAdmin, userAdmin},
@@ -114,7 +115,7 @@ func TestRolePermissions(t *testing.T) {
},
{
Name: "AUser",
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
Resource: rbac.ResourceUser,
AuthorizeMap: map[bool][]authSubject{
true: {owner, userAdmin},
@@ -124,7 +125,7 @@ func TestRolePermissions(t *testing.T) {
{
Name: "ReadMyWorkspaceInOrg",
// When creating the WithID won't be set, but it does not change the result.
Actions: []rbac.Action{rbac.ActionRead},
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]authSubject{
true: {owner, orgMemberMe, orgAdmin, templateAdmin},
@@ -134,7 +135,7 @@ func TestRolePermissions(t *testing.T) {
{
Name: "C_RDMyWorkspaceInOrg",
// When creating the WithID won't be set, but it does not change the result.
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
Resource: rbac.ResourceWorkspace.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]authSubject{
true: {owner, orgMemberMe, orgAdmin},
@@ -144,7 +145,7 @@ func TestRolePermissions(t *testing.T) {
{
Name: "MyWorkspaceInOrgExecution",
// When creating the WithID won't be set, but it does not change the result.
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
Resource: rbac.ResourceWorkspaceExecution.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]authSubject{
true: {owner, orgMemberMe},
@@ -154,7 +155,7 @@ func TestRolePermissions(t *testing.T) {
{
Name: "MyWorkspaceInOrgAppConnect",
// When creating the WithID won't be set, but it does not change the result.
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
Resource: rbac.ResourceWorkspaceApplicationConnect.WithID(workspaceID).InOrg(orgID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]authSubject{
true: {owner, orgAdmin, orgMemberMe},
@@ -163,7 +164,7 @@ func TestRolePermissions(t *testing.T) {
},
{
Name: "Templates",
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
Resource: rbac.ResourceTemplate.WithID(templateID).InOrg(orgID),
AuthorizeMap: map[bool][]authSubject{
true: {owner, orgAdmin, templateAdmin},
@@ -172,7 +173,7 @@ func TestRolePermissions(t *testing.T) {
},
{
Name: "ReadTemplates",
Actions: []rbac.Action{rbac.ActionRead},
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceTemplate.InOrg(orgID),
AuthorizeMap: map[bool][]authSubject{
true: {owner, orgAdmin, templateAdmin},
@@ -181,7 +182,7 @@ func TestRolePermissions(t *testing.T) {
},
{
Name: "Files",
Actions: []rbac.Action{rbac.ActionCreate},
Actions: []policy.Action{policy.ActionCreate},
Resource: rbac.ResourceFile.WithID(fileID),
AuthorizeMap: map[bool][]authSubject{
true: {owner, templateAdmin},
@@ -190,7 +191,7 @@ func TestRolePermissions(t *testing.T) {
},
{
Name: "MyFile",
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
Resource: rbac.ResourceFile.WithID(fileID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]authSubject{
true: {owner, memberMe, orgMemberMe, templateAdmin},
@@ -199,7 +200,7 @@ func TestRolePermissions(t *testing.T) {
},
{
Name: "CreateOrganizations",
Actions: []rbac.Action{rbac.ActionCreate},
Actions: []policy.Action{policy.ActionCreate},
Resource: rbac.ResourceOrganization,
AuthorizeMap: map[bool][]authSubject{
true: {owner},
@@ -208,7 +209,7 @@ func TestRolePermissions(t *testing.T) {
},
{
Name: "Organizations",
Actions: []rbac.Action{rbac.ActionUpdate, rbac.ActionDelete},
Actions: []policy.Action{policy.ActionUpdate, policy.ActionDelete},
Resource: rbac.ResourceOrganization.WithID(orgID).InOrg(orgID),
AuthorizeMap: map[bool][]authSubject{
true: {owner, orgAdmin},
@@ -217,7 +218,7 @@ func TestRolePermissions(t *testing.T) {
},
{
Name: "ReadOrganizations",
Actions: []rbac.Action{rbac.ActionRead},
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceOrganization.WithID(orgID).InOrg(orgID),
AuthorizeMap: map[bool][]authSubject{
true: {owner, orgAdmin, orgMemberMe, templateAdmin},
@@ -226,7 +227,7 @@ func TestRolePermissions(t *testing.T) {
},
{
Name: "RoleAssignment",
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
Resource: rbac.ResourceRoleAssignment,
AuthorizeMap: map[bool][]authSubject{
true: {owner, userAdmin},
@@ -235,7 +236,7 @@ func TestRolePermissions(t *testing.T) {
},
{
Name: "ReadRoleAssignment",
Actions: []rbac.Action{rbac.ActionRead},
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceRoleAssignment,
AuthorizeMap: map[bool][]authSubject{
true: {owner, orgAdmin, orgMemberMe, otherOrgAdmin, otherOrgMember, memberMe, templateAdmin, userAdmin},
@@ -244,7 +245,7 @@ func TestRolePermissions(t *testing.T) {
},
{
Name: "OrgRoleAssignment",
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
Resource: rbac.ResourceOrgRoleAssignment.InOrg(orgID),
AuthorizeMap: map[bool][]authSubject{
true: {owner, orgAdmin},
@@ -253,7 +254,7 @@ func TestRolePermissions(t *testing.T) {
},
{
Name: "ReadOrgRoleAssignment",
Actions: []rbac.Action{rbac.ActionRead},
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceOrgRoleAssignment.InOrg(orgID),
AuthorizeMap: map[bool][]authSubject{
true: {owner, orgAdmin, orgMemberMe},
@@ -262,7 +263,7 @@ func TestRolePermissions(t *testing.T) {
},
{
Name: "APIKey",
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
Resource: rbac.ResourceAPIKey.WithID(apiKeyID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]authSubject{
true: {owner, orgMemberMe, memberMe},
@@ -271,7 +272,7 @@ func TestRolePermissions(t *testing.T) {
},
{
Name: "UserData",
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionRead, rbac.ActionUpdate, rbac.ActionDelete},
Actions: []policy.Action{policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
Resource: rbac.ResourceUserData.WithID(currentUser).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]authSubject{
true: {owner, orgMemberMe, memberMe, userAdmin},
@@ -280,7 +281,7 @@ func TestRolePermissions(t *testing.T) {
},
{
Name: "ManageOrgMember",
Actions: []rbac.Action{rbac.ActionCreate, rbac.ActionUpdate, rbac.ActionDelete},
Actions: []policy.Action{policy.ActionCreate, policy.ActionUpdate, policy.ActionDelete},
Resource: rbac.ResourceOrganizationMember.WithID(currentUser).InOrg(orgID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]authSubject{
true: {owner, orgAdmin, userAdmin},
@@ -289,7 +290,7 @@ func TestRolePermissions(t *testing.T) {
},
{
Name: "ReadOrgMember",
Actions: []rbac.Action{rbac.ActionRead},
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceOrganizationMember.WithID(currentUser).InOrg(orgID).WithOwner(currentUser.String()),
AuthorizeMap: map[bool][]authSubject{
true: {owner, orgAdmin, userAdmin, orgMemberMe, templateAdmin},
@@ -298,10 +299,10 @@ func TestRolePermissions(t *testing.T) {
},
{
Name: "AllUsersGroupACL",
Actions: []rbac.Action{rbac.ActionRead},
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceTemplate.WithID(templateID).InOrg(orgID).WithGroupACL(
map[string][]rbac.Action{
orgID.String(): {rbac.ActionRead},
map[string][]policy.Action{
orgID.String(): {policy.ActionRead},
}),
AuthorizeMap: map[bool][]authSubject{
@@ -311,7 +312,7 @@ func TestRolePermissions(t *testing.T) {
},
{
Name: "Groups",
Actions: []rbac.Action{rbac.ActionRead},
Actions: []policy.Action{policy.ActionRead},
Resource: rbac.ResourceGroup.WithID(groupID).InOrg(orgID),
AuthorizeMap: map[bool][]authSubject{
true: {owner, orgAdmin, userAdmin, templateAdmin},
+5 -3
View File
@@ -6,6 +6,8 @@ import (
"github.com/google/uuid"
"golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/rbac/policy"
)
type WorkspaceAgentScopeParams struct {
@@ -58,7 +60,7 @@ var builtinScopes = map[ScopeName]Scope{
Role: Role{
Name: fmt.Sprintf("Scope_%s", ScopeAll),
DisplayName: "All operations",
Site: Permissions(map[string][]Action{
Site: Permissions(map[string][]policy.Action{
ResourceWildcard.Type: {WildcardSymbol},
}),
Org: map[string][]Permission{},
@@ -71,8 +73,8 @@ var builtinScopes = map[ScopeName]Scope{
Role: Role{
Name: fmt.Sprintf("Scope_%s", ScopeApplicationConnect),
DisplayName: "Ability to connect to applications",
Site: Permissions(map[string][]Action{
ResourceWorkspaceApplicationConnect.Type: {ActionCreate},
Site: Permissions(map[string][]policy.Action{
ResourceWorkspaceApplicationConnect.Type: {policy.ActionCreate},
}),
Org: map[string][]Permission{},
User: []Permission{},