Skip to content

Commit 28ae08d

Browse files
authored
fix(policy): currentModel and currentOperation inside of collection predicates (#2537)
1 parent a0a6424 commit 28ae08d

File tree

3 files changed

+61
-0
lines changed

3 files changed

+61
-0
lines changed

packages/plugins/policy/src/expression-evaluator.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { invariant } from '@zenstackhq/common-helpers';
2+
import type { CRUD_EXT } from '@zenstackhq/orm';
23
import {
34
ExpressionUtils,
45
type ArrayExpression,
@@ -18,6 +19,8 @@ type ExpressionEvaluatorContext = {
1819
thisValue?: any;
1920
// scope for resolving references to collection predicate bindings
2021
bindingScope?: Record<string, any>;
22+
operation: CRUD_EXT;
23+
thisType: string;
2124
};
2225

2326
/**
@@ -44,6 +47,10 @@ export class ExpressionEvaluator {
4447
private evaluateCall(expr: CallExpression, context: ExpressionEvaluatorContext): any {
4548
if (expr.function === 'auth') {
4649
return context.auth;
50+
} else if (expr.function === 'currentModel') {
51+
return context.thisType;
52+
} else if (expr.function === 'currentOperation') {
53+
return context.operation;
4754
} else {
4855
throw new Error(`Unsupported call expression function: ${expr.function}`);
4956
}

packages/plugins/policy/src/expression-transformer.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,8 @@ export class ExpressionTransformer<Schema extends SchemaDef> {
337337
thisValue: context.contextValue,
338338
auth: this.auth,
339339
bindingScope: this.getEvaluationBindingScope(context.bindingScope),
340+
operation: context.operation,
341+
thisType: context.thisType,
340342
});
341343

342344
// get LHS's type
@@ -436,6 +438,8 @@ export class ExpressionTransformer<Schema extends SchemaDef> {
436438
auth: this.auth,
437439
thisValue: context.contextValue,
438440
bindingScope: this.getEvaluationBindingScope(context.bindingScope),
441+
operation: context.operation,
442+
thisType: context.thisType,
439443
});
440444
return this.transformValue(value, 'Boolean');
441445
} else {
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { createPolicyTestClient } from '@zenstackhq/testtools';
2+
import { describe, expect, it } from 'vitest';
3+
4+
describe('Regression for issue #2536', () => {
5+
it('supports currentModel and currentOperation in nested expressions', async () => {
6+
const db = await createPolicyTestClient(
7+
`
8+
model User {
9+
id String @id
10+
groups Group[] @relation("UserGroups")
11+
}
12+
13+
model Group {
14+
id String @id
15+
modelName String
16+
modelOperation String
17+
users User[] @relation("UserGroups")
18+
}
19+
20+
// define a mixin to also check that currentModel correctly resolves to the model where the mixin is applied
21+
type AuthPolicyMixin {
22+
@@allow('all', auth().groups?[modelName == currentModel() && modelOperation == currentOperation()])
23+
}
24+
25+
model Foo with AuthPolicyMixin {
26+
id String @id @default(cuid())
27+
}
28+
`,
29+
);
30+
31+
const readGroup = { modelName: 'Foo', modelOperation: 'read' };
32+
33+
await expect(db.$setAuth({ id: 'user1', groups: [readGroup] }).foo.create({ data: {} })).toBeRejectedByPolicy();
34+
await expect(
35+
db
36+
.$setAuth({ id: 'user1', groups: [{ modelName: 'FooBar', modelOperation: 'create' }, readGroup] })
37+
.foo.create({ data: {} }),
38+
).toBeRejectedByPolicy();
39+
await expect(
40+
db
41+
.$setAuth({ id: 'user1', groups: [{ modelName: 'Foo', modelOperation: 'read' }, readGroup] })
42+
.foo.create({ data: {} }),
43+
).toBeRejectedByPolicy();
44+
await expect(
45+
db
46+
.$setAuth({ id: 'user1', groups: [{ modelName: 'Foo', modelOperation: 'create' }, readGroup] })
47+
.foo.create({ data: {} }),
48+
).toResolveTruthy();
49+
});
50+
});

0 commit comments

Comments
 (0)