@@ -2,11 +2,14 @@ package rbac
22
33import (
44"context"
5+ "crypto/sha256"
56_"embed"
7+ "encoding/json"
68"strings"
79"sync"
810"time"
911
12+ "github.com/ammario/tlru"
1013"github.com/open-policy-agent/opa/ast"
1114"github.com/open-policy-agent/opa/rego"
1215"github.com/prometheus/client_golang/prometheus"
@@ -42,6 +45,31 @@ type AuthCall struct {
4245Object Object
4346}
4447
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+
4573// Subject is a struct that contains all the elements of a subject in an rbac
4674// authorize.
4775type Subject struct {
@@ -101,6 +129,9 @@ func (s Subject) SafeRoleNames() []string {
101129}
102130
103131type 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.
104135Authorize (ctx context.Context ,subject Subject ,action Action ,object Object )error
105136Prepare (ctx context.Context ,subject Subject ,action Action ,objectType string ) (PreparedAuthorized ,error )
106137}
@@ -310,6 +341,7 @@ func (a RegoAuthorizer) Authorize(ctx context.Context, subject Subject, action A
310341defer span .End ()
311342
312343err := a .authorize (ctx ,subject ,action ,object )
344+
313345span .SetAttributes (attribute .Bool ("authorized" ,err == nil ))
314346
315347dur := time .Since (start )
@@ -605,7 +637,12 @@ func (a *authorizedSQLFilter) SQLString() string {
605637return a .sqlString
606638}
607639
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+
609646authz Authorizer
610647}
611648
@@ -617,94 +654,35 @@ type cachedCalls struct {
617654//
618655// Cacher is safe for multiple actors.
619656func 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+ }
621663}
622664
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 )
625667
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 )
629674}
630675
631- err := c .authz .Authorize (ctx ,subject ,action ,object )
632- cache .Save (subject ,action ,object ,err )
633676return err
634677}
635678
636679// Prepare returns the underlying PreparedAuthorized. The cache does not apply
637680// to prepared authorizations. These should be using a SQL filter, and
638681// 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 ) {
640683return c .authz .Prepare (ctx ,subject ,action ,objectType )
641684}
642685
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-
708686// rbacTraceAttributes are the attributes that are added to all spans created by
709687// the rbac package. These attributes should help to debug slow spans.
710688func rbacTraceAttributes (actor Subject ,action Action ,objectType string ,extra ... attribute.KeyValue ) trace.SpanStartOption {