Skip to content

Commit 56bf34a

Browse files
orius123yosiharan
andauthored
feat: add lookup cache for WhoCanAccess/WhatCanTargetAccess (#367)
Co-authored-by: Yosi Haran <yosi.haran@descope.com>
1 parent 7aafafb commit 56bf34a

File tree

7 files changed

+633
-12
lines changed

7 files changed

+633
-12
lines changed

internal/config/config.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ const (
1111
ConfigKeyPurgeCooldownWindowInMinutes = "AUTHZCACHE_PURGE_COOLDOWN_WINDOW_IN_MINUTES"
1212
ConfigKeySDKDebugLog = "AUTHZCACHE_SDK_DEBUG_LOG" // TRUE/FALSE, default is FALSE
1313
MetricsKeyResourceServiceName = "service_name"
14+
15+
// Lookup cache configuration
16+
ConfigKeyLookupCacheEnabled = "AUTHZCACHE_LOOKUP_CACHE_ENABLED" // TRUE/FALSE, default is TRUE
17+
ConfigKeyLookupCacheSizePerProject = "AUTHZCACHE_LOOKUP_CACHE_SIZE_PER_PROJECT" // max entries per project
18+
ConfigKeyLookupCacheTTLInSeconds = "AUTHZCACHE_LOOKUP_CACHE_TTL_IN_SECONDS" // TTL for lookup cache entries
19+
ConfigKeyLookupCacheMaxResultSize = "AUTHZCACHE_LOOKUP_CACHE_MAX_RESULT_SIZE" // max result size to cache (skip caching large results)
1420
)
1521

1622
func GetDirectRelationCacheSizePerProject() int {
@@ -32,3 +38,19 @@ func GetSDKDebugLog() bool {
3238
func GetPurgeCooldownWindowInMinutes() int {
3339
return max(0, cconfig.GetIntOrProvidedLocal(ConfigKeyPurgeCooldownWindowInMinutes, 0))
3440
}
41+
42+
func GetLookupCacheEnabled() bool {
43+
return cconfig.GetBoolOrProvidedLocal(ConfigKeyLookupCacheEnabled, true)
44+
}
45+
46+
func GetLookupCacheSizePerProject() int {
47+
return cconfig.GetIntOrProvidedLocal(ConfigKeyLookupCacheSizePerProject, 10_000)
48+
}
49+
50+
func GetLookupCacheTTLInSeconds() int {
51+
return max(1, cconfig.GetIntOrProvidedLocal(ConfigKeyLookupCacheTTLInSeconds, 60))
52+
}
53+
54+
func GetLookupCacheMaxResultSize() int {
55+
return cconfig.GetIntOrProvidedLocal(ConfigKeyLookupCacheMaxResultSize, 1000)
56+
}

internal/services/authz.go

Lines changed: 100 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ type AuthzCache interface {
1616
CreateFGARelations(ctx context.Context, relations []*descope.FGARelation) error
1717
DeleteFGARelations(ctx context.Context, relations []*descope.FGARelation) error
1818
Check(ctx context.Context, relations []*descope.FGARelation) ([]*descope.FGACheck, error)
19+
WhoCanAccess(ctx context.Context, resource, relationDefinition, namespace string) ([]string, error)
20+
WhatCanTargetAccess(ctx context.Context, target string) ([]*descope.AuthzRelation, error)
1921
}
2022

2123
type RemoteClientCreator func(projectID string, logger logger.LoggerInterface) (sdk.Management, error)
@@ -140,26 +142,119 @@ func (a *authzCache) Check(ctx context.Context, relations []*descope.FGARelation
140142
return result, nil
141143
}
142144

145+
func (a *authzCache) WhoCanAccess(ctx context.Context, resource, relationDefinition, namespace string) ([]string, error) {
146+
projectCache, mgmtSDK, err := a.getOrCreateProjectCache(ctx)
147+
if err != nil {
148+
return nil, err // notest
149+
}
150+
candidates, cacheHit := projectCache.GetWhoCanAccessCached(ctx, resource, relationDefinition, namespace)
151+
if cacheHit && len(candidates) > 0 {
152+
verified, err := a.filterWhoCanAccessCandidates(ctx, resource, relationDefinition, namespace, candidates)
153+
if err != nil {
154+
return nil, err // notest
155+
}
156+
cctx.Logger(ctx).Debug().
157+
Str("resource", resource).
158+
Int("candidates", len(candidates)).
159+
Int("verified", len(verified)).
160+
Msg("WhoCanAccess cache hit with candidate filtering")
161+
return verified, nil
162+
}
163+
targets, err := mgmtSDK.Authz().WhoCanAccess(ctx, resource, relationDefinition, namespace)
164+
if err != nil {
165+
return nil, err // notest
166+
}
167+
projectCache.SetWhoCanAccessCached(ctx, resource, relationDefinition, namespace, targets)
168+
return targets, nil
169+
}
170+
171+
func (a *authzCache) filterWhoCanAccessCandidates(ctx context.Context, resource, relationDefinition, namespace string, candidates []string) ([]string, error) {
172+
relations := make([]*descope.FGARelation, len(candidates))
173+
for i, target := range candidates {
174+
relations[i] = &descope.FGARelation{
175+
Resource: resource,
176+
ResourceType: namespace,
177+
Relation: relationDefinition,
178+
Target: target,
179+
}
180+
}
181+
checks, err := a.Check(ctx, relations)
182+
if err != nil {
183+
return nil, err
184+
}
185+
var verified []string
186+
for i, check := range checks {
187+
if check.Allowed {
188+
verified = append(verified, candidates[i])
189+
}
190+
}
191+
return verified, nil
192+
}
193+
194+
func (a *authzCache) WhatCanTargetAccess(ctx context.Context, target string) ([]*descope.AuthzRelation, error) {
195+
projectCache, mgmtSDK, err := a.getOrCreateProjectCache(ctx)
196+
if err != nil {
197+
return nil, err // notest
198+
}
199+
candidates, cacheHit := projectCache.GetWhatCanTargetAccessCached(ctx, target)
200+
if cacheHit && len(candidates) > 0 {
201+
verified, err := a.filterWhatCanTargetAccessCandidates(ctx, target, candidates)
202+
if err != nil {
203+
return nil, err // notest
204+
}
205+
cctx.Logger(ctx).Debug().
206+
Str("target", target).
207+
Int("candidates", len(candidates)).
208+
Int("verified", len(verified)).
209+
Msg("WhatCanTargetAccess cache hit with candidate filtering")
210+
return verified, nil
211+
}
212+
relations, err := mgmtSDK.Authz().WhatCanTargetAccess(ctx, target)
213+
if err != nil {
214+
return nil, err // notest
215+
}
216+
projectCache.SetWhatCanTargetAccessCached(ctx, target, relations)
217+
return relations, nil
218+
}
219+
220+
func (a *authzCache) filterWhatCanTargetAccessCandidates(ctx context.Context, target string, candidates []*descope.AuthzRelation) ([]*descope.AuthzRelation, error) {
221+
relations := make([]*descope.FGARelation, len(candidates))
222+
for i, r := range candidates {
223+
relations[i] = &descope.FGARelation{
224+
Resource: r.Resource,
225+
ResourceType: r.Namespace,
226+
Relation: r.RelationDefinition,
227+
Target: target,
228+
}
229+
}
230+
checks, err := a.Check(ctx, relations)
231+
if err != nil {
232+
return nil, err
233+
}
234+
var verified []*descope.AuthzRelation
235+
for i, check := range checks {
236+
if check.Allowed {
237+
verified = append(verified, candidates[i])
238+
}
239+
}
240+
return verified, nil
241+
}
242+
143243
func (a *authzCache) getOrCreateProjectCache(ctx context.Context) (caches.ProjectAuthzCache, sdk.Management, error) {
144244
projectID := cctx.ProjectID(ctx)
145245
if p, ok := a.projects.Load(projectID); ok {
146246
return p.(project).cache, p.(project).mgmtSDK, nil
147247
}
148248
cctx.Logger(ctx).Info().Msg("Creating new project cache")
149-
// create project mgmt sdk
150249
projectMgmtSDK, err := a.remoteClientCreator(projectID, cctx.Logger(ctx))
151250
if err != nil {
152251
return nil, nil, err // notest
153252
}
154-
// create project cache
155253
projectCache, err := a.projectCacheCreator(ctx, projectMgmtSDK.Authz())
156254
if err != nil {
157255
return nil, nil, err // notest
158256
}
159-
// start remote changes polling
160257
projectCache.StartRemoteChangesPolling(ctx)
161-
// save cache and sdk
162258
a.projects.Store(projectID, project{cache: projectCache, mgmtSDK: projectMgmtSDK})
163-
// return cache and sdk
164259
return projectCache, projectMgmtSDK, nil
165260
}

internal/services/authz_mock.go

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ import (
99
var _ AuthzCache = &AuthzCacheMock{} // ensure AuthzMock implements Authz
1010

1111
type AuthzCacheMock struct {
12-
GetSchemaFunc func() *descope.FGASchema
13-
CheckFunc func(ctx context.Context, relations []*descope.FGARelation) ([]*descope.FGACheck, error)
14-
CreateFGARelationsFunc func(ctx context.Context, relations []*descope.FGARelation) error
15-
CreateFGASchemaFunc func(ctx context.Context, dsl string) error
16-
DeleteFGARelationsFunc func(ctx context.Context, relations []*descope.FGARelation) error
12+
GetSchemaFunc func() *descope.FGASchema
13+
CheckFunc func(ctx context.Context, relations []*descope.FGARelation) ([]*descope.FGACheck, error)
14+
CreateFGARelationsFunc func(ctx context.Context, relations []*descope.FGARelation) error
15+
CreateFGASchemaFunc func(ctx context.Context, dsl string) error
16+
DeleteFGARelationsFunc func(ctx context.Context, relations []*descope.FGARelation) error
17+
WhoCanAccessFunc func(ctx context.Context, resource, relationDefinition, namespace string) ([]string, error)
18+
WhatCanTargetAccessFunc func(ctx context.Context, target string) ([]*descope.AuthzRelation, error)
1719
}
1820

1921
func (a *AuthzCacheMock) Check(ctx context.Context, relations []*descope.FGARelation) ([]*descope.FGACheck, error) {
@@ -31,3 +33,17 @@ func (a *AuthzCacheMock) CreateFGASchema(ctx context.Context, dsl string) error
3133
func (a *AuthzCacheMock) DeleteFGARelations(ctx context.Context, relations []*descope.FGARelation) error {
3234
return a.DeleteFGARelationsFunc(ctx, relations)
3335
}
36+
37+
func (a *AuthzCacheMock) WhoCanAccess(ctx context.Context, resource, relationDefinition, namespace string) ([]string, error) {
38+
if a.WhoCanAccessFunc != nil {
39+
return a.WhoCanAccessFunc(ctx, resource, relationDefinition, namespace)
40+
}
41+
return nil, nil
42+
}
43+
44+
func (a *AuthzCacheMock) WhatCanTargetAccess(ctx context.Context, target string) ([]*descope.AuthzRelation, error) {
45+
if a.WhatCanTargetAccessFunc != nil {
46+
return a.WhatCanTargetAccessFunc(ctx, target)
47+
}
48+
return nil, nil
49+
}

0 commit comments

Comments
 (0)