From 6fef6d1a195beef8e63ed7e1d1626eaf8034b478 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 31 Mar 2023 09:53:39 -0500 Subject: [PATCH 1/3] chore: Merge more rbac files - Remove cache.go -> authz.go - Remove query.go -> authz.go - Remove role.go -> roles.go --- coderd/rbac/authz.go | 163 ++++++++++++++++++++++++++++++++++++++ coderd/rbac/authz_test.go | 105 ++++++++++++++++++++++++ coderd/rbac/cache.go | 109 ------------------------- coderd/rbac/cache_test.go | 114 -------------------------- coderd/rbac/query.go | 59 -------------- coderd/rbac/role.go | 58 -------------- coderd/rbac/roles.go | 58 ++++++++++++++ coderd/rbac/scopes.go | 70 ++++++++-------- 8 files changed, 361 insertions(+), 375 deletions(-) delete mode 100644 coderd/rbac/cache.go delete mode 100644 coderd/rbac/cache_test.go delete mode 100644 coderd/rbac/query.go delete mode 100644 coderd/rbac/role.go diff --git a/coderd/rbac/authz.go b/coderd/rbac/authz.go index 8ebb047b7c8c0..1aa4bed30d628 100644 --- a/coderd/rbac/authz.go +++ b/coderd/rbac/authz.go @@ -3,9 +3,12 @@ package rbac import ( "context" _ "embed" + "strings" "sync" "time" + "github.com/coder/coder/coderd/rbac/regosql/sqltypes" + "github.com/open-policy-agent/opa/ast" "github.com/open-policy-agent/opa/rego" "github.com/prometheus/client_golang/prometheus" @@ -34,6 +37,12 @@ func AllActions() []Action { return []Action{ActionCreate, ActionRead, ActionUpdate, ActionDelete} } +type AuthCall struct { + Actor Subject + Action Action + Object Object +} + // Subject is a struct that contains all the elements of a subject in an rbac // authorize. type Subject struct { @@ -519,6 +528,160 @@ func (a RegoAuthorizer) newPartialAuthorizer(ctx context.Context, subject Subjec return pAuth, nil } +// AuthorizeFilter is a compiled partial query that can be converted to SQL. +// This allows enforcing the policy on the database side in a WHERE clause. +type AuthorizeFilter interface { + SQLString() string +} + +type authorizedSQLFilter struct { + sqlString string + auth *PartialAuthorizer +} + +// ConfigWithACL is the basic configuration for converting rego to SQL when +// the object has group and user ACL fields. +func ConfigWithACL() regosql.ConvertConfig { + return regosql.ConvertConfig{ + VariableConverter: regosql.DefaultVariableConverter(), + } +} + +// ConfigWithoutACL is the basic configuration for converting rego to SQL when +// the object has no ACL fields. +func ConfigWithoutACL() regosql.ConvertConfig { + return regosql.ConvertConfig{ + VariableConverter: regosql.NoACLConverter(), + } +} + +func Compile(cfg regosql.ConvertConfig, pa *PartialAuthorizer) (AuthorizeFilter, error) { + root, err := regosql.ConvertRegoAst(cfg, pa.partialQueries) + if err != nil { + return nil, xerrors.Errorf("convert rego ast: %w", err) + } + + // Generate the SQL + gen := sqltypes.NewSQLGenerator() + sqlString := root.SQLString(gen) + if len(gen.Errors()) > 0 { + var errStrings []string + for _, err := range gen.Errors() { + errStrings = append(errStrings, err.Error()) + } + return nil, xerrors.Errorf("sql generation errors: %v", strings.Join(errStrings, ", ")) + } + + return &authorizedSQLFilter{ + sqlString: sqlString, + auth: pa, + }, nil +} + +func (a *authorizedSQLFilter) SQLString() string { + return a.sqlString +} + +type cachedCalls struct { + authz Authorizer +} + +// Cacher returns an Authorizer that can use a cache stored on a context +// to short circuit duplicate calls to the Authorizer. This is useful when +// multiple calls are made to the Authorizer for the same subject, action, and +// object. The cache is on each `ctx` and is not shared between requests. +// If no cache is found on the context, the Authorizer is called as normal. +// +// Cacher is safe for multiple actors. +func Cacher(authz Authorizer) Authorizer { + return &cachedCalls{authz: authz} +} + +func (c *cachedCalls) Authorize(ctx context.Context, subject Subject, action Action, object Object) error { + cache := cacheFromContext(ctx) + + resp, ok := cache.Load(subject, action, object) + if ok { + return resp + } + + err := c.authz.Authorize(ctx, subject, action, object) + cache.Save(subject, action, object, err) + return err +} + +// 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 *cachedCalls) Prepare(ctx context.Context, subject Subject, action Action, objectType string) (PreparedAuthorized, error) { + return c.authz.Prepare(ctx, subject, action, objectType) +} + +// authorizeCache enabled caching of Authorizer calls for a given request. This +// prevents the cost of running the same rbac checks multiple times. +// A cache hit must match on all 3 values: subject, action, and object. +type authorizeCache struct { + sync.Mutex + // calls is a list of all calls made to the Authorizer. + // This list is cached per request context. The size of this list is expected + // to be incredibly small. Often 1 or 2 calls. + calls []cachedAuthCall +} + +type cachedAuthCall struct { + AuthCall + Err error +} + +// cacheContextKey is a context key used to store the cache in the context. +type cacheContextKey struct{} + +// cacheFromContext returns the cache from the context. +// If there is no cache, a nil value is returned. +// The nil cache can still be called as a normal cache, but will not cache or +// return any values. +func cacheFromContext(ctx context.Context) *authorizeCache { + cache, _ := ctx.Value(cacheContextKey{}).(*authorizeCache) + return cache +} + +func WithCacheCtx(ctx context.Context) context.Context { + return context.WithValue(ctx, cacheContextKey{}, &authorizeCache{}) +} + +//nolint:revive +func (c *authorizeCache) Load(subject Subject, action Action, object Object) (error, bool) { + if c == nil { + return nil, false + } + c.Lock() + defer c.Unlock() + + for _, call := range c.calls { + if call.Action == action && call.Object.Equal(object) && call.Actor.Equal(subject) { + return call.Err, true + } + } + return nil, false +} + +func (c *authorizeCache) Save(subject Subject, action Action, object Object, err error) { + if c == nil { + return + } + c.Lock() + defer c.Unlock() + + c.calls = append(c.calls, cachedAuthCall{ + AuthCall: AuthCall{ + Actor: subject, + Action: action, + Object: object, + }, + Err: err, + }) +} + // 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 { diff --git a/coderd/rbac/authz_test.go b/coderd/rbac/authz_test.go index 381319176a347..4201f81231347 100644 --- a/coderd/rbac/authz_test.go +++ b/coderd/rbac/authz_test.go @@ -2,6 +2,8 @@ package rbac_test import ( "context" + "fmt" + "github.com/coder/coder/coderd/coderdtest" "testing" "github.com/google/uuid" @@ -233,3 +235,106 @@ func benchmarkSetup(orgs []uuid.UUID, users []uuid.UUID, size int, opts ...func( return objectList } + + +// BenchmarkCacher benchmarks the performance of the cacher with a given +// cache size. The expected cache size in prod will usually be 1-2. In Filter +// cases it can get as high as 10. +func BenchmarkCacher(b *testing.B) { + b.ResetTimer() + // Size of the cache. + sizes := []int{1, 10, 100, 1000} + for _, size := range sizes { + b.Run(fmt.Sprintf("Size%d", size), func(b *testing.B) { + ctx := rbac.WithCacheCtx(context.Background()) + authz := rbac.Cacher(&coderdtest.FakeAuthorizer{AlwaysReturn: nil}) + for i := 0; i < size; i++ { + // Preload the cache of a given size + subj, obj, action := coderdtest.RandomRBACSubject(), coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction() + _ = authz.Authorize(ctx, subj, action, obj) + } + + // Cache is loaded as a slice, so this cache hit is always the last element. + subj, obj, action := coderdtest.RandomRBACSubject(), coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = authz.Authorize(ctx, subj, action, obj) + } + }) + } +} + +func TestCacher(t *testing.T) { + t.Parallel() + + t.Run("EmptyCacheCtx", func(t *testing.T) { + t.Parallel() + + ctx := context.Background() + rec := &coderdtest.RecordingAuthorizer{ + Wrapped: &coderdtest.FakeAuthorizer{AlwaysReturn: nil}, + } + authz := rbac.Cacher(rec) + subj, obj, action := coderdtest.RandomRBACSubject(), coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction() + + // Two identical calls + _ = authz.Authorize(ctx, subj, action, obj) + _ = authz.Authorize(ctx, subj, action, obj) + + // Yields two calls to the wrapped Authorizer + rec.AssertActor(t, subj, rec.Pair(action, obj), rec.Pair(action, obj)) + require.NoError(t, rec.AllAsserted(), "all assertions should have been made") + }) + + t.Run("CacheCtx", func(t *testing.T) { + t.Parallel() + + ctx := rbac.WithCacheCtx(context.Background()) + rec := &coderdtest.RecordingAuthorizer{ + Wrapped: &coderdtest.FakeAuthorizer{AlwaysReturn: nil}, + } + authz := rbac.Cacher(rec) + subj, obj, action := coderdtest.RandomRBACSubject(), coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction() + + // Two identical calls + _ = authz.Authorize(ctx, subj, action, obj) + _ = authz.Authorize(ctx, subj, action, obj) + + // Yields only 1 call to the wrapped Authorizer for that subject + rec.AssertActor(t, subj, rec.Pair(action, obj)) + require.NoError(t, rec.AllAsserted(), "all assertions should have been made") + }) + + t.Run("MultipleSubjects", func(t *testing.T) { + t.Parallel() + + ctx := rbac.WithCacheCtx(context.Background()) + rec := &coderdtest.RecordingAuthorizer{ + Wrapped: &coderdtest.FakeAuthorizer{AlwaysReturn: nil}, + } + authz := rbac.Cacher(rec) + subj1, obj1, action1 := coderdtest.RandomRBACSubject(), coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction() + + // Two identical calls + _ = authz.Authorize(ctx, subj1, action1, obj1) + _ = authz.Authorize(ctx, subj1, action1, obj1) + + // Extra unique calls + var pairs []coderdtest.ActionObjectPair + subj2, obj2, action2 := coderdtest.RandomRBACSubject(), coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction() + _ = authz.Authorize(ctx, subj2, action2, obj2) + pairs = append(pairs, rec.Pair(action2, obj2)) + + obj3, action3 := coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction() + _ = authz.Authorize(ctx, subj2, action3, obj3) + pairs = append(pairs, rec.Pair(action3, obj3)) + + // Extra identical call after some unique calls + _ = authz.Authorize(ctx, subj1, action1, obj1) + + // Yields 3 calls, 1 for the first subject, 2 for the unique subjects + rec.AssertActor(t, subj1, rec.Pair(action1, obj1)) + rec.AssertActor(t, subj2, pairs...) + require.NoError(t, rec.AllAsserted(), "all assertions should have been made") + }) +} diff --git a/coderd/rbac/cache.go b/coderd/rbac/cache.go deleted file mode 100644 index 6095f8b3d60a0..0000000000000 --- a/coderd/rbac/cache.go +++ /dev/null @@ -1,109 +0,0 @@ -package rbac - -import ( - "context" - "sync" -) - -type AuthCall struct { - Actor Subject - Action Action - Object Object -} - -type cachedCalls struct { - authz Authorizer -} - -// Cacher returns an Authorizer that can use a cache stored on a context -// to short circuit duplicate calls to the Authorizer. This is useful when -// multiple calls are made to the Authorizer for the same subject, action, and -// object. The cache is on each `ctx` and is not shared between requests. -// If no cache is found on the context, the Authorizer is called as normal. -// -// Cacher is safe for multiple actors. -func Cacher(authz Authorizer) Authorizer { - return &cachedCalls{authz: authz} -} - -func (c *cachedCalls) Authorize(ctx context.Context, subject Subject, action Action, object Object) error { - cache := cacheFromContext(ctx) - - resp, ok := cache.Load(subject, action, object) - if ok { - return resp - } - - err := c.authz.Authorize(ctx, subject, action, object) - cache.Save(subject, action, object, err) - return err -} - -// 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 *cachedCalls) Prepare(ctx context.Context, subject Subject, action Action, objectType string) (PreparedAuthorized, error) { - return c.authz.Prepare(ctx, subject, action, objectType) -} - -type cachedAuthCall struct { - AuthCall - Err error -} - -type authorizeCache struct { - sync.Mutex - // calls is a list of all calls made to the Authorizer. - // This list is cached per request context. The size of this list is expected - // to be incredibly small. Often 1 or 2 calls. - calls []cachedAuthCall -} - -//nolint:revive -func (c *authorizeCache) Load(subject Subject, action Action, object Object) (error, bool) { - if c == nil { - return nil, false - } - c.Lock() - defer c.Unlock() - - for _, call := range c.calls { - if call.Action == action && call.Object.Equal(object) && call.Actor.Equal(subject) { - return call.Err, true - } - } - return nil, false -} - -func (c *authorizeCache) Save(subject Subject, action Action, object Object, err error) { - if c == nil { - return - } - c.Lock() - defer c.Unlock() - - c.calls = append(c.calls, cachedAuthCall{ - AuthCall: AuthCall{ - Actor: subject, - Action: action, - Object: object, - }, - Err: err, - }) -} - -// cacheContextKey is a context key used to store the cache in the context. -type cacheContextKey struct{} - -// cacheFromContext returns the cache from the context. -// If there is no cache, a nil value is returned. -// The nil cache can still be called as a normal cache, but will not cache or -// return any values. -func cacheFromContext(ctx context.Context) *authorizeCache { - cache, _ := ctx.Value(cacheContextKey{}).(*authorizeCache) - return cache -} - -func WithCacheCtx(ctx context.Context) context.Context { - return context.WithValue(ctx, cacheContextKey{}, &authorizeCache{}) -} diff --git a/coderd/rbac/cache_test.go b/coderd/rbac/cache_test.go deleted file mode 100644 index 03f7068f415a7..0000000000000 --- a/coderd/rbac/cache_test.go +++ /dev/null @@ -1,114 +0,0 @@ -package rbac_test - -import ( - "context" - "fmt" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/coder/coder/coderd/coderdtest" - "github.com/coder/coder/coderd/rbac" -) - -// BenchmarkCacher benchmarks the performance of the cacher with a given -// cache size. The expected cache size in prod will usually be 1-2. In Filter -// cases it can get as high as 10. -func BenchmarkCacher(b *testing.B) { - b.ResetTimer() - // Size of the cache. - sizes := []int{1, 10, 100, 1000} - for _, size := range sizes { - b.Run(fmt.Sprintf("Size%d", size), func(b *testing.B) { - ctx := rbac.WithCacheCtx(context.Background()) - authz := rbac.Cacher(&coderdtest.FakeAuthorizer{AlwaysReturn: nil}) - for i := 0; i < size; i++ { - // Preload the cache of a given size - subj, obj, action := coderdtest.RandomRBACSubject(), coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction() - _ = authz.Authorize(ctx, subj, action, obj) - } - - // Cache is loaded as a slice, so this cache hit is always the last element. - subj, obj, action := coderdtest.RandomRBACSubject(), coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction() - b.ResetTimer() - for i := 0; i < b.N; i++ { - _ = authz.Authorize(ctx, subj, action, obj) - } - }) - } -} - -func TestCacher(t *testing.T) { - t.Parallel() - - t.Run("EmptyCacheCtx", func(t *testing.T) { - t.Parallel() - - ctx := context.Background() - rec := &coderdtest.RecordingAuthorizer{ - Wrapped: &coderdtest.FakeAuthorizer{AlwaysReturn: nil}, - } - authz := rbac.Cacher(rec) - subj, obj, action := coderdtest.RandomRBACSubject(), coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction() - - // Two identical calls - _ = authz.Authorize(ctx, subj, action, obj) - _ = authz.Authorize(ctx, subj, action, obj) - - // Yields two calls to the wrapped Authorizer - rec.AssertActor(t, subj, rec.Pair(action, obj), rec.Pair(action, obj)) - require.NoError(t, rec.AllAsserted(), "all assertions should have been made") - }) - - t.Run("CacheCtx", func(t *testing.T) { - t.Parallel() - - ctx := rbac.WithCacheCtx(context.Background()) - rec := &coderdtest.RecordingAuthorizer{ - Wrapped: &coderdtest.FakeAuthorizer{AlwaysReturn: nil}, - } - authz := rbac.Cacher(rec) - subj, obj, action := coderdtest.RandomRBACSubject(), coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction() - - // Two identical calls - _ = authz.Authorize(ctx, subj, action, obj) - _ = authz.Authorize(ctx, subj, action, obj) - - // Yields only 1 call to the wrapped Authorizer for that subject - rec.AssertActor(t, subj, rec.Pair(action, obj)) - require.NoError(t, rec.AllAsserted(), "all assertions should have been made") - }) - - t.Run("MultipleSubjects", func(t *testing.T) { - t.Parallel() - - ctx := rbac.WithCacheCtx(context.Background()) - rec := &coderdtest.RecordingAuthorizer{ - Wrapped: &coderdtest.FakeAuthorizer{AlwaysReturn: nil}, - } - authz := rbac.Cacher(rec) - subj1, obj1, action1 := coderdtest.RandomRBACSubject(), coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction() - - // Two identical calls - _ = authz.Authorize(ctx, subj1, action1, obj1) - _ = authz.Authorize(ctx, subj1, action1, obj1) - - // Extra unique calls - var pairs []coderdtest.ActionObjectPair - subj2, obj2, action2 := coderdtest.RandomRBACSubject(), coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction() - _ = authz.Authorize(ctx, subj2, action2, obj2) - pairs = append(pairs, rec.Pair(action2, obj2)) - - obj3, action3 := coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction() - _ = authz.Authorize(ctx, subj2, action3, obj3) - pairs = append(pairs, rec.Pair(action3, obj3)) - - // Extra identical call after some unique calls - _ = authz.Authorize(ctx, subj1, action1, obj1) - - // Yields 3 calls, 1 for the first subject, 2 for the unique subjects - rec.AssertActor(t, subj1, rec.Pair(action1, obj1)) - rec.AssertActor(t, subj2, pairs...) - require.NoError(t, rec.AllAsserted(), "all assertions should have been made") - }) -} diff --git a/coderd/rbac/query.go b/coderd/rbac/query.go deleted file mode 100644 index fcf75c51f2956..0000000000000 --- a/coderd/rbac/query.go +++ /dev/null @@ -1,59 +0,0 @@ -package rbac - -import ( - "strings" - - "github.com/coder/coder/coderd/rbac/regosql" - - "github.com/coder/coder/coderd/rbac/regosql/sqltypes" - - "golang.org/x/xerrors" -) - -type AuthorizeFilter interface { - SQLString() string -} - -type authorizedSQLFilter struct { - sqlString string - auth *PartialAuthorizer -} - -func ConfigWithACL() regosql.ConvertConfig { - return regosql.ConvertConfig{ - VariableConverter: regosql.DefaultVariableConverter(), - } -} - -func ConfigWithoutACL() regosql.ConvertConfig { - return regosql.ConvertConfig{ - VariableConverter: regosql.NoACLConverter(), - } -} - -func Compile(cfg regosql.ConvertConfig, pa *PartialAuthorizer) (AuthorizeFilter, error) { - root, err := regosql.ConvertRegoAst(cfg, pa.partialQueries) - if err != nil { - return nil, xerrors.Errorf("convert rego ast: %w", err) - } - - // Generate the SQL - gen := sqltypes.NewSQLGenerator() - sqlString := root.SQLString(gen) - if len(gen.Errors()) > 0 { - var errStrings []string - for _, err := range gen.Errors() { - errStrings = append(errStrings, err.Error()) - } - return nil, xerrors.Errorf("sql generation errors: %v", strings.Join(errStrings, ", ")) - } - - return &authorizedSQLFilter{ - sqlString: sqlString, - auth: pa, - }, nil -} - -func (a *authorizedSQLFilter) SQLString() string { - return a.sqlString -} diff --git a/coderd/rbac/role.go b/coderd/rbac/role.go deleted file mode 100644 index c181ba6fe80a4..0000000000000 --- a/coderd/rbac/role.go +++ /dev/null @@ -1,58 +0,0 @@ -package rbac - -// ExpandableRoles is any type that can be expanded into a []Role. This is implemented -// as an interface so we can have RoleNames for user defined roles, and implement -// custom ExpandableRoles for system type users (eg autostart/autostop system role). -// We want a clear divide between the two types of roles so users have no codepath -// to interact or assign system roles. -// -// Note: We may also want to do the same thing with scopes to allow custom scope -// support unavailable to the user. Eg: Scope to a single resource. -type ExpandableRoles interface { - Expand() ([]Role, error) - // Names is for logging and tracing purposes, we want to know the human - // names of the expanded roles. - Names() []string -} - -// 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"` -} - -// Role is a set of permissions at multiple levels: -// - Site level permissions apply EVERYWHERE -// - Org level permissions apply to EVERYTHING in a given ORG -// - User level permissions are the lowest -// This is the type passed into the rego as a json payload. -// Users of this package should instead **only** use the role names, and -// this package will expand the role names into their json payloads. -type Role struct { - Name string `json:"name"` - // DisplayName is used for UI purposes. If the role has no display name, - // that means the UI should never display it. - DisplayName string `json:"display_name"` - Site []Permission `json:"site"` - // Org is a map of orgid to permissions. We represent orgid as a string. - // We scope the organizations in the role so we can easily combine all the - // roles. - Org map[string][]Permission `json:"org"` - User []Permission `json:"user"` -} - -type Roles []Role - -func (roles Roles) Expand() ([]Role, error) { - return roles, nil -} - -func (roles Roles) Names() []string { - names := make([]string, 0, len(roles)) - for _, r := range roles { - return append(names, r.Name) - } - return names -} diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index a4cacd2208e81..36c9efdff44db 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -242,6 +242,64 @@ var assignRoles = map[string]map[string]bool{ }, } +// ExpandableRoles is any type that can be expanded into a []Role. This is implemented +// as an interface so we can have RoleNames for user defined roles, and implement +// custom ExpandableRoles for system type users (eg autostart/autostop system role). +// We want a clear divide between the two types of roles so users have no codepath +// to interact or assign system roles. +// +// Note: We may also want to do the same thing with scopes to allow custom scope +// support unavailable to the user. Eg: Scope to a single resource. +type ExpandableRoles interface { + Expand() ([]Role, error) + // Names is for logging and tracing purposes, we want to know the human + // names of the expanded roles. + Names() []string +} + +// 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"` +} + +// Role is a set of permissions at multiple levels: +// - Site level permissions apply EVERYWHERE +// - Org level permissions apply to EVERYTHING in a given ORG +// - User level permissions are the lowest +// This is the type passed into the rego as a json payload. +// Users of this package should instead **only** use the role names, and +// this package will expand the role names into their json payloads. +type Role struct { + Name string `json:"name"` + // DisplayName is used for UI purposes. If the role has no display name, + // that means the UI should never display it. + DisplayName string `json:"display_name"` + Site []Permission `json:"site"` + // Org is a map of orgid to permissions. We represent orgid as a string. + // We scope the organizations in the role so we can easily combine all the + // roles. + Org map[string][]Permission `json:"org"` + User []Permission `json:"user"` +} + +type Roles []Role + +func (roles Roles) Expand() ([]Role, error) { + return roles, nil +} + +func (roles Roles) Names() []string { + names := make([]string, 0, len(roles)) + for _, r := range roles { + return append(names, r.Name) + } + return names +} + + // CanAssignRole is a helper function that returns true if the user can assign // the specified role. This also can be used for removing a role. // This is a simple implementation for now. diff --git a/coderd/rbac/scopes.go b/coderd/rbac/scopes.go index 6bec8b5533d78..76ec79ed0be85 100644 --- a/coderd/rbac/scopes.go +++ b/coderd/rbac/scopes.go @@ -8,41 +8,6 @@ import ( "golang.org/x/xerrors" ) -type ExpandableScope interface { - Expand() (Scope, error) - // Name is for logging and tracing purposes, we want to know the human - // name of the scope. - Name() string -} - -type ScopeName string - -func (name ScopeName) Expand() (Scope, error) { - return ExpandScope(name) -} - -func (name ScopeName) Name() string { - return string(name) -} - -// Scope acts the exact same as a Role with the addition that is can also -// apply an AllowIDList. Any resource being checked against a Scope will -// reject any resource that is not in the AllowIDList. -// To not use an AllowIDList to reject authorization, use a wildcard for the -// AllowIDList. Eg: 'AllowIDList: []string{WildcardSymbol}' -type Scope struct { - Role - AllowIDList []string `json:"allow_list"` -} - -func (s Scope) Expand() (Scope, error) { - return s, nil -} - -func (s Scope) Name() string { - return s.Role.Name -} - // WorkspaceAgentScope returns a scope that is the same as ScopeAll but can only // affect resources in the allow list. Only a scope is returned as the roles // should come from the workspace owner. @@ -102,6 +67,41 @@ var builtinScopes = map[ScopeName]Scope{ }, } +type ExpandableScope interface { + Expand() (Scope, error) + // Name is for logging and tracing purposes, we want to know the human + // name of the scope. + Name() string +} + +type ScopeName string + +func (name ScopeName) Expand() (Scope, error) { + return ExpandScope(name) +} + +func (name ScopeName) Name() string { + return string(name) +} + +// Scope acts the exact same as a Role with the addition that is can also +// apply an AllowIDList. Any resource being checked against a Scope will +// reject any resource that is not in the AllowIDList. +// To not use an AllowIDList to reject authorization, use a wildcard for the +// AllowIDList. Eg: 'AllowIDList: []string{WildcardSymbol}' +type Scope struct { + Role + AllowIDList []string `json:"allow_list"` +} + +func (s Scope) Expand() (Scope, error) { + return s, nil +} + +func (s Scope) Name() string { + return s.Role.Name +} + func ExpandScope(scope ScopeName) (Scope, error) { role, ok := builtinScopes[scope] if !ok { From a388748d7d11dc574c073da05153bea9de8d5c96 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 31 Mar 2023 09:55:23 -0500 Subject: [PATCH 2/3] Order imports --- coderd/rbac/authz.go | 3 +-- coderd/rbac/authz_test.go | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/coderd/rbac/authz.go b/coderd/rbac/authz.go index 1aa4bed30d628..43aeb68583be9 100644 --- a/coderd/rbac/authz.go +++ b/coderd/rbac/authz.go @@ -7,8 +7,6 @@ import ( "sync" "time" - "github.com/coder/coder/coderd/rbac/regosql/sqltypes" - "github.com/open-policy-agent/opa/ast" "github.com/open-policy-agent/opa/rego" "github.com/prometheus/client_golang/prometheus" @@ -18,6 +16,7 @@ import ( "golang.org/x/xerrors" "github.com/coder/coder/coderd/rbac/regosql" + "github.com/coder/coder/coderd/rbac/regosql/sqltypes" "github.com/coder/coder/coderd/tracing" "github.com/coder/coder/coderd/util/slice" ) diff --git a/coderd/rbac/authz_test.go b/coderd/rbac/authz_test.go index 4201f81231347..1dd9b3b085196 100644 --- a/coderd/rbac/authz_test.go +++ b/coderd/rbac/authz_test.go @@ -3,13 +3,13 @@ package rbac_test import ( "context" "fmt" - "github.com/coder/coder/coderd/coderdtest" "testing" "github.com/google/uuid" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/require" + "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/coderd/rbac" ) @@ -236,7 +236,6 @@ func benchmarkSetup(orgs []uuid.UUID, users []uuid.UUID, size int, opts ...func( return objectList } - // BenchmarkCacher benchmarks the performance of the cacher with a given // cache size. The expected cache size in prod will usually be 1-2. In Filter // cases it can get as high as 10. From edea703a8a74fd43c1af505d239d70b9f5fee435 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Fri, 31 Mar 2023 10:47:09 -0500 Subject: [PATCH 3/3] fmt --- coderd/rbac/roles.go | 1 - 1 file changed, 1 deletion(-) diff --git a/coderd/rbac/roles.go b/coderd/rbac/roles.go index 36c9efdff44db..f5b5736db0f72 100644 --- a/coderd/rbac/roles.go +++ b/coderd/rbac/roles.go @@ -299,7 +299,6 @@ func (roles Roles) Names() []string { return names } - // CanAssignRole is a helper function that returns true if the user can assign // the specified role. This also can be used for removing a role. // This is a simple implementation for now.