Skip to content

Commit 9a35f88

Browse files
authored
fix: conditions hoisted from nested read overwrites toplevel where conditions (#635)
1 parent 7c76df1 commit 9a35f88

File tree

3 files changed

+244
-5
lines changed

3 files changed

+244
-5
lines changed

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

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -345,8 +345,11 @@ export class PolicyUtil {
345345
}
346346

347347
if (injected.where && Object.keys(injected.where).length > 0 && !this.isTrue(injected.where)) {
348-
args.where = args.where ?? {};
349-
Object.assign(args.where, injected.where);
348+
if (!args.where) {
349+
args.where = injected.where;
350+
} else {
351+
this.mergeWhereClause(args.where, injected.where);
352+
}
350353
}
351354

352355
// recursively inject read guard conditions into nested select, include, and _count
@@ -355,8 +358,11 @@ export class PolicyUtil {
355358
// the injection process may generate conditions that need to be hoisted to the toplevel,
356359
// if so, merge it with the existing where
357360
if (hoistedConditions.length > 0) {
358-
args.where = args.where ?? {};
359-
Object.assign(args.where, ...hoistedConditions);
361+
if (!args.where) {
362+
args.where = this.and(...hoistedConditions);
363+
} else {
364+
this.mergeWhereClause(args.where, this.and(...hoistedConditions));
365+
}
360366
}
361367

362368
return true;
@@ -800,5 +806,32 @@ export class PolicyUtil {
800806
return Object.assign({}, ...idFields.map((f) => ({ [f.name]: true })));
801807
}
802808

809+
private mergeWhereClause(where: any, extra: any) {
810+
if (!where) {
811+
throw new Error('invalid where clause');
812+
}
813+
814+
extra = this.reduce(extra);
815+
if (this.isTrue(extra)) {
816+
return;
817+
}
818+
819+
// instead of simply wrapping with AND, we preserve the structure
820+
// of the original where clause and merge `extra` into it so that
821+
// unique query can continue working
822+
if (where.AND) {
823+
// merge into existing AND clause
824+
const conditions = Array.isArray(where.AND) ? [...where.AND] : [where.AND];
825+
conditions.push(extra);
826+
const combined: any = this.and(...conditions);
827+
828+
// make sure the merging always goes under AND
829+
where.AND = combined.AND ?? combined;
830+
} else {
831+
// insert an AND clause
832+
where.AND = [extra];
833+
}
834+
}
835+
803836
//#endregion
804837
}

pnpm-lock.yaml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/integration/tests/regression/issues.test.ts

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -627,4 +627,210 @@ model TwoEnumsOneModelTest {
627627
await dropPostgresDb('issue-632');
628628
}
629629
});
630+
631+
it('issue 634', async () => {
632+
const { prisma, withPolicy } = await loadSchema(
633+
`
634+
model User {
635+
id String @id @default(uuid())
636+
email String @unique
637+
password String? @password @omit
638+
name String?
639+
orgs Organization[]
640+
posts Post[]
641+
groups Group[]
642+
comments Comment[]
643+
// can be created by anyone, even not logged in
644+
@@allow('create', true)
645+
// can be read by users in the same organization
646+
@@allow('read', orgs?[members?[auth() == this]])
647+
// full access by oneself
648+
@@allow('all', auth() == this)
649+
}
650+
651+
model Organization {
652+
id String @id @default(uuid())
653+
name String
654+
members User[]
655+
post Post[]
656+
groups Group[]
657+
comments Comment[]
658+
659+
// everyone can create a organization
660+
@@allow('create', true)
661+
// any user in the organization can read the organization
662+
@@allow('read', members?[auth() == this])
663+
}
664+
665+
abstract model organizationBaseEntity {
666+
id String @id @default(uuid())
667+
createdAt DateTime @default(now())
668+
updatedAt DateTime @updatedAt
669+
isDeleted Boolean @default(false) @omit
670+
isPublic Boolean @default(false)
671+
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
672+
ownerId String
673+
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
674+
orgId String
675+
groups Group[]
676+
677+
// when create, owner must be set to current user, and user must be in the organization
678+
@@allow('create', owner == auth() && org.members?[this == auth()])
679+
// only the owner can update it and is not allowed to change the owner
680+
@@allow('update', owner == auth() && org.members?[this == auth()] && future().owner == owner)
681+
// allow owner to read
682+
@@allow('read', owner == auth())
683+
// allow shared group members to read it
684+
@@allow('read', groups?[users?[this == auth()]])
685+
// allow organization to access if public
686+
@@allow('read', isPublic && org.members?[this == auth()])
687+
// can not be read if deleted
688+
@@deny('all', isDeleted == true)
689+
}
690+
691+
model Post extends organizationBaseEntity {
692+
title String
693+
content String
694+
comments Comment[]
695+
}
696+
697+
model Comment extends organizationBaseEntity {
698+
content String
699+
post Post @relation(fields: [postId], references: [id])
700+
postId String
701+
}
702+
703+
model Group {
704+
id String @id @default(uuid())
705+
name String
706+
users User[]
707+
posts Post[]
708+
comments Comment[]
709+
org Organization @relation(fields: [orgId], references: [id])
710+
orgId String
711+
712+
// group is shared by organization
713+
@@allow('all', org.members?[auth() == this])
714+
}
715+
`
716+
);
717+
718+
const userData = [
719+
{
720+
id: 'robin@prisma.io',
721+
name: 'Robin',
722+
email: 'robin@prisma.io',
723+
orgs: {
724+
create: [
725+
{
726+
id: 'prisma',
727+
name: 'prisma',
728+
},
729+
],
730+
},
731+
groups: {
732+
create: [
733+
{
734+
id: 'community',
735+
name: 'community',
736+
orgId: 'prisma',
737+
},
738+
],
739+
},
740+
posts: {
741+
create: [
742+
{
743+
id: 'slack',
744+
title: 'Join the Prisma Slack',
745+
content: 'https://slack.prisma.io',
746+
orgId: 'prisma',
747+
comments: {
748+
create: [
749+
{
750+
id: 'comment-1',
751+
content: 'This is the first comment',
752+
orgId: 'prisma',
753+
ownerId: 'robin@prisma.io',
754+
},
755+
],
756+
},
757+
},
758+
],
759+
},
760+
},
761+
{
762+
id: 'bryan@prisma.io',
763+
name: 'Bryan',
764+
email: 'bryan@prisma.io',
765+
orgs: {
766+
connect: {
767+
id: 'prisma',
768+
},
769+
},
770+
posts: {
771+
create: [
772+
{
773+
id: 'discord',
774+
title: 'Join the Prisma Discord',
775+
content: 'https://discord.gg/jS3XY7vp46',
776+
orgId: 'prisma',
777+
groups: {
778+
connect: {
779+
id: 'community',
780+
},
781+
},
782+
},
783+
],
784+
},
785+
},
786+
];
787+
788+
for (const u of userData) {
789+
const user = await prisma.user.create({
790+
data: u,
791+
});
792+
console.log(`Created user with id: ${user.id}`);
793+
}
794+
795+
const db = withPolicy({ id: 'robin@prisma.io' });
796+
await expect(
797+
db.comment.findMany({
798+
where: {
799+
owner: {
800+
name: 'Bryan',
801+
},
802+
},
803+
select: {
804+
id: true,
805+
content: true,
806+
owner: {
807+
select: {
808+
id: true,
809+
name: true,
810+
},
811+
},
812+
},
813+
})
814+
).resolves.toHaveLength(0);
815+
816+
await expect(
817+
db.comment.findMany({
818+
where: {
819+
owner: {
820+
name: 'Robin',
821+
},
822+
},
823+
select: {
824+
id: true,
825+
content: true,
826+
owner: {
827+
select: {
828+
id: true,
829+
name: true,
830+
},
831+
},
832+
},
833+
})
834+
).resolves.toHaveLength(1);
835+
});
630836
});

0 commit comments

Comments
 (0)