Skip to content

Commit fab8da6

Browse files
authored
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
1 parent 333718d commit fab8da6

File tree

8 files changed

+358
-375
lines changed

8 files changed

+358
-375
lines changed

coderd/rbac/authz.go

+162
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package rbac
33
import (
44
"context"
55
_ "embed"
6+
"strings"
67
"sync"
78
"time"
89

@@ -15,6 +16,7 @@ import (
1516
"golang.org/x/xerrors"
1617

1718
"github.com/coder/coder/coderd/rbac/regosql"
19+
"github.com/coder/coder/coderd/rbac/regosql/sqltypes"
1820
"github.com/coder/coder/coderd/tracing"
1921
"github.com/coder/coder/coderd/util/slice"
2022
)
@@ -34,6 +36,12 @@ func AllActions() []Action {
3436
return []Action{ActionCreate, ActionRead, ActionUpdate, ActionDelete}
3537
}
3638

39+
type AuthCall struct {
40+
Actor Subject
41+
Action Action
42+
Object Object
43+
}
44+
3745
// Subject is a struct that contains all the elements of a subject in an rbac
3846
// authorize.
3947
type Subject struct {
@@ -519,6 +527,160 @@ func (a RegoAuthorizer) newPartialAuthorizer(ctx context.Context, subject Subjec
519527
return pAuth, nil
520528
}
521529

530+
// AuthorizeFilter is a compiled partial query that can be converted to SQL.
531+
// This allows enforcing the policy on the database side in a WHERE clause.
532+
type AuthorizeFilter interface {
533+
SQLString() string
534+
}
535+
536+
type authorizedSQLFilter struct {
537+
sqlString string
538+
auth *PartialAuthorizer
539+
}
540+
541+
// ConfigWithACL is the basic configuration for converting rego to SQL when
542+
// the object has group and user ACL fields.
543+
func ConfigWithACL() regosql.ConvertConfig {
544+
return regosql.ConvertConfig{
545+
VariableConverter: regosql.DefaultVariableConverter(),
546+
}
547+
}
548+
549+
// ConfigWithoutACL is the basic configuration for converting rego to SQL when
550+
// the object has no ACL fields.
551+
func ConfigWithoutACL() regosql.ConvertConfig {
552+
return regosql.ConvertConfig{
553+
VariableConverter: regosql.NoACLConverter(),
554+
}
555+
}
556+
557+
func Compile(cfg regosql.ConvertConfig, pa *PartialAuthorizer) (AuthorizeFilter, error) {
558+
root, err := regosql.ConvertRegoAst(cfg, pa.partialQueries)
559+
if err != nil {
560+
return nil, xerrors.Errorf("convert rego ast: %w", err)
561+
}
562+
563+
// Generate the SQL
564+
gen := sqltypes.NewSQLGenerator()
565+
sqlString := root.SQLString(gen)
566+
if len(gen.Errors()) > 0 {
567+
var errStrings []string
568+
for _, err := range gen.Errors() {
569+
errStrings = append(errStrings, err.Error())
570+
}
571+
return nil, xerrors.Errorf("sql generation errors: %v", strings.Join(errStrings, ", "))
572+
}
573+
574+
return &authorizedSQLFilter{
575+
sqlString: sqlString,
576+
auth: pa,
577+
}, nil
578+
}
579+
580+
func (a *authorizedSQLFilter) SQLString() string {
581+
return a.sqlString
582+
}
583+
584+
type cachedCalls struct {
585+
authz Authorizer
586+
}
587+
588+
// Cacher returns an Authorizer that can use a cache stored on a context
589+
// to short circuit duplicate calls to the Authorizer. This is useful when
590+
// multiple calls are made to the Authorizer for the same subject, action, and
591+
// object. The cache is on each `ctx` and is not shared between requests.
592+
// If no cache is found on the context, the Authorizer is called as normal.
593+
//
594+
// Cacher is safe for multiple actors.
595+
func Cacher(authz Authorizer) Authorizer {
596+
return &cachedCalls{authz: authz}
597+
}
598+
599+
func (c *cachedCalls) Authorize(ctx context.Context, subject Subject, action Action, object Object) error {
600+
cache := cacheFromContext(ctx)
601+
602+
resp, ok := cache.Load(subject, action, object)
603+
if ok {
604+
return resp
605+
}
606+
607+
err := c.authz.Authorize(ctx, subject, action, object)
608+
cache.Save(subject, action, object, err)
609+
return err
610+
}
611+
612+
// Prepare returns the underlying PreparedAuthorized. The cache does not apply
613+
// to prepared authorizations. These should be using a SQL filter, and
614+
// therefore the cache is not needed.
615+
func (c *cachedCalls) Prepare(ctx context.Context, subject Subject, action Action, objectType string) (PreparedAuthorized, error) {
616+
return c.authz.Prepare(ctx, subject, action, objectType)
617+
}
618+
619+
// authorizeCache enabled caching of Authorizer calls for a given request. This
620+
// prevents the cost of running the same rbac checks multiple times.
621+
// A cache hit must match on all 3 values: subject, action, and object.
622+
type authorizeCache struct {
623+
sync.Mutex
624+
// calls is a list of all calls made to the Authorizer.
625+
// This list is cached per request context. The size of this list is expected
626+
// to be incredibly small. Often 1 or 2 calls.
627+
calls []cachedAuthCall
628+
}
629+
630+
type cachedAuthCall struct {
631+
AuthCall
632+
Err error
633+
}
634+
635+
// cacheContextKey is a context key used to store the cache in the context.
636+
type cacheContextKey struct{}
637+
638+
// cacheFromContext returns the cache from the context.
639+
// If there is no cache, a nil value is returned.
640+
// The nil cache can still be called as a normal cache, but will not cache or
641+
// return any values.
642+
func cacheFromContext(ctx context.Context) *authorizeCache {
643+
cache, _ := ctx.Value(cacheContextKey{}).(*authorizeCache)
644+
return cache
645+
}
646+
647+
func WithCacheCtx(ctx context.Context) context.Context {
648+
return context.WithValue(ctx, cacheContextKey{}, &authorizeCache{})
649+
}
650+
651+
//nolint:revive
652+
func (c *authorizeCache) Load(subject Subject, action Action, object Object) (error, bool) {
653+
if c == nil {
654+
return nil, false
655+
}
656+
c.Lock()
657+
defer c.Unlock()
658+
659+
for _, call := range c.calls {
660+
if call.Action == action && call.Object.Equal(object) && call.Actor.Equal(subject) {
661+
return call.Err, true
662+
}
663+
}
664+
return nil, false
665+
}
666+
667+
func (c *authorizeCache) Save(subject Subject, action Action, object Object, err error) {
668+
if c == nil {
669+
return
670+
}
671+
c.Lock()
672+
defer c.Unlock()
673+
674+
c.calls = append(c.calls, cachedAuthCall{
675+
AuthCall: AuthCall{
676+
Actor: subject,
677+
Action: action,
678+
Object: object,
679+
},
680+
Err: err,
681+
})
682+
}
683+
522684
// rbacTraceAttributes are the attributes that are added to all spans created by
523685
// the rbac package. These attributes should help to debug slow spans.
524686
func rbacTraceAttributes(actor Subject, action Action, objectType string, extra ...attribute.KeyValue) trace.SpanStartOption {

coderd/rbac/authz_test.go

+104
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,14 @@ package rbac_test
22

33
import (
44
"context"
5+
"fmt"
56
"testing"
67

78
"github.com/google/uuid"
89
"github.com/prometheus/client_golang/prometheus"
910
"github.com/stretchr/testify/require"
1011

12+
"github.com/coder/coder/coderd/coderdtest"
1113
"github.com/coder/coder/coderd/rbac"
1214
)
1315

@@ -233,3 +235,105 @@ func benchmarkSetup(orgs []uuid.UUID, users []uuid.UUID, size int, opts ...func(
233235

234236
return objectList
235237
}
238+
239+
// BenchmarkCacher benchmarks the performance of the cacher with a given
240+
// cache size. The expected cache size in prod will usually be 1-2. In Filter
241+
// cases it can get as high as 10.
242+
func BenchmarkCacher(b *testing.B) {
243+
b.ResetTimer()
244+
// Size of the cache.
245+
sizes := []int{1, 10, 100, 1000}
246+
for _, size := range sizes {
247+
b.Run(fmt.Sprintf("Size%d", size), func(b *testing.B) {
248+
ctx := rbac.WithCacheCtx(context.Background())
249+
authz := rbac.Cacher(&coderdtest.FakeAuthorizer{AlwaysReturn: nil})
250+
for i := 0; i < size; i++ {
251+
// Preload the cache of a given size
252+
subj, obj, action := coderdtest.RandomRBACSubject(), coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction()
253+
_ = authz.Authorize(ctx, subj, action, obj)
254+
}
255+
256+
// Cache is loaded as a slice, so this cache hit is always the last element.
257+
subj, obj, action := coderdtest.RandomRBACSubject(), coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction()
258+
b.ResetTimer()
259+
for i := 0; i < b.N; i++ {
260+
_ = authz.Authorize(ctx, subj, action, obj)
261+
}
262+
})
263+
}
264+
}
265+
266+
func TestCacher(t *testing.T) {
267+
t.Parallel()
268+
269+
t.Run("EmptyCacheCtx", func(t *testing.T) {
270+
t.Parallel()
271+
272+
ctx := context.Background()
273+
rec := &coderdtest.RecordingAuthorizer{
274+
Wrapped: &coderdtest.FakeAuthorizer{AlwaysReturn: nil},
275+
}
276+
authz := rbac.Cacher(rec)
277+
subj, obj, action := coderdtest.RandomRBACSubject(), coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction()
278+
279+
// Two identical calls
280+
_ = authz.Authorize(ctx, subj, action, obj)
281+
_ = authz.Authorize(ctx, subj, action, obj)
282+
283+
// Yields two calls to the wrapped Authorizer
284+
rec.AssertActor(t, subj, rec.Pair(action, obj), rec.Pair(action, obj))
285+
require.NoError(t, rec.AllAsserted(), "all assertions should have been made")
286+
})
287+
288+
t.Run("CacheCtx", func(t *testing.T) {
289+
t.Parallel()
290+
291+
ctx := rbac.WithCacheCtx(context.Background())
292+
rec := &coderdtest.RecordingAuthorizer{
293+
Wrapped: &coderdtest.FakeAuthorizer{AlwaysReturn: nil},
294+
}
295+
authz := rbac.Cacher(rec)
296+
subj, obj, action := coderdtest.RandomRBACSubject(), coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction()
297+
298+
// Two identical calls
299+
_ = authz.Authorize(ctx, subj, action, obj)
300+
_ = authz.Authorize(ctx, subj, action, obj)
301+
302+
// Yields only 1 call to the wrapped Authorizer for that subject
303+
rec.AssertActor(t, subj, rec.Pair(action, obj))
304+
require.NoError(t, rec.AllAsserted(), "all assertions should have been made")
305+
})
306+
307+
t.Run("MultipleSubjects", func(t *testing.T) {
308+
t.Parallel()
309+
310+
ctx := rbac.WithCacheCtx(context.Background())
311+
rec := &coderdtest.RecordingAuthorizer{
312+
Wrapped: &coderdtest.FakeAuthorizer{AlwaysReturn: nil},
313+
}
314+
authz := rbac.Cacher(rec)
315+
subj1, obj1, action1 := coderdtest.RandomRBACSubject(), coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction()
316+
317+
// Two identical calls
318+
_ = authz.Authorize(ctx, subj1, action1, obj1)
319+
_ = authz.Authorize(ctx, subj1, action1, obj1)
320+
321+
// Extra unique calls
322+
var pairs []coderdtest.ActionObjectPair
323+
subj2, obj2, action2 := coderdtest.RandomRBACSubject(), coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction()
324+
_ = authz.Authorize(ctx, subj2, action2, obj2)
325+
pairs = append(pairs, rec.Pair(action2, obj2))
326+
327+
obj3, action3 := coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction()
328+
_ = authz.Authorize(ctx, subj2, action3, obj3)
329+
pairs = append(pairs, rec.Pair(action3, obj3))
330+
331+
// Extra identical call after some unique calls
332+
_ = authz.Authorize(ctx, subj1, action1, obj1)
333+
334+
// Yields 3 calls, 1 for the first subject, 2 for the unique subjects
335+
rec.AssertActor(t, subj1, rec.Pair(action1, obj1))
336+
rec.AssertActor(t, subj2, pairs...)
337+
require.NoError(t, rec.AllAsserted(), "all assertions should have been made")
338+
})
339+
}

0 commit comments

Comments
 (0)