Skip to content

Commit 916baab

Browse files
authored
fix(cache): prevent index memory leak when resource/target IDs contain colons (#385)
1 parent 19f9e24 commit 916baab

File tree

2 files changed

+49
-4
lines changed

2 files changed

+49
-4
lines changed

internal/services/caches/projectauthzcache.go

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ type resource string
3434
type target string
3535
type lookupKey string
3636

37+
type keyComponents struct {
38+
r resource
39+
t target
40+
}
41+
3742
// LookupCacheEntry holds a cached lookup result with TTL tracking
3843
type LookupCacheEntry struct {
3944
Results []string
@@ -46,6 +51,7 @@ type projectAuthzCache struct {
4651
directRelationCache *lru.MonitoredLRUCache[resourceTargetRelation, bool] // resource:target:relation -> bool, e.g. file1:user2:owner -> false
4752
directResourcesIndex map[resource]map[target][]resourceTargetRelation
4853
directTargetsIndex map[target]map[resource][]resourceTargetRelation
54+
directKeyComponents map[resourceTargetRelation]keyComponents
4955
indirectRelationCache *lru.MonitoredLRUCache[resourceTargetRelation, bool] // resource:target:relation -> bool, e.g. file1:user2:owner -> false
5056
lookupCache *lru.MonitoredLRUCache[lookupKey, *LookupCacheEntry] // lookup cache for WhoCanAccess/WhatCanTargetAccess
5157
lookupCacheEnabled bool
@@ -89,6 +95,7 @@ func NewProjectAuthzCache(ctx context.Context, remoteChangesChecker RemoteChange
8995
pc := &projectAuthzCache{
9096
directResourcesIndex: make(map[resource]map[target][]resourceTargetRelation),
9197
directTargetsIndex: make(map[target]map[resource][]resourceTargetRelation),
98+
directKeyComponents: make(map[resourceTargetRelation]keyComponents),
9299
indirectRelationCache: indirectRelationCache,
93100
lookupCache: lookupCache,
94101
lookupCacheEnabled: lookupCacheEnabled,
@@ -271,6 +278,7 @@ func (pc *projectAuthzCache) addDirectRelation(ctx context.Context, r *descope.F
271278
pc.directRelationCache.Add(ctx, key, isAllowed)
272279
resource := resource(r.Resource)
273280
target := target(r.Target)
281+
pc.directKeyComponents[key] = keyComponents{r: resource, t: target}
274282
pc.addKeyToDirectResourceIndex(key, resource, target)
275283
pc.addKeyToDirectTargetIndex(key, resource, target)
276284
}
@@ -303,6 +311,7 @@ func (pc *projectAuthzCache) removeDirectRelation(ctx context.Context, r *descop
303311
target := target(r.Target)
304312
pc.removeKeyFromResourceIndex(resource, target, key)
305313
pc.removeKeyFromTargetIndex(resource, target, key)
314+
delete(pc.directKeyComponents, key)
306315
}
307316

308317
func (pc *projectAuthzCache) removeDirectRelationByResource(ctx context.Context, r resource) {
@@ -387,10 +396,13 @@ func key(r *descope.FGARelation) resourceTargetRelation {
387396

388397
func (pc *projectAuthzCache) removeIndexOnCacheEviction(key resourceTargetRelation, _ bool) {
389398
// on eviction, we need to remove the keys from the indexes as well
390-
splitKey := strings.Split(string(key), ":")
391-
resource, target := resource(splitKey[0]), target(splitKey[1])
392-
pc.removeKeyFromResourceIndex(resource, target, key)
393-
pc.removeKeyFromTargetIndex(resource, target, key)
399+
components, ok := pc.directKeyComponents[key]
400+
if !ok {
401+
return
402+
}
403+
pc.removeKeyFromResourceIndex(components.r, components.t, key)
404+
pc.removeKeyFromTargetIndex(components.r, components.t, key)
405+
delete(pc.directKeyComponents, key)
394406
}
395407

396408
// must be called while holding the mutex
@@ -404,6 +416,7 @@ func (pc *projectAuthzCache) purgeAllCaches(ctx context.Context) {
404416
func (pc *projectAuthzCache) purgeRelationCaches(ctx context.Context) {
405417
pc.directResourcesIndex = make(map[resource]map[target][]resourceTargetRelation)
406418
pc.directTargetsIndex = make(map[target]map[resource][]resourceTargetRelation)
419+
pc.directKeyComponents = make(map[resourceTargetRelation]keyComponents)
407420
pc.directRelationCache.Purge(ctx)
408421
pc.indirectRelationCache.Purge(ctx)
409422
if pc.lookupCache != nil {

internal/services/caches/projectauthzcache_test.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -862,6 +862,38 @@ func TestRemoveIndexOnEviction(t *testing.T) {
862862
assert.True(t, ok)
863863
}
864864

865+
func TestRemoveIndexOnEviction_ColonInIDs(t *testing.T) {
866+
ctx := context.TODO()
867+
// set cache size to 2 so that 3rd addition triggers an eviction
868+
t.Setenv(config.ConfigKeyDirectRelationCacheSizePerProject, "2")
869+
cache, _ := setup(t)
870+
// use IDs that contain colons (e.g., "org:r1", "user:t1")
871+
r1 := &descope.FGARelation{Resource: "org:r1", Target: "user:t1", Relation: "owner"}
872+
r2 := &descope.FGARelation{Resource: "org:r1", Target: "user:t2", Relation: "owner"}
873+
r3 := &descope.FGARelation{Resource: "org:r1", Target: "user:t3", Relation: "owner"}
874+
cache.addDirectRelation(ctx, r1, true)
875+
cache.addDirectRelation(ctx, r2, true)
876+
// assert all indices created and added
877+
assert.True(t, slices.Contains(cache.directResourcesIndex["org:r1"]["user:t1"], key(r1)))
878+
assert.True(t, slices.Contains(cache.directResourcesIndex["org:r1"]["user:t2"], key(r2)))
879+
assert.True(t, slices.Contains(cache.directTargetsIndex["user:t1"]["org:r1"], key(r1)))
880+
assert.True(t, slices.Contains(cache.directTargetsIndex["user:t2"]["org:r1"], key(r2)))
881+
// add 3rd relation (triggers eviction of r1)
882+
cache.addDirectRelation(ctx, r3, true)
883+
// assert that r1 was evicted from the cache and indices (no orphaned entries)
884+
_, _, ok := checkRelation(ctx, cache, r1)
885+
assert.False(t, ok)
886+
_, ok = cache.directResourcesIndex["org:r1"]["user:t1"]
887+
assert.False(t, ok, "evicted key should be removed from resource index")
888+
_, ok = cache.directTargetsIndex["user:t1"]["org:r1"]
889+
assert.False(t, ok, "evicted key should be removed from target index")
890+
// assert that r2 and r3 are still present
891+
assert.True(t, slices.Contains(cache.directResourcesIndex["org:r1"]["user:t2"], key(r2)))
892+
assert.True(t, slices.Contains(cache.directTargetsIndex["user:t2"]["org:r1"], key(r2)))
893+
assert.True(t, slices.Contains(cache.directResourcesIndex["org:r1"]["user:t3"], key(r3)))
894+
assert.True(t, slices.Contains(cache.directTargetsIndex["user:t3"]["org:r1"], key(r3)))
895+
}
896+
865897
// benchmark cache checks with 1,000,000 direct relations
866898
func BenchmarkCheckRelation(b *testing.B) {
867899
// prepare the cache with 1,000,000 direct relations

0 commit comments

Comments
 (0)