From 8ad3cdbb0f8ba0967c35a797fc845256c128f15c Mon Sep 17 00:00:00 2001 From: ymc9 <104139426+ymc9@users.noreply.github.com> Date: Fri, 23 Feb 2024 23:18:21 -0800 Subject: [PATCH] fix: should not reject "update" when there's only field-level override but no model-level policy Fixes #1014 --- .../src/enhancements/policy/policy-utils.ts | 21 ++++++-- .../tests/regression/issue-1014.test.ts | 53 +++++++++++++++++++ .../integration/tests/tsconfig.template.json | 10 ---- 3 files changed, 71 insertions(+), 13 deletions(-) create mode 100644 tests/integration/tests/regression/issue-1014.test.ts delete mode 100644 tests/integration/tests/tsconfig.template.json diff --git a/packages/runtime/src/enhancements/policy/policy-utils.ts b/packages/runtime/src/enhancements/policy/policy-utils.ts index 63b83b79f..ea5816f6c 100644 --- a/packages/runtime/src/enhancements/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/policy/policy-utils.ts @@ -319,7 +319,7 @@ export class PolicyUtil { /** * Checks if the given model has a policy guard for the given operation. */ - hasAuthGuard(model: string, operation: PolicyOperationKind): boolean { + hasAuthGuard(model: string, operation: PolicyOperationKind) { const guard = this.policy.guard[lowerCaseFirst(model)]; if (!guard) { return false; @@ -328,6 +328,21 @@ export class PolicyUtil { return typeof provider !== 'boolean' || provider !== true; } + /** + * Checks if the given model has any field-level override policy guard for the given operation. + */ + hasOverrideAuthGuard(model: string, operation: PolicyOperationKind) { + const guard = this.requireGuard(model); + switch (operation) { + case 'read': + return Object.keys(guard).some((k) => k.startsWith(FIELD_LEVEL_OVERRIDE_READ_GUARD_PREFIX)); + case 'update': + return Object.keys(guard).some((k) => k.startsWith(FIELD_LEVEL_OVERRIDE_UPDATE_GUARD_PREFIX)); + default: + return false; + } + } + /** * Checks model creation policy based on static analysis to the input args. * @@ -731,7 +746,7 @@ export class PolicyUtil { preValue?: any ) { let guard = this.getAuthGuard(db, model, operation, preValue); - if (this.isFalse(guard)) { + if (this.isFalse(guard) && !this.hasOverrideAuthGuard(model, operation)) { throw this.deniedByPolicy( model, operation, @@ -904,7 +919,7 @@ export class PolicyUtil { */ tryReject(db: Record, model: string, operation: PolicyOperationKind) { const guard = this.getAuthGuard(db, model, operation); - if (this.isFalse(guard)) { + if (this.isFalse(guard) && !this.hasOverrideAuthGuard(model, operation)) { throw this.deniedByPolicy(model, operation, undefined, CrudFailureReason.ACCESS_POLICY_VIOLATION); } } diff --git a/tests/integration/tests/regression/issue-1014.test.ts b/tests/integration/tests/regression/issue-1014.test.ts new file mode 100644 index 000000000..7f374d24d --- /dev/null +++ b/tests/integration/tests/regression/issue-1014.test.ts @@ -0,0 +1,53 @@ +import { loadSchema } from '@zenstackhq/testtools'; + +describe('issue 1014', () => { + it('update', async () => { + const { prisma, enhance } = await loadSchema( + ` + model User { + id Int @id() @default(autoincrement()) + name String + posts Post[] + } + + model Post { + id Int @id() @default(autoincrement()) + title String + content String? + author User? @relation(fields: [authorId], references: [id]) + authorId Int? @allow('update', true, true) + + @@allow('read', true) + } + `, + { logPrismaQuery: true } + ); + + const db = enhance(); + + const user = await prisma.user.create({ data: { name: 'User1' } }); + const post = await prisma.post.create({ data: { title: 'Post1' } }); + await expect(db.post.update({ where: { id: post.id }, data: { authorId: user.id } })).toResolveTruthy(); + }); + + it('read', async () => { + const { prisma, enhance } = await loadSchema( + ` + model Post { + id Int @id() @default(autoincrement()) + title String @allow('read', true, true) + content String + } + `, + { logPrismaQuery: true } + ); + + const db = enhance(); + + const post = await prisma.post.create({ data: { title: 'Post1', content: 'Content' } }); + await expect(db.post.findUnique({ where: { id: post.id } })).toResolveNull(); + await expect(db.post.findUnique({ where: { id: post.id }, select: { title: true } })).resolves.toEqual({ + title: 'Post1', + }); + }); +}); diff --git a/tests/integration/tests/tsconfig.template.json b/tests/integration/tests/tsconfig.template.json deleted file mode 100644 index 18a6bedec..000000000 --- a/tests/integration/tests/tsconfig.template.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "target": "es2016", - "module": "commonjs", - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "strict": true, - "skipLibCheck": true - } -}