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');
+});