@@ -2,11 +2,14 @@ package rbac
2
2
3
3
import (
4
4
"context"
5
+ "crypto/sha256"
5
6
_ "embed"
7
+ "encoding/json"
6
8
"strings"
7
9
"sync"
8
10
"time"
9
11
12
+ "github.com/ammario/tlru"
10
13
"github.com/open-policy-agent/opa/ast"
11
14
"github.com/open-policy-agent/opa/rego"
12
15
"github.com/prometheus/client_golang/prometheus"
@@ -42,6 +45,31 @@ type AuthCall struct {
42
45
Object Object
43
46
}
44
47
48
+ // hashAuthorizeCall guarantees a unique hash for a given auth call.
49
+ // If two hashes are equal, then the result of a given authorize() call
50
+ // will be the same.
51
+ //
52
+ // Note that this ignores some fields such as the permissions within a given
53
+ // role, as this assumes all roles are static to a given role name.
54
+ func hashAuthorizeCall (actor Subject , action Action , object Object ) [32 ]byte {
55
+ var hashOut [32 ]byte
56
+ hash := sha256 .New ()
57
+
58
+ // We use JSON for the forward security benefits if the rbac structs are
59
+ // modified without consideration for the caching layer.
60
+ enc := json .NewEncoder (hash )
61
+ _ = enc .Encode (actor )
62
+ _ = enc .Encode (action )
63
+ _ = enc .Encode (object )
64
+
65
+ // We might be able to avoid this extra copy?
66
+ // sha256.Sum256() returns a [32]byte. We need to return
67
+ // an array vs a slice so we can use it as a key in the cache.
68
+ image := hash .Sum (nil )
69
+ copy (hashOut [:], image )
70
+ return hashOut
71
+ }
72
+
45
73
// Subject is a struct that contains all the elements of a subject in an rbac
46
74
// authorize.
47
75
type Subject struct {
@@ -101,6 +129,9 @@ func (s Subject) SafeRoleNames() []string {
101
129
}
102
130
103
131
type Authorizer interface {
132
+ // Authorize will authorize the given subject to perform the given action
133
+ // on the given object. Authorize is pure and deterministic with respect to
134
+ // its arguments and the surrounding object.
104
135
Authorize (ctx context.Context , subject Subject , action Action , object Object ) error
105
136
Prepare (ctx context.Context , subject Subject , action Action , objectType string ) (PreparedAuthorized , error )
106
137
}
@@ -310,6 +341,7 @@ func (a RegoAuthorizer) Authorize(ctx context.Context, subject Subject, action A
310
341
defer span .End ()
311
342
312
343
err := a .authorize (ctx , subject , action , object )
344
+
313
345
span .SetAttributes (attribute .Bool ("authorized" , err == nil ))
314
346
315
347
dur := time .Since (start )
@@ -605,7 +637,12 @@ func (a *authorizedSQLFilter) SQLString() string {
605
637
return a .sqlString
606
638
}
607
639
608
- type cachedCalls struct {
640
+ type authCache struct {
641
+ // cache is a cache of hashed Authorize inputs to the result of the Authorize
642
+ // call.
643
+ // determistic function.
644
+ cache * tlru.Cache [[32 ]byte , error ]
645
+
609
646
authz Authorizer
610
647
}
611
648
@@ -617,94 +654,35 @@ type cachedCalls struct {
617
654
//
618
655
// Cacher is safe for multiple actors.
619
656
func Cacher (authz Authorizer ) Authorizer {
620
- return & cachedCalls {authz : authz }
657
+ return & authCache {
658
+ authz : authz ,
659
+ // In practice, this cache should never come close to filling since the
660
+ // authorization calls are kept for a minute at most.
661
+ cache : tlru .New [[32 ]byte ](tlru .ConstantCost [error ], 64 * 1024 ),
662
+ }
621
663
}
622
664
623
- func (c * cachedCalls ) Authorize (ctx context.Context , subject Subject , action Action , object Object ) error {
624
- cache := cacheFromContext ( ctx )
665
+ func (c * authCache ) Authorize (ctx context.Context , subject Subject , action Action , object Object ) error {
666
+ authorizeCacheKey := hashAuthorizeCall ( subject , action , object )
625
667
626
- resp , ok := cache .Load (subject , action , object )
627
- if ok {
628
- return resp
668
+ var err error
669
+ err , _ , ok := c .cache .Get (authorizeCacheKey )
670
+ if ! ok {
671
+ err = c .authz .Authorize (ctx , subject , action , object )
672
+ // In case there is a caching bug, bound the TTL to 1 minute.
673
+ c .cache .Set (authorizeCacheKey , err , time .Minute )
629
674
}
630
675
631
- err := c .authz .Authorize (ctx , subject , action , object )
632
- cache .Save (subject , action , object , err )
633
676
return err
634
677
}
635
678
636
679
// Prepare returns the underlying PreparedAuthorized. The cache does not apply
637
680
// to prepared authorizations. These should be using a SQL filter, and
638
681
// therefore the cache is not needed.
639
- func (c * cachedCalls ) Prepare (ctx context.Context , subject Subject , action Action , objectType string ) (PreparedAuthorized , error ) {
682
+ func (c * authCache ) Prepare (ctx context.Context , subject Subject , action Action , objectType string ) (PreparedAuthorized , error ) {
640
683
return c .authz .Prepare (ctx , subject , action , objectType )
641
684
}
642
685
643
- // authorizeCache enabled caching of Authorizer calls for a given request. This
644
- // prevents the cost of running the same rbac checks multiple times.
645
- // A cache hit must match on all 3 values: subject, action, and object.
646
- type authorizeCache struct {
647
- sync.Mutex
648
- // calls is a list of all calls made to the Authorizer.
649
- // This list is cached per request context. The size of this list is expected
650
- // to be incredibly small. Often 1 or 2 calls.
651
- calls []cachedAuthCall
652
- }
653
-
654
- type cachedAuthCall struct {
655
- AuthCall
656
- Err error
657
- }
658
-
659
- // cacheContextKey is a context key used to store the cache in the context.
660
- type cacheContextKey struct {}
661
-
662
- // cacheFromContext returns the cache from the context.
663
- // If there is no cache, a nil value is returned.
664
- // The nil cache can still be called as a normal cache, but will not cache or
665
- // return any values.
666
- func cacheFromContext (ctx context.Context ) * authorizeCache {
667
- cache , _ := ctx .Value (cacheContextKey {}).(* authorizeCache )
668
- return cache
669
- }
670
-
671
- func WithCacheCtx (ctx context.Context ) context.Context {
672
- return context .WithValue (ctx , cacheContextKey {}, & authorizeCache {})
673
- }
674
-
675
- //nolint:revive
676
- func (c * authorizeCache ) Load (subject Subject , action Action , object Object ) (error , bool ) {
677
- if c == nil {
678
- return nil , false
679
- }
680
- c .Lock ()
681
- defer c .Unlock ()
682
-
683
- for _ , call := range c .calls {
684
- if call .Action == action && call .Object .Equal (object ) && call .Actor .Equal (subject ) {
685
- return call .Err , true
686
- }
687
- }
688
- return nil , false
689
- }
690
-
691
- func (c * authorizeCache ) Save (subject Subject , action Action , object Object , err error ) {
692
- if c == nil {
693
- return
694
- }
695
- c .Lock ()
696
- defer c .Unlock ()
697
-
698
- c .calls = append (c .calls , cachedAuthCall {
699
- AuthCall : AuthCall {
700
- Actor : subject ,
701
- Action : action ,
702
- Object : object ,
703
- },
704
- Err : err ,
705
- })
706
- }
707
-
708
686
// rbacTraceAttributes are the attributes that are added to all spans created by
709
687
// the rbac package. These attributes should help to debug slow spans.
710
688
func rbacTraceAttributes (actor Subject , action Action , objectType string , extra ... attribute.KeyValue ) trace.SpanStartOption {
0 commit comments