mirror of https://github.com/coder/coder.git
feat: Add cachable authorizer to elimate duplicate rbac calls (#6107)
* feat: Add cachable authorizer to elimate duplicate rbac calls Cache is context bound, so only prevents duplicate rbac calls in the same request context.
This commit is contained in:
parent
6f3f7f2937
commit
e6da7afd33
|
@ -12,7 +12,10 @@ import (
|
|||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coder/coder/cryptorand"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/google/uuid"
|
||||
"github.com/moby/moby/pkg/namesgenerator"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/xerrors"
|
||||
|
@ -543,9 +546,7 @@ func (a *AuthTester) Test(ctx context.Context, assertRoute map[string]RouteCheck
|
|||
}
|
||||
|
||||
type authCall struct {
|
||||
Actor rbac.Subject
|
||||
Action rbac.Action
|
||||
Object rbac.Object
|
||||
rbac.AuthCall
|
||||
|
||||
asserted bool
|
||||
}
|
||||
|
@ -621,9 +622,11 @@ func (r *RecordingAuthorizer) recordAuthorize(subject rbac.Subject, action rbac.
|
|||
r.Lock()
|
||||
defer r.Unlock()
|
||||
r.Called = append(r.Called, authCall{
|
||||
Actor: subject,
|
||||
Action: action,
|
||||
Object: object,
|
||||
AuthCall: rbac.AuthCall{
|
||||
Actor: subject,
|
||||
Action: action,
|
||||
Object: object,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -743,3 +746,67 @@ func (f *fakePreparedAuthorizer) Authorize(ctx context.Context, object rbac.Obje
|
|||
func (*fakePreparedAuthorizer) CompileToSQL(_ context.Context, _ regosql.ConvertConfig) (string, error) {
|
||||
return "not a valid sql string", nil
|
||||
}
|
||||
|
||||
// Random rbac helper funcs
|
||||
|
||||
func RandomRBACAction() rbac.Action {
|
||||
all := rbac.AllActions()
|
||||
return all[must(cryptorand.Intn(len(all)))]
|
||||
}
|
||||
|
||||
func RandomRBACObject() rbac.Object {
|
||||
return rbac.Object{
|
||||
ID: uuid.NewString(),
|
||||
Owner: uuid.NewString(),
|
||||
OrgID: uuid.NewString(),
|
||||
Type: randomRBACType(),
|
||||
ACLUserList: map[string][]rbac.Action{
|
||||
namesgenerator.GetRandomName(1): {RandomRBACAction()},
|
||||
},
|
||||
ACLGroupList: map[string][]rbac.Action{
|
||||
namesgenerator.GetRandomName(1): {RandomRBACAction()},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func randomRBACType() string {
|
||||
all := []string{
|
||||
rbac.ResourceWorkspace.Type,
|
||||
rbac.ResourceWorkspaceExecution.Type,
|
||||
rbac.ResourceWorkspaceApplicationConnect.Type,
|
||||
rbac.ResourceAuditLog.Type,
|
||||
rbac.ResourceTemplate.Type,
|
||||
rbac.ResourceGroup.Type,
|
||||
rbac.ResourceFile.Type,
|
||||
rbac.ResourceProvisionerDaemon.Type,
|
||||
rbac.ResourceOrganization.Type,
|
||||
rbac.ResourceRoleAssignment.Type,
|
||||
rbac.ResourceOrgRoleAssignment.Type,
|
||||
rbac.ResourceAPIKey.Type,
|
||||
rbac.ResourceUser.Type,
|
||||
rbac.ResourceUserData.Type,
|
||||
rbac.ResourceOrganizationMember.Type,
|
||||
rbac.ResourceWildcard.Type,
|
||||
rbac.ResourceLicense.Type,
|
||||
rbac.ResourceDeploymentConfig.Type,
|
||||
rbac.ResourceReplicas.Type,
|
||||
rbac.ResourceDebugInfo.Type,
|
||||
}
|
||||
return all[must(cryptorand.Intn(len(all)))]
|
||||
}
|
||||
|
||||
func RandomRBACSubject() rbac.Subject {
|
||||
return rbac.Subject{
|
||||
ID: uuid.NewString(),
|
||||
Roles: rbac.RoleNames{rbac.RoleMember()},
|
||||
Groups: []string{namesgenerator.GetRandomName(1)},
|
||||
Scope: rbac.ScopeAll,
|
||||
}
|
||||
}
|
||||
|
||||
func must[T any](value T, err error) T {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/moby/moby/pkg/namesgenerator"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
|
@ -34,7 +33,7 @@ func TestAuthzRecorder(t *testing.T) {
|
|||
rec := &coderdtest.RecordingAuthorizer{
|
||||
Wrapped: &coderdtest.FakeAuthorizer{},
|
||||
}
|
||||
sub := randomSubject()
|
||||
sub := coderdtest.RandomRBACSubject()
|
||||
pairs := fuzzAuthz(t, sub, rec, 10)
|
||||
rec.AssertActor(t, sub, pairs...)
|
||||
require.NoError(t, rec.AllAsserted(), "all assertions should have been made")
|
||||
|
@ -46,10 +45,10 @@ func TestAuthzRecorder(t *testing.T) {
|
|||
rec := &coderdtest.RecordingAuthorizer{
|
||||
Wrapped: &coderdtest.FakeAuthorizer{},
|
||||
}
|
||||
a := randomSubject()
|
||||
a := coderdtest.RandomRBACSubject()
|
||||
aPairs := fuzzAuthz(t, a, rec, 10)
|
||||
|
||||
b := randomSubject()
|
||||
b := coderdtest.RandomRBACSubject()
|
||||
bPairs := fuzzAuthz(t, b, rec, 10)
|
||||
|
||||
rec.AssertActor(t, b, bPairs...)
|
||||
|
@ -63,12 +62,12 @@ func TestAuthzRecorder(t *testing.T) {
|
|||
rec := &coderdtest.RecordingAuthorizer{
|
||||
Wrapped: &coderdtest.FakeAuthorizer{},
|
||||
}
|
||||
a := randomSubject()
|
||||
a := coderdtest.RandomRBACSubject()
|
||||
aPairs := fuzzAuthz(t, a, rec, 10)
|
||||
|
||||
b := randomSubject()
|
||||
b := coderdtest.RandomRBACSubject()
|
||||
|
||||
act, objTy := randomAction(), randomObject().Type
|
||||
act, objTy := coderdtest.RandomRBACAction(), coderdtest.RandomRBACObject().Type
|
||||
prep, _ := rec.Prepare(context.Background(), b, act, objTy)
|
||||
bPairs := fuzzAuthzPrep(t, prep, 10, act, objTy)
|
||||
|
||||
|
@ -84,7 +83,7 @@ func fuzzAuthzPrep(t *testing.T, prep rbac.PreparedAuthorized, n int, action rba
|
|||
pairs := make([]coderdtest.ActionObjectPair, 0, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
obj := randomObject()
|
||||
obj := coderdtest.RandomRBACObject()
|
||||
obj.Type = objectType
|
||||
p := coderdtest.ActionObjectPair{Action: action, Object: obj}
|
||||
_ = prep.Authorize(context.Background(), p.Object)
|
||||
|
@ -98,37 +97,9 @@ func fuzzAuthz(t *testing.T, sub rbac.Subject, rec rbac.Authorizer, n int) []cod
|
|||
pairs := make([]coderdtest.ActionObjectPair, 0, n)
|
||||
|
||||
for i := 0; i < n; i++ {
|
||||
p := coderdtest.ActionObjectPair{Action: randomAction(), Object: randomObject()}
|
||||
p := coderdtest.ActionObjectPair{Action: coderdtest.RandomRBACAction(), Object: coderdtest.RandomRBACObject()}
|
||||
_ = rec.Authorize(context.Background(), sub, p.Action, p.Object)
|
||||
pairs = append(pairs, p)
|
||||
}
|
||||
return pairs
|
||||
}
|
||||
|
||||
func randomAction() rbac.Action {
|
||||
return rbac.Action(namesgenerator.GetRandomName(1))
|
||||
}
|
||||
|
||||
func randomObject() rbac.Object {
|
||||
return rbac.Object{
|
||||
ID: namesgenerator.GetRandomName(1),
|
||||
Owner: namesgenerator.GetRandomName(1),
|
||||
OrgID: namesgenerator.GetRandomName(1),
|
||||
Type: namesgenerator.GetRandomName(1),
|
||||
ACLUserList: map[string][]rbac.Action{
|
||||
namesgenerator.GetRandomName(1): {rbac.ActionRead},
|
||||
},
|
||||
ACLGroupList: map[string][]rbac.Action{
|
||||
namesgenerator.GetRandomName(1): {rbac.ActionRead},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func randomSubject() rbac.Subject {
|
||||
return rbac.Subject{
|
||||
ID: namesgenerator.GetRandomName(1),
|
||||
Roles: rbac.RoleNames{rbac.RoleMember()},
|
||||
Groups: []string{namesgenerator.GetRandomName(1)},
|
||||
Scope: rbac.ScopeAll,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,3 +9,8 @@ const (
|
|||
ActionUpdate Action = "update"
|
||||
ActionDelete Action = "delete"
|
||||
)
|
||||
|
||||
// AllActions is a helper function to return all the possible actions types.
|
||||
func AllActions() []Action {
|
||||
return []Action{ActionCreate, ActionRead, ActionUpdate, ActionDelete}
|
||||
}
|
||||
|
|
|
@ -222,16 +222,16 @@ func TestAuthorizeDomain(t *testing.T) {
|
|||
testAuthorize(t, "UserACLList", user, []authTestCase{
|
||||
{
|
||||
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(unuseID).WithACLUserList(map[string][]Action{
|
||||
user.ID: allActions(),
|
||||
user.ID: AllActions(),
|
||||
}),
|
||||
actions: allActions(),
|
||||
actions: AllActions(),
|
||||
allow: true,
|
||||
},
|
||||
{
|
||||
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(unuseID).WithACLUserList(map[string][]Action{
|
||||
user.ID: {WildcardSymbol},
|
||||
}),
|
||||
actions: allActions(),
|
||||
actions: AllActions(),
|
||||
allow: true,
|
||||
},
|
||||
{
|
||||
|
@ -254,16 +254,16 @@ func TestAuthorizeDomain(t *testing.T) {
|
|||
testAuthorize(t, "GroupACLList", user, []authTestCase{
|
||||
{
|
||||
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(defOrg).WithGroupACL(map[string][]Action{
|
||||
allUsersGroup: allActions(),
|
||||
allUsersGroup: AllActions(),
|
||||
}),
|
||||
actions: allActions(),
|
||||
actions: AllActions(),
|
||||
allow: true,
|
||||
},
|
||||
{
|
||||
resource: ResourceWorkspace.WithOwner(unuseID.String()).InOrg(defOrg).WithGroupACL(map[string][]Action{
|
||||
allUsersGroup: {WildcardSymbol},
|
||||
}),
|
||||
actions: allActions(),
|
||||
actions: AllActions(),
|
||||
allow: true,
|
||||
},
|
||||
{
|
||||
|
@ -285,27 +285,27 @@ func TestAuthorizeDomain(t *testing.T) {
|
|||
|
||||
testAuthorize(t, "Member", user, []authTestCase{
|
||||
// Org + me
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: allActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg), actions: allActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: AllActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg), actions: AllActions(), allow: false},
|
||||
|
||||
{resource: ResourceWorkspace.WithOwner(user.ID), actions: allActions(), allow: true},
|
||||
{resource: ResourceWorkspace.WithOwner(user.ID), actions: AllActions(), allow: true},
|
||||
|
||||
{resource: ResourceWorkspace.All(), actions: allActions(), allow: false},
|
||||
{resource: ResourceWorkspace.All(), actions: AllActions(), allow: false},
|
||||
|
||||
// Other org + me
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), actions: allActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: allActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: AllActions(), allow: false},
|
||||
|
||||
// Other org + other user
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: AllActions(), allow: false},
|
||||
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: AllActions(), allow: false},
|
||||
|
||||
// Other org + other us
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: allActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: AllActions(), allow: false},
|
||||
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: AllActions(), allow: false},
|
||||
})
|
||||
|
||||
user = Subject{
|
||||
|
@ -326,27 +326,27 @@ func TestAuthorizeDomain(t *testing.T) {
|
|||
|
||||
testAuthorize(t, "DeletedMember", user, []authTestCase{
|
||||
// Org + me
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: allActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg), actions: allActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg), actions: AllActions(), allow: false},
|
||||
|
||||
{resource: ResourceWorkspace.WithOwner(user.ID), actions: allActions(), allow: false},
|
||||
{resource: ResourceWorkspace.WithOwner(user.ID), actions: AllActions(), allow: false},
|
||||
|
||||
{resource: ResourceWorkspace.All(), actions: allActions(), allow: false},
|
||||
{resource: ResourceWorkspace.All(), actions: AllActions(), allow: false},
|
||||
|
||||
// Other org + me
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), actions: allActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: allActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: AllActions(), allow: false},
|
||||
|
||||
// Other org + other user
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: AllActions(), allow: false},
|
||||
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: AllActions(), allow: false},
|
||||
|
||||
// Other org + other use
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: allActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: AllActions(), allow: false},
|
||||
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: AllActions(), allow: false},
|
||||
})
|
||||
|
||||
user = Subject{
|
||||
|
@ -360,27 +360,27 @@ func TestAuthorizeDomain(t *testing.T) {
|
|||
|
||||
testAuthorize(t, "OrgAdmin", user, []authTestCase{
|
||||
// Org + me
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: allActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg), actions: allActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: AllActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg), actions: AllActions(), allow: true},
|
||||
|
||||
{resource: ResourceWorkspace.WithOwner(user.ID), actions: allActions(), allow: true},
|
||||
{resource: ResourceWorkspace.WithOwner(user.ID), actions: AllActions(), allow: true},
|
||||
|
||||
{resource: ResourceWorkspace.All(), actions: allActions(), allow: false},
|
||||
{resource: ResourceWorkspace.All(), actions: AllActions(), allow: false},
|
||||
|
||||
// Other org + me
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), actions: allActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: allActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: AllActions(), allow: false},
|
||||
|
||||
// Other org + other user
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: allActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: AllActions(), allow: true},
|
||||
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: AllActions(), allow: false},
|
||||
|
||||
// Other org + other use
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: allActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: AllActions(), allow: false},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: AllActions(), allow: false},
|
||||
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: false},
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: AllActions(), allow: false},
|
||||
})
|
||||
|
||||
user = Subject{
|
||||
|
@ -394,27 +394,27 @@ func TestAuthorizeDomain(t *testing.T) {
|
|||
|
||||
testAuthorize(t, "SiteAdmin", user, []authTestCase{
|
||||
// Org + me
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: allActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg), actions: allActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner(user.ID), actions: AllActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg), actions: AllActions(), allow: true},
|
||||
|
||||
{resource: ResourceWorkspace.WithOwner(user.ID), actions: allActions(), allow: true},
|
||||
{resource: ResourceWorkspace.WithOwner(user.ID), actions: AllActions(), allow: true},
|
||||
|
||||
{resource: ResourceWorkspace.All(), actions: allActions(), allow: true},
|
||||
{resource: ResourceWorkspace.All(), actions: AllActions(), allow: true},
|
||||
|
||||
// Other org + me
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), actions: allActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: allActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner(user.ID), actions: AllActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: AllActions(), allow: true},
|
||||
|
||||
// Other org + other user
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: allActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(defOrg).WithOwner("not-me"), actions: AllActions(), allow: true},
|
||||
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: true},
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: AllActions(), allow: true},
|
||||
|
||||
// Other org + other use
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: allActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: allActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID).WithOwner("not-me"), actions: AllActions(), allow: true},
|
||||
{resource: ResourceWorkspace.InOrg(unuseID), actions: AllActions(), allow: true},
|
||||
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: allActions(), allow: true},
|
||||
{resource: ResourceWorkspace.WithOwner("not-me"), actions: AllActions(), allow: true},
|
||||
})
|
||||
|
||||
user = Subject{
|
||||
|
@ -642,7 +642,7 @@ func TestAuthorizeLevels(t *testing.T) {
|
|||
|
||||
testAuthorize(t, "AdminAlwaysAllow", user,
|
||||
cases(func(c authTestCase) authTestCase {
|
||||
c.actions = allActions()
|
||||
c.actions = AllActions()
|
||||
c.allow = true
|
||||
return c
|
||||
}, []authTestCase{
|
||||
|
@ -701,7 +701,7 @@ func TestAuthorizeLevels(t *testing.T) {
|
|||
|
||||
testAuthorize(t, "OrgAllowAll", user,
|
||||
cases(func(c authTestCase) authTestCase {
|
||||
c.actions = allActions()
|
||||
c.actions = AllActions()
|
||||
return c
|
||||
}, []authTestCase{
|
||||
// Org + me
|
||||
|
@ -1034,10 +1034,6 @@ func testAuthorize(t *testing.T, name string, subject Subject, sets ...[]authTes
|
|||
}
|
||||
}
|
||||
|
||||
// allActions is a helper function to return all the possible actions types.
|
||||
func allActions() []Action {
|
||||
return []Action{ActionCreate, ActionRead, ActionUpdate, ActionDelete}
|
||||
}
|
||||
|
||||
func must[T any](value T, err error) T {
|
||||
if err != nil {
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
package rbac
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type AuthCall struct {
|
||||
Actor Subject
|
||||
Action Action
|
||||
Object Object
|
||||
}
|
||||
|
||||
type cachedCalls struct {
|
||||
authz Authorizer
|
||||
}
|
||||
|
||||
// Cacher returns an Authorizer that can use a cache stored on a context
|
||||
// to short circuit duplicate calls to the Authorizer. This is useful when
|
||||
// multiple calls are made to the Authorizer for the same subject, action, and
|
||||
// object. The cache is on each `ctx` and is not shared between requests.
|
||||
// If no cache is found on the context, the Authorizer is called as normal.
|
||||
func Cacher(authz Authorizer) Authorizer {
|
||||
return &cachedCalls{authz: authz}
|
||||
}
|
||||
|
||||
func (c *cachedCalls) Authorize(ctx context.Context, subject Subject, action Action, object Object) error {
|
||||
cache := cacheFromContext(ctx)
|
||||
|
||||
resp, ok := cache.Load(subject, action, object)
|
||||
if ok {
|
||||
return resp
|
||||
}
|
||||
|
||||
err := c.authz.Authorize(ctx, subject, action, object)
|
||||
cache.Save(subject, action, object, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Prepare returns the underlying PreparedAuthorized. The cache does not apply
|
||||
// to prepared authorizations. These should be using a SQL filter, and
|
||||
// therefore the cache is not needed.
|
||||
func (c *cachedCalls) Prepare(ctx context.Context, subject Subject, action Action, objectType string) (PreparedAuthorized, error) {
|
||||
return c.authz.Prepare(ctx, subject, action, objectType)
|
||||
}
|
||||
|
||||
type cachedAuthCall struct {
|
||||
AuthCall
|
||||
Err error
|
||||
}
|
||||
|
||||
type authorizeCache struct {
|
||||
sync.Mutex
|
||||
// calls is a list of all calls made to the Authorizer.
|
||||
// This list is cached per request context. The size of this list is expected
|
||||
// to be incredibly small. Often 1 or 2 calls.
|
||||
calls []cachedAuthCall
|
||||
}
|
||||
|
||||
//nolint:error-return,revive
|
||||
func (c *authorizeCache) Load(subject Subject, action Action, object Object) (error, bool) {
|
||||
if c == nil {
|
||||
return nil, false
|
||||
}
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
for _, call := range c.calls {
|
||||
if call.Action == action && call.Object.Equal(object) && call.Actor.Equal(subject) {
|
||||
return call.Err, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (c *authorizeCache) Save(subject Subject, action Action, object Object, err error) {
|
||||
if c == nil {
|
||||
return
|
||||
}
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
c.calls = append(c.calls, cachedAuthCall{
|
||||
AuthCall: AuthCall{
|
||||
Actor: subject,
|
||||
Action: action,
|
||||
Object: object,
|
||||
},
|
||||
Err: err,
|
||||
})
|
||||
}
|
||||
|
||||
// cacheContextKey is a context key used to store the cache in the context.
|
||||
type cacheContextKey struct{}
|
||||
|
||||
// cacheFromContext returns the cache from the context.
|
||||
// If there is no cache, a nil value is returned.
|
||||
// The nil cache can still be called as a normal cache, but will not cache or
|
||||
// return any values.
|
||||
func cacheFromContext(ctx context.Context) *authorizeCache {
|
||||
cache, _ := ctx.Value(cacheContextKey{}).(*authorizeCache)
|
||||
return cache
|
||||
}
|
||||
|
||||
func WithCacheCtx(ctx context.Context) context.Context {
|
||||
return context.WithValue(ctx, cacheContextKey{}, &authorizeCache{})
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
package rbac_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/coderdtest"
|
||||
"github.com/coder/coder/coderd/rbac"
|
||||
)
|
||||
|
||||
func TestCacher(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("EmptyCacheCtx", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
rec := &coderdtest.RecordingAuthorizer{
|
||||
Wrapped: &coderdtest.FakeAuthorizer{AlwaysReturn: nil},
|
||||
}
|
||||
authz := rbac.Cacher(rec)
|
||||
subj, obj, action := coderdtest.RandomRBACSubject(), coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction()
|
||||
|
||||
// Two identical calls
|
||||
_ = authz.Authorize(ctx, subj, action, obj)
|
||||
_ = authz.Authorize(ctx, subj, action, obj)
|
||||
|
||||
// Yields two calls to the wrapped Authorizer
|
||||
rec.AssertActor(t, subj, rec.Pair(action, obj), rec.Pair(action, obj))
|
||||
require.NoError(t, rec.AllAsserted(), "all assertions should have been made")
|
||||
})
|
||||
|
||||
t.Run("CacheCtx", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := rbac.WithCacheCtx(context.Background())
|
||||
rec := &coderdtest.RecordingAuthorizer{
|
||||
Wrapped: &coderdtest.FakeAuthorizer{AlwaysReturn: nil},
|
||||
}
|
||||
authz := rbac.Cacher(rec)
|
||||
subj, obj, action := coderdtest.RandomRBACSubject(), coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction()
|
||||
|
||||
// Two identical calls
|
||||
_ = authz.Authorize(ctx, subj, action, obj)
|
||||
_ = authz.Authorize(ctx, subj, action, obj)
|
||||
|
||||
// Yields only 1 call to the wrapped Authorizer for that subject
|
||||
rec.AssertActor(t, subj, rec.Pair(action, obj))
|
||||
require.NoError(t, rec.AllAsserted(), "all assertions should have been made")
|
||||
})
|
||||
|
||||
t.Run("MultipleSubjects", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := rbac.WithCacheCtx(context.Background())
|
||||
rec := &coderdtest.RecordingAuthorizer{
|
||||
Wrapped: &coderdtest.FakeAuthorizer{AlwaysReturn: nil},
|
||||
}
|
||||
authz := rbac.Cacher(rec)
|
||||
subj1, obj1, action1 := coderdtest.RandomRBACSubject(), coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction()
|
||||
|
||||
// Two identical calls
|
||||
_ = authz.Authorize(ctx, subj1, action1, obj1)
|
||||
_ = authz.Authorize(ctx, subj1, action1, obj1)
|
||||
|
||||
// Extra unique calls
|
||||
var pairs []coderdtest.ActionObjectPair
|
||||
subj2, obj2, action2 := coderdtest.RandomRBACSubject(), coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction()
|
||||
_ = authz.Authorize(ctx, subj2, action2, obj2)
|
||||
pairs = append(pairs, rec.Pair(action2, obj2))
|
||||
|
||||
obj3, action3 := coderdtest.RandomRBACObject(), coderdtest.RandomRBACAction()
|
||||
_ = authz.Authorize(ctx, subj2, action3, obj3)
|
||||
pairs = append(pairs, rec.Pair(action3, obj3))
|
||||
|
||||
// Extra identical call after some unique calls
|
||||
_ = authz.Authorize(ctx, subj1, action1, obj1)
|
||||
|
||||
// Yields 3 calls, 1 for the first subject, 2 for the unique subjects
|
||||
rec.AssertActor(t, subj1, rec.Pair(action1, obj1))
|
||||
rec.AssertActor(t, subj2, pairs...)
|
||||
require.NoError(t, rec.AllAsserted(), "all assertions should have been made")
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue