diff --git a/src/internalEnforcer.ts b/src/internalEnforcer.ts index 3eeb759..b4bba9a 100644 --- a/src/internalEnforcer.ts +++ b/src/internalEnforcer.ts @@ -29,6 +29,7 @@ export class InternalEnforcer extends CoreEnforcer { return false; } + // Persist when an adapter is configured and autoSave is enabled. if (this.adapter && this.autoSave) { try { await this.adapter.addPolicy(sec, ptype, rule); @@ -67,7 +68,8 @@ export class InternalEnforcer extends CoreEnforcer { } } - if (this.autoSave) { + // Persist when an adapter is configured and autoSave is enabled. + if (this.adapter && this.autoSave) { if ('addPolicies' in this.adapter) { try { await this.adapter.addPolicies(sec, ptype, rules); @@ -113,7 +115,8 @@ export class InternalEnforcer extends CoreEnforcer { return false; } - if (this.autoSave) { + // Persist when an adapter is configured and autoSave is enabled. + if (this.adapter && this.autoSave) { if ('addPolicies' in this.adapter) { try { await this.adapter.addPolicies(sec, ptype, newRules); @@ -158,8 +161,8 @@ export class InternalEnforcer extends CoreEnforcer { if (!this.model.hasPolicy(sec, ptype, oldRule)) { return false; } - - if (this.autoSave) { + // Persist when an adapter is configured and autoSave is enabled. + if (this.adapter && this.autoSave) { if ('updatePolicy' in this.adapter) { try { await this.adapter.updatePolicy(sec, ptype, oldRule, newRule); @@ -200,6 +203,7 @@ export class InternalEnforcer extends CoreEnforcer { return false; } + // Persist when an adapter is configured and autoSave is enabled. if (this.adapter && this.autoSave) { try { await this.adapter.removePolicy(sec, ptype, rule); @@ -236,7 +240,8 @@ export class InternalEnforcer extends CoreEnforcer { } } - if (this.autoSave) { + // Persist when an adapter is configured and autoSave is enabled. + if (this.adapter && this.autoSave) { if ('removePolicies' in this.adapter) { try { await this.adapter.removePolicies(sec, ptype, rules); @@ -278,6 +283,7 @@ export class InternalEnforcer extends CoreEnforcer { fieldValues: string[], useWatcher: boolean ): Promise { + // Persist when an adapter is configured and autoSave is enabled. if (this.adapter && this.autoSave) { try { await this.adapter.removeFilteredPolicy(sec, ptype, fieldIndex, ...fieldValues); @@ -320,4 +326,110 @@ export class InternalEnforcer extends CoreEnforcer { const assertion = this.model.model.get('p')?.get(ptype); assertion?.fieldIndexMap.set(field, index); } + + protected async addPolicyWithoutNotify(sec: string, ptype: string, rule: string[]): Promise { + if (this.model.hasPolicy(sec, ptype, rule)) { + return false; + } + + const ok = this.model.addPolicy(sec, ptype, rule); + if (sec === 'g' && ok) { + await this.buildIncrementalRoleLinks(PolicyOp.PolicyAdd, ptype, [rule]); + } + return ok; + } + + protected async addPoliciesWithoutNotify(sec: string, ptype: string, rules: string[][]): Promise { + for (const rule of rules) { + if (this.model.hasPolicy(sec, ptype, rule)) { + return false; + } + } + + const [ok, effects] = await this.model.addPolicies(sec, ptype, rules); + if (sec === 'g' && ok && effects?.length) { + await this.buildIncrementalRoleLinks(PolicyOp.PolicyAdd, ptype, effects); + } + return ok; + } + + protected async addPoliciesWithoutNotifyEx(sec: string, ptype: string, rules: string[][]): Promise { + const newRules = rules.filter((rule) => !this.model.hasPolicy(sec, ptype, rule)); + if (newRules.length === 0) { + return false; + } + + const [ok, effects] = await this.model.addPolicies(sec, ptype, newRules); + if (sec === 'g' && ok && effects?.length) { + await this.buildIncrementalRoleLinks(PolicyOp.PolicyAdd, ptype, effects); + } + return ok; + } + + protected async updatePolicyWithoutNotify(sec: string, ptype: string, oldRule: string[], newRule: string[]): Promise { + if (!this.model.hasPolicy(sec, ptype, oldRule)) { + return false; + } + + const ok = this.model.updatePolicy(sec, ptype, oldRule, newRule); + if (sec === 'g' && ok) { + await this.buildIncrementalRoleLinks(PolicyOp.PolicyRemove, ptype, [oldRule]); + await this.buildIncrementalRoleLinks(PolicyOp.PolicyAdd, ptype, [newRule]); + } + return ok; + } + + protected async removePolicyWithoutNotify(sec: string, ptype: string, rule: string[]): Promise { + if (!this.model.hasPolicy(sec, ptype, rule)) { + return false; + } + + const ok = await this.model.removePolicy(sec, ptype, rule); + if (sec === 'g' && ok) { + await this.buildIncrementalRoleLinks(PolicyOp.PolicyRemove, ptype, [rule]); + } + return ok; + } + + protected async removePoliciesWithoutNotify(sec: string, ptype: string, rules: string[][]): Promise { + for (const rule of rules) { + if (!this.model.hasPolicy(sec, ptype, rule)) { + return false; + } + } + + const [ok, effects] = this.model.removePolicies(sec, ptype, rules); + if (sec === 'g' && ok && effects?.length) { + await this.buildIncrementalRoleLinks(PolicyOp.PolicyRemove, ptype, effects); + } + return ok; + } + + protected async removeFilteredPolicyWithoutNotify( + sec: string, + ptype: string, + fieldIndex: number, + fieldValues: string[] + ): Promise { + const [ok, effects] = this.model.removeFilteredPolicy(sec, ptype, fieldIndex, ...fieldValues); + if (sec === 'g' && ok && effects?.length) { + await this.buildIncrementalRoleLinks(PolicyOp.PolicyRemove, ptype, effects); + } + return ok; + } + + protected async updatePoliciesWithoutNotify(sec: string, ptype: string, oldRules: string[][], newRules: string[][]): Promise { + // Mirror the Go updatePoliciesWithoutNotify; reuse the existing internal flow. + // Because updatePoliciesInternal isn't implemented yet, fall back to per-item updates. + if (oldRules.length !== newRules.length) { + throw new Error('the length of oldRules should be equal to the length of newRules'); + } + for (let i = 0; i < oldRules.length; i++) { + const ok = await this.updatePolicyWithoutNotify(sec, ptype, oldRules[i], newRules[i]); + if (!ok) { + return false; + } + } + return true; + } } diff --git a/src/managementEnforcer.ts b/src/managementEnforcer.ts index 2d8bea1..f5d0025 100644 --- a/src/managementEnforcer.ts +++ b/src/managementEnforcer.ts @@ -589,28 +589,31 @@ export class ManagementEnforcer extends InternalEnforcer { public async addFunction(name: string, func: MatchingFunction): Promise { this.fm.addFunction(name, func); } - public async selfAddPolicy(sec: string, ptype: string, rule: string[]): Promise { - return this.addPolicyInternal(sec, ptype, rule, false); + return this.addPolicyWithoutNotify(sec, ptype, rule); + } + + public async selfAddPolicies(sec: string, ptype: string, rules: string[][]): Promise { + return this.addPoliciesWithoutNotify(sec, ptype, rules); } public async selfRemovePolicy(sec: string, ptype: string, rule: string[]): Promise { - return this.removePolicyInternal(sec, ptype, rule, false); + return this.removePolicyWithoutNotify(sec, ptype, rule); } - public async selfRemoveFilteredPolicy(sec: string, ptype: string, fieldIndex: number, fieldValues: string[]): Promise { - return this.removeFilteredPolicyInternal(sec, ptype, fieldIndex, fieldValues, false); + public async selfRemovePolicies(sec: string, ptype: string, rules: string[][]): Promise { + return this.removePoliciesWithoutNotify(sec, ptype, rules); } - public async selfUpdatePolicy(sec: string, ptype: string, oldRule: string[], newRule: string[]): Promise { - return this.updatePolicyInternal(sec, ptype, oldRule, newRule, false); + public async selfRemoveFilteredPolicy(sec: string, ptype: string, fieldIndex: number, ...fieldValues: string[]): Promise { + return this.removeFilteredPolicyWithoutNotify(sec, ptype, fieldIndex, fieldValues); } - public async selfAddPolicies(sec: string, ptype: string, rule: string[][]): Promise { - return this.addPoliciesInternal(sec, ptype, rule, false); + public async selfUpdatePolicy(sec: string, ptype: string, oldRule: string[], newRule: string[]): Promise { + return this.updatePolicyWithoutNotify(sec, ptype, oldRule, newRule); } - public async selfRemovePolicies(sec: string, ptype: string, rule: string[][]): Promise { - return this.removePoliciesInternal(sec, ptype, rule, false); + public async selfUpdatePolicies(sec: string, ptype: string, oldRules: string[][], newRules: string[][]): Promise { + return this.updatePoliciesWithoutNotify(sec, ptype, oldRules, newRules); } } diff --git a/test/managementAPI_self.test.ts b/test/managementAPI_self.test.ts new file mode 100644 index 0000000..736440f --- /dev/null +++ b/test/managementAPI_self.test.ts @@ -0,0 +1,263 @@ +import { newEnforcer, Enforcer } from '../src'; + +interface SpyAdapter { + loadPolicy: jest.Mock, any[]>; + addPolicy: jest.Mock; + removePolicy: jest.Mock; + updatePolicy: jest.Mock; + removeFilteredPolicy: jest.Mock; + addPolicies: jest.Mock; + removePolicies: jest.Mock; + updatePolicies: jest.Mock; + savePolicy: jest.Mock; +} + +function createSpyAdapter(): SpyAdapter { + return { + loadPolicy: jest.fn(async () => true), + addPolicy: jest.fn(async () => true), + removePolicy: jest.fn(async () => true), + updatePolicy: jest.fn(async () => true), + removeFilteredPolicy: jest.fn(async () => true), + addPolicies: jest.fn(async () => true), + removePolicies: jest.fn(async () => true), + updatePolicies: jest.fn(async () => true), + savePolicy: jest.fn(async () => true), + }; +} + +describe('Management self* APIs bypass adapter and update memory', () => { + let e: Enforcer; + let adapter: SpyAdapter; + + beforeEach(async () => { + adapter = createSpyAdapter(); + e = (await newEnforcer('examples/basic_model.conf', adapter as any)) as Enforcer; + e.enableAutoSave(true); + }); + + test('selfAddPolicy adds to memory and skips adapter', async () => { + const rule = ['alice', 'data1', 'read']; + + const added = await e.selfAddPolicy('p', 'p', rule); + + expect(added).toBe(true); + expect(await e.hasPolicy(...rule)).toBe(true); + expect(adapter.addPolicy).not.toHaveBeenCalled(); + }); + + test('selfRemovePolicy removes from memory and skips adapter', async () => { + const rule = ['bob', 'data2', 'write']; + await e.selfAddPolicy('p', 'p', rule); + + const removed = await e.selfRemovePolicy('p', 'p', rule); + + expect(removed).toBe(true); + expect(await e.hasPolicy(...rule)).toBe(false); + expect(adapter.removePolicy).not.toHaveBeenCalled(); + }); + + test('selfUpdatePolicy updates in memory and skips adapter', async () => { + const oldRule = ['carol', 'data3', 'read']; + const newRule = ['carol', 'data3', 'write']; + await e.selfAddPolicy('p', 'p', oldRule); + + const updated = await e.selfUpdatePolicy('p', 'p', oldRule, newRule); + + expect(updated).toBe(true); + expect(await e.hasPolicy(...oldRule)).toBe(false); + expect(await e.hasPolicy(...newRule)).toBe(true); + expect(adapter.updatePolicy).not.toHaveBeenCalled(); + }); + + test('selfAddPolicies and selfRemovePolicies work in memory only', async () => { + const rules = [ + ['dave', 'data4', 'read'], + ['erin', 'data5', 'write'], + ]; + + const added = await e.selfAddPolicies('p', 'p', rules); + expect(added).toBe(true); + expect(await e.hasPolicy(...rules[0])).toBe(true); + expect(await e.hasPolicy(...rules[1])).toBe(true); + expect(adapter.addPolicies).not.toHaveBeenCalled(); + + const removed = await e.selfRemovePolicies('p', 'p', rules); + expect(removed).toBe(true); + expect(await e.hasPolicy(...rules[0])).toBe(false); + expect(await e.hasPolicy(...rules[1])).toBe(false); + expect(adapter.removePolicies).not.toHaveBeenCalled(); + }); + + test('selfRemoveFilteredPolicy removes matching rules without adapter', async () => { + await e.selfAddPolicy('p', 'p', ['frank', 'data6', 'read']); + await e.selfAddPolicy('p', 'p', ['frank', 'data7', 'write']); + + const removed = await e.selfRemoveFilteredPolicy('p', 'p', 0, 'frank'); + + expect(removed).toBe(true); + expect(await e.hasPolicy('frank', 'data6', 'read')).toBe(false); + expect(await e.hasPolicy('frank', 'data7', 'write')).toBe(false); + expect(adapter.removeFilteredPolicy).not.toHaveBeenCalled(); + }); + + test('selfUpdatePolicies updates multiple rules in memory', async () => { + const oldRules = [ + ['gina', 'data8', 'read'], + ['harry', 'data9', 'read'], + ]; + const newRules = [ + ['gina', 'data8', 'write'], + ['harry', 'data9', 'write'], + ]; + + await e.selfAddPolicies('p', 'p', oldRules); + const updated = await e.selfUpdatePolicies('p', 'p', oldRules, newRules); + + expect(updated).toBe(true); + expect(await e.hasPolicy('gina', 'data8', 'write')).toBe(true); + expect(await e.hasPolicy('harry', 'data9', 'write')).toBe(true); + expect(await e.hasPolicy('gina', 'data8', 'read')).toBe(false); + expect(await e.hasPolicy('harry', 'data9', 'read')).toBe(false); + expect(adapter.updatePolicies).not.toHaveBeenCalled(); + }); + + test('selfAddPolicy returns false when policy already exists', async () => { + const rule = ['ivy', 'data10', 'read']; + await e.selfAddPolicy('p', 'p', rule); + + const added = await e.selfAddPolicy('p', 'p', rule); + + expect(added).toBe(false); + }); + + test('selfRemovePolicy returns false when policy does not exist', async () => { + const rule = ['nonexistent', 'data11', 'read']; + + const removed = await e.selfRemovePolicy('p', 'p', rule); + + expect(removed).toBe(false); + }); + + test('selfUpdatePolicy returns false when old policy does not exist', async () => { + const oldRule = ['nonexistent', 'data12', 'read']; + const newRule = ['nonexistent', 'data12', 'write']; + + const updated = await e.selfUpdatePolicy('p', 'p', oldRule, newRule); + + expect(updated).toBe(false); + }); + + test('selfAddPolicies returns false when any policy already exists', async () => { + const rule1 = ['jack', 'data13', 'read']; + await e.selfAddPolicy('p', 'p', rule1); + + const rules = [rule1, ['karen', 'data14', 'write']]; + + const added = await e.selfAddPolicies('p', 'p', rules); + + expect(added).toBe(false); + }); + + test('selfRemovePolicies returns false when any policy does not exist', async () => { + const rule1 = ['leo', 'data15', 'read']; + await e.selfAddPolicy('p', 'p', rule1); + + const rules = [rule1, ['nonexistent', 'data16', 'write']]; + + const removed = await e.selfRemovePolicies('p', 'p', rules); + + expect(removed).toBe(false); + }); + + test('selfUpdatePolicies throws error when array lengths mismatch', async () => { + const oldRules = [['mia', 'data17', 'read']]; + const newRules = [ + ['mia', 'data17', 'write'], + ['nancy', 'data18', 'read'], + ]; + + await expect(e.selfUpdatePolicies('p', 'p', oldRules, newRules)).rejects.toThrow( + 'the length of oldRules should be equal to the length of newRules' + ); + }); + + test('selfUpdatePolicies returns false when any old policy does not exist', async () => { + const oldRules = [ + ['oscar', 'data19', 'read'], + ['nonexistent', 'data20', 'read'], + ]; + const newRules = [ + ['oscar', 'data19', 'write'], + ['nonexistent', 'data20', 'write'], + ]; + await e.selfAddPolicy('p', 'p', oldRules[0]); + + const updated = await e.selfUpdatePolicies('p', 'p', oldRules, newRules); + + expect(updated).toBe(false); + }); +}); + +describe('Management self* APIs with grouping policies', () => { + let e: Enforcer; + let adapter: SpyAdapter; + + beforeEach(async () => { + adapter = createSpyAdapter(); + e = (await newEnforcer('examples/rbac_model.conf', adapter as any)) as Enforcer; + e.enableAutoSave(true); + }); + + test('selfAddPolicy works with g section for role links', async () => { + const rule = ['alice', 'admin']; + + const added = await e.selfAddPolicy('g', 'g', rule); + + expect(added).toBe(true); + expect(adapter.addPolicy).not.toHaveBeenCalled(); + }); + + test('selfRemovePolicy works with g section for role links', async () => { + const rule = ['bob', 'editor']; + await e.selfAddPolicy('g', 'g', rule); + + const removed = await e.selfRemovePolicy('g', 'g', rule); + + expect(removed).toBe(true); + expect(adapter.removePolicy).not.toHaveBeenCalled(); + }); + + test('selfUpdatePolicy works with g section for role links', async () => { + const oldRule = ['carol', 'viewer']; + const newRule = ['carol', 'editor']; + await e.selfAddPolicy('g', 'g', oldRule); + + const updated = await e.selfUpdatePolicy('g', 'g', oldRule, newRule); + + expect(updated).toBe(true); + expect(adapter.updatePolicy).not.toHaveBeenCalled(); + }); + + test('selfAddPolicies works with multiple g policies', async () => { + const rules = [ + ['dave', 'admin'], + ['eve', 'editor'], + ]; + + const added = await e.selfAddPolicies('g', 'g', rules); + + expect(added).toBe(true); + expect(adapter.addPolicies).not.toHaveBeenCalled(); + }); + + test('selfRemoveFilteredPolicy works with g section', async () => { + await e.selfAddPolicy('g', 'g', ['frank', 'admin']); + await e.selfAddPolicy('g', 'g', ['frank', 'editor']); + + const removed = await e.selfRemoveFilteredPolicy('g', 'g', 0, 'frank'); + + expect(removed).toBe(true); + expect(adapter.removeFilteredPolicy).not.toHaveBeenCalled(); + }); +});