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