feat: support subjectPriority (#417)
* feat: support subjectPriority
* fix: remove wrong param
diff --git a/examples/subject_priority_model.conf b/examples/subject_priority_model.conf
new file mode 100644
index 0000000..352654f
--- /dev/null
+++ b/examples/subject_priority_model.conf
@@ -0,0 +1,14 @@
+[request_definition]
+r = sub, obj, act
+
+[policy_definition]
+p = sub, obj, act, eft
+
+[role_definition]
+g = _, _
+
+[policy_effect]
+e = subjectPriority(p.eft) || deny
+
+[matchers]
+m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
diff --git a/examples/subject_priority_model_with_domain.conf b/examples/subject_priority_model_with_domain.conf
new file mode 100644
index 0000000..e903781
--- /dev/null
+++ b/examples/subject_priority_model_with_domain.conf
@@ -0,0 +1,14 @@
+[request_definition]
+r = sub, obj, dom, act
+
+[policy_definition]
+p = sub, obj, dom, act, eft
+
+[role_definition]
+g = _, _, _
+
+[policy_effect]
+e = subjectPriority(p.eft) || deny
+
+[matchers]
+m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && r.act == p.act
diff --git a/examples/subject_priority_policy.csv b/examples/subject_priority_policy.csv
new file mode 100644
index 0000000..a714cb9
--- /dev/null
+++ b/examples/subject_priority_policy.csv
@@ -0,0 +1,16 @@
+p, root, data1, read, deny
+p, admin, data1, read, deny
+
+p, editor, data1, read, deny
+p, subscriber, data1, deny
+
+p, jane, data1, read, allow
+p, alice, data1, read, allow
+
+g, admin, root
+
+g, editor, admin
+g, subscriber, admin
+
+g, jane, editor
+g, alice, subscriber
diff --git a/examples/subject_priority_policy_with_domain.csv b/examples/subject_priority_policy_with_domain.csv
new file mode 100644
index 0000000..7346296
--- /dev/null
+++ b/examples/subject_priority_policy_with_domain.csv
@@ -0,0 +1,7 @@
+p, admin, data1, domain1, write, deny
+p, alice, data1, domain1, write, allow
+p, admin, data2, domain2, write, deny
+p, bob, data2, domain2, write, allow
+
+g, alice, admin, domain1
+g, bob, admin, domain2
diff --git a/src/coreEnforcer.ts b/src/coreEnforcer.ts
index b37e81a..d3bb9be 100644
--- a/src/coreEnforcer.ts
+++ b/src/coreEnforcer.ts
@@ -218,6 +218,7 @@
await this.adapter.loadPolicy(this.model);
this.sortPolicies();
+ this.model.sortPoliciesBySubjectHierarchy();
if (this.autoBuildRoleLinks) {
await this.buildRoleLinksInternal();
@@ -233,6 +234,9 @@
public async loadFilteredPolicy(filter: any): Promise<boolean> {
this.model.clearPolicy();
+ this.sortPolicies();
+ this.model.sortPoliciesBySubjectHierarchy();
+
return this.loadIncrementalFilteredPolicy(filter);
}
diff --git a/src/effect/defaultEffectorStream.ts b/src/effect/defaultEffectorStream.ts
index 8b50217..3d578b5 100644
--- a/src/effect/defaultEffectorStream.ts
+++ b/src/effect/defaultEffectorStream.ts
@@ -63,6 +63,7 @@
}
break;
case EffectExpress.PRIORITY:
+ case EffectExpress.SUBJECT_PRIORITY:
if (eft !== Effect.Indeterminate) {
this.res = eft === Effect.Allow;
this.done = true;
diff --git a/src/model/model.ts b/src/model/model.ts
index 75f491d..4c83ed4 100644
--- a/src/model/model.ts
+++ b/src/model/model.ts
@@ -18,6 +18,10 @@
import { Assertion } from './assertion';
import { getLogger, logPrint } from '../log';
import { DefaultRoleManager } from '../rbac';
+import { EffectExpress, FieldIndex } from '../constants';
+
+const defaultDomain = '';
+const defaultSeparator = '::';
export const sectionNameMap: { [index: string]: string } = {
r: 'request_definition',
@@ -479,6 +483,89 @@
assertion.fieldIndexMap.set(field, index);
return index;
}
+
+ /**
+ * sort policies by subject hieraichy
+ */
+ public sortPoliciesBySubjectHierarchy(): void {
+ if (this.model.get('e')?.get('e')?.value !== EffectExpress.SUBJECT_PRIORITY) {
+ return;
+ }
+
+ this.model.get('p')?.forEach((assertion, ptype) => {
+ const domainIndex = this.getFieldIndex(ptype, FieldIndex.Domain);
+ const subIndex = this.getFieldIndex(ptype, FieldIndex.Subject);
+ // eslint-disable-next-line
+ const subjectHierarchyMap = this.getSubjectHierarchyMap(this.model.get('g')!.get('g')!.policy);
+
+ assertion.policy.sort((policyA, policyB) => {
+ const domainA = domainIndex === -1 ? defaultDomain : policyA[domainIndex];
+ const domainB = domainIndex === -1 ? defaultDomain : policyB[domainIndex];
+ // eslint-disable-next-line
+ const priorityA = subjectHierarchyMap.get(this.getNameWithDomain(domainA, policyA[subIndex]))!;
+ // eslint-disable-next-line
+ const priorityB = subjectHierarchyMap.get(this.getNameWithDomain(domainB, policyB[subIndex]))!;
+ return priorityB - priorityA;
+ });
+ });
+ }
+
+ /**
+ * Calculate the priority of each policy store in Map<string, number>
+ */
+ getSubjectHierarchyMap(groupPolicies: string[][]): Map<string, number> {
+ const subjectHierarchyMap = new Map<string, number>();
+ if (!groupPolicies) {
+ return subjectHierarchyMap;
+ }
+
+ const policyMap = new Map<string, string>();
+ let domain = defaultDomain;
+
+ groupPolicies.forEach((policy) => {
+ if (policy.length !== 2) domain = policy[this.getFieldIndex('p', FieldIndex.Domain)];
+ const child = this.getNameWithDomain(domain, policy[this.getFieldIndex('p', FieldIndex.Subject)]);
+ const parent = this.getNameWithDomain(domain, policy[this.getFieldIndex('p', FieldIndex.Object)]);
+ policyMap.set(child, parent);
+ if (!subjectHierarchyMap.has(child)) {
+ subjectHierarchyMap.set(child, 0);
+ }
+ if (!subjectHierarchyMap.has(parent)) {
+ subjectHierarchyMap.set(parent, 0);
+ }
+ subjectHierarchyMap.set(child, 1);
+ });
+
+ const set = new Set<string>();
+ subjectHierarchyMap.forEach((_, key) => {
+ if (subjectHierarchyMap.get(key) !== 0) set.add(key);
+ });
+ while (set.size !== 0) {
+ for (const child of set.values()) {
+ this.findHierarchy(policyMap, subjectHierarchyMap, set, child);
+ }
+ }
+ return subjectHierarchyMap;
+ }
+
+ findHierarchy(policyMap: Map<string, string>, subjectHierarchyMap: Map<string, number>, set: Set<string>, child: string): void {
+ set.delete(child);
+ // eslint-disable-next-line
+ const parent = policyMap.get(child)!;
+
+ if (set.has(parent)) {
+ this.findHierarchy(policyMap, subjectHierarchyMap, set, parent);
+ }
+ // eslint-disable-next-line
+ subjectHierarchyMap.set(child, subjectHierarchyMap.get(parent)! + 10);
+ }
+
+ /**
+ * get full name with domain
+ */
+ getNameWithDomain(domain: string, name: string): string {
+ return domain + defaultSeparator + name;
+ }
}
/**
diff --git a/test/enforcer.test.ts b/test/enforcer.test.ts
index e3565c1..d31aa55 100644
--- a/test/enforcer.test.ts
+++ b/test/enforcer.test.ts
@@ -24,8 +24,12 @@
expect(e.enforceSync(sub, obj, act)).toBe(res);
}
-async function testEnforceEx(e: Enforcer, sub: any, obj: string, act: string, res: [boolean, string[]]): Promise<void> {
- await expect(e.enforceEx(sub, obj, act)).resolves.toEqual(res);
+async function testEnforceEx(e: Enforcer, sub: any, obj: string, act: string, res: [boolean, string[]], domain?: string): Promise<void> {
+ if (domain) {
+ await expect(e.enforceEx(sub, obj, domain, act)).resolves.toEqual(res);
+ } else {
+ await expect(e.enforceEx(sub, obj, act)).resolves.toEqual(res);
+ }
}
function testEnforceExSync(e: Enforcer, sub: any, obj: string, act: string, res: [boolean, string[]]): void {
@@ -697,3 +701,15 @@
testEnforceEx(e, 'bob', 'data2', 'read', [true, ['data2_allow_group', 'data2', 'read', 'allow']]);
testEnforceEx(e, 'alice', 'data2', 'read', [false, []]);
});
+
+test('TestSubjectPriority', async () => {
+ const e = await newEnforcer('examples/subject_priority_model.conf', 'examples/subject_priority_policy.csv');
+ testEnforceEx(e, 'jane', 'data1', 'read', [true, ['jane', 'data1', 'read', 'allow']]);
+ testEnforceEx(e, 'alice', 'data1', 'read', [true, ['alice', 'data1', 'read', 'allow']]);
+});
+
+test('TestSubjectPriorityWithDomain', async () => {
+ const e = await newEnforcer('examples/subject_priority_model_with_domain.conf', 'examples/subject_priority_policy_with_domain.csv');
+ testEnforceEx(e, 'alice', 'data1', 'write', [true, ['alice', 'data1', 'domain1', 'write', 'allow']], 'domain1');
+ testEnforceEx(e, 'bob', 'data2', 'write', [true, ['bob', 'data2', 'domain2', 'write', 'allow']], 'domain2');
+});