@@ -3,6 +3,7 @@ package rbac
3
3
import (
4
4
"context"
5
5
_ "embed"
6
+ "strings"
6
7
"sync"
7
8
"time"
8
9
@@ -15,6 +16,7 @@ import (
15
16
"golang.org/x/xerrors"
16
17
17
18
"github.com/coder/coder/coderd/rbac/regosql"
19
+ "github.com/coder/coder/coderd/rbac/regosql/sqltypes"
18
20
"github.com/coder/coder/coderd/tracing"
19
21
"github.com/coder/coder/coderd/util/slice"
20
22
)
@@ -34,6 +36,12 @@ func AllActions() []Action {
34
36
return []Action {ActionCreate , ActionRead , ActionUpdate , ActionDelete }
35
37
}
36
38
39
+ type AuthCall struct {
40
+ Actor Subject
41
+ Action Action
42
+ Object Object
43
+ }
44
+
37
45
// Subject is a struct that contains all the elements of a subject in an rbac
38
46
// authorize.
39
47
type Subject struct {
@@ -519,6 +527,160 @@ func (a RegoAuthorizer) newPartialAuthorizer(ctx context.Context, subject Subjec
519
527
return pAuth , nil
520
528
}
521
529
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
+
522
684
// rbacTraceAttributes are the attributes that are added to all spans created by
523
685
// the rbac package. These attributes should help to debug slow spans.
524
686
func rbacTraceAttributes (actor Subject , action Action , objectType string , extra ... attribute.KeyValue ) trace.SpanStartOption {
0 commit comments