From ffcdf514f8eec61933de768d64501f9cb688a63c Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sat, 12 Aug 2023 22:30:44 +0800 Subject: [PATCH 1/3] fix: incorrect filter injection for nested reads --- .../src/enhancements/policy/policy-utils.ts | 19 +- .../with-policy/nested-to-many.test.ts | 82 ++++++++ .../with-policy/nested-to-one.test.ts | 53 +++++ .../tests/regression/issues.test.ts | 195 ++++++++++++++++++ 4 files changed, 342 insertions(+), 7 deletions(-) diff --git a/packages/runtime/src/enhancements/policy/policy-utils.ts b/packages/runtime/src/enhancements/policy/policy-utils.ts index 7508760fa..de1fc1f1f 100644 --- a/packages/runtime/src/enhancements/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/policy/policy-utils.ts @@ -489,16 +489,21 @@ export class PolicyUtil { } // inject extra condition for to-many or nullable to-one relation await this.injectAuthGuard(injectTarget[field], fieldInfo.type, 'read'); + + // recurse + const subHoisted = await this.injectNestedReadConditions(fieldInfo.type, injectTarget[field]); + if (subHoisted.length > 0) { + // we can convert it to a where at this level + injectTarget[field].where = this.and(injectTarget[field].where, ...subHoisted); + } } else { // hoist non-nullable to-one filter to the parent level hoisted = this.getAuthGuard(fieldInfo.type, 'read'); - } - - // recurse - const subHoisted = await this.injectNestedReadConditions(fieldInfo.type, injectTarget[field]); - - if (subHoisted.length > 0) { - hoisted = this.and(hoisted, ...subHoisted); + // recurse + const subHoisted = await this.injectNestedReadConditions(fieldInfo.type, injectTarget[field]); + if (subHoisted.length > 0) { + hoisted = this.and(hoisted, ...subHoisted); + } } if (hoisted && !this.isTrue(hoisted)) { diff --git a/tests/integration/tests/enhancements/with-policy/nested-to-many.test.ts b/tests/integration/tests/enhancements/with-policy/nested-to-many.test.ts index d0511dd5f..fa7059faa 100644 --- a/tests/integration/tests/enhancements/with-policy/nested-to-many.test.ts +++ b/tests/integration/tests/enhancements/with-policy/nested-to-many.test.ts @@ -61,6 +61,88 @@ describe('With Policy:nested to-many', () => { expect(read.m2).toHaveLength(2); }); + it('read condition hoisting', async () => { + const { withPolicy } = await loadSchema( + ` + model M1 { + id String @id @default(uuid()) + m2 M2[] + + @@allow('all', true) + } + + model M2 { + id String @id @default(uuid()) + value Int + + m1 M1 @relation(fields: [m1Id], references:[id]) + m1Id String + + m3 M3 @relation(fields: [m3Id], references:[id]) + m3Id String @unique + + m4 M4 @relation(fields: [m4Id], references:[id]) + m4Id String + + @@allow('create', true) + @@allow('read', value > 0) + } + + model M3 { + id String @id @default(uuid()) + value Int + m2 M2? + + @@allow('create', true) + @@allow('read', value > 1) + } + + model M4 { + id String @id @default(uuid()) + value Int + m2 M2[] + + @@allow('create', true) + @@allow('read', value > 1) + } + ` + ); + + const db = withPolicy(); + + await db.m1.create({ + include: { m2: true }, + data: { + id: '1', + m2: { + create: [ + { id: 'm2-1', value: 1, m3: { create: { value: 1 } }, m4: { create: { value: 1 } } }, + { id: 'm2-2', value: 1, m3: { create: { value: 2 } }, m4: { create: { value: 2 } } }, + ], + }, + }, + }); + + let read = await db.m1.findFirst({ include: { m2: true } }); + expect(read.m2).toHaveLength(2); + read = await db.m1.findFirst({ select: { m2: { select: { id: true } } } }); + expect(read.m2).toHaveLength(2); + + // check m2-m3 filtering + // including m3 causes m2 to be filtered since m3 is not nullable + read = await db.m1.findFirst({ include: { m2: { include: { m3: true } } } }); + expect(read.m2).toHaveLength(1); + read = await db.m1.findFirst({ select: { m2: { select: { m3: true } } } }); + expect(read.m2).toHaveLength(1); + + // check m2-m4 filtering + // including m3 causes m2 to be filtered since m4 is not nullable + read = await db.m1.findFirst({ include: { m2: { include: { m4: true } } } }); + expect(read.m2).toHaveLength(1); + read = await db.m1.findFirst({ select: { m2: { select: { m4: true } } } }); + expect(read.m2).toHaveLength(1); + }); + it('create simple', async () => { const { withPolicy } = await loadSchema( ` diff --git a/tests/integration/tests/enhancements/with-policy/nested-to-one.test.ts b/tests/integration/tests/enhancements/with-policy/nested-to-one.test.ts index 9d5b9be4b..2e14b6d02 100644 --- a/tests/integration/tests/enhancements/with-policy/nested-to-one.test.ts +++ b/tests/integration/tests/enhancements/with-policy/nested-to-one.test.ts @@ -99,6 +99,59 @@ describe('With Policy:nested to-one', () => { await expect(db.m2.findMany({ include: { m1: true } })).toResolveTruthy(); }); + it('read condition hoisting', async () => { + const { withPolicy } = await loadSchema( + ` + model M1 { + id String @id @default(uuid()) + m2 M2 @relation(fields: [m2Id], references:[id]) + m2Id String @unique + + @@allow('all', true) + } + + model M2 { + id String @id @default(uuid()) + value Int + + m1 M1? + + m3 M3 @relation(fields: [m3Id], references:[id]) + m3Id String @unique + + @@allow('create', true) + @@allow('read', value > 0) + } + + model M3 { + id String @id @default(uuid()) + value Int + m2 M2? + + @@allow('create', true) + @@allow('read', value > 1) + } + ` + ); + + const db = withPolicy(); + + await db.m1.create({ + include: { m2: true }, + data: { + id: '1', + m2: { + create: { id: 'm2-1', value: 1, m3: { create: { value: 1 } } }, + }, + }, + }); + + // check m2-m3 filtering + // including m3 causes m1 to be filtered due to hosting + await expect(db.m1.findFirst({ include: { m2: { include: { m3: true } } } })).toResolveNull(); + await expect(db.m1.findFirst({ select: { m2: { select: { m3: true } } } })).toResolveNull(); + }); + it('create and update tests', async () => { const { withPolicy } = await loadSchema( ` diff --git a/tests/integration/tests/regression/issues.test.ts b/tests/integration/tests/regression/issues.test.ts index afb709667..d3bdbd52a 100644 --- a/tests/integration/tests/regression/issues.test.ts +++ b/tests/integration/tests/regression/issues.test.ts @@ -354,4 +354,199 @@ describe('GitHub issues regression', () => { }) ).toResolveTruthy(); }); + + it('issue 624', 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]) +} + `, + { logPrismaQuery: true } + ); + + 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.post.findMany({ + where: {}, + select: { + id: true, + content: true, + owner: { + select: { + id: true, + name: true, + }, + }, + comments: { + select: { + id: true, + content: true, + owner: { + select: { + id: true, + name: true, + }, + }, + }, + }, + }, + }) + ).resolves.toHaveLength(2); + }); }); From 58108444ed3d86b5320e6c08922d5b7538c18aa7 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sat, 12 Aug 2023 22:32:07 +0800 Subject: [PATCH 2/3] chore: bump version --- package.json | 2 +- packages/language/package.json | 2 +- packages/plugins/openapi/package.json | 2 +- packages/plugins/swr/package.json | 2 +- packages/plugins/tanstack-query/package.json | 2 +- packages/plugins/trpc/package.json | 2 +- packages/runtime/package.json | 2 +- packages/schema/package.json | 2 +- packages/sdk/package.json | 2 +- packages/server/package.json | 2 +- packages/testtools/package.json | 2 +- 11 files changed, 11 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 9631afe17..8a23bbd4a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "zenstack-monorepo", - "version": "1.0.0-beta.16", + "version": "1.0.0-beta.17", "description": "", "scripts": { "build": "pnpm -r build", diff --git a/packages/language/package.json b/packages/language/package.json index 3604446c4..e52c47113 100644 --- a/packages/language/package.json +++ b/packages/language/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/language", - "version": "1.0.0-beta.16", + "version": "1.0.0-beta.17", "displayName": "ZenStack modeling language compiler", "description": "ZenStack modeling language compiler", "homepage": "https://zenstack.dev", diff --git a/packages/plugins/openapi/package.json b/packages/plugins/openapi/package.json index cdb896de0..5f2cbbecb 100644 --- a/packages/plugins/openapi/package.json +++ b/packages/plugins/openapi/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/openapi", "displayName": "ZenStack Plugin and Runtime for OpenAPI", - "version": "1.0.0-beta.16", + "version": "1.0.0-beta.17", "description": "ZenStack plugin and runtime supporting OpenAPI", "main": "index.js", "repository": { diff --git a/packages/plugins/swr/package.json b/packages/plugins/swr/package.json index 99fa6cccc..2d7f9086c 100644 --- a/packages/plugins/swr/package.json +++ b/packages/plugins/swr/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/swr", "displayName": "ZenStack plugin for generating SWR hooks", - "version": "1.0.0-beta.16", + "version": "1.0.0-beta.17", "description": "ZenStack plugin for generating SWR hooks", "main": "index.js", "repository": { diff --git a/packages/plugins/tanstack-query/package.json b/packages/plugins/tanstack-query/package.json index 9cfa6d24f..657460b4d 100644 --- a/packages/plugins/tanstack-query/package.json +++ b/packages/plugins/tanstack-query/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/tanstack-query", "displayName": "ZenStack plugin for generating tanstack-query hooks", - "version": "1.0.0-beta.16", + "version": "1.0.0-beta.17", "description": "ZenStack plugin for generating tanstack-query hooks", "main": "index.js", "exports": { diff --git a/packages/plugins/trpc/package.json b/packages/plugins/trpc/package.json index 3a52cbf82..c6c5a3732 100644 --- a/packages/plugins/trpc/package.json +++ b/packages/plugins/trpc/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/trpc", "displayName": "ZenStack plugin for tRPC", - "version": "1.0.0-beta.16", + "version": "1.0.0-beta.17", "description": "ZenStack plugin for tRPC", "main": "index.js", "repository": { diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 5b4c40678..d8eae6123 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -1,7 +1,7 @@ { "name": "@zenstackhq/runtime", "displayName": "ZenStack Runtime Library", - "version": "1.0.0-beta.16", + "version": "1.0.0-beta.17", "description": "Runtime of ZenStack for both client-side and server-side environments.", "repository": { "type": "git", diff --git a/packages/schema/package.json b/packages/schema/package.json index 8e15e68f4..8e511030a 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -3,7 +3,7 @@ "publisher": "zenstack", "displayName": "ZenStack Language Tools", "description": "A toolkit for building secure CRUD apps with Next.js + Typescript", - "version": "1.0.0-beta.16", + "version": "1.0.0-beta.17", "author": { "name": "ZenStack Team" }, diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 326a38ea1..5f6ab4fc8 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/sdk", - "version": "1.0.0-beta.16", + "version": "1.0.0-beta.17", "description": "ZenStack plugin development SDK", "main": "index.js", "scripts": { diff --git a/packages/server/package.json b/packages/server/package.json index 7239f2da5..cd599c5e9 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/server", - "version": "1.0.0-beta.16", + "version": "1.0.0-beta.17", "displayName": "ZenStack Server-side Adapters", "description": "ZenStack server-side adapters", "homepage": "https://zenstack.dev", diff --git a/packages/testtools/package.json b/packages/testtools/package.json index 05fc78a90..465021a46 100644 --- a/packages/testtools/package.json +++ b/packages/testtools/package.json @@ -1,6 +1,6 @@ { "name": "@zenstackhq/testtools", - "version": "1.0.0-beta.16", + "version": "1.0.0-beta.17", "description": "ZenStack Test Tools", "main": "index.js", "publishConfig": { From 25d1afc85f2b082aaea6ca69a71aa6fc880eb616 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Sat, 12 Aug 2023 22:32:50 +0800 Subject: [PATCH 3/3] update tests --- tests/integration/tests/regression/issues.test.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/integration/tests/regression/issues.test.ts b/tests/integration/tests/regression/issues.test.ts index d3bdbd52a..cb46a8f46 100644 --- a/tests/integration/tests/regression/issues.test.ts +++ b/tests/integration/tests/regression/issues.test.ts @@ -439,8 +439,7 @@ model Group { // group is shared by organization @@allow('all', org.members?[auth() == this]) } - `, - { logPrismaQuery: true } + ` ); const userData = [