mirror of https://github.com/coder/coder.git
chore: Merge more rbac files (#6927)
* chore: Merge more rbac files - Remove cache.go -> authz.go - Remove query.go -> authz.go - Remove role.go -> roles.go * Order imports * fmt
This commit is contained in:
parent
333718d1fa
commit
fab8da633b
|
@ -3,6 +3,7 @@ package rbac
|
|||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
@ -15,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"
|
||||
)
|
||||
|
@ -34,6 +36,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 +527,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 {
|
||||
|
|
|
@ -2,12 +2,14 @@ package rbac_test
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"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"
|
||||
)
|
||||
|
||||
|
@ -233,3 +235,105 @@ 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")
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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{})
|
||||
}
|
|
@ -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")
|
||||
})
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -242,6 +242,63 @@ 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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue