Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 37 additions & 4 deletions packages/runtime/src/enhancements/policy/policy-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,8 +345,11 @@ export class PolicyUtil {
}

if (injected.where && Object.keys(injected.where).length > 0 && !this.isTrue(injected.where)) {
args.where = args.where ?? {};
Object.assign(args.where, injected.where);
if (!args.where) {
args.where = injected.where;
} else {
this.mergeWhereClause(args.where, injected.where);
}
}

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

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

private mergeWhereClause(where: any, extra: any) {
if (!where) {
throw new Error('invalid where clause');
}

extra = this.reduce(extra);
if (this.isTrue(extra)) {
return;
}

// instead of simply wrapping with AND, we preserve the structure
// of the original where clause and merge `extra` into it so that
// unique query can continue working
if (where.AND) {
// merge into existing AND clause
const conditions = Array.isArray(where.AND) ? [...where.AND] : [where.AND];
conditions.push(extra);
const combined: any = this.and(...conditions);

// make sure the merging always goes under AND
where.AND = combined.AND ?? combined;
} else {
// insert an AND clause
where.AND = [extra];
}
}

//#endregion
}
43 changes: 39 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

206 changes: 206 additions & 0 deletions tests/integration/tests/regression/issues.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -627,4 +627,210 @@ model TwoEnumsOneModelTest {
await dropPostgresDb('issue-632');
}
});

it('issue 634', async () => {
const { prisma, withPolicy } = await loadSchema(
`
model User {
id String @id @default(uuid())
email String @unique
password String? @password @omit
name String?
orgs Organization[]
posts Post[]
groups Group[]
comments Comment[]
// can be created by anyone, even not logged in
@@allow('create', true)
// can be read by users in the same organization
@@allow('read', orgs?[members?[auth() == this]])
// full access by oneself
@@allow('all', auth() == this)
}

model Organization {
id String @id @default(uuid())
name String
members User[]
post Post[]
groups Group[]
comments Comment[]

// everyone can create a organization
@@allow('create', true)
// any user in the organization can read the organization
@@allow('read', members?[auth() == this])
}

abstract model organizationBaseEntity {
id String @id @default(uuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
isDeleted Boolean @default(false) @omit
isPublic Boolean @default(false)
owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade)
ownerId String
org Organization @relation(fields: [orgId], references: [id], onDelete: Cascade)
orgId String
groups Group[]

// when create, owner must be set to current user, and user must be in the organization
@@allow('create', owner == auth() && org.members?[this == auth()])
// only the owner can update it and is not allowed to change the owner
@@allow('update', owner == auth() && org.members?[this == auth()] && future().owner == owner)
// allow owner to read
@@allow('read', owner == auth())
// allow shared group members to read it
@@allow('read', groups?[users?[this == auth()]])
// allow organization to access if public
@@allow('read', isPublic && org.members?[this == auth()])
// can not be read if deleted
@@deny('all', isDeleted == true)
}

model Post extends organizationBaseEntity {
title String
content String
comments Comment[]
}

model Comment extends organizationBaseEntity {
content String
post Post @relation(fields: [postId], references: [id])
postId String
}

model Group {
id String @id @default(uuid())
name String
users User[]
posts Post[]
comments Comment[]
org Organization @relation(fields: [orgId], references: [id])
orgId String

// group is shared by organization
@@allow('all', org.members?[auth() == this])
}
`
);

const userData = [
{
id: 'robin@prisma.io',
name: 'Robin',
email: 'robin@prisma.io',
orgs: {
create: [
{
id: 'prisma',
name: 'prisma',
},
],
},
groups: {
create: [
{
id: 'community',
name: 'community',
orgId: 'prisma',
},
],
},
posts: {
create: [
{
id: 'slack',
title: 'Join the Prisma Slack',
content: 'https://slack.prisma.io',
orgId: 'prisma',
comments: {
create: [
{
id: 'comment-1',
content: 'This is the first comment',
orgId: 'prisma',
ownerId: 'robin@prisma.io',
},
],
},
},
],
},
},
{
id: 'bryan@prisma.io',
name: 'Bryan',
email: 'bryan@prisma.io',
orgs: {
connect: {
id: 'prisma',
},
},
posts: {
create: [
{
id: 'discord',
title: 'Join the Prisma Discord',
content: 'https://discord.gg/jS3XY7vp46',
orgId: 'prisma',
groups: {
connect: {
id: 'community',
},
},
},
],
},
},
];

for (const u of userData) {
const user = await prisma.user.create({
data: u,
});
console.log(`Created user with id: ${user.id}`);
}

const db = withPolicy({ id: 'robin@prisma.io' });
await expect(
db.comment.findMany({
where: {
owner: {
name: 'Bryan',
},
},
select: {
id: true,
content: true,
owner: {
select: {
id: true,
name: true,
},
},
},
})
).resolves.toHaveLength(0);

await expect(
db.comment.findMany({
where: {
owner: {
name: 'Robin',
},
},
select: {
id: true,
content: true,
owner: {
select: {
id: true,
name: true,
},
},
},
})
).resolves.toHaveLength(1);
});
});