diff --git a/packages/runtime/src/enhancements/policy/policy-utils.ts b/packages/runtime/src/enhancements/policy/policy-utils.ts index 839ad15b9..de5c9c721 100644 --- a/packages/runtime/src/enhancements/policy/policy-utils.ts +++ b/packages/runtime/src/enhancements/policy/policy-utils.ts @@ -247,7 +247,7 @@ export class PolicyUtil { const result: any[] = await this.db[model].findMany(args); - await Promise.all(result.map((item) => this.postProcessForRead(item, model, args, 'read'))); + await this.postProcessForRead(result, model, args, 'read'); return result; } @@ -320,54 +320,47 @@ export class PolicyUtil { * (which can't be trimmed at query time) and removes fields that should be * omitted. */ - async postProcessForRead(entityData: any, model: string, args: any, operation: PolicyOperationKind) { - if (typeof entityData !== 'object' || !entityData) { - return; - } - - const ids = this.getEntityIds(model, entityData); - if (Object.keys(ids).length === 0) { - return; - } - - // strip auxiliary fields - for (const auxField of AUXILIARY_FIELDS) { - if (auxField in entityData) { - delete entityData[auxField]; - } - } - - const injectTarget = args.select ?? args.include; - if (!injectTarget) { - return; - } - - // to-one relation data cannot be trimmed by injected guards, we have to - // post-check them - - for (const field of getModelFields(injectTarget)) { - if (!entityData?.[field]) { + async postProcessForRead(data: any, model: string, args: any, operation: PolicyOperationKind) { + for (const entityData of enumerate(data)) { + if (typeof entityData !== 'object' || !entityData) { continue; } - const fieldInfo = resolveField(this.modelMeta, model, field); - if (!fieldInfo || !fieldInfo.isDataModel || fieldInfo.isArray) { - continue; + // strip auxiliary fields + for (const auxField of AUXILIARY_FIELDS) { + if (auxField in entityData) { + delete entityData[auxField]; + } } - const ids = this.getEntityIds(fieldInfo.type, entityData[field]); - - if (Object.keys(ids).length === 0) { + const injectTarget = args.select ?? args.include; + if (!injectTarget) { continue; } - // DEBUG - // this.logger.info(`Validating read of to-one relation: ${fieldInfo.type}#${formatObject(ids)}`); + // recurse into nested entities + for (const field of Object.keys(injectTarget)) { + const fieldData = entityData[field]; + if (typeof fieldData !== 'object' || !fieldData) { + continue; + } - await this.checkPolicyForFilter(fieldInfo.type, ids, operation, this.db); + const fieldInfo = resolveField(this.modelMeta, model, field); + if (fieldInfo && fieldInfo.isDataModel && !fieldInfo.isArray) { + // to-one relation data cannot be trimmed by injected guards, we have to + // post-check them + const ids = this.getEntityIds(fieldInfo.type, fieldData); - // recurse - await this.postProcessForRead(entityData[field], fieldInfo.type, injectTarget[field], operation); + if (Object.keys(ids).length !== 0) { + // DEBUG + // this.logger.info(`Validating read of to-one relation: ${fieldInfo.type}#${formatObject(ids)}`); + await this.checkPolicyForFilter(fieldInfo.type, ids, operation, this.db); + } + } + + // recurse + await this.postProcessForRead(fieldData, fieldInfo.type, injectTarget[field], operation); + } } } diff --git a/tests/integration/tests/regression/issues.test.ts b/tests/integration/tests/regression/issues.test.ts new file mode 100644 index 000000000..6683a9de2 --- /dev/null +++ b/tests/integration/tests/regression/issues.test.ts @@ -0,0 +1,57 @@ +import { loadSchema } from '@zenstackhq/testtools'; +import path from 'path'; + +describe('GitHub issues regression', () => { + let origDir: string; + + beforeAll(async () => { + origDir = path.resolve('.'); + }); + + afterEach(() => { + process.chdir(origDir); + }); + + it('issue 386', async () => { + const { withPolicy } = await loadSchema( + ` + model User { + id String @id @unique @default(uuid()) + posts Post[] + + @@allow('all', true) + } + + model Post { + id String @id @default(uuid()) + title String + published Boolean @default(false) + author User @relation(fields: [authorId], references: [id]) + authorId String + + @@allow('all', contains(title, 'Post')) + } + ` + ); + + const db = withPolicy(); + const created = await db.user.create({ + data: { + posts: { + create: { + title: 'Post 1', + }, + }, + }, + include: { + posts: true, + }, + }); + expect(created.posts[0].zenstack_guard).toBeUndefined(); + expect(created.posts[0].zenstack_transaction).toBeUndefined(); + + const queried = await db.user.findFirst({ include: { posts: true } }); + expect(queried.posts[0].zenstack_guard).toBeUndefined(); + expect(queried.posts[0].zenstack_transaction).toBeUndefined(); + }); +});