Skip to content

Commit 8b65a51

Browse files
committed
support for field-level policies, fix tests
1 parent c5ba68c commit 8b65a51

File tree

10 files changed

+477
-316
lines changed

10 files changed

+477
-316
lines changed

packages/runtime/src/enhancements/policy/constraint-solver.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import Logic from 'logic-solver';
22
import { match } from 'ts-pattern';
33
import type {
4-
CheckerConstraint,
54
ComparisonConstraint,
65
ComparisonTerm,
76
LogicalConstraint,
7+
PermissionCheckerConstraint,
88
ValueConstraint,
99
VariableConstraint,
1010
} from '../types';
@@ -22,7 +22,7 @@ export class ConstraintSolver {
2222
/**
2323
* Check the satisfiability of the given constraint.
2424
*/
25-
checkSat(constraint: CheckerConstraint): boolean {
25+
checkSat(constraint: PermissionCheckerConstraint): boolean {
2626
// reset state
2727
this.stringTable = [];
2828
this.variables = new Map<string, Logic.Formula>();
@@ -46,7 +46,7 @@ export class ConstraintSolver {
4646
return !!solver.solve();
4747
}
4848

49-
private buildFormula(constraint: CheckerConstraint): Logic.Formula {
49+
private buildFormula(constraint: PermissionCheckerConstraint): Logic.Formula {
5050
return match(constraint)
5151
.when(
5252
(c): c is ValueConstraint => c.kind === 'value',
@@ -100,11 +100,11 @@ export class ConstraintSolver {
100100
return Logic.not(this.buildFormula(constraint.children[0]));
101101
}
102102

103-
private isTrue(constraint: CheckerConstraint): unknown {
103+
private isTrue(constraint: PermissionCheckerConstraint): unknown {
104104
return constraint.kind === 'value' && constraint.value === true;
105105
}
106106

107-
private isFalse(constraint: CheckerConstraint): unknown {
107+
private isFalse(constraint: PermissionCheckerConstraint): unknown {
108108
return constraint.kind === 'value' && constraint.value === false;
109109
}
110110

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

+17-21
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { Logger } from '../logger';
2424
import { createDeferredPromise, createFluentPromise } from '../promise';
2525
import { PrismaProxyHandler } from '../proxy';
2626
import { QueryUtils } from '../query-utils';
27-
import type { AdditionalCheckerFunc, CheckerConstraint } from '../types';
27+
import type { EntityCheckerFunc, PermissionCheckerConstraint } from '../types';
2828
import { clone, formatObject, isUnsafeMutate, prismaClientValidationError } from '../utils';
2929
import { ConstraintSolver } from './constraint-solver';
3030
import { PolicyUtil } from './policy-utils';
@@ -1182,15 +1182,15 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
11821182

11831183
args.data = this.validateUpdateInputSchema(this.model, args.data);
11841184

1185-
const additionalChecker = this.policyUtils.getAdditionalChecker(this.model, 'update');
1185+
const entityChecker = this.policyUtils.getEntityChecker(this.model, 'update');
11861186

11871187
const canProceedWithoutTransaction =
11881188
// no post-update rules
11891189
!this.policyUtils.hasAuthGuard(this.model, 'postUpdate') &&
11901190
// no Zod schema
11911191
!this.policyUtils.getZodSchema(this.model) &&
1192-
// no additional checker
1193-
!additionalChecker;
1192+
// no entity checker
1193+
!entityChecker;
11941194

11951195
if (canProceedWithoutTransaction) {
11961196
// proceed without a transaction
@@ -1212,9 +1212,9 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
12121212
}
12131213

12141214
// merge selection required for running additional checker
1215-
const additionalCheckerSelector = this.policyUtils.getAdditionalCheckerSelector(this.model, 'update');
1216-
if (additionalCheckerSelector) {
1217-
select = deepmerge(select, additionalCheckerSelector);
1215+
const entityChecker = this.policyUtils.getEntityChecker(this.model, 'update');
1216+
if (entityChecker?.selector) {
1217+
select = deepmerge(select, entityChecker.selector);
12181218
}
12191219

12201220
const currentSetQuery = { select, where: args.where };
@@ -1225,9 +1225,9 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
12251225
}
12261226
let candidates = await tx[this.model].findMany(currentSetQuery);
12271227

1228-
if (additionalChecker) {
1228+
if (entityChecker) {
12291229
// filter candidates with additional checker and build an id filter
1230-
const r = this.buildIdFilterWithAdditionalChecker(candidates, additionalChecker);
1230+
const r = this.buildIdFilterWithEntityChecker(candidates, entityChecker.func);
12311231
candidates = r.filteredCandidates;
12321232

12331233
// merge id filter into update's where clause
@@ -1383,19 +1383,15 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
13831383
args = clone(args);
13841384
this.policyUtils.injectAuthGuardAsWhere(this.prisma, args, this.model, 'delete');
13851385

1386-
const additionalChecker = this.policyUtils.getAdditionalChecker(this.model, 'delete');
1387-
if (additionalChecker) {
1386+
const entityChecker = this.policyUtils.getEntityChecker(this.model, 'delete');
1387+
if (entityChecker) {
13881388
// additional checker exists, need to run deletion inside a transaction
13891389
return this.queryUtils.transaction(this.prisma, async (tx) => {
13901390
// find the delete candidates, selecting id fields and fields needed for
13911391
// running the additional checker
13921392
let candidateSelect = this.policyUtils.makeIdSelection(this.model);
1393-
const additionalCheckerSelector = this.policyUtils.getAdditionalCheckerSelector(
1394-
this.model,
1395-
'delete'
1396-
);
1397-
if (additionalCheckerSelector) {
1398-
candidateSelect = deepmerge(candidateSelect, additionalCheckerSelector);
1393+
if (entityChecker.selector) {
1394+
candidateSelect = deepmerge(candidateSelect, entityChecker.selector);
13991395
}
14001396

14011397
if (this.shouldLogQuery) {
@@ -1409,7 +1405,7 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
14091405
const candidates = await tx[this.model].findMany({ where: args.where, select: candidateSelect });
14101406

14111407
// build a ID filter based on id values filtered by the additional checker
1412-
const { idFilter } = this.buildIdFilterWithAdditionalChecker(candidates, additionalChecker);
1408+
const { idFilter } = this.buildIdFilterWithEntityChecker(candidates, entityChecker.func);
14131409

14141410
// merge the ID filter into the where clause
14151411
args.where = args.where ? { AND: [args.where, idFilter] } : idFilter;
@@ -1560,7 +1556,7 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
15601556
if (args.where) {
15611557
// combine runtime filters with generated constraints
15621558

1563-
const extraConstraints: CheckerConstraint[] = [];
1559+
const extraConstraints: PermissionCheckerConstraint[] = [];
15641560
for (const [field, value] of Object.entries(args.where)) {
15651561
if (value === undefined) {
15661562
continue;
@@ -1690,8 +1686,8 @@ export class PolicyProxyHandler<DbClient extends DbClientContract> implements Pr
16901686
}
16911687
}
16921688

1693-
private buildIdFilterWithAdditionalChecker(candidates: any[], additionalChecker: AdditionalCheckerFunc) {
1694-
const filteredCandidates = candidates.filter((value) => additionalChecker({ user: this.context?.user }, value));
1689+
private buildIdFilterWithEntityChecker(candidates: any[], entityChecker: EntityCheckerFunc) {
1690+
const filteredCandidates = candidates.filter((value) => entityChecker(value, { user: this.context?.user }));
16951691
const idFields = this.policyUtils.getIdFields(this.model);
16961692
let idFilter: any;
16971693
if (idFields.length === 1) {

0 commit comments

Comments
 (0)