Skip to content

Commit 4f00cf1

Browse files
authored
fix(delegate): policies inherited from delegate base models are not injected into proper hierarchy (#1776)
1 parent ff997e7 commit 4f00cf1

File tree

4 files changed

+113
-5
lines changed

4 files changed

+113
-5
lines changed

packages/runtime/src/enhancements/node/delegate.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -365,12 +365,14 @@ export class DelegateProxyHandler extends DefaultPrismaProxyHandler {
365365
if (this.options.processIncludeRelationPayload) {
366366
// use the callback in options to process the include payload, so enhancements
367367
// like 'policy' can do extra work (e.g., inject policy rules)
368+
369+
// TODO: this causes both delegate base's policy rules and concrete model's rules to be injected,
370+
// which is not wrong but redundant
371+
368372
await this.options.processIncludeRelationPayload(this.prisma, model, result, this.options, this.context);
369373

370-
// the callback may directly reference fields from polymorphic bases, we need to fix it
371-
// into a proper hierarchy by moving base field references to the base layer relations
372-
const properHierarchy = await this.buildSelectIncludeHierarchy(model, result, false);
373-
result = { ...result, ...properHierarchy };
374+
const properSelectIncludeHierarchy = await this.buildSelectIncludeHierarchy(model, result, false);
375+
result = { ...result, ...properSelectIncludeHierarchy };
374376
}
375377

376378
return result;

packages/schema/src/utils/ast-utils.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,17 @@ function cloneAst<T extends InheritableNode>(
116116
): Mutable<T> {
117117
const clone = copyAstNode(node, buildReference) as Mutable<T>;
118118
clone.$container = newContainer;
119-
clone.$inheritedFrom = node.$inheritedFrom ?? getContainerOfType(node, isDataModel);
119+
120+
if (isDataModel(newContainer) && isDataModelField(node)) {
121+
// walk up the hierarchy to find the upper-most delegate ancestor that defines the field
122+
const delegateBases = getRecursiveBases(newContainer).filter(isDelegateModel);
123+
clone.$inheritedFrom = delegateBases.findLast((base) => base.fields.some((f) => f.name === node.name));
124+
}
125+
126+
if (!clone.$inheritedFrom) {
127+
clone.$inheritedFrom = node.$inheritedFrom ?? getContainerOfType(node, isDataModel);
128+
}
129+
120130
return clone;
121131
}
122132

tests/integration/tests/enhancements/with-delegate/policy-interaction.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,53 @@ describe('Polymorphic Policy Test', () => {
484484
await expect(prisma.post.findUnique({ where: { id: post.id } })).toResolveFalsy();
485485
});
486486

487+
it('respects sub model policies when queried from a base: case 3', async () => {
488+
const { enhance } = await loadSchema(
489+
`
490+
model User {
491+
id Int @id @default(autoincrement())
492+
assets Asset[]
493+
@@allow('all', true)
494+
}
495+
496+
model Asset {
497+
id Int @id @default(autoincrement())
498+
user User @relation(fields: [userId], references: [id])
499+
userId Int
500+
value Int @default(0)
501+
type String
502+
@@delegate(type)
503+
@@allow('all', value > 0)
504+
}
505+
506+
model Post extends Asset {
507+
title String
508+
deleted Boolean @default(false)
509+
@@deny('read', deleted)
510+
}
511+
`
512+
);
513+
514+
const db = enhance();
515+
const user = await db.user.create({ data: { id: 1 } });
516+
517+
// can't create
518+
await expect(
519+
db.post.create({ data: { id: 1, title: 'Post1', userId: user.id, value: 0 } })
520+
).toBeRejectedByPolicy();
521+
522+
// can't read back
523+
await expect(
524+
db.post.create({ data: { id: 1, title: 'Post1', userId: user.id, value: 1, deleted: true } })
525+
).toBeRejectedByPolicy();
526+
527+
await expect(
528+
db.post.create({ data: { id: 2, title: 'Post1', userId: user.id, value: 1, deleted: false } })
529+
).toResolveTruthy();
530+
531+
await expect(db.asset.findMany()).resolves.toHaveLength(2);
532+
});
533+
487534
it('respects field-level policies', async () => {
488535
const { enhance } = await loadSchema(`
489536
model User {
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { loadSchema } from '@zenstackhq/testtools';
2+
3+
describe('issue 1770', () => {
4+
it('regression', async () => {
5+
const { enhance } = await loadSchema(
6+
`
7+
model User {
8+
id String @id @default(cuid())
9+
orgs OrgUser[]
10+
}
11+
12+
model OrgUser {
13+
id String @id @default(cuid())
14+
user User @relation(fields: [userId], references: [id])
15+
userId String
16+
org Organization @relation(fields: [orgId], references: [id])
17+
orgId String
18+
}
19+
20+
model Organization {
21+
id String @id @default(uuid())
22+
users OrgUser[]
23+
resources Resource[] @relation("organization")
24+
}
25+
26+
abstract model BaseAuth {
27+
id String @id @default(uuid())
28+
organizationId String?
29+
organization Organization? @relation(fields: [organizationId], references: [id], name: "organization")
30+
31+
@@allow('all', organization.users?[user == auth()])
32+
}
33+
34+
model Resource extends BaseAuth {
35+
name String?
36+
type String?
37+
38+
@@delegate(type)
39+
}
40+
41+
model Personnel extends Resource {
42+
}
43+
`
44+
);
45+
46+
const db = enhance();
47+
await expect(db.resource.findMany()).toResolveTruthy();
48+
});
49+
});

0 commit comments

Comments
 (0)