@zenstackhq/runtime 1.0.0-beta.13 → 1.0.0-beta.16
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/constants.d.ts +21 -2
- package/constants.js +21 -2
- package/constants.js.map +1 -1
- package/enhancements/model-data-visitor.d.ts +16 -0
- package/enhancements/model-data-visitor.js +41 -0
- package/enhancements/model-data-visitor.js.map +1 -0
- package/enhancements/model-meta.d.ts +3 -2
- package/enhancements/model-meta.js.map +1 -1
- package/enhancements/nested-write-vistor.d.ts +18 -14
- package/enhancements/nested-write-vistor.js +67 -28
- package/enhancements/nested-write-vistor.js.map +1 -1
- package/enhancements/omit.d.ts +1 -1
- package/enhancements/policy/handler.d.ts +21 -13
- package/enhancements/policy/handler.js +711 -129
- package/enhancements/policy/handler.js.map +1 -1
- package/enhancements/policy/index.js +9 -0
- package/enhancements/policy/index.js.map +1 -1
- package/enhancements/policy/policy-utils.d.ts +83 -49
- package/enhancements/policy/policy-utils.js +389 -588
- package/enhancements/policy/policy-utils.js.map +1 -1
- package/enhancements/proxy.js +5 -5
- package/enhancements/proxy.js.map +1 -1
- package/enhancements/types.d.ts +6 -0
- package/enhancements/utils.js +4 -1
- package/enhancements/utils.js.map +1 -1
- package/error.js +9 -3
- package/error.js.map +1 -1
- package/package.json +1 -2
- package/types.d.ts +6 -2
- package/types.js +1 -0
- package/types.js.map +1 -1
|
@@ -14,86 +14,133 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
14
14
|
};
|
|
15
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
16
|
exports.PolicyUtil = void 0;
|
|
17
|
-
const cuid2_1 = require("@paralleldrive/cuid2");
|
|
18
17
|
const deepcopy_1 = __importDefault(require("deepcopy"));
|
|
19
18
|
const lower_case_first_1 = require("lower-case-first");
|
|
20
|
-
const pluralize_1 = __importDefault(require("pluralize"));
|
|
21
19
|
const upper_case_first_1 = require("upper-case-first");
|
|
22
20
|
const zod_validation_error_1 = require("zod-validation-error");
|
|
23
21
|
const constants_1 = require("../../constants");
|
|
24
|
-
const error_1 = require("../../error");
|
|
25
22
|
const version_1 = require("../../version");
|
|
26
23
|
const model_meta_1 = require("../model-meta");
|
|
27
|
-
const nested_write_vistor_1 = require("../nested-write-vistor");
|
|
28
24
|
const utils_1 = require("../utils");
|
|
29
25
|
const logger_1 = require("./logger");
|
|
30
|
-
const semver_1 = __importDefault(require("semver"));
|
|
31
26
|
/**
|
|
32
27
|
* Access policy enforcement utilities
|
|
33
28
|
*/
|
|
34
29
|
class PolicyUtil {
|
|
35
|
-
constructor(db, modelMeta, policy, zodSchemas, user,
|
|
30
|
+
constructor(db, modelMeta, policy, zodSchemas, user, shouldLogQuery = false) {
|
|
36
31
|
this.db = db;
|
|
37
32
|
this.modelMeta = modelMeta;
|
|
38
33
|
this.policy = policy;
|
|
39
34
|
this.zodSchemas = zodSchemas;
|
|
40
35
|
this.user = user;
|
|
41
|
-
this.
|
|
42
|
-
this.supportNestedToOneFilter = false;
|
|
36
|
+
this.shouldLogQuery = shouldLogQuery;
|
|
43
37
|
this.logger = new logger_1.Logger(db);
|
|
44
|
-
// use Prisma version to detect if we can filter when nested-fetching to-one relation
|
|
45
|
-
const prismaVersion = (0, version_1.getPrismaVersion)();
|
|
46
|
-
this.supportNestedToOneFilter = prismaVersion ? semver_1.default.gte(prismaVersion, '4.8.0') : false;
|
|
47
38
|
}
|
|
39
|
+
//#region Logical operators
|
|
48
40
|
/**
|
|
49
41
|
* Creates a conjunction of a list of query conditions.
|
|
50
42
|
*/
|
|
51
43
|
and(...conditions) {
|
|
52
|
-
|
|
53
|
-
// always false
|
|
54
|
-
return { [constants_1.GUARD_FIELD_NAME]: false };
|
|
55
|
-
}
|
|
56
|
-
const filtered = conditions.filter((c) => typeof c === 'object' && !!c && Object.keys(c).length > 0);
|
|
57
|
-
if (filtered.length === 0) {
|
|
58
|
-
return undefined;
|
|
59
|
-
}
|
|
60
|
-
else if (filtered.length === 1) {
|
|
61
|
-
return filtered[0];
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
return { AND: filtered };
|
|
65
|
-
}
|
|
44
|
+
return this.reduce({ AND: conditions });
|
|
66
45
|
}
|
|
67
46
|
/**
|
|
68
47
|
* Creates a disjunction of a list of query conditions.
|
|
69
48
|
*/
|
|
70
49
|
or(...conditions) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
50
|
+
return this.reduce({ OR: conditions });
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Creates a negation of a query condition.
|
|
54
|
+
*/
|
|
55
|
+
not(condition) {
|
|
56
|
+
if (condition === undefined) {
|
|
57
|
+
return this.makeTrue();
|
|
74
58
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return undefined;
|
|
59
|
+
else if (typeof condition === 'boolean') {
|
|
60
|
+
return this.reduce(!condition);
|
|
78
61
|
}
|
|
79
|
-
else
|
|
80
|
-
return
|
|
62
|
+
else {
|
|
63
|
+
return this.reduce({ NOT: condition });
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Static True/False conditions
|
|
67
|
+
// https://www.prisma.io/docs/concepts/components/prisma-client/null-and-undefined#the-effect-of-null-and-undefined-on-conditionals
|
|
68
|
+
isTrue(condition) {
|
|
69
|
+
if (condition === null || condition === undefined) {
|
|
70
|
+
return false;
|
|
81
71
|
}
|
|
82
72
|
else {
|
|
83
|
-
return
|
|
73
|
+
return ((typeof condition === 'object' && Object.keys(condition).length === 0) ||
|
|
74
|
+
('AND' in condition && Array.isArray(condition.AND) && condition.AND.length === 0));
|
|
84
75
|
}
|
|
85
76
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
not(condition) {
|
|
90
|
-
if (typeof condition === 'boolean') {
|
|
91
|
-
return !condition;
|
|
77
|
+
isFalse(condition) {
|
|
78
|
+
if (condition === null || condition === undefined) {
|
|
79
|
+
return false;
|
|
92
80
|
}
|
|
93
81
|
else {
|
|
94
|
-
return
|
|
82
|
+
return 'OR' in condition && Array.isArray(condition.OR) && condition.OR.length === 0;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
makeTrue() {
|
|
86
|
+
return { AND: [] };
|
|
87
|
+
}
|
|
88
|
+
makeFalse() {
|
|
89
|
+
return { OR: [] };
|
|
90
|
+
}
|
|
91
|
+
reduce(condition) {
|
|
92
|
+
if (condition === true || condition === undefined) {
|
|
93
|
+
return this.makeTrue();
|
|
94
|
+
}
|
|
95
|
+
if (condition === false) {
|
|
96
|
+
return this.makeFalse();
|
|
97
|
+
}
|
|
98
|
+
if ('AND' in condition && Array.isArray(condition.AND)) {
|
|
99
|
+
const children = condition.AND.map((c) => this.reduce(c)).filter((c) => c !== undefined && !this.isTrue(c));
|
|
100
|
+
if (children.length === 0) {
|
|
101
|
+
return this.makeTrue();
|
|
102
|
+
}
|
|
103
|
+
else if (children.some((c) => this.isFalse(c))) {
|
|
104
|
+
return this.makeFalse();
|
|
105
|
+
}
|
|
106
|
+
else if (children.length === 1) {
|
|
107
|
+
return children[0];
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
return { AND: children };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if ('OR' in condition && Array.isArray(condition.OR)) {
|
|
114
|
+
const children = condition.OR.map((c) => this.reduce(c)).filter((c) => c !== undefined && !this.isFalse(c));
|
|
115
|
+
if (children.length === 0) {
|
|
116
|
+
return this.makeFalse();
|
|
117
|
+
}
|
|
118
|
+
else if (children.some((c) => this.isTrue(c))) {
|
|
119
|
+
return this.makeTrue();
|
|
120
|
+
}
|
|
121
|
+
else if (children.length === 1) {
|
|
122
|
+
return children[0];
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
return { OR: children };
|
|
126
|
+
}
|
|
95
127
|
}
|
|
128
|
+
if ('NOT' in condition && condition.NOT !== null && typeof condition.NOT === 'object') {
|
|
129
|
+
const child = this.reduce(condition.NOT);
|
|
130
|
+
if (this.isTrue(child)) {
|
|
131
|
+
return this.makeFalse();
|
|
132
|
+
}
|
|
133
|
+
else if (this.isFalse(child)) {
|
|
134
|
+
return this.makeTrue();
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
return { NOT: child };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return condition;
|
|
96
141
|
}
|
|
142
|
+
//#endregion
|
|
143
|
+
//# Auth guard
|
|
97
144
|
/**
|
|
98
145
|
* Gets pregenerated authorization guard object for a given model and operation.
|
|
99
146
|
*
|
|
@@ -107,46 +154,65 @@ class PolicyUtil {
|
|
|
107
154
|
}
|
|
108
155
|
const provider = guard[operation];
|
|
109
156
|
if (typeof provider === 'boolean') {
|
|
110
|
-
return provider;
|
|
157
|
+
return this.reduce(provider);
|
|
111
158
|
}
|
|
112
159
|
if (!provider) {
|
|
113
160
|
throw this.unknownError(`zenstack: unable to load authorization guard for ${model}`);
|
|
114
161
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
hasValidation(model) {
|
|
118
|
-
var _a, _b;
|
|
119
|
-
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;
|
|
162
|
+
const r = provider({ user: this.user, preValue });
|
|
163
|
+
return this.reduce(r);
|
|
120
164
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return
|
|
128
|
-
}
|
|
165
|
+
/**
|
|
166
|
+
* Checks if the given model has a policy guard for the given operation.
|
|
167
|
+
*/
|
|
168
|
+
hasAuthGuard(model, operation) {
|
|
169
|
+
const guard = this.policy.guard[(0, lower_case_first_1.lowerCaseFirst)(model)];
|
|
170
|
+
if (!guard) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
const provider = guard[operation];
|
|
174
|
+
return typeof provider !== 'boolean' || provider !== true;
|
|
129
175
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
176
|
+
/**
|
|
177
|
+
* Checks model creation policy based on static analysis to the input args.
|
|
178
|
+
*
|
|
179
|
+
* @returns boolean if static analysis is enough to determine the result, undefined if not
|
|
180
|
+
*/
|
|
181
|
+
checkInputGuard(model, args, operation) {
|
|
182
|
+
const guard = this.policy.guard[(0, lower_case_first_1.lowerCaseFirst)(model)];
|
|
183
|
+
if (!guard) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
const provider = guard[`${operation}_input`];
|
|
187
|
+
if (typeof provider === 'boolean') {
|
|
188
|
+
return provider;
|
|
189
|
+
}
|
|
190
|
+
if (!provider) {
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
return provider(args, { user: this.user });
|
|
133
194
|
}
|
|
134
195
|
/**
|
|
135
196
|
* Injects model auth guard as where clause.
|
|
136
197
|
*/
|
|
137
198
|
injectAuthGuard(args, model, operation) {
|
|
138
199
|
return __awaiter(this, void 0, void 0, function* () {
|
|
200
|
+
const guard = this.getAuthGuard(model, operation);
|
|
201
|
+
if (this.isFalse(guard)) {
|
|
202
|
+
args.where = this.makeFalse();
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
139
205
|
if (args.where) {
|
|
140
206
|
// inject into relation fields:
|
|
141
207
|
// to-many: some/none/every
|
|
142
208
|
// to-one: direct-conditions/is/isNot
|
|
143
|
-
yield this.
|
|
209
|
+
yield this.injectGuardForRelationFields(model, args.where, operation);
|
|
144
210
|
}
|
|
145
|
-
const guard = this.getAuthGuard(model, operation);
|
|
146
211
|
args.where = this.and(args.where, guard);
|
|
212
|
+
return true;
|
|
147
213
|
});
|
|
148
214
|
}
|
|
149
|
-
|
|
215
|
+
injectGuardForRelationFields(model, payload, operation) {
|
|
150
216
|
return __awaiter(this, void 0, void 0, function* () {
|
|
151
217
|
for (const [field, subPayload] of Object.entries(payload)) {
|
|
152
218
|
if (!subPayload) {
|
|
@@ -169,12 +235,12 @@ class PolicyUtil {
|
|
|
169
235
|
return __awaiter(this, void 0, void 0, function* () {
|
|
170
236
|
const guard = this.getAuthGuard(fieldInfo.type, operation);
|
|
171
237
|
if (payload.some) {
|
|
172
|
-
yield this.
|
|
238
|
+
yield this.injectGuardForRelationFields(fieldInfo.type, payload.some, operation);
|
|
173
239
|
// turn "some" into: { some: { AND: [guard, payload.some] } }
|
|
174
240
|
payload.some = this.and(payload.some, guard);
|
|
175
241
|
}
|
|
176
242
|
if (payload.none) {
|
|
177
|
-
yield this.
|
|
243
|
+
yield this.injectGuardForRelationFields(fieldInfo.type, payload.none, operation);
|
|
178
244
|
// turn none into: { none: { AND: [guard, payload.none] } }
|
|
179
245
|
payload.none = this.and(payload.none, guard);
|
|
180
246
|
}
|
|
@@ -182,7 +248,7 @@ class PolicyUtil {
|
|
|
182
248
|
typeof payload.every === 'object' &&
|
|
183
249
|
// ignore empty every clause
|
|
184
250
|
Object.keys(payload.every).length > 0) {
|
|
185
|
-
yield this.
|
|
251
|
+
yield this.injectGuardForRelationFields(fieldInfo.type, payload.every, operation);
|
|
186
252
|
// turn "every" into: { none: { AND: [guard, { NOT: payload.every }] } }
|
|
187
253
|
if (!payload.none) {
|
|
188
254
|
payload.none = {};
|
|
@@ -197,19 +263,19 @@ class PolicyUtil {
|
|
|
197
263
|
const guard = this.getAuthGuard(fieldInfo.type, operation);
|
|
198
264
|
if (payload.is || payload.isNot) {
|
|
199
265
|
if (payload.is) {
|
|
200
|
-
yield this.
|
|
266
|
+
yield this.injectGuardForRelationFields(fieldInfo.type, payload.is, operation);
|
|
201
267
|
// turn "is" into: { is: { AND: [ originalIs, guard ] }
|
|
202
268
|
payload.is = this.and(payload.is, guard);
|
|
203
269
|
}
|
|
204
270
|
if (payload.isNot) {
|
|
205
|
-
yield this.
|
|
271
|
+
yield this.injectGuardForRelationFields(fieldInfo.type, payload.isNot, operation);
|
|
206
272
|
// turn "isNot" into: { isNot: { AND: [ originalIsNot, { NOT: guard } ] } }
|
|
207
273
|
payload.isNot = this.and(payload.isNot, this.not(guard));
|
|
208
274
|
delete payload.isNot;
|
|
209
275
|
}
|
|
210
276
|
}
|
|
211
277
|
else {
|
|
212
|
-
yield this.
|
|
278
|
+
yield this.injectGuardForRelationFields(fieldInfo.type, payload, operation);
|
|
213
279
|
// turn direct conditions into: { is: { AND: [ originalConditions, guard ] } }
|
|
214
280
|
const combined = this.and((0, deepcopy_1.default)(payload), guard);
|
|
215
281
|
Object.keys(payload).forEach((key) => delete payload[key]);
|
|
@@ -218,63 +284,120 @@ class PolicyUtil {
|
|
|
218
284
|
});
|
|
219
285
|
}
|
|
220
286
|
/**
|
|
221
|
-
*
|
|
222
|
-
* are guaranteed to fully satisfy 'read' policy rules recursively.
|
|
223
|
-
*
|
|
224
|
-
* For to-many relations involved, items not satisfying policy are
|
|
225
|
-
* silently trimmed. For to-one relation, if relation data fails policy
|
|
226
|
-
* an error is thrown.
|
|
287
|
+
* Injects auth guard for read operations.
|
|
227
288
|
*/
|
|
228
|
-
|
|
289
|
+
injectForRead(model, args) {
|
|
290
|
+
var _a, _b;
|
|
229
291
|
return __awaiter(this, void 0, void 0, function* () {
|
|
230
|
-
|
|
292
|
+
const injected = {};
|
|
293
|
+
if (!(yield this.injectAuthGuard(injected, model, 'read'))) {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
231
296
|
if (args.where) {
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
yield this.
|
|
236
|
-
}
|
|
237
|
-
yield this.injectAuthGuard(args, model, 'read');
|
|
238
|
-
// recursively inject read guard conditions into the query args
|
|
239
|
-
yield this.injectNestedReadConditions(model, args);
|
|
240
|
-
if (this.shouldLogQuery) {
|
|
241
|
-
this.logger.info(`[withPolicy] \`findMany\`:\n${(0, utils_1.formatObject)(args)}`);
|
|
297
|
+
// inject into relation fields:
|
|
298
|
+
// to-many: some/none/every
|
|
299
|
+
// to-one: direct-conditions/is/isNot
|
|
300
|
+
yield this.injectGuardForRelationFields(model, args.where, 'read');
|
|
242
301
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
302
|
+
if (injected.where && Object.keys(injected.where).length > 0 && !this.isTrue(injected.where)) {
|
|
303
|
+
args.where = (_a = args.where) !== null && _a !== void 0 ? _a : {};
|
|
304
|
+
Object.assign(args.where, injected.where);
|
|
305
|
+
}
|
|
306
|
+
// recursively inject read guard conditions into nested select, include, and _count
|
|
307
|
+
const hoistedConditions = yield this.injectNestedReadConditions(model, args);
|
|
308
|
+
// the injection process may generate conditions that need to be hoisted to the toplevel,
|
|
309
|
+
// if so, merge it with the existing where
|
|
310
|
+
if (hoistedConditions.length > 0) {
|
|
311
|
+
args.where = (_b = args.where) !== null && _b !== void 0 ? _b : {};
|
|
312
|
+
Object.assign(args.where, ...hoistedConditions);
|
|
313
|
+
}
|
|
314
|
+
return true;
|
|
246
315
|
});
|
|
247
316
|
}
|
|
248
317
|
// flatten unique constraint filters
|
|
249
318
|
flattenGeneratedUniqueField(model, args) {
|
|
250
319
|
var _a;
|
|
320
|
+
// e.g.: { a_b: { a: '1', b: '1' } } => { a: '1', b: '1' }
|
|
321
|
+
const uniqueConstraints = (_a = this.modelMeta.uniqueConstraints) === null || _a === void 0 ? void 0 : _a[(0, lower_case_first_1.lowerCaseFirst)(model)];
|
|
322
|
+
if (uniqueConstraints && Object.keys(uniqueConstraints).length > 0) {
|
|
323
|
+
for (const [field, value] of Object.entries(args)) {
|
|
324
|
+
if (uniqueConstraints[field] &&
|
|
325
|
+
uniqueConstraints[field].fields.length > 1 &&
|
|
326
|
+
typeof value === 'object') {
|
|
327
|
+
// multi-field unique constraint, flatten it
|
|
328
|
+
delete args[field];
|
|
329
|
+
for (const [f, v] of Object.entries(value)) {
|
|
330
|
+
args[f] = v;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Gets unique constraints for the given model.
|
|
338
|
+
*/
|
|
339
|
+
getUniqueConstraints(model) {
|
|
340
|
+
var _a, _b;
|
|
341
|
+
return (_b = (_a = this.modelMeta.uniqueConstraints) === null || _a === void 0 ? void 0 : _a[(0, lower_case_first_1.lowerCaseFirst)(model)]) !== null && _b !== void 0 ? _b : {};
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Builds a reversed query for the given nested path.
|
|
345
|
+
*/
|
|
346
|
+
buildReversedQuery(context) {
|
|
251
347
|
return __awaiter(this, void 0, void 0, function* () {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
let
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
348
|
+
let result, currQuery;
|
|
349
|
+
let currField;
|
|
350
|
+
for (let i = context.nestingPath.length - 1; i >= 0; i--) {
|
|
351
|
+
const { field, model, where } = context.nestingPath[i];
|
|
352
|
+
// never modify the original where because it's shared in the structure
|
|
353
|
+
const visitWhere = Object.assign({}, where);
|
|
354
|
+
if (model && where) {
|
|
355
|
+
// make sure composite unique condition is flattened
|
|
356
|
+
this.flattenGeneratedUniqueField(model, visitWhere);
|
|
357
|
+
}
|
|
358
|
+
if (!result) {
|
|
359
|
+
// first segment (bottom), just use its where clause
|
|
360
|
+
result = currQuery = Object.assign({}, visitWhere);
|
|
361
|
+
currField = field;
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
if (!currField) {
|
|
365
|
+
throw this.unknownError(`missing field in nested path`);
|
|
366
|
+
}
|
|
367
|
+
if (!currField.backLink) {
|
|
368
|
+
throw this.unknownError(`field ${currField.type}.${currField.name} doesn't have a backLink`);
|
|
369
|
+
}
|
|
370
|
+
const backLinkField = this.getModelField(currField.type, currField.backLink);
|
|
371
|
+
if (backLinkField === null || backLinkField === void 0 ? void 0 : backLinkField.isArray) {
|
|
372
|
+
// many-side of relationship, wrap with "some" query
|
|
373
|
+
currQuery[currField.backLink] = { some: Object.assign({}, visitWhere) };
|
|
374
|
+
}
|
|
375
|
+
else {
|
|
376
|
+
if (where && backLinkField.isRelationOwner && backLinkField.foreignKeyMapping) {
|
|
377
|
+
for (const [r, fk] of Object.entries(backLinkField.foreignKeyMapping)) {
|
|
378
|
+
currQuery[fk] = visitWhere[r];
|
|
379
|
+
}
|
|
380
|
+
if (i > 0) {
|
|
381
|
+
currQuery[currField.backLink] = {};
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
currQuery[currField.backLink] = Object.assign({}, visitWhere);
|
|
260
386
|
}
|
|
261
|
-
delete args[field];
|
|
262
|
-
flattened = true;
|
|
263
387
|
}
|
|
388
|
+
currQuery = currQuery[currField.backLink];
|
|
389
|
+
currField = field;
|
|
264
390
|
}
|
|
265
391
|
}
|
|
266
|
-
|
|
267
|
-
// DEBUG
|
|
268
|
-
// this.logger.info(`Filter flattened: ${JSON.stringify(args)}`);
|
|
269
|
-
}
|
|
392
|
+
return result;
|
|
270
393
|
});
|
|
271
394
|
}
|
|
272
395
|
injectNestedReadConditions(model, args) {
|
|
273
|
-
var _a
|
|
396
|
+
var _a;
|
|
274
397
|
return __awaiter(this, void 0, void 0, function* () {
|
|
275
398
|
const injectTarget = (_a = args.select) !== null && _a !== void 0 ? _a : args.include;
|
|
276
399
|
if (!injectTarget) {
|
|
277
|
-
return;
|
|
400
|
+
return [];
|
|
278
401
|
}
|
|
279
402
|
if (injectTarget._count !== undefined) {
|
|
280
403
|
// _count needs to respect read policies of related models
|
|
@@ -305,424 +428,143 @@ class PolicyUtil {
|
|
|
305
428
|
yield this.injectAuthGuard(injectTarget._count.select[field], fieldInfo.type, 'read');
|
|
306
429
|
}
|
|
307
430
|
}
|
|
308
|
-
|
|
431
|
+
// collect filter conditions that should be hoisted to the toplevel
|
|
432
|
+
const hoistedConditions = [];
|
|
309
433
|
for (const field of (0, utils_1.getModelFields)(injectTarget)) {
|
|
310
434
|
const fieldInfo = (0, model_meta_1.resolveField)(this.modelMeta, model, field);
|
|
311
435
|
if (!fieldInfo || !fieldInfo.isDataModel) {
|
|
312
436
|
// only care about relation fields
|
|
313
437
|
continue;
|
|
314
438
|
}
|
|
439
|
+
let hoisted;
|
|
315
440
|
if (fieldInfo.isArray ||
|
|
316
|
-
//
|
|
317
|
-
// fetching a nullable to-one relation, let's do it that way
|
|
441
|
+
// Injecting where at include/select level for nullable to-one relation is supported since Prisma 4.8.0
|
|
318
442
|
// https://github.com/prisma/prisma/discussions/20350
|
|
319
|
-
|
|
443
|
+
fieldInfo.isOptional) {
|
|
320
444
|
if (typeof injectTarget[field] !== 'object') {
|
|
321
445
|
injectTarget[field] = {};
|
|
322
446
|
}
|
|
323
447
|
// inject extra condition for to-many or nullable to-one relation
|
|
324
448
|
yield this.injectAuthGuard(injectTarget[field], fieldInfo.type, 'read');
|
|
325
|
-
// recurse
|
|
326
|
-
yield this.injectNestedReadConditions(fieldInfo.type, injectTarget[field]);
|
|
327
449
|
}
|
|
328
450
|
else {
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
}
|
|
451
|
+
// hoist non-nullable to-one filter to the parent level
|
|
452
|
+
hoisted = this.getAuthGuard(fieldInfo.type, 'read');
|
|
453
|
+
}
|
|
454
|
+
// recurse
|
|
455
|
+
const subHoisted = yield this.injectNestedReadConditions(fieldInfo.type, injectTarget[field]);
|
|
456
|
+
if (subHoisted.length > 0) {
|
|
457
|
+
hoisted = this.and(hoisted, ...subHoisted);
|
|
458
|
+
}
|
|
459
|
+
if (hoisted && !this.isTrue(hoisted)) {
|
|
460
|
+
hoistedConditions.push({ [field]: hoisted });
|
|
340
461
|
}
|
|
341
462
|
}
|
|
463
|
+
return hoistedConditions;
|
|
342
464
|
});
|
|
343
465
|
}
|
|
344
466
|
/**
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
* omitted.
|
|
467
|
+
* Given a model and a unique filter, checks the operation is allowed by policies and field validations.
|
|
468
|
+
* Rejects with an error if not allowed.
|
|
348
469
|
*/
|
|
349
|
-
|
|
470
|
+
checkPolicyForUnique(model, uniqueFilter, operation, db, preValue) {
|
|
350
471
|
return __awaiter(this, void 0, void 0, function* () {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
if (
|
|
472
|
+
const guard = this.getAuthGuard(model, operation, preValue);
|
|
473
|
+
if (this.isFalse(guard)) {
|
|
474
|
+
throw this.deniedByPolicy(model, operation, `entity ${(0, utils_1.formatObject)(uniqueFilter)} failed policy check`);
|
|
475
|
+
}
|
|
476
|
+
// Zod schema is to be checked for "create" and "postUpdate"
|
|
477
|
+
const schema = ['create', 'postUpdate'].includes(operation) ? this.getZodSchema(model) : undefined;
|
|
478
|
+
if (this.isTrue(guard) && !schema) {
|
|
479
|
+
// unconditionally allowed
|
|
358
480
|
return;
|
|
359
481
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
482
|
+
const select = schema
|
|
483
|
+
? // need to validate against schema, need to fetch all fields
|
|
484
|
+
undefined
|
|
485
|
+
: // only fetch id fields
|
|
486
|
+
this.makeIdSelection(model);
|
|
487
|
+
let where = this.clone(uniqueFilter);
|
|
488
|
+
// query args may have be of combined-id form, need to flatten it to call findFirst
|
|
489
|
+
this.flattenGeneratedUniqueField(model, where);
|
|
490
|
+
// query with policy guard
|
|
491
|
+
where = this.and(where, guard);
|
|
492
|
+
const query = { select, where };
|
|
493
|
+
if (this.shouldLogQuery) {
|
|
494
|
+
this.logger.info(`[policy] checking ${model} for ${operation}, \`findFirst\`:\n${(0, utils_1.formatObject)(query)}`);
|
|
365
495
|
}
|
|
366
|
-
const
|
|
367
|
-
if (!
|
|
368
|
-
|
|
496
|
+
const result = yield db[model].findFirst(query);
|
|
497
|
+
if (!result) {
|
|
498
|
+
throw this.deniedByPolicy(model, operation, `entity ${(0, utils_1.formatObject)(uniqueFilter)} failed policy check`);
|
|
369
499
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
const
|
|
373
|
-
if (
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
if (fieldInfo) {
|
|
378
|
-
if (fieldInfo.isDataModel &&
|
|
379
|
-
!fieldInfo.isArray &&
|
|
380
|
-
// if Prisma version supports filtering nullable to-one relation, no need to further check
|
|
381
|
-
!(this.supportNestedToOneFilter && fieldInfo.isOptional)) {
|
|
382
|
-
// to-one relation data cannot be trimmed by injected guards, we have to
|
|
383
|
-
// post-check them
|
|
384
|
-
const ids = this.getEntityIds(fieldInfo.type, fieldData);
|
|
385
|
-
if (Object.keys(ids).length !== 0) {
|
|
386
|
-
if (this.logger.enabled('info')) {
|
|
387
|
-
this.logger.info(`Validating read of to-one relation: ${fieldInfo.type}#${(0, utils_1.formatObject)(ids)}`);
|
|
388
|
-
}
|
|
389
|
-
try {
|
|
390
|
-
yield this.checkPolicyForFilter(fieldInfo.type, ids, operation, this.db);
|
|
391
|
-
}
|
|
392
|
-
catch (err) {
|
|
393
|
-
if ((0, error_1.isPrismaClientKnownRequestError)(err) &&
|
|
394
|
-
err.code === constants_1.PrismaErrorCode.CONSTRAINED_FAILED) {
|
|
395
|
-
// denied by policy
|
|
396
|
-
if (fieldInfo.isOptional) {
|
|
397
|
-
// if the relation is optional, just nullify it
|
|
398
|
-
data[field] = null;
|
|
399
|
-
}
|
|
400
|
-
else {
|
|
401
|
-
// otherwise reject
|
|
402
|
-
throw err;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
else {
|
|
406
|
-
// unknown error
|
|
407
|
-
throw err;
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
}
|
|
500
|
+
if (schema) {
|
|
501
|
+
// TODO: push down schema check to the database
|
|
502
|
+
const parseResult = schema.safeParse(result);
|
|
503
|
+
if (!parseResult.success) {
|
|
504
|
+
const error = (0, zod_validation_error_1.fromZodError)(parseResult.error);
|
|
505
|
+
if (this.logger.enabled('info')) {
|
|
506
|
+
this.logger.info(`entity ${model} failed validation for operation ${operation}: ${error}`);
|
|
411
507
|
}
|
|
412
|
-
|
|
413
|
-
yield this.postProcessForRead(fieldData, fieldInfo.type, injectTarget[field], operation);
|
|
508
|
+
throw this.deniedByPolicy(model, operation, `entities ${JSON.stringify(uniqueFilter)} failed validation: [${error}]`, constants_1.CrudFailureReason.DATA_VALIDATION_VIOLATION);
|
|
414
509
|
}
|
|
415
510
|
}
|
|
416
511
|
});
|
|
417
512
|
}
|
|
418
513
|
/**
|
|
419
|
-
*
|
|
514
|
+
* Tries rejecting a request based on static "false" policy.
|
|
515
|
+
*/
|
|
516
|
+
tryReject(model, operation) {
|
|
517
|
+
const guard = this.getAuthGuard(model, operation);
|
|
518
|
+
if (this.isFalse(guard)) {
|
|
519
|
+
throw this.deniedByPolicy(model, operation);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Checks if a model exists given a unique filter.
|
|
420
524
|
*/
|
|
421
|
-
|
|
525
|
+
checkExistence(db, model, uniqueFilter, throwIfNotFound = false) {
|
|
422
526
|
return __awaiter(this, void 0, void 0, function* () {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
// values before update, so we can post-check if they satisfy
|
|
428
|
-
// model => { ids, entity value }
|
|
429
|
-
const updatedModels = new Map();
|
|
430
|
-
function addUpdatedEntity(model, ids, entity) {
|
|
431
|
-
let modelEntities = updatedModels.get(model);
|
|
432
|
-
if (!modelEntities) {
|
|
433
|
-
modelEntities = [];
|
|
434
|
-
updatedModels.set(model, modelEntities);
|
|
435
|
-
}
|
|
436
|
-
modelEntities.push({ ids, value: entity });
|
|
437
|
-
}
|
|
438
|
-
const idFields = this.getIdFields(model);
|
|
439
|
-
if (args.select) {
|
|
440
|
-
// make sure id fields are selected, we need it to
|
|
441
|
-
// read back the updated entity
|
|
442
|
-
for (const idField of idFields) {
|
|
443
|
-
if (!args.select[idField.name]) {
|
|
444
|
-
args.select[idField.name] = true;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
527
|
+
uniqueFilter = this.clone(uniqueFilter);
|
|
528
|
+
this.flattenGeneratedUniqueField(model, uniqueFilter);
|
|
529
|
+
if (this.shouldLogQuery) {
|
|
530
|
+
this.logger.info(`[policy] checking ${model} existence, \`findFirst\`:\n${(0, utils_1.formatObject)(uniqueFilter)}`);
|
|
447
531
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
// args processor for create
|
|
452
|
-
const processCreate = (model, args) => __awaiter(this, void 0, void 0, function* () {
|
|
453
|
-
const guard = this.getAuthGuard(model, 'create');
|
|
454
|
-
const schema = this.getModelSchema(model);
|
|
455
|
-
if (guard === false) {
|
|
456
|
-
throw this.deniedByPolicy(model, 'create');
|
|
457
|
-
}
|
|
458
|
-
else if (guard !== true || schema) {
|
|
459
|
-
// mark the create with a transaction tag so we can check them later
|
|
460
|
-
args[constants_1.TRANSACTION_FIELD_NAME] = `${transactionId}:create`;
|
|
461
|
-
createdModels.add(model);
|
|
462
|
-
}
|
|
463
|
-
});
|
|
464
|
-
// build a reversed query for fetching entities affected by nested updates
|
|
465
|
-
const buildReversedQuery = (context) => __awaiter(this, void 0, void 0, function* () {
|
|
466
|
-
let result, currQuery;
|
|
467
|
-
let currField;
|
|
468
|
-
for (let i = context.nestingPath.length - 1; i >= 0; i--) {
|
|
469
|
-
const { field, model, where, unique } = context.nestingPath[i];
|
|
470
|
-
// never modify the original where because it's shared in the structure
|
|
471
|
-
const visitWhere = Object.assign({}, where);
|
|
472
|
-
if (model && where) {
|
|
473
|
-
// make sure composite unique condition is flattened
|
|
474
|
-
yield this.flattenGeneratedUniqueField(model, visitWhere);
|
|
475
|
-
}
|
|
476
|
-
if (!result) {
|
|
477
|
-
// first segment (bottom), just use its where clause
|
|
478
|
-
result = currQuery = Object.assign({}, visitWhere);
|
|
479
|
-
currField = field;
|
|
480
|
-
}
|
|
481
|
-
else {
|
|
482
|
-
if (!currField) {
|
|
483
|
-
throw this.unknownError(`missing field in nested path`);
|
|
484
|
-
}
|
|
485
|
-
if (!currField.backLink) {
|
|
486
|
-
throw this.unknownError(`field ${currField.type}.${currField.name} doesn't have a backLink`);
|
|
487
|
-
}
|
|
488
|
-
const backLinkField = this.getModelField(currField.type, currField.backLink);
|
|
489
|
-
if (backLinkField === null || backLinkField === void 0 ? void 0 : backLinkField.isArray) {
|
|
490
|
-
// many-side of relationship, wrap with "some" query
|
|
491
|
-
currQuery[currField.backLink] = { some: Object.assign({}, visitWhere) };
|
|
492
|
-
}
|
|
493
|
-
else {
|
|
494
|
-
currQuery[currField.backLink] = Object.assign({}, visitWhere);
|
|
495
|
-
}
|
|
496
|
-
currQuery = currQuery[currField.backLink];
|
|
497
|
-
currField = field;
|
|
498
|
-
}
|
|
499
|
-
if (unique) {
|
|
500
|
-
// hit a unique filter, no need to traverse further up
|
|
501
|
-
break;
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
return result;
|
|
505
|
-
});
|
|
506
|
-
// args processor for update/upsert
|
|
507
|
-
const processUpdate = (model, where, context) => __awaiter(this, void 0, void 0, function* () {
|
|
508
|
-
const preGuard = this.getAuthGuard(model, 'update');
|
|
509
|
-
if (preGuard === false) {
|
|
510
|
-
throw this.deniedByPolicy(model, 'update');
|
|
511
|
-
}
|
|
512
|
-
else if (preGuard !== true) {
|
|
513
|
-
if (this.isToOneRelation(context.field)) {
|
|
514
|
-
// To-one relation field is complicated because there's no way to
|
|
515
|
-
// filter it during update (args doesn't carry a 'where' clause).
|
|
516
|
-
//
|
|
517
|
-
// We need to recursively walk up its hierarcy in the query args
|
|
518
|
-
// to construct a reversed query to identify the nested entity
|
|
519
|
-
// under update, and then check if it satisfies policy.
|
|
520
|
-
//
|
|
521
|
-
// E.g.:
|
|
522
|
-
// A - B - C
|
|
523
|
-
//
|
|
524
|
-
// update A with:
|
|
525
|
-
// {
|
|
526
|
-
// where: { id: 'aId' },
|
|
527
|
-
// data: {
|
|
528
|
-
// b: {
|
|
529
|
-
// c: { value: 1 }
|
|
530
|
-
// }
|
|
531
|
-
// }
|
|
532
|
-
// }
|
|
533
|
-
//
|
|
534
|
-
// To check if the update to 'c' field is permitted, we
|
|
535
|
-
// reverse the query stack into a filter for C model, like:
|
|
536
|
-
// {
|
|
537
|
-
// where: {
|
|
538
|
-
// b: { a: { id: 'aId' } }
|
|
539
|
-
// }
|
|
540
|
-
// }
|
|
541
|
-
// , and with this we can filter out the C entity that's going
|
|
542
|
-
// to be nestedly updated, and check if it's allowed.
|
|
543
|
-
//
|
|
544
|
-
// The same logic applies to nested delete.
|
|
545
|
-
const subQuery = yield buildReversedQuery(context);
|
|
546
|
-
yield this.checkPolicyForFilter(model, subQuery, 'update', this.db);
|
|
547
|
-
}
|
|
548
|
-
else {
|
|
549
|
-
if (!where) {
|
|
550
|
-
throw this.unknownError(`Missing 'where' parameter`);
|
|
551
|
-
}
|
|
552
|
-
yield this.checkPolicyForFilter(model, where, 'update', this.db);
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
yield preparePostUpdateCheck(model, context);
|
|
556
|
-
});
|
|
557
|
-
// args processor for updateMany
|
|
558
|
-
const processUpdateMany = (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
559
|
-
const guard = this.getAuthGuard(model, 'update');
|
|
560
|
-
if (guard === false) {
|
|
561
|
-
throw this.deniedByPolicy(model, 'update');
|
|
562
|
-
}
|
|
563
|
-
else if (guard !== true) {
|
|
564
|
-
// inject policy filter
|
|
565
|
-
yield this.injectAuthGuard(args, model, 'update');
|
|
566
|
-
}
|
|
567
|
-
yield preparePostUpdateCheck(model, context);
|
|
568
|
-
});
|
|
569
|
-
// for models with post-update rules, we need to read and store
|
|
570
|
-
// entity values before the update for post-update check
|
|
571
|
-
const preparePostUpdateCheck = (model, context) => __awaiter(this, void 0, void 0, function* () {
|
|
572
|
-
const postGuard = this.getAuthGuard(model, 'postUpdate');
|
|
573
|
-
const schema = this.getModelSchema(model);
|
|
574
|
-
// post-update check is needed if there's post-update rule or validation schema
|
|
575
|
-
if (postGuard !== true || schema) {
|
|
576
|
-
// fetch preValue selection (analyzed from the post-update rules)
|
|
577
|
-
const preValueSelect = yield this.getPreValueSelect(model);
|
|
578
|
-
const filter = yield buildReversedQuery(context);
|
|
579
|
-
// query args will be used with findMany, so we need to
|
|
580
|
-
// translate unique constraint filters into a flat filter
|
|
581
|
-
// e.g.: { a_b: { a: '1', b: '1' } } => { a: '1', b: '1' }
|
|
582
|
-
yield this.flattenGeneratedUniqueField(model, filter);
|
|
583
|
-
const idFields = this.getIdFields(model);
|
|
584
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
585
|
-
const select = Object.assign({}, preValueSelect);
|
|
586
|
-
for (const idField of idFields) {
|
|
587
|
-
select[idField.name] = true;
|
|
588
|
-
}
|
|
589
|
-
const query = { where: filter, select };
|
|
590
|
-
if (this.shouldLogQuery) {
|
|
591
|
-
this.logger.info(`[withPolicy] \`findMany\` for fetching pre-update entities:\n${(0, utils_1.formatObject)(args)}`);
|
|
592
|
-
}
|
|
593
|
-
const entities = yield this.db[model].findMany(query);
|
|
594
|
-
entities.forEach((entity) => {
|
|
595
|
-
addUpdatedEntity(model, this.getEntityIds(model, entity), entity);
|
|
596
|
-
});
|
|
597
|
-
}
|
|
598
|
-
});
|
|
599
|
-
// args processor for delete
|
|
600
|
-
const processDelete = (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
601
|
-
const guard = this.getAuthGuard(model, 'delete');
|
|
602
|
-
if (guard === false) {
|
|
603
|
-
throw this.deniedByPolicy(model, 'delete');
|
|
604
|
-
}
|
|
605
|
-
else if (guard !== true) {
|
|
606
|
-
if (this.isToOneRelation(context.field)) {
|
|
607
|
-
// see comments in processUpdate
|
|
608
|
-
const subQuery = yield buildReversedQuery(context);
|
|
609
|
-
yield this.checkPolicyForFilter(model, subQuery, 'delete', this.db);
|
|
610
|
-
}
|
|
611
|
-
else {
|
|
612
|
-
yield this.checkPolicyForFilter(model, args, 'delete', this.db);
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
});
|
|
616
|
-
// process relation updates: connect, connectOrCreate, and disconnect
|
|
617
|
-
const processRelationUpdate = (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
618
|
-
// CHECK ME: equire the entity being connected readable?
|
|
619
|
-
// await this.checkPolicyForFilter(model, args, 'read', this.db);
|
|
620
|
-
var _a;
|
|
621
|
-
if ((_a = context.field) === null || _a === void 0 ? void 0 : _a.backLink) {
|
|
622
|
-
// fetch the backlink field of the model being connected
|
|
623
|
-
const backLinkField = (0, model_meta_1.resolveField)(this.modelMeta, model, context.field.backLink);
|
|
624
|
-
if (backLinkField.isRelationOwner) {
|
|
625
|
-
// the target side of relation owns the relation,
|
|
626
|
-
// mark it as updated
|
|
627
|
-
yield processUpdate(model, args, context);
|
|
628
|
-
}
|
|
629
|
-
}
|
|
532
|
+
const existing = yield db[model].findFirst({
|
|
533
|
+
where: uniqueFilter,
|
|
534
|
+
select: this.makeIdSelection(model),
|
|
630
535
|
});
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
create: (model, args) => __awaiter(this, void 0, void 0, function* () {
|
|
634
|
-
yield processCreate(model, args);
|
|
635
|
-
}),
|
|
636
|
-
connectOrCreate: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
637
|
-
if (args.create) {
|
|
638
|
-
yield processCreate(model, args.create);
|
|
639
|
-
}
|
|
640
|
-
if (args.where) {
|
|
641
|
-
yield processRelationUpdate(model, args.where, context);
|
|
642
|
-
}
|
|
643
|
-
}),
|
|
644
|
-
connect: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
645
|
-
yield processRelationUpdate(model, args, context);
|
|
646
|
-
}),
|
|
647
|
-
disconnect: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
648
|
-
yield processRelationUpdate(model, args, context);
|
|
649
|
-
}),
|
|
650
|
-
update: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
651
|
-
yield processUpdate(model, args.where, context);
|
|
652
|
-
}),
|
|
653
|
-
updateMany: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
654
|
-
yield processUpdateMany(model, args, context);
|
|
655
|
-
}),
|
|
656
|
-
upsert: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
657
|
-
if (args.create) {
|
|
658
|
-
yield processCreate(model, args.create);
|
|
659
|
-
}
|
|
660
|
-
if (args.update) {
|
|
661
|
-
yield processUpdate(model, args.where, context);
|
|
662
|
-
}
|
|
663
|
-
}),
|
|
664
|
-
delete: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
|
|
665
|
-
yield processDelete(model, args, context);
|
|
666
|
-
}),
|
|
667
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
668
|
-
deleteMany: (model, args, _context) => __awaiter(this, void 0, void 0, function* () {
|
|
669
|
-
const guard = this.getAuthGuard(model, 'delete');
|
|
670
|
-
if (guard === false) {
|
|
671
|
-
throw this.deniedByPolicy(model, 'delete');
|
|
672
|
-
}
|
|
673
|
-
else if (guard !== true) {
|
|
674
|
-
if (args.where) {
|
|
675
|
-
args.where = this.and(args.where, guard);
|
|
676
|
-
}
|
|
677
|
-
else {
|
|
678
|
-
const copy = (0, deepcopy_1.default)(args);
|
|
679
|
-
for (const key of Object.keys(args)) {
|
|
680
|
-
delete args[key];
|
|
681
|
-
}
|
|
682
|
-
const combined = this.and(copy, guard);
|
|
683
|
-
Object.assign(args, combined);
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
}),
|
|
687
|
-
});
|
|
688
|
-
yield visitor.visit(model, action, args);
|
|
689
|
-
if (createdModels.size === 0 && updatedModels.size === 0) {
|
|
690
|
-
// no post-check needed, we can proceed with the write without transaction
|
|
691
|
-
return yield writeAction(this.db[model], args);
|
|
692
|
-
}
|
|
693
|
-
else {
|
|
694
|
-
return yield this.transaction(this.db, (tx) => __awaiter(this, void 0, void 0, function* () {
|
|
695
|
-
// proceed with the update (with args processed)
|
|
696
|
-
const result = yield writeAction(tx[model], args);
|
|
697
|
-
if (createdModels.size > 0) {
|
|
698
|
-
// do post-check on created entities
|
|
699
|
-
yield Promise.all([...createdModels].map((model) => this.checkPolicyForFilter(model, { [constants_1.TRANSACTION_FIELD_NAME]: `${transactionId}:create` }, 'create', tx)));
|
|
700
|
-
}
|
|
701
|
-
if (updatedModels.size > 0) {
|
|
702
|
-
// do post-check on updated entities
|
|
703
|
-
yield Promise.all([...updatedModels.entries()]
|
|
704
|
-
.map(([model, modelEntities]) => modelEntities.map(({ ids, value: preValue }) => __awaiter(this, void 0, void 0, function* () { return this.checkPostUpdate(model, ids, tx, preValue); })))
|
|
705
|
-
.flat());
|
|
706
|
-
}
|
|
707
|
-
return result;
|
|
708
|
-
}));
|
|
536
|
+
if (!existing && throwIfNotFound) {
|
|
537
|
+
throw this.notFound(model);
|
|
709
538
|
}
|
|
539
|
+
return existing;
|
|
710
540
|
});
|
|
711
541
|
}
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
542
|
+
/**
|
|
543
|
+
* Returns an entity given a unique filter with read policy checked. Reject if not readable.
|
|
544
|
+
*/
|
|
545
|
+
readBack(db, model, operation, selectInclude, uniqueFilter) {
|
|
546
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
547
|
+
uniqueFilter = this.clone(uniqueFilter);
|
|
548
|
+
this.flattenGeneratedUniqueField(model, uniqueFilter);
|
|
549
|
+
const readArgs = { select: selectInclude.select, include: selectInclude.include, where: uniqueFilter };
|
|
550
|
+
const error = this.deniedByPolicy(model, operation, 'result is not allowed to be read back', constants_1.CrudFailureReason.RESULT_NOT_READABLE);
|
|
551
|
+
const injectResult = yield this.injectForRead(model, readArgs);
|
|
552
|
+
if (!injectResult) {
|
|
553
|
+
return { error, result: undefined };
|
|
554
|
+
}
|
|
555
|
+
if (this.shouldLogQuery) {
|
|
556
|
+
this.logger.info(`[policy] checking read-back, \`findFirst\` ${model}:\n${(0, utils_1.formatObject)(readArgs)}`);
|
|
557
|
+
}
|
|
558
|
+
const result = yield db[model].findFirst(readArgs);
|
|
559
|
+
if (!result) {
|
|
560
|
+
return { error, result: undefined };
|
|
561
|
+
}
|
|
562
|
+
this.postProcessForRead(result);
|
|
563
|
+
return { result, error: undefined };
|
|
564
|
+
});
|
|
725
565
|
}
|
|
566
|
+
//#endregion
|
|
567
|
+
//#region Errors
|
|
726
568
|
deniedByPolicy(model, operation, extra, reason) {
|
|
727
569
|
return (0, utils_1.prismaClientKnownRequestError)(this.db, `denied by policy: ${model} entities failed '${operation}' check${extra ? ', ' + extra : ''}`, { clientVersion: (0, version_1.getVersion)(), code: constants_1.PrismaErrorCode.CONSTRAINED_FAILED, meta: { reason } });
|
|
728
570
|
}
|
|
@@ -732,122 +574,77 @@ class PolicyUtil {
|
|
|
732
574
|
code: 'P2025',
|
|
733
575
|
});
|
|
734
576
|
}
|
|
577
|
+
validationError(message) {
|
|
578
|
+
return (0, utils_1.prismaClientValidationError)(this.db, message, {
|
|
579
|
+
clientVersion: (0, version_1.getVersion)(),
|
|
580
|
+
});
|
|
581
|
+
}
|
|
735
582
|
unknownError(message) {
|
|
736
583
|
return (0, utils_1.prismaClientUnknownRequestError)(this.db, message, {
|
|
737
584
|
clientVersion: (0, version_1.getVersion)(),
|
|
738
585
|
});
|
|
739
586
|
}
|
|
587
|
+
//#endregion
|
|
588
|
+
//#region Misc
|
|
740
589
|
/**
|
|
741
|
-
*
|
|
742
|
-
* in data being trimmed, and if so, throw an error.
|
|
590
|
+
* Gets field selection for fetching pre-update entity values for the given model.
|
|
743
591
|
*/
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
592
|
+
getPreValueSelect(model) {
|
|
593
|
+
const guard = this.policy.guard[(0, lower_case_first_1.lowerCaseFirst)(model)];
|
|
594
|
+
if (!guard) {
|
|
595
|
+
throw this.unknownError(`unable to load policy guard for ${model}`);
|
|
596
|
+
}
|
|
597
|
+
return guard.preValueSelect;
|
|
598
|
+
}
|
|
599
|
+
hasFieldValidation(model) {
|
|
600
|
+
var _a, _b;
|
|
601
|
+
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;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Gets Zod schema for the given model and access kind.
|
|
605
|
+
*
|
|
606
|
+
* @param kind If undefined, returns the full schema.
|
|
607
|
+
*/
|
|
608
|
+
getZodSchema(model, kind = undefined) {
|
|
609
|
+
var _a, _b;
|
|
610
|
+
if (!this.hasFieldValidation(model)) {
|
|
611
|
+
return undefined;
|
|
612
|
+
}
|
|
613
|
+
const schemaKey = `${(0, upper_case_first_1.upperCaseFirst)(model)}${kind ? (0, upper_case_first_1.upperCaseFirst)(kind) : ''}Schema`;
|
|
614
|
+
return (_b = (_a = this.zodSchemas) === null || _a === void 0 ? void 0 : _a.models) === null || _b === void 0 ? void 0 : _b[schemaKey];
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Post processing checks and clean-up for read model entities.
|
|
618
|
+
*/
|
|
619
|
+
postProcessForRead(data) {
|
|
620
|
+
if (data === null || data === undefined) {
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
for (const entityData of (0, utils_1.enumerate)(data)) {
|
|
624
|
+
if (typeof entityData !== 'object' || !entityData) {
|
|
769
625
|
return;
|
|
770
626
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
// build a query condition with policy injected
|
|
776
|
-
const guardedQuery = { where: this.and(queryFilter, guard) };
|
|
777
|
-
if (schema) {
|
|
778
|
-
// we've got schemas, so have to fetch entities and validate them
|
|
779
|
-
// if (this.shouldLogQuery) {
|
|
780
|
-
// this.logger.info(
|
|
781
|
-
// `[withPolicy] \`findMany\` for policy check with guard:\n${formatObject(countArgs)}`
|
|
782
|
-
// );
|
|
783
|
-
// }
|
|
784
|
-
const entities = yield db[model].findMany(guardedQuery);
|
|
785
|
-
if (entities.length < count) {
|
|
786
|
-
if (this.logger.enabled('info')) {
|
|
787
|
-
this.logger.info(`entity ${model} failed policy check for operation ${operation}`);
|
|
788
|
-
}
|
|
789
|
-
throw this.deniedByPolicy(model, operation, `${count - entities.length} ${(0, pluralize_1.default)('entity', count - entities.length)} failed policy check`);
|
|
790
|
-
}
|
|
791
|
-
// TODO: push down schema check to the database
|
|
792
|
-
const schemaCheckErrors = entities.map((entity) => schema.safeParse(entity)).filter((r) => !r.success);
|
|
793
|
-
if (schemaCheckErrors.length > 0) {
|
|
794
|
-
const error = schemaCheckErrors.map((r) => !r.success && (0, zod_validation_error_1.fromZodError)(r.error).message).join(', ');
|
|
795
|
-
if (this.logger.enabled('info')) {
|
|
796
|
-
this.logger.info(`entity ${model} failed schema check for operation ${operation}: ${error}`);
|
|
797
|
-
}
|
|
798
|
-
throw this.deniedByPolicy(model, operation, `entities failed schema check: [${error}]`, constants_1.CrudFailureReason.DATA_VALIDATION_VIOLATION);
|
|
799
|
-
}
|
|
800
|
-
}
|
|
801
|
-
else {
|
|
802
|
-
// count entities with policy injected and see if any of them are filtered out
|
|
803
|
-
// if (this.shouldLogQuery) {
|
|
804
|
-
// this.logger.info(
|
|
805
|
-
// `[withPolicy] \`count\` for policy check with guard:\n${formatObject(guardedQuery)}`
|
|
806
|
-
// );
|
|
807
|
-
// }
|
|
808
|
-
const guardedCount = (yield db[model].count(guardedQuery));
|
|
809
|
-
if (guardedCount < count) {
|
|
810
|
-
if (this.logger.enabled('info')) {
|
|
811
|
-
this.logger.info(`entity ${model} failed policy check for operation ${operation}`);
|
|
812
|
-
}
|
|
813
|
-
throw this.deniedByPolicy(model, operation, `${count - guardedCount} ${(0, pluralize_1.default)('entity', count - guardedCount)} failed policy check`);
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
});
|
|
817
|
-
}
|
|
818
|
-
checkPostUpdate(model, ids, db, preValue) {
|
|
819
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
820
|
-
// if (this.logger.enabled('info')) {
|
|
821
|
-
// this.logger.info(`Checking post-update policy for ${model}#${ids}, preValue: ${formatObject(preValue)}`);
|
|
822
|
-
// }
|
|
823
|
-
const guard = this.getAuthGuard(model, 'postUpdate', preValue);
|
|
824
|
-
// build a query condition with policy injected
|
|
825
|
-
const guardedQuery = { where: this.and(ids, guard) };
|
|
826
|
-
// query with policy injected
|
|
827
|
-
const entity = yield db[model].findFirst(guardedQuery);
|
|
828
|
-
// see if we get fewer items with policy, if so, reject with an throw
|
|
829
|
-
if (!entity) {
|
|
830
|
-
if (this.logger.enabled('info')) {
|
|
831
|
-
this.logger.info(`entity ${model} failed policy check for operation postUpdate`);
|
|
627
|
+
// strip auxiliary fields
|
|
628
|
+
for (const auxField of constants_1.AUXILIARY_FIELDS) {
|
|
629
|
+
if (auxField in entityData) {
|
|
630
|
+
delete entityData[auxField];
|
|
832
631
|
}
|
|
833
|
-
throw this.deniedByPolicy(model, 'postUpdate');
|
|
834
632
|
}
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
const schemaCheckResult = schema.safeParse(entity);
|
|
839
|
-
if (!schemaCheckResult.success) {
|
|
840
|
-
const error = (0, zod_validation_error_1.fromZodError)(schemaCheckResult.error).message;
|
|
841
|
-
if (this.logger.enabled('info')) {
|
|
842
|
-
this.logger.info(`entity ${model} failed schema check for operation postUpdate: ${error}`);
|
|
843
|
-
}
|
|
844
|
-
throw this.deniedByPolicy(model, 'postUpdate', `entity failed schema check: ${error}`);
|
|
633
|
+
for (const fieldData of Object.values(entityData)) {
|
|
634
|
+
if (typeof fieldData !== 'object' || !fieldData) {
|
|
635
|
+
continue;
|
|
845
636
|
}
|
|
637
|
+
this.postProcessForRead(fieldData);
|
|
846
638
|
}
|
|
847
|
-
}
|
|
639
|
+
}
|
|
848
640
|
}
|
|
849
|
-
|
|
850
|
-
|
|
641
|
+
/**
|
|
642
|
+
* Gets information for a specific model field.
|
|
643
|
+
*/
|
|
644
|
+
getModelField(model, field) {
|
|
645
|
+
var _a;
|
|
646
|
+
model = (0, lower_case_first_1.lowerCaseFirst)(model);
|
|
647
|
+
return (_a = this.modelMeta.fields[model]) === null || _a === void 0 ? void 0 : _a[field];
|
|
851
648
|
}
|
|
852
649
|
/**
|
|
853
650
|
* Clones an object and makes sure it's not empty.
|
|
@@ -856,13 +653,13 @@ class PolicyUtil {
|
|
|
856
653
|
return value ? (0, deepcopy_1.default)(value) : {};
|
|
857
654
|
}
|
|
858
655
|
/**
|
|
859
|
-
* Gets "id"
|
|
656
|
+
* Gets "id" fields for a given model.
|
|
860
657
|
*/
|
|
861
658
|
getIdFields(model) {
|
|
862
659
|
return (0, utils_1.getIdFields)(this.modelMeta, model, true);
|
|
863
660
|
}
|
|
864
661
|
/**
|
|
865
|
-
* Gets id field
|
|
662
|
+
* Gets id field values from an entity.
|
|
866
663
|
*/
|
|
867
664
|
getEntityIds(model, entityData) {
|
|
868
665
|
const idFields = this.getIdFields(model);
|
|
@@ -872,8 +669,12 @@ class PolicyUtil {
|
|
|
872
669
|
}
|
|
873
670
|
return result;
|
|
874
671
|
}
|
|
875
|
-
|
|
876
|
-
|
|
672
|
+
/**
|
|
673
|
+
* Creates a selection object for id fields for the given model.
|
|
674
|
+
*/
|
|
675
|
+
makeIdSelection(model) {
|
|
676
|
+
const idFields = this.getIdFields(model);
|
|
677
|
+
return Object.assign({}, ...idFields.map((f) => ({ [f.name]: true })));
|
|
877
678
|
}
|
|
878
679
|
}
|
|
879
680
|
exports.PolicyUtil = PolicyUtil;
|