Skip to content

Commit ee08e06

Browse files
committed
fix(policy): update fails for model using both @password and @@validate
fixes #2000
1 parent d42dc32 commit ee08e06

File tree

2 files changed

+124
-17
lines changed

2 files changed

+124
-17
lines changed

packages/runtime/src/enhancements/node/policy/policy-utils.ts

+55-17
Original file line numberDiff line numberDiff line change
@@ -826,6 +826,8 @@ export class PolicyUtil extends QueryUtils {
826826
/**
827827
* Given a model and a unique filter, checks the operation is allowed by policies and field validations.
828828
* Rejects with an error if not allowed.
829+
*
830+
* This method is only called by mutation operations.
829831
*/
830832
async checkPolicyForUnique(
831833
model: string,
@@ -1365,32 +1367,68 @@ export class PolicyUtil extends QueryUtils {
13651367
excludePasswordFields: boolean = true,
13661368
kind: 'create' | 'update' | undefined = undefined
13671369
) {
1370+
if (!this.zodSchemas) {
1371+
return undefined;
1372+
}
1373+
13681374
if (!this.hasFieldValidation(model)) {
13691375
return undefined;
13701376
}
1377+
13711378
const schemaKey = `${upperCaseFirst(model)}${kind ? 'Prisma' + upperCaseFirst(kind) : ''}Schema`;
1372-
let result = this.zodSchemas?.models?.[schemaKey] as ZodObject<any> | undefined;
1373-
1374-
if (result && excludePasswordFields) {
1375-
// fields with `@password` attribute changes at runtime, so we cannot directly use the generated
1376-
// zod schema to validate it, instead, the validation happens when checking the input of "create"
1377-
// and "update" operations
1378-
const modelFields = this.modelMeta.models[lowerCaseFirst(model)]?.fields;
1379-
if (modelFields) {
1380-
for (const [key, field] of Object.entries(modelFields)) {
1381-
if (field.attributes?.some((attr) => attr.name === '@password')) {
1382-
// override `@password` field schema with a string schema
1383-
let pwFieldSchema: ZodSchema = z.string();
1384-
if (field.isOptional) {
1385-
pwFieldSchema = pwFieldSchema.nullish();
1379+
1380+
if (excludePasswordFields) {
1381+
// The `excludePasswordFields` mode is to handle the issue the fields marked with `@password` change at runtime,
1382+
// so they can only be fully validated when processing the input of "create" and "update" operations.
1383+
//
1384+
// When excluding them, we need to override them with plain string schemas. However, since the scheme is not always
1385+
// an `ZodObject` (this happens when there's `@@validate` refinement), we need to fetch the `ZodObject` schema before
1386+
// the refinement is applied, override the `@password` fields and then re-apply the refinement.
1387+
1388+
let schema: ZodObject<any> | undefined;
1389+
1390+
const overridePasswordFields = (schema: z.ZodObject<any>) => {
1391+
let result = schema;
1392+
const modelFields = this.modelMeta.models[lowerCaseFirst(model)]?.fields;
1393+
if (modelFields) {
1394+
for (const [key, field] of Object.entries(modelFields)) {
1395+
if (field.attributes?.some((attr) => attr.name === '@password')) {
1396+
// override `@password` field schema with a string schema
1397+
let pwFieldSchema: ZodSchema = z.string();
1398+
if (field.isOptional) {
1399+
pwFieldSchema = pwFieldSchema.nullish();
1400+
}
1401+
result = result.merge(z.object({ [key]: pwFieldSchema }));
13861402
}
1387-
result = result?.merge(z.object({ [key]: pwFieldSchema }));
13881403
}
13891404
}
1405+
return result;
1406+
};
1407+
1408+
// get the schema without refinement: `[Model]WithoutRefineSchema`
1409+
const withoutRefineSchemaKey = `${upperCaseFirst(model)}${
1410+
kind ? 'Prisma' + upperCaseFirst(kind) : ''
1411+
}WithoutRefineSchema`;
1412+
schema = this.zodSchemas.models[withoutRefineSchemaKey] as ZodObject<any> | undefined;
1413+
1414+
if (schema) {
1415+
// the schema has refinement, need to call refine function after schema merge
1416+
schema = overridePasswordFields(schema);
1417+
// refine function: `refine[Model]`
1418+
const refineFuncKey = `refine${upperCaseFirst(model)}`;
1419+
const refineFunc = this.zodSchemas.models[refineFuncKey] as unknown as (
1420+
schema: ZodObject<any>
1421+
) => ZodSchema;
1422+
return typeof refineFunc === 'function' ? refineFunc(schema) : schema;
1423+
} else {
1424+
// otherwise, directly override the `@password` fields
1425+
schema = this.zodSchemas.models[schemaKey] as ZodObject<any> | undefined;
1426+
return schema ? overridePasswordFields(schema) : undefined;
13901427
}
1428+
} else {
1429+
// simply return the schema
1430+
return this.zodSchemas.models[schemaKey];
13911431
}
1392-
1393-
return result;
13941432
}
13951433

13961434
/**
+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { loadSchema } from '@zenstackhq/testtools';
2+
3+
describe('issue 2000', () => {
4+
it('regression', async () => {
5+
const { enhance } = await loadSchema(
6+
`
7+
abstract model Base {
8+
id String @id @default(uuid()) @deny('update', true)
9+
createdAt DateTime @default(now()) @deny('update', true)
10+
updatedAt DateTime @updatedAt @deny('update', true)
11+
active Boolean @default(false)
12+
published Boolean @default(true)
13+
deleted Boolean @default(false)
14+
startDate DateTime?
15+
endDate DateTime?
16+
17+
@@allow('create', true)
18+
@@allow('read', true)
19+
@@allow('update', true)
20+
}
21+
22+
enum EntityType {
23+
User
24+
Alias
25+
Group
26+
Service
27+
Device
28+
Organization
29+
Guest
30+
}
31+
32+
model Entity extends Base {
33+
entityType EntityType
34+
name String? @unique
35+
members Entity[] @relation("members")
36+
memberOf Entity[] @relation("members")
37+
@@delegate(entityType)
38+
39+
40+
@@allow('create', true)
41+
@@allow('read', true)
42+
@@allow('update', true)
43+
@@validate(!active || (active && name != null), "Active Entities Must Have A Name")
44+
}
45+
46+
model User extends Entity {
47+
profile Json?
48+
username String @unique
49+
password String @password
50+
51+
@@allow('create', true)
52+
@@allow('read', true)
53+
@@allow('update', true)
54+
}
55+
`
56+
);
57+
58+
const db = enhance();
59+
await expect(db.user.create({ data: { username: 'admin', password: 'abc12345' } })).toResolveTruthy();
60+
await expect(
61+
db.user.update({ where: { username: 'admin' }, data: { password: 'abc123456789123' } })
62+
).toResolveTruthy();
63+
64+
// violating validation rules
65+
await expect(
66+
await db.user.update({ where: { username: 'admin' }, data: { active: true } })
67+
).toBeRejectedByPolicy();
68+
});
69+
});

0 commit comments

Comments
 (0)