diff --git a/packages/plugins/policy/src/expression-evaluator.ts b/packages/plugins/policy/src/expression-evaluator.ts index 85e97a03e..91cc4a433 100644 --- a/packages/plugins/policy/src/expression-evaluator.ts +++ b/packages/plugins/policy/src/expression-evaluator.ts @@ -1,4 +1,5 @@ import { invariant } from '@zenstackhq/common-helpers'; +import type { CRUD_EXT } from '@zenstackhq/orm'; import { ExpressionUtils, type ArrayExpression, @@ -18,6 +19,8 @@ type ExpressionEvaluatorContext = { thisValue?: any; // scope for resolving references to collection predicate bindings bindingScope?: Record; + operation: CRUD_EXT; + thisType: string; }; /** @@ -44,6 +47,10 @@ export class ExpressionEvaluator { private evaluateCall(expr: CallExpression, context: ExpressionEvaluatorContext): any { if (expr.function === 'auth') { return context.auth; + } else if (expr.function === 'currentModel') { + return context.thisType; + } else if (expr.function === 'currentOperation') { + return context.operation; } else { throw new Error(`Unsupported call expression function: ${expr.function}`); } diff --git a/packages/plugins/policy/src/expression-transformer.ts b/packages/plugins/policy/src/expression-transformer.ts index f85c35faf..96c4adad5 100644 --- a/packages/plugins/policy/src/expression-transformer.ts +++ b/packages/plugins/policy/src/expression-transformer.ts @@ -337,6 +337,8 @@ export class ExpressionTransformer { thisValue: context.contextValue, auth: this.auth, bindingScope: this.getEvaluationBindingScope(context.bindingScope), + operation: context.operation, + thisType: context.thisType, }); // get LHS's type @@ -436,6 +438,8 @@ export class ExpressionTransformer { auth: this.auth, thisValue: context.contextValue, bindingScope: this.getEvaluationBindingScope(context.bindingScope), + operation: context.operation, + thisType: context.thisType, }); return this.transformValue(value, 'Boolean'); } else { diff --git a/tests/regression/test/issue-2536.test.ts b/tests/regression/test/issue-2536.test.ts new file mode 100644 index 000000000..e814cc3ae --- /dev/null +++ b/tests/regression/test/issue-2536.test.ts @@ -0,0 +1,50 @@ +import { createPolicyTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +describe('Regression for issue #2536', () => { + it('supports currentModel and currentOperation in nested expressions', async () => { + const db = await createPolicyTestClient( + ` + model User { + id String @id + groups Group[] @relation("UserGroups") + } + + model Group { + id String @id + modelName String + modelOperation String + users User[] @relation("UserGroups") + } + + // define a mixin to also check that currentModel correctly resolves to the model where the mixin is applied + type AuthPolicyMixin { + @@allow('all', auth().groups?[modelName == currentModel() && modelOperation == currentOperation()]) + } + + model Foo with AuthPolicyMixin { + id String @id @default(cuid()) + } + `, + ); + + const readGroup = { modelName: 'Foo', modelOperation: 'read' }; + + await expect(db.$setAuth({ id: 'user1', groups: [readGroup] }).foo.create({ data: {} })).toBeRejectedByPolicy(); + await expect( + db + .$setAuth({ id: 'user1', groups: [{ modelName: 'FooBar', modelOperation: 'create' }, readGroup] }) + .foo.create({ data: {} }), + ).toBeRejectedByPolicy(); + await expect( + db + .$setAuth({ id: 'user1', groups: [{ modelName: 'Foo', modelOperation: 'read' }, readGroup] }) + .foo.create({ data: {} }), + ).toBeRejectedByPolicy(); + await expect( + db + .$setAuth({ id: 'user1', groups: [{ modelName: 'Foo', modelOperation: 'create' }, readGroup] }) + .foo.create({ data: {} }), + ).toResolveTruthy(); + }); +});