Skip to content

Commit d30fa37

Browse files
Copilotmserico
andcommitted
Add policy indexing infrastructure for performance optimization
Co-authored-by: mserico <[email protected]>
1 parent 01a809f commit d30fa37

File tree

4 files changed

+1857
-1566
lines changed

4 files changed

+1857
-1566
lines changed

src/coreEnforcer.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export class CoreEnforcer {
6262
protected autoBuildRoleLinks = true;
6363
protected autoNotifyWatcher = true;
6464
protected acceptJsonRequest = false;
65+
protected autoBuildPolicyIndex = false;
6566
protected fs?: FileSystem;
6667

6768
/**
@@ -245,6 +246,10 @@ export class CoreEnforcer {
245246
if (this.autoBuildRoleLinks) {
246247
await this.buildRoleLinksInternal();
247248
}
249+
250+
if (this.autoBuildPolicyIndex) {
251+
this.buildPolicyIndexInternal();
252+
}
248253
}
249254

250255
/**
@@ -280,6 +285,11 @@ export class CoreEnforcer {
280285
if (this.autoBuildRoleLinks) {
281286
await this.buildRoleLinksInternal();
282287
}
288+
289+
if (this.autoBuildPolicyIndex) {
290+
this.buildPolicyIndexInternal();
291+
}
292+
283293
return true;
284294
}
285295

@@ -371,6 +381,18 @@ export class CoreEnforcer {
371381
this.autoBuildRoleLinks = autoBuildRoleLinks;
372382
}
373383

384+
/**
385+
* enableAutoBuildPolicyIndex controls whether to build an index for policies
386+
* to improve performance when checking permissions with many policies.
387+
* The index groups policies by subject (first field), which significantly
388+
* improves performance for RBAC models with wildcard matching.
389+
*
390+
* @param autoBuildPolicyIndex whether to automatically build the policy index.
391+
*/
392+
public enableAutoBuildPolicyIndex(autoBuildPolicyIndex: boolean): void {
393+
this.autoBuildPolicyIndex = autoBuildPolicyIndex;
394+
}
395+
374396
/**
375397
* add matching function to RoleManager by ptype
376398
* @param ptype g
@@ -404,6 +426,23 @@ export class CoreEnforcer {
404426
return this.buildRoleLinksInternal();
405427
}
406428

429+
/**
430+
* buildPolicyIndex manually rebuilds the policy index.
431+
* This improves enforcement performance for models with many policies.
432+
*/
433+
public buildPolicyIndex(): void {
434+
return this.buildPolicyIndexInternal();
435+
}
436+
437+
protected buildPolicyIndexInternal(): void {
438+
const pMap = this.model.model.get('p');
439+
if (pMap) {
440+
pMap.forEach((ast) => {
441+
ast.buildPolicyIndex();
442+
});
443+
}
444+
}
445+
407446
/**
408447
* buildIncrementalRoleLinks provides incremental build the role inheritance relations.
409448
* @param op policy operation
@@ -426,6 +465,53 @@ export class CoreEnforcer {
426465
}
427466
}
428467

468+
/**
469+
* Get policy indices to check for a given subject.
470+
* This method is called before enforcement to optimize which policies to check.
471+
*/
472+
protected async getPolicyIndicesToCheck(subject: string, enforceContext: EnforceContext): Promise<number[] | null> {
473+
if (!this.autoBuildPolicyIndex) {
474+
return null;
475+
}
476+
477+
const p = this.model.model.get('p')?.get(enforceContext.pType);
478+
if (!p || !p.policyIndexMap || p.policyIndexMap.size === 0) {
479+
return null;
480+
}
481+
482+
const subjects = new Set<string>();
483+
subjects.add(subject);
484+
485+
// Get all roles for the subject
486+
const astMap = this.model.model.get('g');
487+
if (astMap) {
488+
for (const [key, value] of astMap) {
489+
const rm = value.rm;
490+
if (rm) {
491+
try {
492+
const roles = await rm.getRoles(subject);
493+
roles.forEach((role) => subjects.add(role));
494+
} catch (e) {
495+
// If there's an error getting roles, fall back to checking all policies
496+
return null;
497+
}
498+
}
499+
}
500+
}
501+
502+
// Collect all policy indices for the subject and its roles
503+
const indices: number[] = [];
504+
for (const sub of subjects) {
505+
const subIndices = p.policyIndexMap.get(sub);
506+
if (subIndices) {
507+
indices.push(...subIndices);
508+
}
509+
}
510+
511+
// If we found specific indices, return them; otherwise return null to check all
512+
return indices.length > 0 ? indices : null;
513+
}
514+
429515
private *privateEnforce(
430516
asyncCompile = true,
431517
explain = false,

src/model/assertion.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export class Assertion {
2525
public policy: string[][];
2626
public rm: rbac.RoleManager;
2727
public fieldIndexMap: Map<string, number>;
28+
public policyIndexMap: Map<string, number[]>;
2829

2930
/**
3031
* constructor is the constructor for Assertion.
@@ -36,6 +37,63 @@ export class Assertion {
3637
this.policy = [];
3738
this.rm = new rbac.DefaultRoleManager(10);
3839
this.fieldIndexMap = new Map<string, number>();
40+
this.policyIndexMap = new Map<string, number[]>();
41+
}
42+
43+
/**
44+
* buildPolicyIndex builds an index for policies by subject (first field).
45+
* This improves performance when checking permissions with many policies.
46+
*/
47+
public buildPolicyIndex(): void {
48+
this.policyIndexMap.clear();
49+
for (let i = 0; i < this.policy.length; i++) {
50+
const rule = this.policy[i];
51+
if (rule.length > 0) {
52+
const subject = rule[0];
53+
const indices = this.policyIndexMap.get(subject);
54+
if (indices) {
55+
indices.push(i);
56+
} else {
57+
this.policyIndexMap.set(subject, [i]);
58+
}
59+
}
60+
}
61+
}
62+
63+
/**
64+
* addPolicyIndex adds an index entry for a newly added policy.
65+
*/
66+
public addPolicyIndex(rule: string[], index: number): void {
67+
if (rule.length > 0) {
68+
const subject = rule[0];
69+
const indices = this.policyIndexMap.get(subject);
70+
if (indices) {
71+
indices.push(index);
72+
} else {
73+
this.policyIndexMap.set(subject, [index]);
74+
}
75+
}
76+
}
77+
78+
/**
79+
* removePolicyIndex removes an index entry for a deleted policy.
80+
*/
81+
public removePolicyIndex(rule: string[]): void {
82+
if (rule.length > 0) {
83+
const subject = rule[0];
84+
const indices = this.policyIndexMap.get(subject);
85+
if (indices) {
86+
// Rebuild the index for this subject since we don't know the exact index
87+
this.policyIndexMap.delete(subject);
88+
for (let i = 0; i < this.policy.length; i++) {
89+
if (this.policy[i][0] === subject) {
90+
const newIndices = this.policyIndexMap.get(subject) || [];
91+
newIndices.push(i);
92+
this.policyIndexMap.set(subject, newIndices);
93+
}
94+
}
95+
}
96+
}
3997
}
4098

4199
public async buildIncrementalRoleLinks(rm: rbac.RoleManager, op: PolicyOp, rules: string[][]): Promise<void> {

src/model/model.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,13 +262,18 @@ export class Model {
262262
const priorityRule = rule[priorityIndex];
263263
const insertIndex = policy.findIndex((oneRule) => oneRule[priorityIndex] >= priorityRule);
264264

265-
if (priorityIndex === -1) {
265+
if (insertIndex === -1) {
266266
policy.push(rule);
267+
ast.addPolicyIndex(rule, policy.length - 1);
267268
} else {
268269
policy.splice(insertIndex, 0, rule);
270+
// Rebuild index for this section since indices have shifted
271+
ast.buildPolicyIndex();
269272
}
270273
} else {
274+
const index = policy.length;
271275
policy.push(rule);
276+
ast.addPolicyIndex(rule, index);
272277
}
273278
return true;
274279
}
@@ -296,7 +301,12 @@ export class Model {
296301
this.addPolicy(sec, ptype, rule);
297302
});
298303
} else {
304+
const startIndex = ast.policy.length;
299305
ast.policy = ast.policy.concat(rules);
306+
// Add index entries for all new policies
307+
rules.forEach((rule, i) => {
308+
ast.addPolicyIndex(rule, startIndex + i);
309+
});
300310
}
301311

302312
return [true, rules];
@@ -319,13 +329,21 @@ export class Model {
319329
if (priorityIndex !== -1) {
320330
if (oldRule[priorityIndex] === newRule[priorityIndex]) {
321331
ast.policy[index] = newRule;
332+
// Update index if subject changed
333+
if (oldRule[0] !== newRule[0]) {
334+
ast.buildPolicyIndex();
335+
}
322336
} else {
323337
// this.removePolicy(sec, ptype, oldRule);
324338
// this.addPolicy(sec, ptype, newRule);
325339
throw new Error('new rule should have the same priority with old rule.');
326340
}
327341
} else {
328342
ast.policy[index] = newRule;
343+
// Update index if subject changed
344+
if (oldRule[0] !== newRule[0]) {
345+
ast.buildPolicyIndex();
346+
}
329347
}
330348

331349
return true;
@@ -339,6 +357,8 @@ export class Model {
339357
return false;
340358
}
341359
ast.policy = ast.policy.filter((r) => !util.arrayEquals(rule, r));
360+
// Rebuild index since we removed a policy
361+
ast.buildPolicyIndex();
342362
return true;
343363
}
344364

@@ -369,6 +389,9 @@ export class Model {
369389
});
370390
}
371391

392+
// Rebuild index since we removed policies
393+
ast.buildPolicyIndex();
394+
372395
return [true, effects];
373396
}
374397

@@ -429,6 +452,8 @@ export class Model {
429452

430453
if (effects.length !== 0) {
431454
ast.policy = res;
455+
// Rebuild index since we removed policies
456+
ast.buildPolicyIndex();
432457
}
433458

434459
return [bool, effects];

0 commit comments

Comments
 (0)