diff --git a/src/util/util.ts b/src/util/util.ts index 8b103e3..bbde905 100644 --- a/src/util/util.ts +++ b/src/util/util.ts @@ -17,11 +17,16 @@ import { mustGetDefaultFileSystem } from '../persist'; -const escapeAssertionReg = new RegExp(/(^|[^A-Za-z0-9_])([rp])[0-9]*\./g); +const escapeAssertionReg = new RegExp(/([()\s|&,=!><+\-*/]|^)((r|p)[0-9]*)\./g); function escapeAssertion(s: string): string { - s = s.replace(escapeAssertionReg, (match, p1, p2) => { - return p1 + p2 + match.substring(p1.length + p2.length).replace('.', '_'); + s = s.replace(escapeAssertionReg, (match) => { + // Replace only the last dot with underscore (preserve the prefix character) + const lastDotIdx = match.lastIndexOf('.'); + if (lastDotIdx > 0) { + return match.substring(0, lastDotIdx) + '_'; + } + return match; }); return s; } diff --git a/test/enforcer.test.ts b/test/enforcer.test.ts index 06f88d8..059e499 100644 --- a/test/enforcer.test.ts +++ b/test/enforcer.test.ts @@ -627,6 +627,26 @@ test('test ABAC single eval() with r. in unexpected places', async () => { await testEnforce(e, { id: 3 }, ({ owner: { id: 2 } } as unknown) as string, 'read', false); }); +test('test escapeAssertion with string literals (issue)', async () => { + // Test case from GitHub issue: escapeAssertion should not replace r./p. inside string literals + const MY_RESOURCE_NAME = 'r.my_resource'; + const model = newModel(); + model.addDef('r', 'r', 'act, obj'); + model.addDef('p', 'p', 'act, obj, rule'); + model.addDef('e', 'e', 'some(where (p.eft == allow))'); + model.addDef('m', 'm', 'r.act == p.act && r.obj == p.obj && eval(p.rule)'); + + const enforcer = await newEnforcer(model); + enforcer.addPolicy('alice', MY_RESOURCE_NAME, `p.obj == "${MY_RESOURCE_NAME}"`); + + // Should work because string literals are not escaped + await expect(enforcer.enforce('alice', MY_RESOURCE_NAME)).resolves.toBe(true); + + // Test with single quotes as well + enforcer.addPolicy('bob', 'p.resource', `p.obj == 'p.resource'`); + await expect(enforcer.enforce('bob', 'p.resource')).resolves.toBe(true); +}); + test('TestEnforceSync', async () => { const m = newModel(); m.addDef('r', 'r', 'sub, obj, act'); diff --git a/test/util.test.ts b/test/util.test.ts index cd9c70e..8a5c3f8 100644 --- a/test/util.test.ts +++ b/test/util.test.ts @@ -228,3 +228,27 @@ test('bracketCompatible', () => { ) ).toEqual("g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act || r.obj in ['data2', 'data3'] || r.obj in ['data4', 'data5']"); }); + +test('test escapeAssertion', () => { + expect(util.escapeAssertion('r_sub == r_obj.value')).toEqual('r_sub == r_obj.value'); + expect(util.escapeAssertion('p_sub == r_sub.value')).toEqual('p_sub == r_sub.value'); + expect(util.escapeAssertion('r.attr.value == p.attr')).toEqual('r_attr.value == p_attr'); + expect(util.escapeAssertion('r.attr.value == p.attr')).toEqual('r_attr.value == p_attr'); + expect(util.escapeAssertion('r.attp.value || p.attr')).toEqual('r_attp.value || p_attr'); + expect(util.escapeAssertion('r2.attr.value == p2.attr')).toEqual('r2_attr.value == p2_attr'); + expect(util.escapeAssertion('r2.attp.value || p2.attr')).toEqual('r2_attp.value || p2_attr'); + expect(util.escapeAssertion('r.attp.value &&p.attr')).toEqual('r_attp.value &&p_attr'); + expect(util.escapeAssertion('r.attp.value >p.attr')).toEqual('r_attp.value >p_attr'); + expect(util.escapeAssertion('r.attp.value