Skip to content

Commit cad7098

Browse files
authored
fix: validating currentModel and currentOperation properly (#300)
* fix: validating currentModel and currentOperation properly * update
1 parent 9699d5b commit cad7098

File tree

11 files changed

+787
-6
lines changed

11 files changed

+787
-6
lines changed

packages/language/src/validators/function-invocation-validator.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,7 @@ export default class FunctionInvocationValidator implements AstValidator<Express
6666
}
6767

6868
// validate the context allowed for the function
69-
const exprContext = match(containerAttribute?.decl.$refText)
70-
.with('@default', () => ExpressionContext.DefaultValue)
71-
.with(P.union('@@allow', '@@deny', '@allow', '@deny'), () => ExpressionContext.AccessPolicy)
72-
.with('@@validate', () => ExpressionContext.ValidationRule)
73-
.with('@@index', () => ExpressionContext.Index)
74-
.otherwise(() => undefined);
69+
const exprContext = this.getExpressionContext(containerAttribute);
7570

7671
// get the context allowed for the function
7772
const funcAllowedContext = getFunctionExpressionContext(funcDecl);
@@ -103,6 +98,24 @@ export default class FunctionInvocationValidator implements AstValidator<Express
10398
}
10499
}
105100

101+
private getExpressionContext(containerAttribute: DataModelAttribute | DataFieldAttribute | undefined) {
102+
if (!containerAttribute) {
103+
return undefined;
104+
}
105+
if (this.isValidationAttribute(containerAttribute)) {
106+
return ExpressionContext.ValidationRule;
107+
}
108+
return match(containerAttribute?.decl.$refText)
109+
.with('@default', () => ExpressionContext.DefaultValue)
110+
.with(P.union('@@allow', '@@deny', '@allow', '@deny'), () => ExpressionContext.AccessPolicy)
111+
.with('@@index', () => ExpressionContext.Index)
112+
.otherwise(() => undefined);
113+
}
114+
115+
private isValidationAttribute(attr: DataModelAttribute | DataFieldAttribute) {
116+
return !!attr.decl.ref?.attributes.some((attr) => attr.decl.$refText === '@@@validation');
117+
}
118+
106119
private validateArgs(funcDecl: FunctionDecl, args: Argument[], accept: ValidationAcceptor) {
107120
let success = true;
108121
for (let i = 0; i < funcDecl.params.length; i++) {
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { createPolicyTestClient } from '@zenstackhq/testtools';
2+
import { describe, expect, it } from 'vitest';
3+
4+
describe('Regression for issue 1955', () => {
5+
it('simple policy', async () => {
6+
const db = await createPolicyTestClient(
7+
`
8+
model Post {
9+
id Int @id @default(autoincrement())
10+
name String
11+
expections String[]
12+
@@allow('all', true)
13+
}
14+
`,
15+
{ provider: 'postgresql' },
16+
);
17+
18+
await expect(
19+
db.post.createManyAndReturn({
20+
data: [
21+
{
22+
name: 'bla',
23+
},
24+
{
25+
name: 'blu',
26+
},
27+
],
28+
}),
29+
).resolves.toEqual(
30+
expect.arrayContaining([
31+
expect.objectContaining({ name: 'bla' }),
32+
expect.objectContaining({ name: 'blu' }),
33+
]),
34+
);
35+
36+
await expect(
37+
db.post.updateManyAndReturn({
38+
data: { name: 'foo' },
39+
}),
40+
).resolves.toEqual(
41+
expect.arrayContaining([
42+
expect.objectContaining({ name: 'foo' }),
43+
expect.objectContaining({ name: 'foo' }),
44+
]),
45+
);
46+
});
47+
48+
it('complex policy', async () => {
49+
const db = await createPolicyTestClient(
50+
`
51+
model Post {
52+
id Int @id @default(autoincrement())
53+
name String
54+
expections String[]
55+
comments Comment[]
56+
57+
@@allow('create', true)
58+
@@allow('read,update', comments^[private])
59+
}
60+
61+
model Comment {
62+
id Int @id @default(autoincrement())
63+
private Boolean @default(false)
64+
postId Int
65+
post Post @relation(fields: [postId], references: [id])
66+
}
67+
`,
68+
{ provider: 'postgresql' },
69+
);
70+
71+
await expect(
72+
db.post.createManyAndReturn({
73+
data: [
74+
{
75+
name: 'bla',
76+
},
77+
{
78+
name: 'blu',
79+
},
80+
],
81+
}),
82+
).resolves.toEqual(
83+
expect.arrayContaining([
84+
expect.objectContaining({ name: 'bla' }),
85+
expect.objectContaining({ name: 'blu' }),
86+
]),
87+
);
88+
89+
await expect(
90+
db.post.updateManyAndReturn({
91+
data: { name: 'foo' },
92+
}),
93+
).resolves.toEqual(
94+
expect.arrayContaining([
95+
expect.objectContaining({ name: 'foo' }),
96+
expect.objectContaining({ name: 'foo' }),
97+
]),
98+
);
99+
});
100+
});
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { createPolicyTestClient } from '@zenstackhq/testtools';
2+
import { describe, expect, it } from 'vitest';
3+
4+
describe('Regression for issue 1964', () => {
5+
it('regression1', async () => {
6+
const db = await createPolicyTestClient(
7+
`
8+
model User {
9+
id Int @id
10+
orgId String
11+
}
12+
13+
model Author {
14+
id Int @id @default(autoincrement())
15+
orgId String
16+
name String
17+
posts Post[]
18+
19+
@@unique([orgId, name])
20+
@@allow('all', auth().orgId == orgId)
21+
}
22+
23+
model Post {
24+
id Int @id @default(autoincrement())
25+
orgId String
26+
title String
27+
author Author @relation(fields: [authorId], references: [id])
28+
authorId Int
29+
30+
@@allow('all', auth().orgId == orgId)
31+
}
32+
`,
33+
);
34+
35+
const authDb = db.$setAuth({ id: 1, orgId: 'org' });
36+
37+
const newauthor = await authDb.author.create({
38+
data: {
39+
name: `Foo ${Date.now()}`,
40+
orgId: 'org',
41+
posts: {
42+
createMany: { data: [{ title: 'Hello', orgId: 'org' }] },
43+
},
44+
},
45+
include: { posts: true },
46+
});
47+
48+
await expect(
49+
authDb.author.update({
50+
where: { orgId_name: { orgId: 'org', name: newauthor.name } },
51+
data: {
52+
name: `Bar ${Date.now()}`,
53+
posts: { deleteMany: { id: { equals: newauthor.posts[0].id } } },
54+
},
55+
}),
56+
).toResolveTruthy();
57+
});
58+
59+
it('regression2', async () => {
60+
const db = await createPolicyTestClient(
61+
`
62+
model User {
63+
id Int @id @default(autoincrement())
64+
slug String @unique
65+
profile Profile?
66+
@@allow('all', true)
67+
}
68+
69+
model Profile {
70+
id Int @id @default(autoincrement())
71+
slug String @unique
72+
name String
73+
addresses Address[]
74+
userId Int? @unique
75+
user User? @relation(fields: [userId], references: [id])
76+
@@allow('all', true)
77+
}
78+
79+
model Address {
80+
id Int @id @default(autoincrement())
81+
profileId Int @unique
82+
profile Profile @relation(fields: [profileId], references: [id])
83+
city String
84+
@@allow('all', true)
85+
}
86+
`,
87+
);
88+
89+
const authDb = db.$setAuth({ id: 1, orgId: 'org' });
90+
91+
await authDb.user.create({
92+
data: {
93+
slug: `user1`,
94+
profile: {
95+
create: {
96+
name: `My Profile`,
97+
slug: 'profile1',
98+
addresses: {
99+
create: { id: 1, city: 'City' },
100+
},
101+
},
102+
},
103+
},
104+
});
105+
106+
await expect(
107+
authDb.user.update({
108+
where: { slug: 'user1' },
109+
data: {
110+
profile: {
111+
update: {
112+
addresses: {
113+
deleteMany: { id: { equals: 1 } },
114+
},
115+
},
116+
},
117+
},
118+
}),
119+
).toResolveTruthy();
120+
121+
await expect(authDb.address.count()).resolves.toEqual(0);
122+
});
123+
});
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { createPolicyTestClient } from '@zenstackhq/testtools';
2+
import { expect, it } from 'vitest';
3+
4+
// TODO: field-level policy support
5+
it.skip('regression', async () => {
6+
const db = await createPolicyTestClient(
7+
`
8+
model User {
9+
id Int @id
10+
posts Post[]
11+
secret String @allow('read', posts?[published])
12+
@@allow('all', true)
13+
}
14+
15+
model Post {
16+
id Int @id
17+
author User @relation(fields: [authorId], references: [id])
18+
authorId Int
19+
published Boolean @default(false)
20+
@@allow('all', true)
21+
}
22+
`,
23+
);
24+
25+
await db.$unuseAll().user.create({
26+
data: { id: 1, secret: 'secret', posts: { create: { id: 1, published: true } } },
27+
});
28+
await db.$unuseAll().user.create({
29+
data: { id: 2, secret: 'secret' },
30+
});
31+
32+
await expect(db.user.findFirst({ where: { id: 1 } })).resolves.toMatchObject({ secret: 'secret' });
33+
await expect(db.user.findFirst({ where: { id: 1 }, select: { id: true } })).resolves.toEqual({ id: 1 });
34+
35+
let r = await db.user.findFirst({ where: { id: 2 } });
36+
expect(r.secret).toBeUndefined();
37+
r = await db.user.findFirst({ where: { id: 2 }, select: { id: true } });
38+
expect(r.secret).toBeUndefined();
39+
});
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { createPolicyTestClient, loadSchemaWithError } from '@zenstackhq/testtools';
2+
import { describe, expect, it } from 'vitest';
3+
4+
describe('Regression for issue 1984', () => {
5+
it('regression1', async () => {
6+
const db = await createPolicyTestClient(
7+
`
8+
model User {
9+
id Int @id @default(autoincrement())
10+
access String
11+
12+
@@allow('all',
13+
contains(auth().access, currentModel()) ||
14+
contains(auth().access, currentOperation()))
15+
}
16+
`,
17+
);
18+
19+
const db1 = db;
20+
await expect(db1.user.create({ data: { access: 'foo' } })).toBeRejectedByPolicy();
21+
22+
const db2 = db.$setAuth({ id: 1, access: 'aUser' });
23+
await expect(db2.user.create({ data: { access: 'aUser' } })).toResolveTruthy();
24+
25+
const db3 = db.$setAuth({ id: 1, access: 'do-create-read' });
26+
await expect(db3.user.create({ data: { access: 'do-create-read' } })).toResolveTruthy();
27+
28+
const db4 = db.$setAuth({ id: 1, access: 'do-read' });
29+
await expect(db4.user.create({ data: { access: 'do-read' } })).toBeRejectedByPolicy();
30+
});
31+
32+
it('regression2', async () => {
33+
await loadSchemaWithError(
34+
`
35+
model User {
36+
id Int @id @default(autoincrement())
37+
modelName String
38+
@@validate(contains(modelName, currentModel()))
39+
}
40+
`,
41+
'function "currentModel" is not allowed in the current context: ValidationRule',
42+
);
43+
});
44+
45+
it('regression3', async () => {
46+
await loadSchemaWithError(
47+
`
48+
model User {
49+
id Int @id @default(autoincrement())
50+
modelName String @contains(currentModel())
51+
}
52+
`,
53+
'function "currentModel" is not allowed in the current context: ValidationRule',
54+
);
55+
});
56+
});

0 commit comments

Comments
 (0)