feat(authz): add caching decorator for PermissionStore
This commit is contained in:
@@ -88,3 +88,73 @@ export function memoryPermissionStore(): PermissionStore {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface CachedPermissionStore extends PermissionStore {
|
||||
/** Drop one subject. Call after changing roles out of band. */
|
||||
invalidate(subjectId: string, scope?: AuthzScope): void;
|
||||
invalidateAll(): void;
|
||||
/** Cached entry count, for tests and diagnostics. */
|
||||
size(): number;
|
||||
}
|
||||
|
||||
export interface CacheOptions {
|
||||
ttlMs?: number;
|
||||
max?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Caches assignment reads. Writes through this decorator invalidate the
|
||||
* affected subject immediately; changes made directly against the inner store
|
||||
* need an explicit `invalidate()` call rather than waiting out the TTL.
|
||||
*/
|
||||
export function cachedPermissionStore(
|
||||
inner: PermissionStore,
|
||||
options: CacheOptions = {},
|
||||
): CachedPermissionStore {
|
||||
const ttlMs = options.ttlMs ?? 5_000;
|
||||
const max = options.max ?? 1_000;
|
||||
const entries = new Map<string, { at: number; value: SubjectAssignments }>();
|
||||
|
||||
const cacheKey = (subjectId: string, scope?: AuthzScope) => `${scopeKey(scope)}�${subjectId}`;
|
||||
const drop = (subjectId: string, scope?: AuthzScope) => {
|
||||
entries.delete(cacheKey(subjectId, scope));
|
||||
// A global write changes what every tenant sees for that subject.
|
||||
if (scopeKey(scope) === "") {
|
||||
for (const key of [...entries.keys()]) {
|
||||
if (key.endsWith(`�${subjectId}`)) entries.delete(key);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
async assignmentsFor(subjectId, scope) {
|
||||
const key = cacheKey(subjectId, scope);
|
||||
const hit = entries.get(key);
|
||||
if (hit && Date.now() - hit.at < ttlMs) return hit.value;
|
||||
const value = await inner.assignmentsFor(subjectId, scope);
|
||||
if (entries.size >= max) entries.delete(entries.keys().next().value!);
|
||||
entries.set(key, { at: Date.now(), value });
|
||||
return value;
|
||||
},
|
||||
async assignRole(subjectId, role, scope) {
|
||||
await inner.assignRole(subjectId, role, scope);
|
||||
drop(subjectId, scope);
|
||||
},
|
||||
async revokeRole(subjectId, role, scope) {
|
||||
await inner.revokeRole(subjectId, role, scope);
|
||||
drop(subjectId, scope);
|
||||
},
|
||||
async grant(subjectId, permission, effect, scope) {
|
||||
await inner.grant(subjectId, permission, effect, scope);
|
||||
drop(subjectId, scope);
|
||||
},
|
||||
async revokeGrant(subjectId, permission, scope) {
|
||||
await inner.revokeGrant(subjectId, permission, scope);
|
||||
drop(subjectId, scope);
|
||||
},
|
||||
listSubjects: (scope) => inner.listSubjects(scope),
|
||||
invalidate: drop,
|
||||
invalidateAll: () => entries.clear(),
|
||||
size: () => entries.size,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user