Skip to content

Commit 032ac8d

Browse files
committed
feat: use an event emitter to invalidate caches
1 parent 03ad4f7 commit 032ac8d

File tree

5 files changed

+84
-21
lines changed

5 files changed

+84
-21
lines changed

lib/policy-resolver.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ interface PolicyResolverOptions {
1818
}
1919

2020
const DEFAULT_CACHE_OPTIONS: LRUCache.Options<string, CompiledFns<unknown>> = {
21-
max: 1000
22-
}
21+
max: 100,
22+
};
2323

2424
type CompiledFns<TContext> = {
2525
can(ctx: TContext): boolean;
@@ -33,13 +33,14 @@ function hasAllPaths(statement: ParsedPolicyStatement, ctx: unknown) {
3333
if (typeof ctx !== "object" || ctx === null) {
3434
return false;
3535
}
36-
return statement.contextPaths.every((path) =>
37-
pathExists(ctx, path)
38-
);
36+
return statement.contextPaths.every((path) => pathExists(ctx, path));
3937
}
4038

4139
export class PolicyResolver {
42-
static fromDocuments(docs: PolicyDocument[], opts: PolicyResolverOptions = {}): PolicyResolver {
40+
static fromDocuments(
41+
docs: PolicyDocument[],
42+
opts: PolicyResolverOptions = {}
43+
): PolicyResolver {
4344
const validator = new PolicyDocumentValidator();
4445

4546
const statements = docs.flatMap((doc) => {
@@ -51,7 +52,10 @@ export class PolicyResolver {
5152
return this.fromStatements(statements, opts);
5253
}
5354

54-
static fromStatements(statements: PolicyStatement[], opts: PolicyResolverOptions = {}): PolicyResolver {
55+
static fromStatements(
56+
statements: PolicyStatement[],
57+
opts: PolicyResolverOptions = {}
58+
): PolicyResolver {
5559
const parsed = statements.map((statement) =>
5660
parsePolicyStatement(statement)
5761
);
@@ -71,10 +75,12 @@ export class PolicyResolver {
7175
policyStore: PolicyStatementStore,
7276
opts: PolicyResolverOptions = {}
7377
) {
74-
this.#policyStore = policyStore;
7578
this.#allowedActions = opts.allowedActions ?? null;
7679
this.#parser = opts.parser ?? jsonLogic;
80+
7781
this.#cache = new LRUCache({ ...DEFAULT_CACHE_OPTIONS, ...opts.cache });
82+
this.#policyStore = policyStore;
83+
this.#policyStore.on("updated", () => this.#cache.clear());
7884
}
7985

8086
/**
@@ -137,18 +143,17 @@ export class PolicyResolver {
137143
return false;
138144
}
139145

140-
const isAllowed = allow.some((statement) => this.#apply(statement, ctx));
141-
return isAllowed;
146+
return allow.some((statement) => this.#apply(statement, ctx));
142147
};
143148

144149
return { can, explain };
145150
}
146151

147-
#apply<TContext>(statement: ParsedPolicyStatement, ctx: TContext) {
152+
#apply<TContext>(statement: ParsedPolicyStatement, ctx: TContext): boolean {
148153
if (!hasAllPaths(statement, ctx)) {
149154
return false;
150155
}
151156

152-
return this.#parser.apply({ or: [statement.constraint, false] }, ctx);
157+
return this.#parser.apply(statement.constraint, ctx) === true;
153158
}
154159
}

lib/store/cached-statements-store.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,45 @@
1+
import LRUCache from "lru-cache";
12
import assert from "node:assert";
23
import { ParsedPolicyStatement, SYM_SID } from "../parsed-policy-statement";
3-
import { PolicyStatementStore } from "./types";
4+
import { TypedEmitter } from "../utils/events";
45
import { IndexedStatementsStore } from "./indexed-statements-store";
6+
import { PolicyStatementStore, StoreEvents } from "./types";
7+
8+
interface CachedStoreOptions {
9+
cache?: LRUCache.Options<string, string[]>;
10+
}
11+
12+
const DEFAULT_CACHE_OPTIONS: LRUCache.Options<string, string[]> = {
13+
max: 1000,
14+
};
515

616
/**
717
* Stores statements and allows for fetching matching statements by action
818
*/
9-
export class CachedStatementsStore implements PolicyStatementStore {
19+
export class CachedStatementsStore
20+
extends TypedEmitter<StoreEvents>
21+
implements PolicyStatementStore
22+
{
1023
#store: PolicyStatementStore;
11-
#cache: Map<string, string[]>;
24+
#cache: LRUCache<string, string[]>;
25+
26+
constructor(store?: PolicyStatementStore, opts: CachedStoreOptions = {}) {
27+
super();
1228

13-
constructor(store?: PolicyStatementStore) {
29+
this.#cache = new LRUCache({ ...DEFAULT_CACHE_OPTIONS, ...opts.cache });
1430
this.#store = store ?? new IndexedStatementsStore();
15-
this.#cache = new Map();
31+
this.#store.on("updated", (sids) => {
32+
this.#cache.clear();
33+
this.emit("updated", sids);
34+
});
1635
}
1736

1837
add(statement: ParsedPolicyStatement) {
1938
this.#store.add(statement);
20-
this.#cache = new Map();
2139
}
2240

2341
addAll(statements: ParsedPolicyStatement[]) {
2442
this.#store.addAll(statements);
25-
this.#cache = new Map();
2643
}
2744

2845
get(sid: string): ParsedPolicyStatement | undefined {

lib/store/indexed-statements-store.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import assert from "node:assert";
22
import { ParsedPolicyStatement, SYM_SID } from "../parsed-policy-statement";
3+
import { TypedEmitter } from "../utils/events";
34
import {
45
ParsedStatementsDB,
56
ByActionIndex,
67
PolicyStatementStore,
8+
StoreEvents,
79
} from "./types";
810

911
type StatementsDBWithIndex = {
@@ -23,11 +25,16 @@ const createStatementsDB = (): StatementsDBWithIndex => ({
2325
/**
2426
* Stores statements and allows for fetching matching statements by action
2527
*/
26-
export class IndexedStatementsStore implements PolicyStatementStore {
28+
export class IndexedStatementsStore
29+
extends TypedEmitter<StoreEvents>
30+
implements PolicyStatementStore
31+
{
2732
#statements: ParsedStatementsDB;
2833
#byAction: ByActionIndex;
2934

3035
constructor(params?: StatementsDBWithIndex) {
36+
super();
37+
3138
const { statements, byAction } = params ?? createStatementsDB();
3239
this.#statements = statements;
3340
this.#byAction = byAction;
@@ -44,6 +51,7 @@ export class IndexedStatementsStore implements PolicyStatementStore {
4451
return sid;
4552
});
4653
this.#reindexAll(sids);
54+
this.emit("updated", sids);
4755
}
4856

4957
get(sid: string): ParsedPolicyStatement | undefined {

lib/store/types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ParsedPolicyStatement } from "../parsed-policy-statement";
2+
import { Emitter } from "../utils/events";
23

34
export type ParsedStatementsDB = Map<string, ParsedPolicyStatement>;
45

@@ -15,7 +16,12 @@ export interface ByActionIndex {
1516
regex: Array<[RegExp, string]>;
1617
}
1718

18-
export interface PolicyStatementStore {
19+
export type StoreEvents = {
20+
/** returns a list of sids that were updated */
21+
updated: string[];
22+
};
23+
24+
export interface PolicyStatementStore extends Emitter<StoreEvents> {
1925
add(statement: ParsedPolicyStatement): void;
2026
addAll(statements: ParsedPolicyStatement[]): void;
2127
get(sid: string): ParsedPolicyStatement | undefined;

lib/utils/events.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import EventEmitter from "node:events";
2+
3+
type EventMap = Record<string, unknown>;
4+
5+
type EventKey<T extends EventMap> = string & keyof T;
6+
type EventReceiver<T> = (params: T) => void;
7+
8+
export interface Emitter<T extends EventMap> {
9+
on<K extends EventKey<T>>(eventName: K, fn: EventReceiver<T[K]>): void;
10+
off<K extends EventKey<T>>(eventName: K, fn: EventReceiver<T[K]>): void;
11+
emit<K extends EventKey<T>>(eventName: K, params: T[K]): void;
12+
}
13+
14+
export class TypedEmitter<T extends EventMap> implements Emitter<T> {
15+
private emitter = new EventEmitter();
16+
on<K extends EventKey<T>>(eventName: K, fn: EventReceiver<T[K]>) {
17+
this.emitter.on(eventName, fn);
18+
}
19+
20+
off<K extends EventKey<T>>(eventName: K, fn: EventReceiver<T[K]>) {
21+
this.emitter.off(eventName, fn);
22+
}
23+
24+
emit<K extends EventKey<T>>(eventName: K, params: T[K]) {
25+
this.emitter.emit(eventName, params);
26+
}
27+
}

0 commit comments

Comments
 (0)