Skip to content

fix: make sure auxiliary fields in nested entities are stripped #387

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 4, 2023
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
71 changes: 32 additions & 39 deletions packages/runtime/src/enhancements/policy/policy-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}
}
}

Expand Down
57 changes: 57 additions & 0 deletions tests/integration/tests/regression/issues.test.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});