Thanks to visit codestin.com
Credit goes to github.com

Skip to content

chore: add global caching to rbac #7439

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
May 8, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
chore: Add ability to hash an authorizationc all to a unqiue hash key.
This is useful for caching purposes.
Order matters for the hash key for slices, so a slice with
the same elements in a different order is a different hash key.
Athlough they are functionally equivalent, our slices have deterministic
orders in the database, so our subjects/objects will match.
There is no need to spend the time or memory to sort these lists.
  • Loading branch information
Emyrk committed May 5, 2023
commit adc07b0dc6706745082b4d22cb3a8abd0d9c43f6
59 changes: 49 additions & 10 deletions coderd/rbac/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package rbac

import (
"context"
"crypto/sha256"
_ "embed"
"encoding/json"
"strings"
Expand Down Expand Up @@ -44,6 +45,27 @@ type AuthCall struct {
Object Object
}

// AuthCallHash guarantees a unique hash for a given auth call.
// If two hashes are equal, then the result of a given authorize() call
// will be the same.
//
// Note that this ignores some fields such as the permissions within a given
// role, as this assumes all roles are static to a given role name.
func AuthCallHash(actor Subject, action Action, object Object) [32]byte {
var hashOut [32]byte
hash := sha256.New()
hash.Write(actor.Hash())
hash.Write([]byte(action))
hash.Write(object.Hash())

// We might be able to avoid this extra copy?
// sha256.Sum256() returns a [32]byte. We need to return
// an array vs a slice so we can use it as a key in the cache.
image := hash.Sum(nil)
copy(hashOut[:], image)
return hashOut
}

// Subject is a struct that contains all the elements of a subject in an rbac
// authorize.
type Subject struct {
Expand All @@ -56,6 +78,28 @@ type Subject struct {
cachedASTValue ast.Value
}

// Hash returns a unique hash for this subject.
// Role and group order MATTER for this hash. We could sort these
// to make the role/group order not matter, but that would require extra
// processing time to allocate and sort the slice. For our purposes, these
// orders should always be the same, so we can just hash the order as is.
func (s *Subject) Hash() []byte {
// TODO: We might want to look into xxhash instead of sha256. Sha256 is fast,
// but we do not need cryptographic security, just collision resistance.
// So we might be able to use a faster hashing algo.
hash := sha256.New()
hash.Write([]byte(s.ID))
for _, roleName := range s.Roles.Names() {
// roleNames are mapped 1:1 with unique permission sets.
hash.Write([]byte(roleName))
}
for _, groupName := range s.Groups {
hash.Write([]byte(groupName))
}
hash.Write([]byte(s.Scope.Name()))
return hash.Sum(nil)
}

// WithCachedASTValue can be called if the subject is static. This will compute
// the ast value once and cache it for future calls.
func (s Subject) WithCachedASTValue() Subject {
Expand Down Expand Up @@ -613,7 +657,7 @@ type authCache struct {
// corresponding errors. When the Authorizer is immutable (e.g. in the case
// of Rego)"
// determistic function.
cache *tlru.Cache[string, error]
cache *tlru.Cache[[32]byte, error]

authz Authorizer
}
Expand All @@ -628,24 +672,19 @@ type authCache struct {
func Cacher(authz Authorizer) Authorizer {
return &authCache{
authz: authz,
cache: tlru.New[string](tlru.ConstantCost[error], 4096),
cache: tlru.New[[32]byte](tlru.ConstantCost[error], 4096),
}
}

func (c *authCache) Authorize(ctx context.Context, subject Subject, action Action, object Object) error {
var authorizeCacheKey strings.Builder
authorizeCacheKey.Grow(256)
enc := json.NewEncoder(&authorizeCacheKey)
_ = enc.Encode(subject)
_ = enc.Encode(action)
_ = enc.Encode(object)
authorizeCacheKey := AuthCallHash(subject, action, object)

var err error
err, _, ok := c.cache.Get(authorizeCacheKey.String())
err, _, ok := c.cache.Get(authorizeCacheKey)
if !ok {
err = c.authz.Authorize(ctx, subject, action, object)
// In case there is a caching bug, bound the TTL to 1 minute.
c.cache.Set(authorizeCacheKey.String(), err, time.Minute)
c.cache.Set(authorizeCacheKey, err, time.Minute)
}

return err
Expand Down
35 changes: 35 additions & 0 deletions coderd/rbac/object.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package rbac

import (
"crypto/sha256"
"io"

"github.com/google/uuid"
)

Expand Down Expand Up @@ -194,6 +197,38 @@ type Object struct {
ACLGroupList map[string][]Action ` json:"acl_group_list"`
}

// hashACLMapPreimage feeds the ACL map preimage into the hash function.
func hashACLMapPreimage(hash io.Writer, prefix string, aclList map[string][]Action) {
// We have to sort the keys so that the hash is consistent since map access
// is random. This allocation is unfortunate.
keys := make([]string, 0, len(aclList))
// Prefix all user & group acl lists with "user:" or "group:"
for _, k := range keys {
v := aclList[k]
// Writing user:<name>:<action>,<action>,
hash.Write([]byte("user:"))
hash.Write([]byte(k))
hash.Write([]byte(":"))
for _, a := range v {
hash.Write([]byte(a))
// A trailing slash doesn't matter.
hash.Write([]byte(","))
}
}
}

func (z Object) Hash() []byte {
hash := sha256.New()
hash.Write([]byte(z.ID))
hash.Write([]byte(z.Owner))
hash.Write([]byte(z.OrgID))
hash.Write([]byte(z.Type))
hashACLMapPreimage(hash, "user:", z.ACLUserList)
hashACLMapPreimage(hash, "user:", z.ACLUserList)

return hash.Sum(nil)
}

func (z Object) Equal(b Object) bool {
if z.ID != b.ID {
return false
Expand Down