Skip to content

Commit 6edfd66

Browse files
authored
chore: merge from dev (#1054)
1 parent e5b5a0f commit 6edfd66

File tree

24 files changed

+389
-132
lines changed

24 files changed

+389
-132
lines changed

jest.config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ export default {
99
// Automatically clear mock calls, instances, contexts and results before every test
1010
clearMocks: true,
1111

12-
globalSetup: path.join(__dirname, './test-setup.ts'),
12+
globalSetup: path.join(__dirname, './script/test-global-setup.ts'),
13+
14+
setupFiles: [path.join(__dirname, './script/set-test-env.ts')],
1315

1416
// Indicates whether the coverage information should be collected while executing the test
1517
collectCoverage: true,

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
"scripts": {
66
"build": "pnpm -r build",
77
"lint": "pnpm -r lint",
8-
"test": "ZENSTACK_TEST=1 pnpm -r --parallel run test --silent --forceExit",
9-
"test-ci": "ZENSTACK_TEST=1 pnpm -r --parallel run test --silent --forceExit",
8+
"test": "pnpm -r --parallel run test --silent --forceExit",
9+
"test-ci": "pnpm -r --parallel run test --silent --forceExit",
1010
"test-scaffold": "tsx script/test-scaffold.ts",
1111
"publish-all": "pnpm --filter \"./packages/**\" -r publish --access public",
1212
"publish-preview": "pnpm --filter \"./packages/**\" -r publish --force --registry https://preview.registry.zenstack.dev/",

packages/plugins/openapi/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && copyfiles ./package.json ./README.md ./LICENSE dist && copyfiles -u 1 ./src/plugin.zmodel dist && pnpm pack dist --pack-destination '../../../../.build'",
1818
"watch": "tsc --watch",
1919
"lint": "eslint src --ext ts",
20-
"test": "ZENSTACK_TEST=1 jest",
20+
"test": "jest",
2121
"prepublishOnly": "pnpm build"
2222
},
2323
"keywords": [

packages/plugins/swr/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && tsup-node --config ./tsup.config.ts && copyfiles ./package.json ./README.md ./LICENSE dist && pnpm pack dist --pack-destination '../../../../.build'",
1414
"watch": "concurrently \"tsc --watch\" \"tsup-node --config ./tsup.config.ts --watch\"",
1515
"lint": "eslint src --ext ts",
16-
"test": "ZENSTACK_TEST=1 jest",
16+
"test": "jest",
1717
"prepublishOnly": "pnpm build"
1818
},
1919
"publishConfig": {

packages/plugins/tanstack-query/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
"build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && tsup-node --config ./tsup.config.ts && tsup-node --config ./tsup-v5.config.ts && node scripts/postbuild && copyfiles ./package.json ./README.md ./LICENSE dist && pnpm pack dist --pack-destination '../../../../.build'",
7070
"watch": "concurrently \"tsc --watch\" \"tsup-node --config ./tsup.config.ts --watch\" \"tsup-node --config ./tsup-v5.config.ts --watch\"",
7171
"lint": "eslint src --ext ts",
72-
"test": "ZENSTACK_TEST=1 jest",
72+
"test": "jest",
7373
"prepublishOnly": "pnpm build"
7474
},
7575
"publishConfig": {

packages/plugins/trpc/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"build": "pnpm lint --max-warnings=0 && pnpm clean && tsc && copyfiles ./package.json ./README.md ./LICENSE 'res/**/*' dist && pnpm pack dist --pack-destination '../../../../.build'",
1414
"watch": "tsc --watch",
1515
"lint": "eslint src --ext ts",
16-
"test": "ZENSTACK_TEST=1 jest",
16+
"test": "jest",
1717
"prepublishOnly": "pnpm build"
1818
},
1919
"publishConfig": {

packages/runtime/src/enhancements/policy/handler.ts

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,7 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
481481
// Validates the given create payload against Zod schema if any
482482
private validateCreateInputSchema(model: string, data: any) {
483483
const schema = this.policyUtils.getZodSchema(model, 'create');
484-
if (schema) {
484+
if (schema && data) {
485485
const parseResult = schema.safeParse(data);
486486
if (!parseResult.success) {
487487
throw this.policyUtils.deniedByPolicy(
@@ -514,26 +514,29 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
514514

515515
args = this.policyUtils.clone(args);
516516

517-
// do static input validation and check if post-create checks are needed
517+
// go through create items, statically check input to determine if post-create
518+
// check is needed, and also validate zod schema
518519
let needPostCreateCheck = false;
519520
for (const item of enumerate(args.data)) {
521+
const validationResult = this.validateCreateInputSchema(this.model, item);
522+
if (validationResult !== item) {
523+
this.policyUtils.replace(item, validationResult);
524+
}
525+
520526
const inputCheck = this.policyUtils.checkInputGuard(this.model, item, 'create');
521527
if (inputCheck === false) {
528+
// unconditionally deny
522529
throw this.policyUtils.deniedByPolicy(
523530
this.model,
524531
'create',
525532
undefined,
526533
CrudFailureReason.ACCESS_POLICY_VIOLATION
527534
);
528535
} else if (inputCheck === true) {
529-
const r = this.validateCreateInputSchema(this.model, item);
530-
if (r !== item) {
531-
this.policyUtils.replace(item, r);
532-
}
536+
// unconditionally allow
533537
} else if (inputCheck === undefined) {
534538
// static policy check is not possible, need to do post-create check
535539
needPostCreateCheck = true;
536-
break;
537540
}
538541
}
539542

@@ -808,7 +811,13 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
808811

809812
// check if the update actually writes to this model
810813
let thisModelUpdate = false;
811-
const updatePayload: any = (args as any).data ?? args;
814+
const updatePayload = (args as any).data ?? args;
815+
816+
const validatedPayload = this.validateUpdateInputSchema(model, updatePayload);
817+
if (validatedPayload !== updatePayload) {
818+
this.policyUtils.replace(updatePayload, validatedPayload);
819+
}
820+
812821
if (updatePayload) {
813822
for (const key of Object.keys(updatePayload)) {
814823
const field = resolveField(this.modelMeta, model, key);
@@ -879,6 +888,8 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
879888
);
880889
}
881890

891+
args.data = this.validateUpdateInputSchema(model, args.data);
892+
882893
const updateGuard = this.policyUtils.getAuthGuard(db, model, 'update');
883894
if (this.policyUtils.isTrue(updateGuard) || this.policyUtils.isFalse(updateGuard)) {
884895
// injects simple auth guard into where clause
@@ -939,7 +950,10 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
939950
await _registerPostUpdateCheck(model, uniqueFilter);
940951

941952
// convert upsert to update
942-
context.parent.update = { where: args.where, data: args.update };
953+
context.parent.update = {
954+
where: args.where,
955+
data: this.validateUpdateInputSchema(model, args.update),
956+
};
943957
delete context.parent.upsert;
944958

945959
// continue visiting the new payload
@@ -1038,6 +1052,37 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
10381052
return { result, postWriteChecks };
10391053
}
10401054

1055+
// Validates the given update payload against Zod schema if any
1056+
private validateUpdateInputSchema(model: string, data: any) {
1057+
const schema = this.policyUtils.getZodSchema(model, 'update');
1058+
if (schema && data) {
1059+
// update payload can contain non-literal fields, like:
1060+
// { x: { increment: 1 } }
1061+
// we should only validate literal fields
1062+
1063+
const literalData = Object.entries(data).reduce<any>(
1064+
(acc, [k, v]) => ({ ...acc, ...(typeof v !== 'object' ? { [k]: v } : {}) }),
1065+
{}
1066+
);
1067+
1068+
const parseResult = schema.safeParse(literalData);
1069+
if (!parseResult.success) {
1070+
throw this.policyUtils.deniedByPolicy(
1071+
model,
1072+
'update',
1073+
`input failed validation: ${fromZodError(parseResult.error)}`,
1074+
CrudFailureReason.DATA_VALIDATION_VIOLATION,
1075+
parseResult.error
1076+
);
1077+
}
1078+
1079+
// schema may have transformed field values, use it to overwrite the original data
1080+
return { ...data, ...parseResult.data };
1081+
} else {
1082+
return data;
1083+
}
1084+
}
1085+
10411086
private isUnsafeMutate(model: string, args: any) {
10421087
if (!args) {
10431088
return false;
@@ -1072,6 +1117,8 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
10721117
args = this.policyUtils.clone(args);
10731118
this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'update');
10741119

1120+
args.data = this.validateUpdateInputSchema(this.model, args.data);
1121+
10751122
if (this.policyUtils.hasAuthGuard(this.model, 'postUpdate') || this.policyUtils.getZodSchema(this.model)) {
10761123
// use a transaction to do post-update checks
10771124
const postWriteChecks: PostWriteCheckRecord[] = [];

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -317,7 +317,7 @@ export class PolicyUtil extends QueryUtils {
317317
/**
318318
* Checks if the given model has a policy guard for the given operation.
319319
*/
320-
hasAuthGuard(model: string, operation: PolicyOperationKind): boolean {
320+
hasAuthGuard(model: string, operation: PolicyOperationKind) {
321321
const guard = this.policy.guard[lowerCaseFirst(model)];
322322
if (!guard) {
323323
return false;
@@ -326,6 +326,21 @@ export class PolicyUtil extends QueryUtils {
326326
return typeof provider !== 'boolean' || provider !== true;
327327
}
328328

329+
/**
330+
* Checks if the given model has any field-level override policy guard for the given operation.
331+
*/
332+
hasOverrideAuthGuard(model: string, operation: PolicyOperationKind) {
333+
const guard = this.requireGuard(model);
334+
switch (operation) {
335+
case 'read':
336+
return Object.keys(guard).some((k) => k.startsWith(FIELD_LEVEL_OVERRIDE_READ_GUARD_PREFIX));
337+
case 'update':
338+
return Object.keys(guard).some((k) => k.startsWith(FIELD_LEVEL_OVERRIDE_UPDATE_GUARD_PREFIX));
339+
default:
340+
return false;
341+
}
342+
}
343+
329344
/**
330345
* Checks model creation policy based on static analysis to the input args.
331346
*
@@ -632,7 +647,7 @@ export class PolicyUtil extends QueryUtils {
632647
preValue?: any
633648
) {
634649
let guard = this.getAuthGuard(db, model, operation, preValue);
635-
if (this.isFalse(guard)) {
650+
if (this.isFalse(guard) && !this.hasOverrideAuthGuard(model, operation)) {
636651
throw this.deniedByPolicy(
637652
model,
638653
operation,
@@ -805,7 +820,7 @@ export class PolicyUtil extends QueryUtils {
805820
*/
806821
tryReject(db: CrudContract, model: string, operation: PolicyOperationKind) {
807822
const guard = this.getAuthGuard(db, model, operation);
808-
if (this.isFalse(guard)) {
823+
if (this.isFalse(guard) && !this.hasOverrideAuthGuard(model, operation)) {
809824
throw this.deniedByPolicy(model, operation, undefined, CrudFailureReason.ACCESS_POLICY_VIOLATION);
810825
}
811826
}

packages/schema/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@
7373
"bundle": "rimraf bundle && pnpm lint --max-warnings=0 && node build/bundle.js --minify",
7474
"watch": "tsc --watch",
7575
"lint": "eslint src tests --ext ts",
76-
"test": "ZENSTACK_TEST=1 jest",
76+
"test": "jest",
7777
"prepublishOnly": "pnpm build",
7878
"postinstall": "node bin/post-install.js"
7979
},

packages/schema/src/cli/cli-util.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { PLUGIN_MODULE_NAME, STD_LIB_MODULE_NAME } from '../language-server/cons
1313
import { ZModelFormatter } from '../language-server/zmodel-formatter';
1414
import { createZModelServices, ZModelServices } from '../language-server/zmodel-module';
1515
import { mergeBaseModel, resolveImport, resolveTransitiveImports } from '../utils/ast-utils';
16-
import { findPackageJson } from '../utils/pkg-utils';
16+
import { findUp } from '../utils/pkg-utils';
1717
import { getVersion } from '../utils/version-utils';
1818
import { CliError } from './cli-error';
1919

@@ -289,7 +289,7 @@ export async function formatDocument(fileName: string) {
289289

290290
export function getDefaultSchemaLocation() {
291291
// handle override from package.json
292-
const pkgJsonPath = findPackageJson();
292+
const pkgJsonPath = findUp(['package.json']);
293293
if (pkgJsonPath) {
294294
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
295295
if (typeof pkgJson?.zenstack?.schema === 'string') {

0 commit comments

Comments
 (0)