@zenstackhq/runtime 2.4.0 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/browser/index.js +3 -2
- package/browser/index.js.map +1 -1
- package/browser/index.mjs +3 -2
- package/browser/index.mjs.map +1 -1
- package/constants.d.ts +1 -1
- package/constants.js +1 -1
- package/constants.js.map +1 -1
- package/edge.d.ts +1 -7
- package/edge.js +1 -7
- package/edge.js.map +1 -1
- package/enhance-edge.d.ts +1 -0
- package/enhance-edge.js +10 -0
- package/enhancements/edge/create-enhancement.d.ts +37 -0
- package/enhancements/{create-enhancement.js → edge/create-enhancement.js} +2 -2
- package/enhancements/edge/create-enhancement.js.map +1 -0
- package/enhancements/{default-auth.d.ts → edge/default-auth.d.ts} +2 -1
- package/enhancements/{default-auth.js → edge/default-auth.js} +1 -1
- package/enhancements/edge/default-auth.js.map +1 -0
- package/enhancements/{delegate.d.ts → edge/delegate.d.ts} +1 -1
- package/enhancements/{delegate.js → edge/delegate.js} +16 -3
- package/enhancements/edge/delegate.js.map +1 -0
- package/enhancements/{index.d.ts → edge/index.d.ts} +1 -1
- package/enhancements/{index.js → edge/index.js} +1 -1
- package/enhancements/edge/index.js.map +1 -0
- package/enhancements/edge/logger.js.map +1 -0
- package/enhancements/{omit.js → edge/omit.js} +33 -3
- package/enhancements/edge/omit.js.map +1 -0
- package/enhancements/{password.js → edge/password.js} +2 -2
- package/enhancements/edge/password.js.map +1 -0
- package/enhancements/edge/policy/check-utils.d.ts +5 -0
- package/enhancements/edge/policy/check-utils.js +20 -0
- package/enhancements/edge/policy/check-utils.js.map +1 -0
- package/enhancements/{policy → edge/policy}/handler.d.ts +24 -9
- package/enhancements/{policy → edge/policy}/handler.js +67 -91
- package/enhancements/edge/policy/handler.js.map +1 -0
- package/enhancements/{policy → edge/policy}/index.d.ts +2 -1
- package/enhancements/{policy → edge/policy}/index.js +2 -2
- package/enhancements/edge/policy/index.js.map +1 -0
- package/enhancements/{policy → edge/policy}/policy-utils.d.ts +5 -5
- package/enhancements/{policy → edge/policy}/policy-utils.js +27 -21
- package/enhancements/edge/policy/policy-utils.js.map +1 -0
- package/enhancements/{promise.d.ts → edge/promise.d.ts} +1 -1
- package/enhancements/{promise.js → edge/promise.js} +1 -1
- package/enhancements/edge/promise.js.map +1 -0
- package/enhancements/{proxy.d.ts → edge/proxy.d.ts} +26 -9
- package/enhancements/{proxy.js → edge/proxy.js} +31 -7
- package/enhancements/edge/proxy.js.map +1 -0
- package/enhancements/{query-utils.d.ts → edge/query-utils.d.ts} +2 -2
- package/enhancements/{query-utils.js → edge/query-utils.js} +3 -4
- package/enhancements/edge/query-utils.js.map +1 -0
- package/enhancements/{types.d.ts → edge/types.d.ts} +5 -12
- package/enhancements/{types.js.map → edge/types.js.map} +1 -1
- package/enhancements/{utils.d.ts → edge/utils.d.ts} +2 -2
- package/enhancements/{utils.js → edge/utils.js} +2 -2
- package/enhancements/edge/utils.js.map +1 -0
- package/enhancements/{where-visitor.d.ts → edge/where-visitor.d.ts} +1 -1
- package/enhancements/{where-visitor.js → edge/where-visitor.js} +1 -1
- package/enhancements/edge/where-visitor.js.map +1 -0
- package/enhancements/node/create-enhancement.d.ts +37 -0
- package/enhancements/node/create-enhancement.js +79 -0
- package/enhancements/node/create-enhancement.js.map +1 -0
- package/enhancements/node/default-auth.d.ts +8 -0
- package/enhancements/node/default-auth.js +129 -0
- package/enhancements/node/default-auth.js.map +1 -0
- package/enhancements/node/delegate.d.ts +69 -0
- package/enhancements/node/delegate.js +1006 -0
- package/enhancements/node/delegate.js.map +1 -0
- package/enhancements/node/index.d.ts +4 -0
- package/enhancements/node/index.js +21 -0
- package/enhancements/node/index.js.map +1 -0
- package/enhancements/node/logger.d.ts +29 -0
- package/enhancements/node/logger.js +65 -0
- package/enhancements/node/logger.js.map +1 -0
- package/enhancements/node/omit.d.ts +7 -0
- package/enhancements/node/omit.js +93 -0
- package/enhancements/node/omit.js.map +1 -0
- package/enhancements/node/password.d.ts +7 -0
- package/enhancements/node/password.js +65 -0
- package/enhancements/node/password.js.map +1 -0
- package/enhancements/node/policy/check-utils.d.ts +5 -0
- package/enhancements/node/policy/check-utils.js +87 -0
- package/enhancements/node/policy/check-utils.js.map +1 -0
- package/enhancements/node/policy/constraint-solver.js.map +1 -0
- package/enhancements/node/policy/handler.d.ts +94 -0
- package/enhancements/node/policy/handler.js +1357 -0
- package/enhancements/node/policy/handler.js.map +1 -0
- package/enhancements/node/policy/index.d.ts +13 -0
- package/enhancements/node/policy/index.js +42 -0
- package/enhancements/node/policy/index.js.map +1 -0
- package/enhancements/node/policy/policy-utils.d.ts +184 -0
- package/enhancements/node/policy/policy-utils.js +1296 -0
- package/enhancements/node/policy/policy-utils.js.map +1 -0
- package/enhancements/node/promise.d.ts +15 -0
- package/enhancements/node/promise.js +99 -0
- package/enhancements/node/promise.js.map +1 -0
- package/enhancements/node/proxy.d.ts +118 -0
- package/enhancements/node/proxy.js +267 -0
- package/enhancements/node/proxy.js.map +1 -0
- package/enhancements/node/query-utils.d.ts +38 -0
- package/enhancements/node/query-utils.js +185 -0
- package/enhancements/node/query-utils.js.map +1 -0
- package/enhancements/node/types.d.ts +224 -0
- package/enhancements/node/types.js +3 -0
- package/enhancements/node/types.js.map +1 -0
- package/enhancements/node/utils.d.ts +11 -0
- package/enhancements/node/utils.js +49 -0
- package/enhancements/node/utils.js.map +1 -0
- package/enhancements/node/where-visitor.d.ts +32 -0
- package/enhancements/node/where-visitor.js +86 -0
- package/enhancements/node/where-visitor.js.map +1 -0
- package/index.d.ts +2 -2
- package/index.js +2 -2
- package/index.js.map +1 -1
- package/package.json +14 -6
- package/types.d.ts +62 -0
- package/enhancements/create-enhancement.d.ts +0 -78
- package/enhancements/create-enhancement.js.map +0 -1
- package/enhancements/default-auth.js.map +0 -1
- package/enhancements/delegate.js.map +0 -1
- package/enhancements/index.js.map +0 -1
- package/enhancements/logger.js.map +0 -1
- package/enhancements/omit.js.map +0 -1
- package/enhancements/password.js.map +0 -1
- package/enhancements/policy/constraint-solver.js.map +0 -1
- package/enhancements/policy/handler.js.map +0 -1
- package/enhancements/policy/index.js.map +0 -1
- package/enhancements/policy/policy-utils.js.map +0 -1
- package/enhancements/promise.js.map +0 -1
- package/enhancements/proxy.js.map +0 -1
- package/enhancements/query-utils.js.map +0 -1
- package/enhancements/utils.js.map +0 -1
- package/enhancements/where-visitor.js.map +0 -1
- /package/enhancements/{logger.d.ts → edge/logger.d.ts} +0 -0
- /package/enhancements/{logger.js → edge/logger.js} +0 -0
- /package/enhancements/{omit.d.ts → edge/omit.d.ts} +0 -0
- /package/enhancements/{password.d.ts → edge/password.d.ts} +0 -0
- /package/enhancements/{types.js → edge/types.js} +0 -0
- /package/enhancements/{policy → node/policy}/constraint-solver.d.ts +0 -0
- /package/enhancements/{policy → node/policy}/constraint-solver.js +0 -0
|
@@ -0,0 +1,1296 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
3
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
4
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
5
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
6
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
7
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
8
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
9
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
13
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
14
|
+
};
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.PolicyUtil = void 0;
|
|
17
|
+
const deepmerge_1 = __importDefault(require("deepmerge"));
|
|
18
|
+
const is_plain_object_1 = require("is-plain-object");
|
|
19
|
+
const lower_case_first_1 = require("lower-case-first");
|
|
20
|
+
const upper_case_first_1 = require("upper-case-first");
|
|
21
|
+
const zod_1 = require("zod");
|
|
22
|
+
const zod_validation_error_1 = require("zod-validation-error");
|
|
23
|
+
const constants_1 = require("../../../constants");
|
|
24
|
+
const cross_1 = require("../../../cross");
|
|
25
|
+
const version_1 = require("../../../version");
|
|
26
|
+
const logger_1 = require("../logger");
|
|
27
|
+
const query_utils_1 = require("../query-utils");
|
|
28
|
+
const utils_1 = require("../utils");
|
|
29
|
+
/**
|
|
30
|
+
* Access policy enforcement utilities
|
|
31
|
+
*/
|
|
32
|
+
class PolicyUtil extends query_utils_1.QueryUtils {
|
|
33
|
+
constructor(db, options, context, shouldLogQuery = false) {
|
|
34
|
+
super(db, options);
|
|
35
|
+
this.db = db;
|
|
36
|
+
this.shouldLogQuery = shouldLogQuery;
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region Auth guard
|
|
39
|
+
this.FULL_OPEN_MODEL_POLICY = {
|
|
40
|
+
modelLevel: {
|
|
41
|
+
read: { guard: true },
|
|
42
|
+
create: { guard: true, inputChecker: true },
|
|
43
|
+
update: { guard: true },
|
|
44
|
+
delete: { guard: true },
|
|
45
|
+
postUpdate: { guard: true },
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
this.logger = new logger_1.Logger(db);
|
|
49
|
+
this.user = context === null || context === void 0 ? void 0 : context.user;
|
|
50
|
+
({
|
|
51
|
+
modelMeta: this.modelMeta,
|
|
52
|
+
policy: this.policy,
|
|
53
|
+
zodSchemas: this.zodSchemas,
|
|
54
|
+
prismaModule: this.prismaModule,
|
|
55
|
+
} = options);
|
|
56
|
+
}
|
|
57
|
+
//#region Logical operators
|
|
58
|
+
/**
|
|
59
|
+
* Creates a conjunction of a list of query conditions.
|
|
60
|
+
*/
|
|
61
|
+
and(...conditions) {
|
|
62
|
+
const filtered = conditions.filter((c) => c !== undefined);
|
|
63
|
+
if (filtered.length === 0) {
|
|
64
|
+
return this.makeTrue();
|
|
65
|
+
}
|
|
66
|
+
else if (filtered.length === 1) {
|
|
67
|
+
return this.reduce(filtered[0]);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
return this.reduce({ AND: filtered });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Creates a disjunction of a list of query conditions.
|
|
75
|
+
*/
|
|
76
|
+
or(...conditions) {
|
|
77
|
+
const filtered = conditions.filter((c) => c !== undefined);
|
|
78
|
+
if (filtered.length === 0) {
|
|
79
|
+
return this.makeFalse();
|
|
80
|
+
}
|
|
81
|
+
else if (filtered.length === 1) {
|
|
82
|
+
return this.reduce(filtered[0]);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
return this.reduce({ OR: filtered });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Creates a negation of a query condition.
|
|
90
|
+
*/
|
|
91
|
+
not(condition) {
|
|
92
|
+
if (condition === undefined) {
|
|
93
|
+
return this.makeTrue();
|
|
94
|
+
}
|
|
95
|
+
else if (typeof condition === 'boolean') {
|
|
96
|
+
return this.reduce(!condition);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
return this.reduce({ NOT: condition });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// Static True/False conditions
|
|
103
|
+
// https://www.prisma.io/docs/concepts/components/prisma-client/null-and-undefined#the-effect-of-null-and-undefined-on-conditionals
|
|
104
|
+
singleKey(obj, key) {
|
|
105
|
+
if (!obj) {
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
return Object.keys(obj).length === 1 && Object.keys(obj)[0] === key;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
isTrue(condition) {
|
|
113
|
+
if (condition === null || condition === undefined || !(0, is_plain_object_1.isPlainObject)(condition)) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
// {} is true
|
|
117
|
+
if (Object.keys(condition).length === 0) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
// { OR: TRUE } is true
|
|
121
|
+
if (this.singleKey(condition, 'OR') && typeof condition.OR === 'object' && this.isTrue(condition.OR)) {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
// { NOT: FALSE } is true
|
|
125
|
+
if (this.singleKey(condition, 'NOT') && typeof condition.NOT === 'object' && this.isFalse(condition.NOT)) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
// { AND: [] } is true
|
|
129
|
+
if (this.singleKey(condition, 'AND') && Array.isArray(condition.AND) && condition.AND.length === 0) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
isFalse(condition) {
|
|
135
|
+
if (condition === null || condition === undefined || !(0, is_plain_object_1.isPlainObject)(condition)) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
// { AND: FALSE } is false
|
|
139
|
+
if (this.singleKey(condition, 'AND') && typeof condition.AND === 'object' && this.isFalse(condition.AND)) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
// { NOT: TRUE } is false
|
|
143
|
+
if (this.singleKey(condition, 'NOT') && typeof condition.NOT === 'object' && this.isTrue(condition.NOT)) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
// { OR: [] } is false
|
|
147
|
+
if (this.singleKey(condition, 'OR') && Array.isArray(condition.OR) && condition.OR.length === 0) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
makeTrue() {
|
|
153
|
+
return { AND: [] };
|
|
154
|
+
}
|
|
155
|
+
makeFalse() {
|
|
156
|
+
return { OR: [] };
|
|
157
|
+
}
|
|
158
|
+
reduce(condition) {
|
|
159
|
+
if (condition === true || condition === undefined) {
|
|
160
|
+
return this.makeTrue();
|
|
161
|
+
}
|
|
162
|
+
if (condition === false) {
|
|
163
|
+
return this.makeFalse();
|
|
164
|
+
}
|
|
165
|
+
if (condition === null) {
|
|
166
|
+
return condition;
|
|
167
|
+
}
|
|
168
|
+
const result = {};
|
|
169
|
+
for (const [key, value] of Object.entries(condition)) {
|
|
170
|
+
if (value === null || value === undefined) {
|
|
171
|
+
result[key] = value;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
switch (key) {
|
|
175
|
+
case 'AND': {
|
|
176
|
+
const children = (0, cross_1.enumerate)(value)
|
|
177
|
+
.map((c) => this.reduce(c))
|
|
178
|
+
.filter((c) => c !== undefined && !this.isTrue(c));
|
|
179
|
+
if (children.length === 0) {
|
|
180
|
+
// { ..., AND: [] }
|
|
181
|
+
result[key] = [];
|
|
182
|
+
}
|
|
183
|
+
else if (children.some((c) => this.isFalse(c))) {
|
|
184
|
+
// { ..., AND: { OR: [] } }
|
|
185
|
+
result[key] = this.makeFalse();
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
result[key] = !Array.isArray(value) && children.length === 1 ? children[0] : children;
|
|
189
|
+
}
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
case 'OR': {
|
|
193
|
+
const children = (0, cross_1.enumerate)(value)
|
|
194
|
+
.map((c) => this.reduce(c))
|
|
195
|
+
.filter((c) => c !== undefined && !this.isFalse(c));
|
|
196
|
+
if (children.length === 0) {
|
|
197
|
+
// { ..., OR: [] }
|
|
198
|
+
result[key] = [];
|
|
199
|
+
}
|
|
200
|
+
else if (children.some((c) => this.isTrue(c))) {
|
|
201
|
+
// { ..., OR: { AND: [] } }
|
|
202
|
+
result[key] = this.makeTrue();
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
result[key] = !Array.isArray(value) && children.length === 1 ? children[0] : children;
|
|
206
|
+
}
|
|
207
|
+
break;
|
|
208
|
+
}
|
|
209
|
+
case 'NOT': {
|
|
210
|
+
const children = (0, cross_1.enumerate)(value).map((c) => this.reduce(c));
|
|
211
|
+
result[key] = !Array.isArray(value) && children.length === 1 ? children[0] : children;
|
|
212
|
+
break;
|
|
213
|
+
}
|
|
214
|
+
default: {
|
|
215
|
+
if (!(0, is_plain_object_1.isPlainObject)(value)) {
|
|
216
|
+
// don't visit into non-plain object values - could be Date, array, etc.
|
|
217
|
+
result[key] = value;
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
result[key] = this.reduce(value);
|
|
221
|
+
}
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
// finally normalize constant true/false conditions
|
|
227
|
+
if (this.isTrue(result)) {
|
|
228
|
+
return this.makeTrue();
|
|
229
|
+
}
|
|
230
|
+
else if (this.isFalse(result)) {
|
|
231
|
+
return this.makeFalse();
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
return result;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
getModelPolicyDef(model) {
|
|
238
|
+
if (this.options.kinds && !this.options.kinds.includes('policy')) {
|
|
239
|
+
// policy enhancement not enabled, return an fully open guard
|
|
240
|
+
return this.FULL_OPEN_MODEL_POLICY;
|
|
241
|
+
}
|
|
242
|
+
const def = this.policy.policy[(0, lower_case_first_1.lowerCaseFirst)(model)];
|
|
243
|
+
if (!def) {
|
|
244
|
+
throw this.unknownError(`unable to load policy guard for ${model}`);
|
|
245
|
+
}
|
|
246
|
+
return def;
|
|
247
|
+
}
|
|
248
|
+
getModelGuardForOperation(model, operation) {
|
|
249
|
+
var _a;
|
|
250
|
+
const def = this.getModelPolicyDef(model);
|
|
251
|
+
return (_a = def.modelLevel[operation].guard) !== null && _a !== void 0 ? _a : true;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Gets pregenerated authorization guard object for a given model and operation.
|
|
255
|
+
*
|
|
256
|
+
* @returns true if operation is unconditionally allowed, false if unconditionally denied,
|
|
257
|
+
* otherwise returns a guard object
|
|
258
|
+
*/
|
|
259
|
+
getAuthGuard(db, model, operation, preValue) {
|
|
260
|
+
const guard = this.getModelGuardForOperation(model, operation);
|
|
261
|
+
// constant guard
|
|
262
|
+
if (typeof guard === 'boolean') {
|
|
263
|
+
return this.reduce(guard);
|
|
264
|
+
}
|
|
265
|
+
// invoke guard function
|
|
266
|
+
const r = guard({ user: this.user, preValue }, db);
|
|
267
|
+
return this.reduce(r);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Get field-level read auth guard
|
|
271
|
+
*/
|
|
272
|
+
getFieldReadAuthGuard(db, model, field) {
|
|
273
|
+
var _a, _b, _c;
|
|
274
|
+
const def = this.getModelPolicyDef(model);
|
|
275
|
+
const guard = (_c = (_b = (_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a.read) === null || _b === void 0 ? void 0 : _b[field]) === null || _c === void 0 ? void 0 : _c.guard;
|
|
276
|
+
if (guard === undefined) {
|
|
277
|
+
// field access is allowed by default
|
|
278
|
+
return this.makeTrue();
|
|
279
|
+
}
|
|
280
|
+
if (typeof guard === 'boolean') {
|
|
281
|
+
return this.reduce(guard);
|
|
282
|
+
}
|
|
283
|
+
const r = guard({ user: this.user }, db);
|
|
284
|
+
return this.reduce(r);
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Get field-level read auth guard that overrides the model-level
|
|
288
|
+
*/
|
|
289
|
+
getFieldOverrideReadAuthGuard(db, model, field) {
|
|
290
|
+
var _a, _b, _c;
|
|
291
|
+
const def = this.getModelPolicyDef(model);
|
|
292
|
+
const guard = (_c = (_b = (_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a.read) === null || _b === void 0 ? void 0 : _b[field]) === null || _c === void 0 ? void 0 : _c.overrideGuard;
|
|
293
|
+
if (guard === undefined) {
|
|
294
|
+
// field access is denied by default in override mode
|
|
295
|
+
return this.makeFalse();
|
|
296
|
+
}
|
|
297
|
+
if (typeof guard === 'boolean') {
|
|
298
|
+
return this.reduce(guard);
|
|
299
|
+
}
|
|
300
|
+
const r = guard({ user: this.user }, db);
|
|
301
|
+
return this.reduce(r);
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Get field-level update auth guard
|
|
305
|
+
*/
|
|
306
|
+
getFieldUpdateAuthGuard(db, model, field) {
|
|
307
|
+
var _a, _b, _c;
|
|
308
|
+
const def = this.getModelPolicyDef(model);
|
|
309
|
+
const guard = (_c = (_b = (_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a.update) === null || _b === void 0 ? void 0 : _b[field]) === null || _c === void 0 ? void 0 : _c.guard;
|
|
310
|
+
if (guard === undefined) {
|
|
311
|
+
// field access is allowed by default
|
|
312
|
+
return this.makeTrue();
|
|
313
|
+
}
|
|
314
|
+
if (typeof guard === 'boolean') {
|
|
315
|
+
return this.reduce(guard);
|
|
316
|
+
}
|
|
317
|
+
const r = guard({ user: this.user }, db);
|
|
318
|
+
return this.reduce(r);
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Get field-level update auth guard that overrides the model-level
|
|
322
|
+
*/
|
|
323
|
+
getFieldOverrideUpdateAuthGuard(db, model, field) {
|
|
324
|
+
var _a, _b, _c;
|
|
325
|
+
const def = this.getModelPolicyDef(model);
|
|
326
|
+
const guard = (_c = (_b = (_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a.update) === null || _b === void 0 ? void 0 : _b[field]) === null || _c === void 0 ? void 0 : _c.overrideGuard;
|
|
327
|
+
if (guard === undefined) {
|
|
328
|
+
// field access is denied by default in override mode
|
|
329
|
+
return this.makeFalse();
|
|
330
|
+
}
|
|
331
|
+
if (typeof guard === 'boolean') {
|
|
332
|
+
return this.reduce(guard);
|
|
333
|
+
}
|
|
334
|
+
const r = guard({ user: this.user }, db);
|
|
335
|
+
return this.reduce(r);
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Checks if the given model has a policy guard for the given operation.
|
|
339
|
+
*/
|
|
340
|
+
hasAuthGuard(model, operation) {
|
|
341
|
+
const guard = this.getModelGuardForOperation(model, operation);
|
|
342
|
+
return typeof guard !== 'boolean' || guard !== true;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Checks if the given model has any field-level override policy guard for the given operation.
|
|
346
|
+
*/
|
|
347
|
+
hasOverrideAuthGuard(model, operation) {
|
|
348
|
+
var _a;
|
|
349
|
+
if (operation !== 'read' && operation !== 'update') {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
const def = this.getModelPolicyDef(model);
|
|
353
|
+
if ((_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a[operation]) {
|
|
354
|
+
return Object.values(def.fieldLevel[operation]).some((f) => f.overrideGuard !== undefined || f.overrideEntityChecker !== undefined);
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
return false;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* Checks model creation policy based on static analysis to the input args.
|
|
362
|
+
*
|
|
363
|
+
* @returns boolean if static analysis is enough to determine the result, undefined if not
|
|
364
|
+
*/
|
|
365
|
+
checkInputGuard(model, args, operation) {
|
|
366
|
+
const def = this.getModelPolicyDef(model);
|
|
367
|
+
const guard = def.modelLevel[operation].inputChecker;
|
|
368
|
+
if (guard === undefined) {
|
|
369
|
+
return undefined;
|
|
370
|
+
}
|
|
371
|
+
if (typeof guard === 'boolean') {
|
|
372
|
+
return guard;
|
|
373
|
+
}
|
|
374
|
+
return guard(args, { user: this.user });
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Injects model auth guard as where clause.
|
|
378
|
+
*/
|
|
379
|
+
injectAuthGuardAsWhere(db, args, model, operation) {
|
|
380
|
+
let guard = this.getAuthGuard(db, model, operation);
|
|
381
|
+
if (operation === 'update' && args) {
|
|
382
|
+
// merge field-level policy guards
|
|
383
|
+
const fieldUpdateGuard = this.getFieldUpdateGuards(db, model, args);
|
|
384
|
+
if (fieldUpdateGuard.rejectedByField) {
|
|
385
|
+
// rejected
|
|
386
|
+
args.where = this.makeFalse();
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
if (fieldUpdateGuard.guard) {
|
|
391
|
+
// merge field-level guard
|
|
392
|
+
guard = this.and(guard, fieldUpdateGuard.guard);
|
|
393
|
+
}
|
|
394
|
+
if (fieldUpdateGuard.overrideGuard) {
|
|
395
|
+
// merge field-level override guard on the top level
|
|
396
|
+
guard = this.or(guard, fieldUpdateGuard.overrideGuard);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (operation === 'read') {
|
|
401
|
+
// merge field-level read override guards
|
|
402
|
+
const fieldReadOverrideGuard = this.getFieldReadGuards(db, model, args);
|
|
403
|
+
if (fieldReadOverrideGuard) {
|
|
404
|
+
guard = this.or(guard, fieldReadOverrideGuard);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (this.isFalse(guard)) {
|
|
408
|
+
args.where = this.makeFalse();
|
|
409
|
+
return false;
|
|
410
|
+
}
|
|
411
|
+
let mergedGuard = guard;
|
|
412
|
+
if (args.where) {
|
|
413
|
+
// inject into fields:
|
|
414
|
+
// to-many: some/none/every
|
|
415
|
+
// to-one: direct-conditions/is/isNot
|
|
416
|
+
// regular fields
|
|
417
|
+
mergedGuard = this.buildReadGuardForFields(db, model, args.where, guard);
|
|
418
|
+
}
|
|
419
|
+
args.where = this.and(args.where, mergedGuard);
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
// Injects guard for relation fields nested in `payload`. The `modelGuard` parameter represents the model-level guard for `model`.
|
|
423
|
+
// The function returns a modified copy of `modelGuard` with field-level policies combined.
|
|
424
|
+
buildReadGuardForFields(db, model, payload, modelGuard) {
|
|
425
|
+
if (!payload || typeof payload !== 'object' || Object.keys(payload).length === 0) {
|
|
426
|
+
return modelGuard;
|
|
427
|
+
}
|
|
428
|
+
const allFieldGuards = [];
|
|
429
|
+
const allFieldOverrideGuards = [];
|
|
430
|
+
for (const [field, subPayload] of Object.entries(payload)) {
|
|
431
|
+
if (!subPayload) {
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
allFieldGuards.push(this.getFieldReadAuthGuard(db, model, field));
|
|
435
|
+
allFieldOverrideGuards.push(this.getFieldOverrideReadAuthGuard(db, model, field));
|
|
436
|
+
const fieldInfo = (0, cross_1.resolveField)(this.modelMeta, model, field);
|
|
437
|
+
if (fieldInfo === null || fieldInfo === void 0 ? void 0 : fieldInfo.isDataModel) {
|
|
438
|
+
if (fieldInfo.isArray) {
|
|
439
|
+
this.injectReadGuardForToManyField(db, fieldInfo, subPayload);
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
this.injectReadGuardForToOneField(db, fieldInfo, subPayload);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
// all existing field-level guards must be true
|
|
447
|
+
const mergedGuard = this.and(...allFieldGuards);
|
|
448
|
+
// all existing field-level override guards must be true for override to take effect; override is disabled by default
|
|
449
|
+
const mergedOverrideGuard = allFieldOverrideGuards.length === 0 ? this.makeFalse() : this.and(...allFieldOverrideGuards);
|
|
450
|
+
// (original-guard && field-level-guard) || field-level-override-guard
|
|
451
|
+
const updatedGuard = this.or(this.and(modelGuard, mergedGuard), mergedOverrideGuard);
|
|
452
|
+
return updatedGuard;
|
|
453
|
+
}
|
|
454
|
+
injectReadGuardForToManyField(db, fieldInfo, payload) {
|
|
455
|
+
const guard = this.getAuthGuard(db, fieldInfo.type, 'read');
|
|
456
|
+
if (payload.some) {
|
|
457
|
+
const mergedGuard = this.buildReadGuardForFields(db, fieldInfo.type, payload.some, guard);
|
|
458
|
+
// turn "some" into: { some: { AND: [guard, payload.some] } }
|
|
459
|
+
payload.some = this.and(payload.some, mergedGuard);
|
|
460
|
+
}
|
|
461
|
+
if (payload.none) {
|
|
462
|
+
const mergedGuard = this.buildReadGuardForFields(db, fieldInfo.type, payload.none, guard);
|
|
463
|
+
// turn none into: { none: { AND: [guard, payload.none] } }
|
|
464
|
+
payload.none = this.and(payload.none, mergedGuard);
|
|
465
|
+
}
|
|
466
|
+
if (payload.every &&
|
|
467
|
+
typeof payload.every === 'object' &&
|
|
468
|
+
// ignore empty every clause
|
|
469
|
+
Object.keys(payload.every).length > 0) {
|
|
470
|
+
const mergedGuard = this.buildReadGuardForFields(db, fieldInfo.type, payload.every, guard);
|
|
471
|
+
// turn "every" into: { none: { AND: [guard, { NOT: payload.every }] } }
|
|
472
|
+
if (!payload.none) {
|
|
473
|
+
payload.none = {};
|
|
474
|
+
}
|
|
475
|
+
payload.none = this.and(payload.none, mergedGuard, this.not(payload.every));
|
|
476
|
+
delete payload.every;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
injectReadGuardForToOneField(db, fieldInfo, payload) {
|
|
480
|
+
const guard = this.getAuthGuard(db, fieldInfo.type, 'read');
|
|
481
|
+
// is|isNot and flat fields conditions are mutually exclusive
|
|
482
|
+
// is and isNot can be null value
|
|
483
|
+
if (payload.is !== undefined || payload.isNot !== undefined) {
|
|
484
|
+
if (payload.is) {
|
|
485
|
+
const mergedGuard = this.buildReadGuardForFields(db, fieldInfo.type, payload.is, guard);
|
|
486
|
+
// merge guard with existing "is": { is: { AND: [originalIs, guard] } }
|
|
487
|
+
payload.is = this.and(payload.is, mergedGuard);
|
|
488
|
+
}
|
|
489
|
+
if (payload.isNot) {
|
|
490
|
+
const mergedGuard = this.buildReadGuardForFields(db, fieldInfo.type, payload.isNot, guard);
|
|
491
|
+
// merge guard with existing "isNot": { isNot: { AND: [originalIsNot, guard] } }
|
|
492
|
+
payload.isNot = this.and(payload.isNot, mergedGuard);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
else {
|
|
496
|
+
const mergedGuard = this.buildReadGuardForFields(db, fieldInfo.type, payload, guard);
|
|
497
|
+
// turn direct conditions into: { is: { AND: [ originalConditions, guard ] } }
|
|
498
|
+
const combined = this.and((0, cross_1.clone)(payload), mergedGuard);
|
|
499
|
+
Object.keys(payload).forEach((key) => delete payload[key]);
|
|
500
|
+
payload.is = combined;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Injects auth guard for read operations.
|
|
505
|
+
*/
|
|
506
|
+
injectForRead(db, model, args) {
|
|
507
|
+
// make select and include visible to the injection
|
|
508
|
+
const injected = { select: args.select, include: args.include };
|
|
509
|
+
if (!this.injectAuthGuardAsWhere(db, injected, model, 'read')) {
|
|
510
|
+
args.where = this.makeFalse();
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
if (args.where) {
|
|
514
|
+
// inject into fields:
|
|
515
|
+
// to-many: some/none/every
|
|
516
|
+
// to-one: direct-conditions/is/isNot
|
|
517
|
+
// regular fields
|
|
518
|
+
const mergedGuard = this.buildReadGuardForFields(db, model, args.where, {});
|
|
519
|
+
this.mergeWhereClause(args.where, mergedGuard);
|
|
520
|
+
}
|
|
521
|
+
if (args.where) {
|
|
522
|
+
if (injected.where && Object.keys(injected.where).length > 0) {
|
|
523
|
+
// merge injected guard with the user-provided where clause
|
|
524
|
+
this.mergeWhereClause(args.where, injected.where);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
else if (injected.where) {
|
|
528
|
+
// no user-provided where clause, use the injected one
|
|
529
|
+
args.where = injected.where;
|
|
530
|
+
}
|
|
531
|
+
// recursively inject read guard conditions into nested select, include, and _count
|
|
532
|
+
const hoistedConditions = this.injectNestedReadConditions(db, model, args);
|
|
533
|
+
// the injection process may generate conditions that need to be hoisted to the toplevel,
|
|
534
|
+
// if so, merge it with the existing where
|
|
535
|
+
if (hoistedConditions.length > 0) {
|
|
536
|
+
if (!args.where) {
|
|
537
|
+
args.where = this.and(...hoistedConditions);
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
this.mergeWhereClause(args.where, this.and(...hoistedConditions));
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return true;
|
|
544
|
+
}
|
|
545
|
+
//#endregion
|
|
546
|
+
//#region Checker
|
|
547
|
+
/**
|
|
548
|
+
* Gets checker constraints for the given model and operation.
|
|
549
|
+
*/
|
|
550
|
+
getCheckerConstraint(model, operation) {
|
|
551
|
+
if (this.options.kinds && !this.options.kinds.includes('policy')) {
|
|
552
|
+
// policy enhancement not enabled, return a constant true checker result
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
const def = this.getModelPolicyDef(model);
|
|
556
|
+
const checker = def.modelLevel[operation].permissionChecker;
|
|
557
|
+
if (checker === undefined) {
|
|
558
|
+
throw new Error(`Generated permission checkers not found. Please make sure the "generatePermissionChecker" option is set to true in the "@core/enhancer" plugin.`);
|
|
559
|
+
}
|
|
560
|
+
if (typeof checker === 'boolean') {
|
|
561
|
+
return checker;
|
|
562
|
+
}
|
|
563
|
+
if (typeof checker !== 'function') {
|
|
564
|
+
throw this.unknownError(`invalid ${operation} checker function for ${model}`);
|
|
565
|
+
}
|
|
566
|
+
// call checker function
|
|
567
|
+
return checker({ user: this.user });
|
|
568
|
+
}
|
|
569
|
+
//#endregion
|
|
570
|
+
/**
|
|
571
|
+
* Gets unique constraints for the given model.
|
|
572
|
+
*/
|
|
573
|
+
getUniqueConstraints(model) {
|
|
574
|
+
var _a, _b;
|
|
575
|
+
return (_b = (_a = this.modelMeta.models[(0, lower_case_first_1.lowerCaseFirst)(model)]) === null || _a === void 0 ? void 0 : _a.uniqueConstraints) !== null && _b !== void 0 ? _b : {};
|
|
576
|
+
}
|
|
577
|
+
injectNestedReadConditions(db, model, args) {
|
|
578
|
+
var _a;
|
|
579
|
+
const injectTarget = (_a = args.select) !== null && _a !== void 0 ? _a : args.include;
|
|
580
|
+
if (!injectTarget) {
|
|
581
|
+
return [];
|
|
582
|
+
}
|
|
583
|
+
if (injectTarget._count !== undefined) {
|
|
584
|
+
// _count needs to respect read policies of related models
|
|
585
|
+
if (injectTarget._count === true) {
|
|
586
|
+
// include count for all relations, expand to all fields
|
|
587
|
+
// so that we can inject guard conditions for each of them
|
|
588
|
+
injectTarget._count = { select: {} };
|
|
589
|
+
const modelFields = (0, cross_1.getFields)(this.modelMeta, model);
|
|
590
|
+
if (modelFields) {
|
|
591
|
+
for (const [k, v] of Object.entries(modelFields)) {
|
|
592
|
+
if (v.isDataModel && v.isArray) {
|
|
593
|
+
// create an entry for to-many relation
|
|
594
|
+
injectTarget._count.select[k] = {};
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
// inject conditions for each relation
|
|
600
|
+
for (const field of Object.keys(injectTarget._count.select)) {
|
|
601
|
+
if (typeof injectTarget._count.select[field] !== 'object') {
|
|
602
|
+
injectTarget._count.select[field] = {};
|
|
603
|
+
}
|
|
604
|
+
const fieldInfo = (0, cross_1.resolveField)(this.modelMeta, model, field);
|
|
605
|
+
if (!fieldInfo) {
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
// inject into the "where" clause inside select
|
|
609
|
+
this.injectAuthGuardAsWhere(db, injectTarget._count.select[field], fieldInfo.type, 'read');
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
// collect filter conditions that should be hoisted to the toplevel
|
|
613
|
+
const hoistedConditions = [];
|
|
614
|
+
for (const field of (0, cross_1.getModelFields)(injectTarget)) {
|
|
615
|
+
if (injectTarget[field] === false) {
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
const fieldInfo = (0, cross_1.resolveField)(this.modelMeta, model, field);
|
|
619
|
+
if (!fieldInfo || !fieldInfo.isDataModel) {
|
|
620
|
+
// only care about relation fields
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
let hoisted;
|
|
624
|
+
if (fieldInfo.isArray ||
|
|
625
|
+
// Injecting where at include/select level for nullable to-one relation is supported since Prisma 4.8.0
|
|
626
|
+
// https://github.com/prisma/prisma/discussions/20350
|
|
627
|
+
fieldInfo.isOptional) {
|
|
628
|
+
if (typeof injectTarget[field] !== 'object') {
|
|
629
|
+
injectTarget[field] = {};
|
|
630
|
+
}
|
|
631
|
+
// inject extra condition for to-many or nullable to-one relation
|
|
632
|
+
this.injectAuthGuardAsWhere(db, injectTarget[field], fieldInfo.type, 'read');
|
|
633
|
+
// recurse
|
|
634
|
+
const subHoisted = this.injectNestedReadConditions(db, fieldInfo.type, injectTarget[field]);
|
|
635
|
+
if (subHoisted.length > 0) {
|
|
636
|
+
// we can convert it to a where at this level
|
|
637
|
+
injectTarget[field].where = this.and(injectTarget[field].where, ...subHoisted);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
// hoist non-nullable to-one filter to the parent level
|
|
642
|
+
let injected = this.safeClone(injectTarget[field]);
|
|
643
|
+
if (typeof injected !== 'object') {
|
|
644
|
+
injected = {};
|
|
645
|
+
}
|
|
646
|
+
this.injectAuthGuardAsWhere(db, injected, fieldInfo.type, 'read');
|
|
647
|
+
hoisted = injected.where;
|
|
648
|
+
// recurse
|
|
649
|
+
const subHoisted = this.injectNestedReadConditions(db, fieldInfo.type, injectTarget[field]);
|
|
650
|
+
if (subHoisted.length > 0) {
|
|
651
|
+
hoisted = this.and(hoisted, ...subHoisted);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
if (hoisted && !this.isTrue(hoisted)) {
|
|
655
|
+
hoistedConditions.push({ [field]: hoisted });
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return hoistedConditions;
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Given a model and a unique filter, checks the operation is allowed by policies and field validations.
|
|
662
|
+
* Rejects with an error if not allowed.
|
|
663
|
+
*/
|
|
664
|
+
checkPolicyForUnique(model, uniqueFilter, operation, db, args, preValue) {
|
|
665
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
666
|
+
let guard = this.getAuthGuard(db, model, operation, preValue);
|
|
667
|
+
if (this.isFalse(guard) && !this.hasOverrideAuthGuard(model, operation)) {
|
|
668
|
+
throw this.deniedByPolicy(model, operation, `entity ${(0, utils_1.formatObject)(uniqueFilter, false)} failed policy check`, constants_1.CrudFailureReason.ACCESS_POLICY_VIOLATION);
|
|
669
|
+
}
|
|
670
|
+
let entityChecker;
|
|
671
|
+
if (operation === 'update' && args) {
|
|
672
|
+
// merge field-level policy guards
|
|
673
|
+
const fieldUpdateGuard = this.getFieldUpdateGuards(db, model, args);
|
|
674
|
+
if (fieldUpdateGuard.rejectedByField) {
|
|
675
|
+
// rejected
|
|
676
|
+
throw this.deniedByPolicy(model, 'update', `entity ${(0, utils_1.formatObject)(uniqueFilter, false)} failed update policy check for field "${fieldUpdateGuard.rejectedByField}"`, constants_1.CrudFailureReason.ACCESS_POLICY_VIOLATION);
|
|
677
|
+
}
|
|
678
|
+
if (fieldUpdateGuard.guard) {
|
|
679
|
+
// merge field-level guard with AND
|
|
680
|
+
guard = this.and(guard, fieldUpdateGuard.guard);
|
|
681
|
+
}
|
|
682
|
+
if (fieldUpdateGuard.overrideGuard) {
|
|
683
|
+
// merge field-level override guard with OR
|
|
684
|
+
guard = this.or(guard, fieldUpdateGuard.overrideGuard);
|
|
685
|
+
}
|
|
686
|
+
// field-level entity checker
|
|
687
|
+
entityChecker = fieldUpdateGuard.entityChecker;
|
|
688
|
+
}
|
|
689
|
+
// Zod schema is to be checked for "create" and "postUpdate"
|
|
690
|
+
const schema = ['create', 'postUpdate'].includes(operation) ? this.getZodSchema(model) : undefined;
|
|
691
|
+
// combine field-level entity checker with model-level
|
|
692
|
+
const modelEntityChecker = this.getEntityChecker(model, operation);
|
|
693
|
+
entityChecker = this.combineEntityChecker(entityChecker, modelEntityChecker, 'and');
|
|
694
|
+
if (this.isTrue(guard) && !schema && !entityChecker) {
|
|
695
|
+
// unconditionally allowed
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
let select = schema
|
|
699
|
+
? // need to validate against schema, need to fetch all fields
|
|
700
|
+
undefined
|
|
701
|
+
: // only fetch id fields
|
|
702
|
+
this.makeIdSelection(model);
|
|
703
|
+
if (entityChecker === null || entityChecker === void 0 ? void 0 : entityChecker.selector) {
|
|
704
|
+
if (!select) {
|
|
705
|
+
select = this.makeAllScalarFieldSelect(model);
|
|
706
|
+
}
|
|
707
|
+
select = Object.assign(Object.assign({}, select), entityChecker.selector);
|
|
708
|
+
}
|
|
709
|
+
let where = this.safeClone(uniqueFilter);
|
|
710
|
+
// query args may have be of combined-id form, need to flatten it to call findFirst
|
|
711
|
+
this.flattenGeneratedUniqueField(model, where);
|
|
712
|
+
// query with policy guard
|
|
713
|
+
where = this.and(where, guard);
|
|
714
|
+
const query = { select, where };
|
|
715
|
+
if (this.shouldLogQuery) {
|
|
716
|
+
this.logger.info(`[policy] checking ${model} for ${operation}, \`findFirst\`:\n${(0, utils_1.formatObject)(query)}`);
|
|
717
|
+
}
|
|
718
|
+
const result = yield db[model].findFirst(query);
|
|
719
|
+
if (!result) {
|
|
720
|
+
throw this.deniedByPolicy(model, operation, `entity ${(0, utils_1.formatObject)(uniqueFilter, false)} failed policy check`, constants_1.CrudFailureReason.ACCESS_POLICY_VIOLATION);
|
|
721
|
+
}
|
|
722
|
+
if (entityChecker) {
|
|
723
|
+
if (this.logger.enabled('info')) {
|
|
724
|
+
this.logger.info(`[policy] running entity checker on ${model} for ${operation}`);
|
|
725
|
+
}
|
|
726
|
+
if (!entityChecker.func(result, { user: this.user, preValue })) {
|
|
727
|
+
throw this.deniedByPolicy(model, operation, `entity ${(0, utils_1.formatObject)(uniqueFilter, false)} failed policy check`, constants_1.CrudFailureReason.ACCESS_POLICY_VIOLATION);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
if (schema) {
|
|
731
|
+
// TODO: push down schema check to the database
|
|
732
|
+
this.validateZodSchema(model, undefined, result, true, (err) => {
|
|
733
|
+
throw this.deniedByPolicy(model, operation, `entity ${(0, utils_1.formatObject)(uniqueFilter, false)} failed validation: [${(0, zod_validation_error_1.fromZodError)(err)}]`, constants_1.CrudFailureReason.DATA_VALIDATION_VIOLATION, err);
|
|
734
|
+
});
|
|
735
|
+
}
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
getEntityChecker(model, operation, field) {
|
|
739
|
+
var _a, _b, _c;
|
|
740
|
+
const def = this.getModelPolicyDef(model);
|
|
741
|
+
if (field) {
|
|
742
|
+
return (_c = (_b = (_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a[operation]) === null || _b === void 0 ? void 0 : _b[field]) === null || _c === void 0 ? void 0 : _c.entityChecker;
|
|
743
|
+
}
|
|
744
|
+
else {
|
|
745
|
+
return def.modelLevel[operation].entityChecker;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
getUpdateOverrideEntityCheckerForField(model, field) {
|
|
749
|
+
var _a, _b, _c;
|
|
750
|
+
const def = this.getModelPolicyDef(model);
|
|
751
|
+
return (_c = (_b = (_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a.update) === null || _b === void 0 ? void 0 : _b[field]) === null || _c === void 0 ? void 0 : _c.overrideEntityChecker;
|
|
752
|
+
}
|
|
753
|
+
getFieldReadGuards(db, model, args) {
|
|
754
|
+
const allFields = Object.values((0, cross_1.getFields)(this.modelMeta, model));
|
|
755
|
+
// all scalar fields by default
|
|
756
|
+
let fields = allFields.filter((f) => !f.isDataModel);
|
|
757
|
+
if (args.select) {
|
|
758
|
+
// explicitly selected fields
|
|
759
|
+
fields = allFields.filter((f) => { var _a; return ((_a = args.select) === null || _a === void 0 ? void 0 : _a[f.name]) === true; });
|
|
760
|
+
}
|
|
761
|
+
else if (args.include) {
|
|
762
|
+
// included relations
|
|
763
|
+
fields.push(...allFields.filter((f) => !fields.includes(f) && args.include[f.name]));
|
|
764
|
+
}
|
|
765
|
+
if (fields.length === 0) {
|
|
766
|
+
// this can happen if only selecting pseudo fields like "_count"
|
|
767
|
+
return undefined;
|
|
768
|
+
}
|
|
769
|
+
const allFieldGuards = fields.map((field) => this.getFieldOverrideReadAuthGuard(db, model, field.name));
|
|
770
|
+
return this.and(...allFieldGuards);
|
|
771
|
+
}
|
|
772
|
+
getFieldUpdateGuards(db, model, args) {
|
|
773
|
+
var _a;
|
|
774
|
+
const allFieldGuards = [];
|
|
775
|
+
const allOverrideFieldGuards = [];
|
|
776
|
+
let entityChecker;
|
|
777
|
+
for (const [field, value] of Object.entries((_a = args.data) !== null && _a !== void 0 ? _a : args)) {
|
|
778
|
+
if (typeof value === 'undefined') {
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
const fieldInfo = (0, cross_1.resolveField)(this.modelMeta, model, field);
|
|
782
|
+
if (fieldInfo === null || fieldInfo === void 0 ? void 0 : fieldInfo.isDataModel) {
|
|
783
|
+
// relation field update should be treated as foreign key update,
|
|
784
|
+
// fetch and merge all foreign key guards
|
|
785
|
+
if (fieldInfo.isRelationOwner && fieldInfo.foreignKeyMapping) {
|
|
786
|
+
const foreignKeys = Object.values(fieldInfo.foreignKeyMapping);
|
|
787
|
+
for (const fk of foreignKeys) {
|
|
788
|
+
const fieldGuard = this.getFieldUpdateAuthGuard(db, model, fk);
|
|
789
|
+
if (this.isFalse(fieldGuard)) {
|
|
790
|
+
return { guard: fieldGuard, rejectedByField: fk };
|
|
791
|
+
}
|
|
792
|
+
// add field guard
|
|
793
|
+
allFieldGuards.push(fieldGuard);
|
|
794
|
+
// add field override guard
|
|
795
|
+
const overrideFieldGuard = this.getFieldOverrideUpdateAuthGuard(db, model, fk);
|
|
796
|
+
allOverrideFieldGuards.push(overrideFieldGuard);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
else {
|
|
801
|
+
const fieldGuard = this.getFieldUpdateAuthGuard(db, model, field);
|
|
802
|
+
if (this.isFalse(fieldGuard)) {
|
|
803
|
+
return { guard: fieldGuard, rejectedByField: field };
|
|
804
|
+
}
|
|
805
|
+
// add field guard
|
|
806
|
+
allFieldGuards.push(fieldGuard);
|
|
807
|
+
// add field override guard
|
|
808
|
+
const overrideFieldGuard = this.getFieldOverrideUpdateAuthGuard(db, model, field);
|
|
809
|
+
allOverrideFieldGuards.push(overrideFieldGuard);
|
|
810
|
+
}
|
|
811
|
+
// merge regular and override entity checkers with OR
|
|
812
|
+
let checker = this.getEntityChecker(model, 'update', field);
|
|
813
|
+
const overrideChecker = this.getUpdateOverrideEntityCheckerForField(model, field);
|
|
814
|
+
checker = this.combineEntityChecker(checker, overrideChecker, 'or');
|
|
815
|
+
// accumulate entity checker across fields
|
|
816
|
+
entityChecker = this.combineEntityChecker(entityChecker, checker, 'and');
|
|
817
|
+
}
|
|
818
|
+
const allFieldsCombined = this.and(...allFieldGuards);
|
|
819
|
+
const allOverrideFieldsCombined = allOverrideFieldGuards.length !== 0 ? this.and(...allOverrideFieldGuards) : undefined;
|
|
820
|
+
return {
|
|
821
|
+
guard: allFieldsCombined,
|
|
822
|
+
overrideGuard: allOverrideFieldsCombined,
|
|
823
|
+
rejectedByField: undefined,
|
|
824
|
+
entityChecker,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
combineEntityChecker(left, right, combiner) {
|
|
828
|
+
var _a, _b;
|
|
829
|
+
if (!left) {
|
|
830
|
+
return right;
|
|
831
|
+
}
|
|
832
|
+
if (!right) {
|
|
833
|
+
return left;
|
|
834
|
+
}
|
|
835
|
+
const func = combiner === 'and'
|
|
836
|
+
? (entity, context) => left.func(entity, context) && right.func(entity, context)
|
|
837
|
+
: (entity, context) => left.func(entity, context) || right.func(entity, context);
|
|
838
|
+
return {
|
|
839
|
+
func,
|
|
840
|
+
selector: (0, deepmerge_1.default)((_a = left.selector) !== null && _a !== void 0 ? _a : {}, (_b = right.selector) !== null && _b !== void 0 ? _b : {}),
|
|
841
|
+
};
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* Tries rejecting a request based on static "false" policy.
|
|
845
|
+
*/
|
|
846
|
+
tryReject(db, model, operation) {
|
|
847
|
+
const guard = this.getAuthGuard(db, model, operation);
|
|
848
|
+
if (this.isFalse(guard) && !this.hasOverrideAuthGuard(model, operation)) {
|
|
849
|
+
throw this.deniedByPolicy(model, operation, undefined, constants_1.CrudFailureReason.ACCESS_POLICY_VIOLATION);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
/**
|
|
853
|
+
* Checks if a model exists given a unique filter.
|
|
854
|
+
*/
|
|
855
|
+
checkExistence(db_1, model_1, uniqueFilter_1) {
|
|
856
|
+
return __awaiter(this, arguments, void 0, function* (db, model, uniqueFilter, throwIfNotFound = false) {
|
|
857
|
+
uniqueFilter = this.safeClone(uniqueFilter);
|
|
858
|
+
this.flattenGeneratedUniqueField(model, uniqueFilter);
|
|
859
|
+
if (this.shouldLogQuery) {
|
|
860
|
+
this.logger.info(`[policy] checking ${model} existence, \`findFirst\`:\n${(0, utils_1.formatObject)(uniqueFilter)}`);
|
|
861
|
+
}
|
|
862
|
+
const existing = yield db[model].findFirst({
|
|
863
|
+
where: uniqueFilter,
|
|
864
|
+
select: this.makeIdSelection(model),
|
|
865
|
+
});
|
|
866
|
+
if (!existing && throwIfNotFound) {
|
|
867
|
+
throw this.notFound(model);
|
|
868
|
+
}
|
|
869
|
+
return existing;
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Returns an entity given a unique filter with read policy checked. Reject if not readable.
|
|
874
|
+
*/
|
|
875
|
+
readBack(db, model, operation, selectInclude, uniqueFilter) {
|
|
876
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
877
|
+
uniqueFilter = this.safeClone(uniqueFilter);
|
|
878
|
+
this.flattenGeneratedUniqueField(model, uniqueFilter);
|
|
879
|
+
// make sure only select and include are picked
|
|
880
|
+
const selectIncludeClean = this.pick(selectInclude, 'select', 'include');
|
|
881
|
+
const readArgs = Object.assign(Object.assign({}, this.safeClone(selectIncludeClean)), { where: uniqueFilter });
|
|
882
|
+
const error = this.deniedByPolicy(model, operation, 'result is not allowed to be read back', constants_1.CrudFailureReason.RESULT_NOT_READABLE);
|
|
883
|
+
const injectResult = this.injectForRead(db, model, readArgs);
|
|
884
|
+
if (!injectResult) {
|
|
885
|
+
return { error, result: undefined };
|
|
886
|
+
}
|
|
887
|
+
// inject select needed for field-level read checks
|
|
888
|
+
this.injectReadCheckSelect(model, readArgs);
|
|
889
|
+
if (this.shouldLogQuery) {
|
|
890
|
+
this.logger.info(`[policy] checking read-back, \`findFirst\` ${model}:\n${(0, utils_1.formatObject)(readArgs)}`);
|
|
891
|
+
}
|
|
892
|
+
const result = yield db[model].findFirst(readArgs);
|
|
893
|
+
if (!result) {
|
|
894
|
+
return { error, result: undefined };
|
|
895
|
+
}
|
|
896
|
+
this.postProcessForRead(result, model, selectIncludeClean);
|
|
897
|
+
return { result, error: undefined };
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
/**
|
|
901
|
+
* Injects field selection needed for checking field-level read policy check and evaluating
|
|
902
|
+
* entity checker into query args.
|
|
903
|
+
*/
|
|
904
|
+
injectReadCheckSelect(model, args) {
|
|
905
|
+
// we need to recurse into relation fields before injecting the current level, because
|
|
906
|
+
// injection into current level can result in relation being selected/included, which
|
|
907
|
+
// can then cause infinite recursion when we visit relation later
|
|
908
|
+
var _a;
|
|
909
|
+
// recurse into relation fields
|
|
910
|
+
const visitTarget = (_a = args.select) !== null && _a !== void 0 ? _a : args.include;
|
|
911
|
+
if (visitTarget) {
|
|
912
|
+
for (const key of Object.keys(visitTarget)) {
|
|
913
|
+
const field = (0, cross_1.resolveField)(this.modelMeta, model, key);
|
|
914
|
+
if ((field === null || field === void 0 ? void 0 : field.isDataModel) && visitTarget[key]) {
|
|
915
|
+
if (typeof visitTarget[key] !== 'object') {
|
|
916
|
+
// v is "true", ensure it's an object
|
|
917
|
+
visitTarget[key] = {};
|
|
918
|
+
}
|
|
919
|
+
this.injectReadCheckSelect(field.type, visitTarget[key]);
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
if (this.hasFieldLevelPolicy(model)) {
|
|
924
|
+
// recursively inject selection for fields needed for field-level read checks
|
|
925
|
+
const readFieldSelect = this.getFieldReadCheckSelector(model);
|
|
926
|
+
if (readFieldSelect) {
|
|
927
|
+
this.doInjectReadCheckSelect(model, args, { select: readFieldSelect });
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
const entityChecker = this.getEntityChecker(model, 'read');
|
|
931
|
+
if (entityChecker === null || entityChecker === void 0 ? void 0 : entityChecker.selector) {
|
|
932
|
+
this.doInjectReadCheckSelect(model, args, { select: entityChecker.selector });
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
doInjectReadCheckSelect(model, args, input) {
|
|
936
|
+
// omit should be ignored to avoid interfering with field selection
|
|
937
|
+
if (args.omit) {
|
|
938
|
+
delete args.omit;
|
|
939
|
+
}
|
|
940
|
+
if (!(input === null || input === void 0 ? void 0 : input.select)) {
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
let target; // injection target
|
|
944
|
+
let isInclude = false; // if the target is include or select
|
|
945
|
+
if (args.select) {
|
|
946
|
+
target = args.select;
|
|
947
|
+
isInclude = false;
|
|
948
|
+
}
|
|
949
|
+
else if (args.include) {
|
|
950
|
+
target = args.include;
|
|
951
|
+
isInclude = true;
|
|
952
|
+
}
|
|
953
|
+
else {
|
|
954
|
+
target = args.select = this.makeAllScalarFieldSelect(model);
|
|
955
|
+
isInclude = false;
|
|
956
|
+
}
|
|
957
|
+
if (!isInclude) {
|
|
958
|
+
// merge selects
|
|
959
|
+
for (const [k, v] of Object.entries(input.select)) {
|
|
960
|
+
if (v === true) {
|
|
961
|
+
if (!target[k]) {
|
|
962
|
+
target[k] = true;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
// recurse into nested selects (relation fields)
|
|
968
|
+
for (const [k, v] of Object.entries(input.select)) {
|
|
969
|
+
if (typeof v === 'object' && (v === null || v === void 0 ? void 0 : v.select)) {
|
|
970
|
+
const field = (0, cross_1.resolveField)(this.modelMeta, model, k);
|
|
971
|
+
if (field === null || field === void 0 ? void 0 : field.isDataModel) {
|
|
972
|
+
// recurse into relation
|
|
973
|
+
if (isInclude && target[k] === true) {
|
|
974
|
+
// select all fields for the relation
|
|
975
|
+
target[k] = { select: this.makeAllScalarFieldSelect(field.type) };
|
|
976
|
+
}
|
|
977
|
+
else if (!target[k]) {
|
|
978
|
+
// ensure an empty select clause
|
|
979
|
+
target[k] = { select: {} };
|
|
980
|
+
}
|
|
981
|
+
// recurse
|
|
982
|
+
this.doInjectReadCheckSelect(field.type, target[k], v);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
makeAllScalarFieldSelect(model) {
|
|
988
|
+
const fields = this.getModelFields(model);
|
|
989
|
+
const result = {};
|
|
990
|
+
if (fields) {
|
|
991
|
+
Object.entries(fields).forEach(([k, v]) => {
|
|
992
|
+
if (!v.isDataModel) {
|
|
993
|
+
result[k] = true;
|
|
994
|
+
}
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
return result;
|
|
998
|
+
}
|
|
999
|
+
//#endregion
|
|
1000
|
+
//#region Errors
|
|
1001
|
+
deniedByPolicy(model, operation, extra, reason, zodErrors) {
|
|
1002
|
+
const args = { clientVersion: (0, version_1.getVersion)(), code: constants_1.PrismaErrorCode.CONSTRAINT_FAILED, meta: {} };
|
|
1003
|
+
if (reason) {
|
|
1004
|
+
args.meta.reason = reason;
|
|
1005
|
+
}
|
|
1006
|
+
if (zodErrors) {
|
|
1007
|
+
args.meta.zodErrors = zodErrors;
|
|
1008
|
+
}
|
|
1009
|
+
return (0, utils_1.prismaClientKnownRequestError)(this.db, this.prismaModule, `denied by policy: ${model} entities failed '${operation}' check${extra ? ', ' + extra : ''}`, args);
|
|
1010
|
+
}
|
|
1011
|
+
notFound(model) {
|
|
1012
|
+
return (0, utils_1.prismaClientKnownRequestError)(this.db, this.prismaModule, `entity not found for model ${model}`, {
|
|
1013
|
+
clientVersion: (0, version_1.getVersion)(),
|
|
1014
|
+
code: 'P2025',
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
//#endregion
|
|
1018
|
+
//#region Misc
|
|
1019
|
+
/**
|
|
1020
|
+
* Gets field selection for fetching pre-update entity values for the given model.
|
|
1021
|
+
*/
|
|
1022
|
+
getPreValueSelect(model) {
|
|
1023
|
+
const def = this.getModelPolicyDef(model);
|
|
1024
|
+
return def.modelLevel.postUpdate.preUpdateSelector;
|
|
1025
|
+
}
|
|
1026
|
+
// get a merged selector object for all field-level read policies
|
|
1027
|
+
getFieldReadCheckSelector(model) {
|
|
1028
|
+
var _a, _b, _c;
|
|
1029
|
+
const def = this.getModelPolicyDef(model);
|
|
1030
|
+
let result = {};
|
|
1031
|
+
const fieldLevel = (_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a.read;
|
|
1032
|
+
if (fieldLevel) {
|
|
1033
|
+
for (const def of Object.values(fieldLevel)) {
|
|
1034
|
+
if ((_b = def.entityChecker) === null || _b === void 0 ? void 0 : _b.selector) {
|
|
1035
|
+
result = (0, deepmerge_1.default)(result, def.entityChecker.selector);
|
|
1036
|
+
}
|
|
1037
|
+
if ((_c = def.overrideEntityChecker) === null || _c === void 0 ? void 0 : _c.selector) {
|
|
1038
|
+
result = (0, deepmerge_1.default)(result, def.overrideEntityChecker.selector);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
1043
|
+
}
|
|
1044
|
+
checkReadField(model, field, entity) {
|
|
1045
|
+
var _a, _b, _c, _d, _e, _f;
|
|
1046
|
+
const def = this.getModelPolicyDef(model);
|
|
1047
|
+
// combine regular and override field-level entity checkers with OR
|
|
1048
|
+
const checker = (_c = (_b = (_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a.read) === null || _b === void 0 ? void 0 : _b[field]) === null || _c === void 0 ? void 0 : _c.entityChecker;
|
|
1049
|
+
const overrideChecker = (_f = (_e = (_d = def.fieldLevel) === null || _d === void 0 ? void 0 : _d.read) === null || _e === void 0 ? void 0 : _e[field]) === null || _f === void 0 ? void 0 : _f.overrideEntityChecker;
|
|
1050
|
+
const combinedChecker = this.combineEntityChecker(checker, overrideChecker, 'or');
|
|
1051
|
+
if (combinedChecker === undefined) {
|
|
1052
|
+
return true;
|
|
1053
|
+
}
|
|
1054
|
+
else {
|
|
1055
|
+
return combinedChecker.func(entity, { user: this.user });
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
hasFieldValidation(model) {
|
|
1059
|
+
var _a, _b;
|
|
1060
|
+
return ((_b = (_a = this.policy.validation) === null || _a === void 0 ? void 0 : _a[(0, lower_case_first_1.lowerCaseFirst)(model)]) === null || _b === void 0 ? void 0 : _b.hasValidation) === true;
|
|
1061
|
+
}
|
|
1062
|
+
hasFieldLevelPolicy(model) {
|
|
1063
|
+
var _a, _b;
|
|
1064
|
+
const def = this.getModelPolicyDef(model);
|
|
1065
|
+
return Object.keys((_b = (_a = def.fieldLevel) === null || _a === void 0 ? void 0 : _a.read) !== null && _b !== void 0 ? _b : {}).length > 0;
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Gets Zod schema for the given model and access kind.
|
|
1069
|
+
*
|
|
1070
|
+
* @param kind kind of Zod schema to get for. If undefined, returns the full schema.
|
|
1071
|
+
*/
|
|
1072
|
+
getZodSchema(model, excludePasswordFields = true, kind = undefined) {
|
|
1073
|
+
var _a, _b, _c, _d;
|
|
1074
|
+
if (!this.hasFieldValidation(model)) {
|
|
1075
|
+
return undefined;
|
|
1076
|
+
}
|
|
1077
|
+
const schemaKey = `${(0, upper_case_first_1.upperCaseFirst)(model)}${kind ? 'Prisma' + (0, upper_case_first_1.upperCaseFirst)(kind) : ''}Schema`;
|
|
1078
|
+
let result = (_b = (_a = this.zodSchemas) === null || _a === void 0 ? void 0 : _a.models) === null || _b === void 0 ? void 0 : _b[schemaKey];
|
|
1079
|
+
if (result && excludePasswordFields) {
|
|
1080
|
+
// fields with `@password` attribute changes at runtime, so we cannot directly use the generated
|
|
1081
|
+
// zod schema to validate it, instead, the validation happens when checking the input of "create"
|
|
1082
|
+
// and "update" operations
|
|
1083
|
+
const modelFields = (_c = this.modelMeta.models[(0, lower_case_first_1.lowerCaseFirst)(model)]) === null || _c === void 0 ? void 0 : _c.fields;
|
|
1084
|
+
if (modelFields) {
|
|
1085
|
+
for (const [key, field] of Object.entries(modelFields)) {
|
|
1086
|
+
if ((_d = field.attributes) === null || _d === void 0 ? void 0 : _d.some((attr) => attr.name === '@password')) {
|
|
1087
|
+
// override `@password` field schema with a string schema
|
|
1088
|
+
let pwFieldSchema = zod_1.z.string();
|
|
1089
|
+
if (field.isOptional) {
|
|
1090
|
+
pwFieldSchema = pwFieldSchema.nullish();
|
|
1091
|
+
}
|
|
1092
|
+
result = result === null || result === void 0 ? void 0 : result.merge(zod_1.z.object({ [key]: pwFieldSchema }));
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
return result;
|
|
1098
|
+
}
|
|
1099
|
+
/**
|
|
1100
|
+
* Validates the given data against the Zod schema for the given model and kind.
|
|
1101
|
+
*
|
|
1102
|
+
* @param model model
|
|
1103
|
+
* @param kind validation kind. Pass undefined to validate against the full schema.
|
|
1104
|
+
* @param data input data
|
|
1105
|
+
* @param excludePasswordFields whether exclude schema validation for `@password` fields
|
|
1106
|
+
* @param onError error callback
|
|
1107
|
+
* @returns Zod-validated data
|
|
1108
|
+
*/
|
|
1109
|
+
validateZodSchema(model, kind, data, excludePasswordFields, onError) {
|
|
1110
|
+
const schema = this.getZodSchema(model, excludePasswordFields, kind);
|
|
1111
|
+
if (!schema) {
|
|
1112
|
+
return data;
|
|
1113
|
+
}
|
|
1114
|
+
const parseResult = schema.safeParse(data);
|
|
1115
|
+
if (!parseResult.success) {
|
|
1116
|
+
if (this.logger.enabled('info')) {
|
|
1117
|
+
this.logger.info(`entity ${model} failed validation for operation ${kind}: ${(0, zod_validation_error_1.fromZodError)(parseResult.error)}`);
|
|
1118
|
+
}
|
|
1119
|
+
onError(parseResult.error);
|
|
1120
|
+
return undefined;
|
|
1121
|
+
}
|
|
1122
|
+
return parseResult.data;
|
|
1123
|
+
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Post processing checks and clean-up for read model entities.
|
|
1126
|
+
*/
|
|
1127
|
+
postProcessForRead(data, model, queryArgs) {
|
|
1128
|
+
// preserve the original data as it may be needed for checking field-level readability,
|
|
1129
|
+
// while the "data" will be manipulated during traversal (deleting unreadable fields)
|
|
1130
|
+
const origData = this.safeClone(data);
|
|
1131
|
+
return this.doPostProcessForRead(data, model, origData, queryArgs, this.hasFieldLevelPolicy(model));
|
|
1132
|
+
}
|
|
1133
|
+
doPostProcessForRead(data, model, fullData, queryArgs, hasFieldLevelPolicy, path = '') {
|
|
1134
|
+
var _a, _b, _c;
|
|
1135
|
+
if (data === null || data === undefined) {
|
|
1136
|
+
return data;
|
|
1137
|
+
}
|
|
1138
|
+
let filteredData = data;
|
|
1139
|
+
let filteredFullData = fullData;
|
|
1140
|
+
const entityChecker = this.getEntityChecker(model, 'read');
|
|
1141
|
+
if (entityChecker) {
|
|
1142
|
+
if (Array.isArray(data)) {
|
|
1143
|
+
filteredData = [];
|
|
1144
|
+
filteredFullData = [];
|
|
1145
|
+
for (const [entityData, entityFullData] of (0, cross_1.zip)(data, fullData)) {
|
|
1146
|
+
if (!entityChecker.func(entityData, { user: this.user })) {
|
|
1147
|
+
if (this.shouldLogQuery) {
|
|
1148
|
+
this.logger.info(`[policy] dropping ${model} entity${path ? ' at ' + path : ''} due to entity checker`);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
else {
|
|
1152
|
+
filteredData.push(entityData);
|
|
1153
|
+
filteredFullData.push(entityFullData);
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
else {
|
|
1158
|
+
if (!entityChecker.func(data, { user: this.user })) {
|
|
1159
|
+
if (this.shouldLogQuery) {
|
|
1160
|
+
this.logger.info(`[policy] dropping ${model} entity${path ? ' at ' + path : ''} due to entity checker`);
|
|
1161
|
+
}
|
|
1162
|
+
return null;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
for (const [entityData, entityFullData] of (0, cross_1.zip)(filteredData, filteredFullData)) {
|
|
1167
|
+
if (typeof entityData !== 'object' || !entityData) {
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
for (const [field, fieldData] of Object.entries(entityData)) {
|
|
1171
|
+
if (fieldData === undefined) {
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
const fieldInfo = (0, cross_1.resolveField)(this.modelMeta, model, field);
|
|
1175
|
+
if (!fieldInfo) {
|
|
1176
|
+
// could be _count, etc.
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
if (((_a = queryArgs === null || queryArgs === void 0 ? void 0 : queryArgs.omit) === null || _a === void 0 ? void 0 : _a[field]) === true) {
|
|
1180
|
+
// respect `{ omit: { [field]: true } }`
|
|
1181
|
+
delete entityData[field];
|
|
1182
|
+
continue;
|
|
1183
|
+
}
|
|
1184
|
+
if (hasFieldLevelPolicy) {
|
|
1185
|
+
// 1. remove fields selected for checking field-level policies but not selected by the original query args
|
|
1186
|
+
// 2. evaluate field-level policies and remove fields that are not readable
|
|
1187
|
+
if (!fieldInfo.isDataModel) {
|
|
1188
|
+
// scalar field, delete unselected ones
|
|
1189
|
+
const select = queryArgs === null || queryArgs === void 0 ? void 0 : queryArgs.select;
|
|
1190
|
+
if (select && typeof select === 'object' && select[field] !== true) {
|
|
1191
|
+
// there's a select clause but this field is not included
|
|
1192
|
+
delete entityData[field];
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
else {
|
|
1197
|
+
// relation field, delete if not selected or included
|
|
1198
|
+
const include = queryArgs === null || queryArgs === void 0 ? void 0 : queryArgs.include;
|
|
1199
|
+
const select = queryArgs === null || queryArgs === void 0 ? void 0 : queryArgs.select;
|
|
1200
|
+
if (!(include === null || include === void 0 ? void 0 : include[field]) && !(select === null || select === void 0 ? void 0 : select[field])) {
|
|
1201
|
+
// relation field not included or selected
|
|
1202
|
+
delete entityData[field];
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
// delete unreadable fields
|
|
1207
|
+
if (!this.checkReadField(model, field, entityFullData)) {
|
|
1208
|
+
if (this.shouldLogQuery) {
|
|
1209
|
+
this.logger.info(`[policy] dropping unreadable field ${path ? path + '.' : ''}${field}`);
|
|
1210
|
+
}
|
|
1211
|
+
delete entityData[field];
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
if (fieldInfo.isDataModel) {
|
|
1216
|
+
// recurse into nested fields
|
|
1217
|
+
const nextArgs = (_c = ((_b = queryArgs === null || queryArgs === void 0 ? void 0 : queryArgs.select) !== null && _b !== void 0 ? _b : queryArgs === null || queryArgs === void 0 ? void 0 : queryArgs.include)) === null || _c === void 0 ? void 0 : _c[field];
|
|
1218
|
+
const nestedResult = this.doPostProcessForRead(fieldData, fieldInfo.type, entityFullData[field], nextArgs, this.hasFieldLevelPolicy(fieldInfo.type), path ? path + '.' + field : field);
|
|
1219
|
+
if (nestedResult === undefined) {
|
|
1220
|
+
delete entityData[field];
|
|
1221
|
+
}
|
|
1222
|
+
else {
|
|
1223
|
+
entityData[field] = nestedResult;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
return filteredData;
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Replace content of `target` object with `withObject` in-place.
|
|
1232
|
+
*/
|
|
1233
|
+
replace(target, withObject) {
|
|
1234
|
+
if (!target || typeof target !== 'object' || !withObject || typeof withObject !== 'object') {
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
// remove missing keys
|
|
1238
|
+
for (const key of Object.keys(target)) {
|
|
1239
|
+
if (!(key in withObject)) {
|
|
1240
|
+
delete target[key];
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
// overwrite keys
|
|
1244
|
+
for (const [key, value] of Object.entries(withObject)) {
|
|
1245
|
+
target[key] = value;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Picks properties from an object.
|
|
1250
|
+
*/
|
|
1251
|
+
pick(value, ...props) {
|
|
1252
|
+
const v = value;
|
|
1253
|
+
return props.reduce(function (result, prop) {
|
|
1254
|
+
if (prop in v) {
|
|
1255
|
+
result[prop] = v[prop];
|
|
1256
|
+
}
|
|
1257
|
+
return result;
|
|
1258
|
+
}, {});
|
|
1259
|
+
}
|
|
1260
|
+
mergeWhereClause(where, extra) {
|
|
1261
|
+
var _a;
|
|
1262
|
+
if (!where) {
|
|
1263
|
+
throw new Error('invalid where clause');
|
|
1264
|
+
}
|
|
1265
|
+
if (this.isTrue(extra)) {
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
// instead of simply wrapping with AND, we preserve the structure
|
|
1269
|
+
// of the original where clause and merge `extra` into it so that
|
|
1270
|
+
// unique query can continue working
|
|
1271
|
+
if (where.AND) {
|
|
1272
|
+
// merge into existing AND clause
|
|
1273
|
+
const conditions = Array.isArray(where.AND) ? [...where.AND] : [where.AND];
|
|
1274
|
+
conditions.push(extra);
|
|
1275
|
+
const combined = this.and(...conditions);
|
|
1276
|
+
// make sure the merging always goes under AND
|
|
1277
|
+
where.AND = (_a = combined.AND) !== null && _a !== void 0 ? _a : combined;
|
|
1278
|
+
}
|
|
1279
|
+
else {
|
|
1280
|
+
// insert an AND clause
|
|
1281
|
+
where.AND = [extra];
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
/**
|
|
1285
|
+
* Given an entity data, returns an object only containing id fields.
|
|
1286
|
+
*/
|
|
1287
|
+
getIdFieldValues(model, data) {
|
|
1288
|
+
if (!data) {
|
|
1289
|
+
return undefined;
|
|
1290
|
+
}
|
|
1291
|
+
const idFields = this.getIdFields(model);
|
|
1292
|
+
return Object.fromEntries(idFields.map((f) => [f.name, data[f.name]]));
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
exports.PolicyUtil = PolicyUtil;
|
|
1296
|
+
//# sourceMappingURL=policy-utils.js.map
|