Skip to content

Commit 050f760

Browse files
authored
fix(policy): field-level override rules don't work properly with non-optional to-one relations (#1592)
1 parent 2b7c42c commit 050f760

File tree

2 files changed

+115
-1
lines changed

2 files changed

+115
-1
lines changed

packages/runtime/src/enhancements/policy/policy-utils.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -720,7 +720,13 @@ export class PolicyUtil extends QueryUtils {
720720
}
721721
} else {
722722
// hoist non-nullable to-one filter to the parent level
723-
hoisted = this.getAuthGuard(db, fieldInfo.type, 'read');
723+
let injected = this.safeClone(injectTarget[field]);
724+
if (typeof injected !== 'object') {
725+
injected = {};
726+
}
727+
this.injectAuthGuardAsWhere(db, injected, fieldInfo.type, 'read');
728+
hoisted = injected.where;
729+
724730
// recurse
725731
const subHoisted = this.injectNestedReadConditions(db, fieldInfo.type, injectTarget[field]);
726732
if (subHoisted.length > 0) {
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { loadSchema } from '@zenstackhq/testtools';
2+
3+
describe('issue 1574', () => {
4+
it('regression', async () => {
5+
const { enhance, prisma } = await loadSchema(
6+
`
7+
model User {
8+
id String @id @default(cuid())
9+
modelA ModelA[]
10+
}
11+
12+
//
13+
// ModelA has model-level access-all by owner, but read-all override for the name property
14+
//
15+
model ModelA {
16+
id String @id @default(cuid())
17+
18+
owner User @relation(fields: [ownerId], references: [id])
19+
ownerId String
20+
21+
name String @allow('read', true, true)
22+
prop2 String?
23+
24+
refsB ModelB[]
25+
refsC ModelC[]
26+
27+
@@allow('all', owner == auth())
28+
}
29+
30+
//
31+
// ModelB and ModelC are both allow-all everyone.
32+
// They both have a reference to ModelA, but in ModelB that reference is optional.
33+
//
34+
model ModelB {
35+
id String @id @default(cuid())
36+
37+
ref ModelA? @relation(fields: [refId], references: [id])
38+
refId String?
39+
40+
@@allow('all', true)
41+
}
42+
model ModelC {
43+
id String @id @default(cuid())
44+
45+
ref ModelA @relation(fields: [refId], references: [id])
46+
refId String
47+
48+
@@allow('all', true)
49+
}
50+
`,
51+
{ enhancements: ['policy'] }
52+
);
53+
54+
// create two users
55+
const user1 = await prisma.user.create({ data: { id: '1' } });
56+
const user2 = await prisma.user.create({ data: { id: '2' } });
57+
58+
// create two db instances, enhanced for users 1 and 2
59+
const db1 = enhance(user1);
60+
const db2 = enhance(user2);
61+
62+
// create a ModelA owned by user1
63+
const a = await db1.modelA.create({ data: { name: 'a', ownerId: user1.id } });
64+
65+
// create a ModelB and a ModelC with refs to ModelA
66+
const b = await db1.modelB.create({ data: { refId: a.id } });
67+
const c = await db2.modelC.create({ data: { refId: a.id } });
68+
69+
// works: user1 should be able to read b as well as the entire referenced a
70+
const t1 = await db1.modelB.findFirst({ select: { ref: true } });
71+
expect(t1.ref.name).toBeTruthy();
72+
73+
// works: user1 also should be able to read b as well as the name of the referenced a
74+
const t2 = await db1.modelB.findFirst({ select: { ref: { select: { name: true } } } });
75+
expect(t2.ref.name).toBeTruthy();
76+
77+
// works: user2 also should be able to read b as well as the name of the referenced a
78+
const t3 = await db2.modelB.findFirst({ select: { ref: { select: { name: true } } } });
79+
expect(t3.ref.name).toBeTruthy();
80+
81+
// works: but user2 should not be able to read b with the entire referenced a
82+
const t4 = await db2.modelB.findFirst({ select: { ref: true } });
83+
expect(t4.ref).toBeFalsy();
84+
85+
//
86+
// The following are essentially the same tests, but with ModelC instead of ModelB
87+
//
88+
89+
// works: user1 should be able to read c as well as the entire referenced a
90+
const t5 = await db1.modelC.findFirst({ select: { ref: true } });
91+
expect(t5.ref.name).toBeTruthy();
92+
93+
// works: user1 also should be able to read c as well as the name of the referenced a
94+
const t6 = await db1.modelC.findFirst({ select: { ref: { select: { name: true } } } });
95+
expect(t6.ref.name).toBeTruthy();
96+
97+
// works: user2 should not be able to read b along with the a reference.
98+
// In this case, the entire query returns null because of the required (but inaccessible) ref.
99+
await expect(db2.modelC.findFirst({ select: { ref: true } })).toResolveFalsy();
100+
101+
// works: if user2 queries c directly and gets the refId to a, it can get the a.name directly
102+
const t7 = await db2.modelC.findFirstOrThrow();
103+
await expect(db2.modelA.findFirst({ select: { name: true }, where: { id: t7.refId } })).toResolveTruthy();
104+
105+
// fails: since the last query worked, we'd expect to be able to query c along with the name of the referenced a directly
106+
await expect(db2.modelC.findFirst({ select: { ref: { select: { name: true } } } })).toResolveTruthy();
107+
});
108+
});

0 commit comments

Comments
 (0)