Skip to content

Commit 3c8cba7

Browse files
authored
fix: handle foreign key field-level access check during relation update (#847)
1 parent 03315cc commit 3c8cba7

File tree

6 files changed

+349
-20
lines changed

6 files changed

+349
-20
lines changed

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -680,8 +680,18 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
680680
if (context.field?.backLink) {
681681
const backLinkField = this.utils.getModelField(model, context.field.backLink);
682682
if (backLinkField.isRelationOwner) {
683-
// update happens on the related model, require updatable
684-
await this.utils.checkPolicyForUnique(model, args, 'update', db, args);
683+
// update happens on the related model, require updatable,
684+
// translate args to foreign keys so field-level policies can be checked
685+
const checkArgs: any = {};
686+
if (args && typeof args === 'object' && backLinkField.foreignKeyMapping) {
687+
for (const key of Object.keys(args)) {
688+
const fk = backLinkField.foreignKeyMapping[key];
689+
if (fk) {
690+
checkArgs[fk] = args[key];
691+
}
692+
}
693+
}
694+
await this.utils.checkPolicyForUnique(model, args, 'update', db, checkArgs);
685695

686696
// register post-update check
687697
await _registerPostUpdateCheck(model, args);

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

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -763,11 +763,29 @@ export class PolicyUtil {
763763
if (typeof v === 'undefined') {
764764
continue;
765765
}
766-
const fieldGuard = this.getFieldUpdateAuthGuard(db, model, k);
767-
if (this.isFalse(fieldGuard)) {
768-
return { guard: allFieldGuards, rejectedByField: k };
766+
767+
const field = resolveField(this.modelMeta, model, k);
768+
769+
if (field?.isDataModel) {
770+
// relation field update should be treated as foreign key update,
771+
// fetch and merge all foreign key guards
772+
if (field.isRelationOwner && field.foreignKeyMapping) {
773+
const foreignKeys = Object.values<string>(field.foreignKeyMapping);
774+
for (const fk of foreignKeys) {
775+
const fieldGuard = this.getFieldUpdateAuthGuard(db, model, fk);
776+
if (this.isFalse(fieldGuard)) {
777+
return { guard: allFieldGuards, rejectedByField: fk };
778+
}
779+
allFieldGuards.push(fieldGuard);
780+
}
781+
}
782+
} else {
783+
const fieldGuard = this.getFieldUpdateAuthGuard(db, model, k);
784+
if (this.isFalse(fieldGuard)) {
785+
return { guard: allFieldGuards, rejectedByField: k };
786+
}
787+
allFieldGuards.push(fieldGuard);
769788
}
770-
allFieldGuards.push(fieldGuard);
771789
}
772790
return { guard: this.and(...allFieldGuards), rejectedByField: undefined };
773791
}

packages/runtime/src/version.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ export function getVersion() {
1919
* "prisma".
2020
*/
2121
export function getPrismaVersion(): string | undefined {
22+
if (process.env.ZENSTACK_TEST === '1') {
23+
// test environment
24+
try {
25+
return require(path.resolve('./node_modules/@prisma/client/package.json')).version;
26+
} catch {
27+
return undefined;
28+
}
29+
}
30+
2231
try {
2332
// eslint-disable-next-line @typescript-eslint/no-var-requires
2433
return require('@prisma/client/package.json').version;
@@ -27,15 +36,6 @@ export function getPrismaVersion(): string | undefined {
2736
// eslint-disable-next-line @typescript-eslint/no-var-requires
2837
return require('prisma/package.json').version;
2938
} catch {
30-
if (process.env.ZENSTACK_TEST === '1') {
31-
// test environment
32-
try {
33-
return require(path.resolve('./node_modules/@prisma/client/package.json')).version;
34-
} catch {
35-
return undefined;
36-
}
37-
}
38-
3939
return undefined;
4040
}
4141
}

packages/schema/src/language-server/validator/attribute-application-validator.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
isEnum,
1616
isReferenceExpr,
1717
} from '@zenstackhq/language/ast';
18-
import { isFutureExpr, resolved } from '@zenstackhq/sdk';
18+
import { isFutureExpr, isRelationshipField, resolved } from '@zenstackhq/sdk';
1919
import { ValidationAcceptor, streamAst } from 'langium';
2020
import pluralize from 'pluralize';
2121
import { AstValidator } from '../types';
@@ -131,12 +131,24 @@ export default class AttributeApplicationValidator implements AstValidator<Attri
131131
accept('error', `expects a string literal`, { node: attr.args[0] });
132132
return;
133133
}
134-
this.validatePolicyKinds(kind, ['read', 'update', 'all'], attr, accept);
134+
const kindItems = this.validatePolicyKinds(kind, ['read', 'update', 'all'], attr, accept);
135135

136136
const expr = attr.args[1].value;
137137
if (streamAst(expr).some((node) => isFutureExpr(node))) {
138138
accept('error', `"future()" is not allowed in field-level policy rules`, { node: expr });
139139
}
140+
141+
// 'update' rules are not allowed for relation fields
142+
if (kindItems.includes('update') || kindItems.includes('all')) {
143+
const field = attr.$container as DataModelField;
144+
if (isRelationshipField(field)) {
145+
accept(
146+
'error',
147+
`Field-level policy rules with "update" or "all" kind are not allowed for relation fields. Put rules on foreign-key fields instead.`,
148+
{ node: attr }
149+
);
150+
}
151+
}
140152
}
141153

142154
private validatePolicyKinds(
@@ -155,6 +167,7 @@ export default class AttributeApplicationValidator implements AstValidator<Attri
155167
);
156168
}
157169
});
170+
return items;
158171
}
159172
}
160173

packages/schema/tests/schema/validation/attribute-validation.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1186,5 +1186,43 @@ describe('Attribute tests', () => {
11861186
}
11871187
`)
11881188
).toContain('"future()" is not allowed in field-level policy rules');
1189+
1190+
expect(
1191+
await loadModelWithError(`
1192+
${prelude}
1193+
model M {
1194+
id String @id
1195+
n N @allow('update', n.x > 0)
1196+
}
1197+
1198+
model N {
1199+
id String @id
1200+
x Int
1201+
m M? @relation(fields: [mId], references: [id])
1202+
mId String
1203+
}
1204+
`)
1205+
).toContain(
1206+
'Field-level policy rules with "update" or "all" kind are not allowed for relation fields. Put rules on foreign-key fields instead.'
1207+
);
1208+
1209+
expect(
1210+
await loadModelWithError(`
1211+
${prelude}
1212+
model M {
1213+
id String @id
1214+
n N[] @allow('update', n.x > 0)
1215+
}
1216+
1217+
model N {
1218+
id String @id
1219+
x Int
1220+
m M? @relation(fields: [mId], references: [id])
1221+
mId String
1222+
}
1223+
`)
1224+
).toContain(
1225+
'Field-level policy rules with "update" or "all" kind are not allowed for relation fields. Put rules on foreign-key fields instead.'
1226+
);
11891227
});
11901228
});

0 commit comments

Comments
 (0)