From 6685de4dbc6e3292c4a02d342151562e625ec108 Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Thu, 6 Feb 2025 12:41:13 -0800 Subject: [PATCH] fix(policy): only include field selections for evaluating field-level policies if the corresponding fields are selected fixes #1978 --- .../enhancements/node/policy/policy-utils.ts | 19 +++++---- tests/regression/tests/issue-1978.test.ts | 41 +++++++++++++++++++ 2 files changed, 52 insertions(+), 8 deletions(-) create mode 100644 tests/regression/tests/issue-1978.test.ts diff --git a/packages/runtime/src/enhancements/node/policy/policy-utils.ts b/packages/runtime/src/enhancements/node/policy/policy-utils.ts index b39ac5b00..82a5bc88e 100644 --- a/packages/runtime/src/enhancements/node/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/node/policy/policy-utils.ts @@ -1183,7 +1183,7 @@ export class PolicyUtil extends QueryUtils { if (this.hasFieldLevelPolicy(model)) { // recursively inject selection for fields needed for field-level read checks - const readFieldSelect = this.getFieldReadCheckSelector(model); + const readFieldSelect = this.getFieldReadCheckSelector(model, args.select); if (readFieldSelect) { this.doInjectReadCheckSelect(model, args, { select: readFieldSelect }); } @@ -1311,17 +1311,20 @@ export class PolicyUtil extends QueryUtils { } // get a merged selector object for all field-level read policies - private getFieldReadCheckSelector(model: string) { + private getFieldReadCheckSelector(model: string, fieldSelection: Record | undefined) { const def = this.getModelPolicyDef(model); let result: any = {}; const fieldLevel = def.fieldLevel?.read; if (fieldLevel) { - for (const def of Object.values(fieldLevel)) { - if (def.entityChecker?.selector) { - result = deepmerge(result, def.entityChecker.selector); - } - if (def.overrideEntityChecker?.selector) { - result = deepmerge(result, def.overrideEntityChecker.selector); + for (const [field, def] of Object.entries(fieldLevel)) { + if (!fieldSelection || fieldSelection[field]) { + // field is selected, merge the field-level selector + if (def.entityChecker?.selector) { + result = deepmerge(result, def.entityChecker.selector); + } + if (def.overrideEntityChecker?.selector) { + result = deepmerge(result, def.overrideEntityChecker.selector); + } } } } diff --git a/tests/regression/tests/issue-1978.test.ts b/tests/regression/tests/issue-1978.test.ts new file mode 100644 index 000000000..63c91a6e5 --- /dev/null +++ b/tests/regression/tests/issue-1978.test.ts @@ -0,0 +1,41 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1978', () => { + it('regression', async () => { + const { prisma, enhance } = await loadSchema( + ` + model User { + id Int @id + posts Post[] + secret String @allow('read', posts?[published]) + @@allow('all', true) + } + + model Post { + id Int @id + author User @relation(fields: [authorId], references: [id]) + authorId Int + published Boolean @default(false) + @@allow('all', true) + } + `, + { logPrismaQuery: true } + ); + + const user1 = await prisma.user.create({ + data: { id: 1, secret: 'secret', posts: { create: { id: 1, published: true } } }, + }); + const user2 = await prisma.user.create({ + data: { id: 2, secret: 'secret' }, + }); + + const db = enhance(); + await expect(db.user.findFirst({ where: { id: 1 } })).resolves.toMatchObject({ secret: 'secret' }); + await expect(db.user.findFirst({ where: { id: 1 }, select: { id: true } })).resolves.toEqual({ id: 1 }); + + let r = await db.user.findFirst({ where: { id: 2 } }); + expect(r.secret).toBeUndefined(); + r = await db.user.findFirst({ where: { id: 2 }, select: { id: true } }); + expect(r.secret).toBeUndefined(); + }); +});