@zenstackhq/runtime 0.6.0-pre.2 → 1.0.0-alpha.101

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.
Files changed (124) hide show
  1. package/constants.d.ts +4 -0
  2. package/constants.js +8 -0
  3. package/constants.js.map +1 -0
  4. package/enhancements/index.d.ts +4 -0
  5. package/enhancements/index.js +21 -0
  6. package/enhancements/index.js.map +1 -0
  7. package/enhancements/model-meta.d.ts +9 -0
  8. package/enhancements/model-meta.js +25 -0
  9. package/enhancements/model-meta.js.map +1 -0
  10. package/enhancements/nested-write-vistor.d.ts +70 -0
  11. package/enhancements/nested-write-vistor.js +173 -0
  12. package/enhancements/nested-write-vistor.js.map +1 -0
  13. package/enhancements/omit.d.ts +5 -0
  14. package/enhancements/omit.js +60 -0
  15. package/enhancements/omit.js.map +1 -0
  16. package/enhancements/password.d.ts +5 -0
  17. package/enhancements/password.js +66 -0
  18. package/enhancements/password.js.map +1 -0
  19. package/enhancements/policy/handler.d.ts +36 -0
  20. package/enhancements/policy/handler.js +281 -0
  21. package/enhancements/policy/handler.js.map +1 -0
  22. package/enhancements/policy/index.d.ts +17 -0
  23. package/enhancements/policy/index.js +31 -0
  24. package/enhancements/policy/index.js.map +1 -0
  25. package/{lib/proxy → enhancements/policy}/logger.d.ts +3 -0
  26. package/{lib/proxy → enhancements/policy}/logger.js +4 -0
  27. package/enhancements/policy/logger.js.map +1 -0
  28. package/enhancements/policy/policy-utils.d.ts +94 -0
  29. package/enhancements/policy/policy-utils.js +755 -0
  30. package/enhancements/policy/policy-utils.js.map +1 -0
  31. package/enhancements/preset.d.ts +16 -0
  32. package/enhancements/preset.js +24 -0
  33. package/enhancements/preset.js.map +1 -0
  34. package/enhancements/proxy.d.ts +75 -0
  35. package/enhancements/proxy.js +196 -0
  36. package/enhancements/proxy.js.map +1 -0
  37. package/enhancements/types.d.ts +33 -0
  38. package/{lib/config.js → enhancements/types.js} +1 -1
  39. package/enhancements/types.js.map +1 -0
  40. package/enhancements/utils.d.ts +17 -0
  41. package/enhancements/utils.js +59 -0
  42. package/enhancements/utils.js.map +1 -0
  43. package/error.d.ts +11 -0
  44. package/error.js +17 -0
  45. package/error.js.map +1 -0
  46. package/index.d.ts +5 -0
  47. package/{lib/index.js → index.js} +3 -4
  48. package/index.js.map +1 -0
  49. package/package.json +8 -8
  50. package/serialization-utils.js.map +1 -0
  51. package/types.d.ts +101 -0
  52. package/types.js +16 -0
  53. package/types.js.map +1 -0
  54. package/{lib/validation.d.ts → validation.d.ts} +7 -0
  55. package/{lib/validation.js → validation.js} +15 -1
  56. package/validation.js.map +1 -0
  57. package/version.js +19 -0
  58. package/version.js.map +1 -0
  59. package/zod.d.ts +10 -0
  60. package/zod.js +17 -0
  61. package/zod.js.map +1 -0
  62. package/client/index.d.ts +0 -3
  63. package/client/index.js +0 -11
  64. package/lib/config.d.ts +0 -14
  65. package/lib/config.js.map +0 -1
  66. package/lib/constants.d.ts +0 -12
  67. package/lib/constants.js +0 -16
  68. package/lib/constants.js.map +0 -1
  69. package/lib/handler/data/crud.d.ts +0 -17
  70. package/lib/handler/data/crud.js +0 -255
  71. package/lib/handler/data/crud.js.map +0 -1
  72. package/lib/handler/data/handler.d.ts +0 -20
  73. package/lib/handler/data/handler.js +0 -150
  74. package/lib/handler/data/handler.js.map +0 -1
  75. package/lib/handler/data/nested-write-vistor.d.ts +0 -31
  76. package/lib/handler/data/nested-write-vistor.js +0 -67
  77. package/lib/handler/data/nested-write-vistor.js.map +0 -1
  78. package/lib/handler/data/policy-utils.d.ts +0 -73
  79. package/lib/handler/data/policy-utils.js +0 -447
  80. package/lib/handler/data/policy-utils.js.map +0 -1
  81. package/lib/handler/index.d.ts +0 -1
  82. package/lib/handler/index.js +0 -9
  83. package/lib/handler/index.js.map +0 -1
  84. package/lib/handler/types.d.ts +0 -28
  85. package/lib/handler/types.js +0 -36
  86. package/lib/handler/types.js.map +0 -1
  87. package/lib/index.d.ts +0 -6
  88. package/lib/index.js.map +0 -1
  89. package/lib/policy.d.ts +0 -11
  90. package/lib/policy.js +0 -10
  91. package/lib/policy.js.map +0 -1
  92. package/lib/proxy/handler.d.ts +0 -37
  93. package/lib/proxy/handler.js +0 -333
  94. package/lib/proxy/handler.js.map +0 -1
  95. package/lib/proxy/logger.js.map +0 -1
  96. package/lib/proxy/nested-write-vistor.d.ts +0 -30
  97. package/lib/proxy/nested-write-vistor.js +0 -69
  98. package/lib/proxy/nested-write-vistor.js.map +0 -1
  99. package/lib/proxy/policy-utils.d.ts +0 -78
  100. package/lib/proxy/policy-utils.js +0 -508
  101. package/lib/proxy/policy-utils.js.map +0 -1
  102. package/lib/request-handler.d.ts +0 -21
  103. package/lib/request-handler.js +0 -37
  104. package/lib/request-handler.js.map +0 -1
  105. package/lib/request.d.ts +0 -37
  106. package/lib/request.js +0 -164
  107. package/lib/request.js.map +0 -1
  108. package/lib/serialization-utils.js.map +0 -1
  109. package/lib/service.d.ts +0 -32
  110. package/lib/service.js +0 -184
  111. package/lib/service.js.map +0 -1
  112. package/lib/types.d.ts +0 -185
  113. package/lib/types.js +0 -71
  114. package/lib/types.js.map +0 -1
  115. package/lib/validation.js.map +0 -1
  116. package/lib/version.js +0 -9
  117. package/lib/version.js.map +0 -1
  118. package/server/index.d.ts +0 -16
  119. package/server/index.js +0 -6
  120. package/types/index.d.ts +0 -1
  121. package/types/index.js +0 -3
  122. /package/{lib/serialization-utils.d.ts → serialization-utils.d.ts} +0 -0
  123. /package/{lib/serialization-utils.js → serialization-utils.js} +0 -0
  124. /package/{lib/version.d.ts → version.d.ts} +0 -0
@@ -0,0 +1,755 @@
1
+ "use strict";
2
+ /* eslint-disable @typescript-eslint/no-explicit-any */
3
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
4
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
5
+ return new (P || (P = Promise))(function (resolve, reject) {
6
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
7
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
8
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
9
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
10
+ });
11
+ };
12
+ var __importDefault = (this && this.__importDefault) || function (mod) {
13
+ return (mod && mod.__esModule) ? mod : { "default": mod };
14
+ };
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.PolicyUtil = void 0;
17
+ const runtime_1 = require("@prisma/client/runtime");
18
+ const sdk_1 = require("@zenstackhq/sdk");
19
+ const change_case_1 = require("change-case");
20
+ const cuid_1 = __importDefault(require("cuid"));
21
+ const deepcopy_1 = __importDefault(require("deepcopy"));
22
+ const zod_validation_error_1 = require("zod-validation-error");
23
+ const version_1 = require("../../version");
24
+ const model_meta_1 = require("../model-meta");
25
+ const nested_write_vistor_1 = require("../nested-write-vistor");
26
+ const utils_1 = require("../utils");
27
+ const logger_1 = require("./logger");
28
+ const pluralize_1 = __importDefault(require("pluralize"));
29
+ /**
30
+ * Access policy enforcement utilities
31
+ */
32
+ class PolicyUtil {
33
+ constructor(db, modelMeta, policy, user) {
34
+ this.db = db;
35
+ this.modelMeta = modelMeta;
36
+ this.policy = policy;
37
+ this.user = user;
38
+ this.logger = new logger_1.Logger(db);
39
+ }
40
+ /**
41
+ * Creates a conjunction of a list of query conditions.
42
+ */
43
+ and(...conditions) {
44
+ if (conditions.includes(false)) {
45
+ // always false
46
+ return { [sdk_1.GUARD_FIELD_NAME]: false };
47
+ }
48
+ const filtered = conditions.filter((c) => typeof c === 'object' && !!c && Object.keys(c).length > 0);
49
+ if (filtered.length === 0) {
50
+ return undefined;
51
+ }
52
+ else if (filtered.length === 1) {
53
+ return filtered[0];
54
+ }
55
+ else {
56
+ return { AND: filtered };
57
+ }
58
+ }
59
+ /**
60
+ * Creates a disjunction of a list of query conditions.
61
+ */
62
+ or(...conditions) {
63
+ if (conditions.includes(true)) {
64
+ // always true
65
+ return { [sdk_1.GUARD_FIELD_NAME]: true };
66
+ }
67
+ const filtered = conditions.filter((c) => typeof c === 'object' && !!c);
68
+ if (filtered.length === 0) {
69
+ return undefined;
70
+ }
71
+ else if (filtered.length === 1) {
72
+ return filtered[0];
73
+ }
74
+ else {
75
+ return { OR: filtered };
76
+ }
77
+ }
78
+ /**
79
+ * Creates a negation of a query condition.
80
+ */
81
+ not(condition) {
82
+ if (typeof condition === 'boolean') {
83
+ return !condition;
84
+ }
85
+ else {
86
+ return { NOT: condition };
87
+ }
88
+ }
89
+ /**
90
+ * Gets pregenerated authorization guard object for a given model and operation.
91
+ *
92
+ * @returns true if operation is unconditionally allowed, false if unconditionally denied,
93
+ * otherwise returns a guard object
94
+ */
95
+ getAuthGuard(model, operation, preValue) {
96
+ return __awaiter(this, void 0, void 0, function* () {
97
+ const guard = this.policy.guard[(0, change_case_1.camelCase)(model)];
98
+ if (!guard) {
99
+ throw this.unknownError(`unable to load policy guard for ${model}`);
100
+ }
101
+ const provider = guard[operation];
102
+ if (typeof provider === 'boolean') {
103
+ return provider;
104
+ }
105
+ if (!provider) {
106
+ throw this.unknownError(`zenstack: unable to load authorization guard for ${model}`);
107
+ }
108
+ return provider({ user: this.user, preValue });
109
+ });
110
+ }
111
+ getPreValueSelect(model) {
112
+ return __awaiter(this, void 0, void 0, function* () {
113
+ const guard = this.policy.guard[(0, change_case_1.camelCase)(model)];
114
+ if (!guard) {
115
+ throw this.unknownError(`unable to load policy guard for ${model}`);
116
+ }
117
+ return guard.preValueSelect;
118
+ });
119
+ }
120
+ getModelSchema(model) {
121
+ return __awaiter(this, void 0, void 0, function* () {
122
+ return this.policy.schema[(0, change_case_1.camelCase)(model)];
123
+ });
124
+ }
125
+ /**
126
+ * Injects model auth guard as where clause.
127
+ */
128
+ injectAuthGuard(args, model, operation) {
129
+ return __awaiter(this, void 0, void 0, function* () {
130
+ if (args.where) {
131
+ // inject into relation fields:
132
+ // to-many: some/none/every
133
+ // to-one: direct-conditions/is/isNot
134
+ yield this.injectGuardForFields(model, args.where, operation);
135
+ }
136
+ const guard = yield this.getAuthGuard(model, operation);
137
+ args.where = this.and(args.where, guard);
138
+ });
139
+ }
140
+ injectGuardForFields(model, payload, operation) {
141
+ return __awaiter(this, void 0, void 0, function* () {
142
+ for (const [field, subPayload] of Object.entries(payload)) {
143
+ if (!subPayload) {
144
+ continue;
145
+ }
146
+ const fieldInfo = yield (0, model_meta_1.resolveField)(this.modelMeta, model, field);
147
+ if (!fieldInfo || !fieldInfo.isDataModel) {
148
+ continue;
149
+ }
150
+ if (fieldInfo.isArray) {
151
+ yield this.injectGuardForToManyField(fieldInfo, subPayload, operation);
152
+ }
153
+ else {
154
+ yield this.injectGuardForToOneField(fieldInfo, subPayload, operation);
155
+ }
156
+ }
157
+ });
158
+ }
159
+ injectGuardForToManyField(fieldInfo, payload, operation) {
160
+ return __awaiter(this, void 0, void 0, function* () {
161
+ const guard = yield this.getAuthGuard(fieldInfo.type, operation);
162
+ if (payload.some) {
163
+ yield this.injectGuardForFields(fieldInfo.type, payload.some, operation);
164
+ // turn "some" into: { some: { AND: [guard, payload.some] } }
165
+ payload.some = this.and(payload.some, guard);
166
+ }
167
+ if (payload.none) {
168
+ yield this.injectGuardForFields(fieldInfo.type, payload.none, operation);
169
+ // turn none into: { none: { AND: [guard, payload.none] } }
170
+ payload.none = this.and(payload.none, guard);
171
+ }
172
+ if (payload.every &&
173
+ typeof payload.every === 'object' &&
174
+ // ignore empty every clause
175
+ Object.keys(payload.every).length > 0) {
176
+ yield this.injectGuardForFields(fieldInfo.type, payload.every, operation);
177
+ // turn "every" into: { none: { AND: [guard, { NOT: payload.every }] } }
178
+ if (!payload.none) {
179
+ payload.none = {};
180
+ }
181
+ payload.none = this.and(payload.none, guard, this.not(payload.every));
182
+ delete payload.every;
183
+ }
184
+ });
185
+ }
186
+ injectGuardForToOneField(fieldInfo, payload, operation) {
187
+ return __awaiter(this, void 0, void 0, function* () {
188
+ const guard = yield this.getAuthGuard(fieldInfo.type, operation);
189
+ if (payload.is || payload.isNot) {
190
+ if (payload.is) {
191
+ yield this.injectGuardForFields(fieldInfo.type, payload.is, operation);
192
+ // turn "is" into: { is: { AND: [ originalIs, guard ] }
193
+ payload.is = this.and(payload.is, guard);
194
+ }
195
+ if (payload.isNot) {
196
+ yield this.injectGuardForFields(fieldInfo.type, payload.isNot, operation);
197
+ // turn "isNot" into: { isNot: { AND: [ originalIsNot, { NOT: guard } ] } }
198
+ payload.isNot = this.and(payload.isNot, this.not(guard));
199
+ delete payload.isNot;
200
+ }
201
+ }
202
+ else {
203
+ yield this.injectGuardForFields(fieldInfo.type, payload, operation);
204
+ // turn direct conditions into: { is: { AND: [ originalConditions, guard ] } }
205
+ const combined = this.and((0, deepcopy_1.default)(payload), guard);
206
+ Object.keys(payload).forEach((key) => delete payload[key]);
207
+ payload.is = combined;
208
+ }
209
+ });
210
+ }
211
+ /**
212
+ * Read model entities w.r.t the given query args. The result list
213
+ * are guaranteed to fully satisfy 'read' policy rules recursively.
214
+ *
215
+ * For to-many relations involved, items not satisfying policy are
216
+ * silently trimmed. For to-one relation, if relation data fails policy
217
+ * an error is thrown.
218
+ */
219
+ readWithCheck(model, args) {
220
+ return __awaiter(this, void 0, void 0, function* () {
221
+ args = this.clone(args);
222
+ if (args.where) {
223
+ // query args will be used with findMany, so we need to
224
+ // translate unique constraint filters into a flat filter
225
+ // e.g.: { a_b: { a: '1', b: '1' } } => { a: '1', b: '1' }
226
+ yield this.flattenGeneratedUniqueField(model, args.where);
227
+ }
228
+ yield this.injectAuthGuard(args, model, 'read');
229
+ // recursively inject read guard conditions into the query args
230
+ yield this.injectNestedReadConditions(model, args);
231
+ this.logger.info(`Reading with validation for ${model}: ${(0, utils_1.formatObject)(args)}`);
232
+ const result = yield this.db[model].findMany(args);
233
+ yield Promise.all(result.map((item) => this.postProcessForRead(item, model, args, 'read')));
234
+ return result;
235
+ });
236
+ }
237
+ // flatten unique constraint filters
238
+ flattenGeneratedUniqueField(model, args) {
239
+ var _a;
240
+ return __awaiter(this, void 0, void 0, function* () {
241
+ // e.g.: { a_b: { a: '1', b: '1' } } => { a: '1', b: '1' }
242
+ const uniqueConstraints = (_a = this.modelMeta.uniqueConstraints) === null || _a === void 0 ? void 0 : _a[(0, change_case_1.camelCase)(model)];
243
+ let flattened = false;
244
+ if (uniqueConstraints) {
245
+ for (const [field, value] of Object.entries(args)) {
246
+ if (uniqueConstraints[field] && typeof value === 'object') {
247
+ for (const [f, v] of Object.entries(value)) {
248
+ args[f] = v;
249
+ }
250
+ delete args[field];
251
+ flattened = true;
252
+ }
253
+ }
254
+ }
255
+ if (flattened) {
256
+ this.logger.info(`Filter flattened: ${JSON.stringify(args)}`);
257
+ }
258
+ });
259
+ }
260
+ injectNestedReadConditions(model, args) {
261
+ var _a, _b;
262
+ return __awaiter(this, void 0, void 0, function* () {
263
+ const injectTarget = (_a = args.select) !== null && _a !== void 0 ? _a : args.include;
264
+ if (!injectTarget) {
265
+ return;
266
+ }
267
+ const idFields = this.getIdFields(model);
268
+ for (const field of (0, utils_1.getModelFields)(injectTarget)) {
269
+ const fieldInfo = (0, model_meta_1.resolveField)(this.modelMeta, model, field);
270
+ if (!fieldInfo || !fieldInfo.isDataModel) {
271
+ // only care about relation fields
272
+ continue;
273
+ }
274
+ if (fieldInfo.isArray) {
275
+ if (typeof injectTarget[field] !== 'object') {
276
+ injectTarget[field] = {};
277
+ }
278
+ // inject extra condition for to-many relation
279
+ yield this.injectAuthGuard(injectTarget[field], fieldInfo.type, 'read');
280
+ }
281
+ else {
282
+ // there's no way of injecting condition for to-one relation, so if there's
283
+ // "select" clause we make sure 'id' fields are selected and check them against
284
+ // query result; nothing needs to be done for "include" clause because all
285
+ // fields are already selected
286
+ if ((_b = injectTarget[field]) === null || _b === void 0 ? void 0 : _b.select) {
287
+ for (const idField of idFields) {
288
+ if (injectTarget[field].select[idField.name] !== true) {
289
+ injectTarget[field].select[idField.name] = true;
290
+ }
291
+ }
292
+ }
293
+ }
294
+ // recurse
295
+ yield this.injectNestedReadConditions(fieldInfo.type, injectTarget[field]);
296
+ }
297
+ });
298
+ }
299
+ /**
300
+ * Post processing checks for read model entities. Validates to-one relations
301
+ * (which can't be trimmed at query time) and removes fields that should be
302
+ * omitted.
303
+ */
304
+ postProcessForRead(entityData, model, args, operation) {
305
+ var _a;
306
+ return __awaiter(this, void 0, void 0, function* () {
307
+ const ids = this.getEntityIds(model, entityData);
308
+ if (Object.keys(ids).length === 0) {
309
+ return;
310
+ }
311
+ // strip auxiliary fields
312
+ for (const auxField of sdk_1.AUXILIARY_FIELDS) {
313
+ if (auxField in entityData) {
314
+ delete entityData[auxField];
315
+ }
316
+ }
317
+ const injectTarget = (_a = args.select) !== null && _a !== void 0 ? _a : args.include;
318
+ if (!injectTarget) {
319
+ return;
320
+ }
321
+ // to-one relation data cannot be trimmed by injected guards, we have to
322
+ // post-check them
323
+ for (const field of (0, utils_1.getModelFields)(injectTarget)) {
324
+ if (!(entityData === null || entityData === void 0 ? void 0 : entityData[field])) {
325
+ continue;
326
+ }
327
+ const fieldInfo = (0, model_meta_1.resolveField)(this.modelMeta, model, field);
328
+ if (!fieldInfo || !fieldInfo.isDataModel || fieldInfo.isArray) {
329
+ continue;
330
+ }
331
+ const ids = this.getEntityIds(fieldInfo.type, entityData[field]);
332
+ if (Object.keys(ids).length === 0) {
333
+ continue;
334
+ }
335
+ this.logger.info(`Validating read of to-one relation: ${fieldInfo.type}#${(0, utils_1.formatObject)(ids)}`);
336
+ yield this.checkPolicyForFilter(fieldInfo.type, ids, operation, this.db);
337
+ // recurse
338
+ yield this.postProcessForRead(entityData[field], fieldInfo.type, injectTarget[field], operation);
339
+ }
340
+ });
341
+ }
342
+ /**
343
+ * Process Prisma write actions.
344
+ */
345
+ processWrite(model, action, args, writeAction) {
346
+ return __awaiter(this, void 0, void 0, function* () {
347
+ // record model types for which new entities are created
348
+ // so we can post-check if they satisfy 'create' policies
349
+ const createdModels = new Set();
350
+ // record model entities that are updated, together with their
351
+ // values before update, so we can post-check if they satisfy
352
+ // model => { ids, entity value }
353
+ const updatedModels = new Map();
354
+ function addUpdatedEntity(model, ids, entity) {
355
+ let modelEntities = updatedModels.get(model);
356
+ if (!modelEntities) {
357
+ modelEntities = [];
358
+ updatedModels.set(model, modelEntities);
359
+ }
360
+ modelEntities.push({ ids, value: entity });
361
+ }
362
+ const idFields = this.getIdFields(model);
363
+ if (args.select) {
364
+ // make sure id fields are selected, we need it to
365
+ // read back the updated entity
366
+ for (const idField of idFields) {
367
+ if (!args.select[idField.name]) {
368
+ args.select[idField.name] = true;
369
+ }
370
+ }
371
+ }
372
+ // use a transaction to conduct write, so in case any create or nested create
373
+ // fails access policies, we can roll back the entire operation
374
+ const transactionId = (0, cuid_1.default)();
375
+ // args processor for create
376
+ const processCreate = (model, args) => __awaiter(this, void 0, void 0, function* () {
377
+ const guard = yield this.getAuthGuard(model, 'create');
378
+ const schema = yield this.getModelSchema(model);
379
+ if (guard === false) {
380
+ throw this.deniedByPolicy(model, 'create');
381
+ }
382
+ else if (guard !== true || schema) {
383
+ // mark the create with a transaction tag so we can check them later
384
+ args[sdk_1.TRANSACTION_FIELD_NAME] = `${transactionId}:create`;
385
+ createdModels.add(model);
386
+ }
387
+ });
388
+ // build a reversed query for fetching entities affected by nested updates
389
+ const buildReversedQuery = (context) => __awaiter(this, void 0, void 0, function* () {
390
+ let result, currQuery;
391
+ let currField;
392
+ for (let i = context.nestingPath.length - 1; i >= 0; i--) {
393
+ const { field, where, unique } = context.nestingPath[i];
394
+ if (!result) {
395
+ // first segment (bottom), just use its where clause
396
+ result = currQuery = Object.assign({}, where);
397
+ currField = field;
398
+ }
399
+ else {
400
+ if (!currField) {
401
+ throw this.unknownError(`missing field in nested path`);
402
+ }
403
+ if (!currField.backLink) {
404
+ throw this.unknownError(`field ${currField.type}.${currField.name} doesn't have a backLink`);
405
+ }
406
+ currQuery[currField.backLink] = Object.assign({}, where);
407
+ currQuery = currQuery[currField.backLink];
408
+ currField = field;
409
+ }
410
+ if (unique) {
411
+ // hit a unique filter, no need to traverse further up
412
+ break;
413
+ }
414
+ }
415
+ return result;
416
+ });
417
+ // args processor for update/upsert
418
+ const processUpdate = (model, where, context) => __awaiter(this, void 0, void 0, function* () {
419
+ const preGuard = yield this.getAuthGuard(model, 'update');
420
+ if (preGuard === false) {
421
+ throw this.deniedByPolicy(model, 'update');
422
+ }
423
+ else if (preGuard !== true) {
424
+ if (this.isToOneRelation(context.field)) {
425
+ // To-one relation field is complicated because there's no way to
426
+ // filter it during update (args doesn't carry a 'where' clause).
427
+ //
428
+ // We need to recursively walk up its hierarcy in the query args
429
+ // to construct a reversed query to identify the nested entity
430
+ // under update, and then check if it satisfies policy.
431
+ //
432
+ // E.g.:
433
+ // A - B - C
434
+ //
435
+ // update A with:
436
+ // {
437
+ // where: { id: 'aId' },
438
+ // data: {
439
+ // b: {
440
+ // c: { value: 1 }
441
+ // }
442
+ // }
443
+ // }
444
+ //
445
+ // To check if the update to 'c' field is permitted, we
446
+ // reverse the query stack into a filter for C model, like:
447
+ // {
448
+ // where: {
449
+ // b: { a: { id: 'aId' } }
450
+ // }
451
+ // }
452
+ // , and with this we can filter out the C entity that's going
453
+ // to be nestedly updated, and check if it's allowed.
454
+ //
455
+ // The same logic applies to nested delete.
456
+ const subQuery = yield buildReversedQuery(context);
457
+ yield this.checkPolicyForFilter(model, subQuery, 'update', this.db);
458
+ }
459
+ else {
460
+ if (!where) {
461
+ throw this.unknownError(`Missing 'where' parameter`);
462
+ }
463
+ yield this.checkPolicyForFilter(model, where, 'update', this.db);
464
+ }
465
+ }
466
+ yield preparePostUpdateCheck(model, context);
467
+ });
468
+ // args processor for updateMany
469
+ const processUpdateMany = (model, args, context) => __awaiter(this, void 0, void 0, function* () {
470
+ const guard = yield this.getAuthGuard(model, 'update');
471
+ if (guard === false) {
472
+ throw this.deniedByPolicy(model, 'update');
473
+ }
474
+ else if (guard !== true) {
475
+ // inject policy filter
476
+ yield this.injectAuthGuard(args, model, 'update');
477
+ }
478
+ yield preparePostUpdateCheck(model, context);
479
+ });
480
+ // for models with post-update rules, we need to read and store
481
+ // entity values before the update for post-update check
482
+ const preparePostUpdateCheck = (model, context) => __awaiter(this, void 0, void 0, function* () {
483
+ const postGuard = yield this.getAuthGuard(model, 'postUpdate');
484
+ const schema = yield this.getModelSchema(model);
485
+ // post-update check is needed if there's post-update rule or validation schema
486
+ if (postGuard !== true || schema) {
487
+ // fetch preValue selection (analyzed from the post-update rules)
488
+ const preValueSelect = yield this.getPreValueSelect(model);
489
+ const filter = yield buildReversedQuery(context);
490
+ // query args will be used with findMany, so we need to
491
+ // translate unique constraint filters into a flat filter
492
+ // e.g.: { a_b: { a: '1', b: '1' } } => { a: '1', b: '1' }
493
+ yield this.flattenGeneratedUniqueField(model, filter);
494
+ const idFields = this.getIdFields(model);
495
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
496
+ const select = Object.assign({}, preValueSelect);
497
+ for (const idField of idFields) {
498
+ select[idField.name] = true;
499
+ }
500
+ const query = { where: filter, select };
501
+ this.logger.info(`fetching pre-update entities for ${model}: ${(0, utils_1.formatObject)(query)})}`);
502
+ const entities = yield this.db[model].findMany(query);
503
+ entities.forEach((entity) => {
504
+ addUpdatedEntity(model, this.getEntityIds(model, entity), entity);
505
+ });
506
+ }
507
+ });
508
+ // args processor for delete
509
+ const processDelete = (model, args, context) => __awaiter(this, void 0, void 0, function* () {
510
+ const guard = yield this.getAuthGuard(model, 'delete');
511
+ if (guard === false) {
512
+ throw this.deniedByPolicy(model, 'delete');
513
+ }
514
+ else if (guard !== true) {
515
+ if (this.isToOneRelation(context.field)) {
516
+ // see comments in processUpdate
517
+ const subQuery = yield buildReversedQuery(context);
518
+ yield this.checkPolicyForFilter(model, subQuery, 'delete', this.db);
519
+ }
520
+ else {
521
+ yield this.checkPolicyForFilter(model, args, 'delete', this.db);
522
+ }
523
+ }
524
+ });
525
+ // process relation updates: connect, connectOrCreate, and disconnect
526
+ const processRelationUpdate = (model, args, context) => __awaiter(this, void 0, void 0, function* () {
527
+ var _a;
528
+ if ((_a = context.field) === null || _a === void 0 ? void 0 : _a.backLink) {
529
+ // fetch the backlink field of the model being connected
530
+ const backLinkField = (0, model_meta_1.resolveField)(this.modelMeta, model, context.field.backLink);
531
+ if (backLinkField.isRelationOwner) {
532
+ // the target side of relation owns the relation,
533
+ // mark it as updated
534
+ yield processUpdate(model, args, context);
535
+ }
536
+ }
537
+ });
538
+ // use a visitor to process args before conducting the write action
539
+ const visitor = new nested_write_vistor_1.NestedWriteVisitor(this.modelMeta, {
540
+ create: (model, args) => __awaiter(this, void 0, void 0, function* () {
541
+ for (const oneArgs of (0, utils_1.enumerate)(args)) {
542
+ yield processCreate(model, oneArgs);
543
+ }
544
+ }),
545
+ connectOrCreate: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
546
+ for (const oneArgs of (0, utils_1.enumerate)(args)) {
547
+ if (oneArgs.create) {
548
+ yield processCreate(model, oneArgs.create);
549
+ }
550
+ if (oneArgs.where) {
551
+ yield processRelationUpdate(model, oneArgs.where, context);
552
+ }
553
+ }
554
+ }),
555
+ connect: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
556
+ for (const oneArgs of (0, utils_1.enumerate)(args)) {
557
+ yield processRelationUpdate(model, oneArgs, context);
558
+ }
559
+ }),
560
+ disconnect: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
561
+ for (const oneArgs of (0, utils_1.enumerate)(args)) {
562
+ yield processRelationUpdate(model, oneArgs, context);
563
+ }
564
+ }),
565
+ update: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
566
+ for (const oneArgs of (0, utils_1.enumerate)(args)) {
567
+ yield processUpdate(model, oneArgs.where, context);
568
+ }
569
+ }),
570
+ updateMany: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
571
+ for (const oneArgs of (0, utils_1.enumerate)(args)) {
572
+ yield processUpdateMany(model, oneArgs, context);
573
+ }
574
+ }),
575
+ upsert: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
576
+ for (const oneArgs of (0, utils_1.enumerate)(args)) {
577
+ if (oneArgs.create) {
578
+ yield processCreate(model, oneArgs.create);
579
+ }
580
+ if (oneArgs.update) {
581
+ yield processUpdate(model, oneArgs.where, context);
582
+ }
583
+ }
584
+ }),
585
+ delete: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
586
+ for (const oneArgs of (0, utils_1.enumerate)(args)) {
587
+ yield processDelete(model, oneArgs, context);
588
+ }
589
+ }),
590
+ deleteMany: (model, args, context) => __awaiter(this, void 0, void 0, function* () {
591
+ const guard = yield this.getAuthGuard(model, 'delete');
592
+ if (guard === false) {
593
+ throw this.deniedByPolicy(model, 'delete');
594
+ }
595
+ else if (guard !== true) {
596
+ if (Array.isArray(args)) {
597
+ context.parent.deleteMany = args.map((oneArgs) => this.and(oneArgs, guard));
598
+ }
599
+ else {
600
+ context.parent.deleteMany = this.and(args, guard);
601
+ }
602
+ }
603
+ }),
604
+ });
605
+ yield visitor.visit(model, action, args);
606
+ if (createdModels.size === 0 && updatedModels.size === 0) {
607
+ // no post-check needed, we can proceed with the write without transaction
608
+ return yield writeAction(this.db[model], args);
609
+ }
610
+ else {
611
+ return yield this.transaction(this.db, (tx) => __awaiter(this, void 0, void 0, function* () {
612
+ // proceed with the update (with args processed)
613
+ const result = yield writeAction(tx[model], args);
614
+ if (createdModels.size > 0) {
615
+ // do post-check on created entities
616
+ yield Promise.all([...createdModels].map((model) => this.checkPolicyForFilter(model, { [sdk_1.TRANSACTION_FIELD_NAME]: `${transactionId}:create` }, 'create', tx)));
617
+ }
618
+ if (updatedModels.size > 0) {
619
+ // do post-check on updated entities
620
+ yield Promise.all([...updatedModels.entries()]
621
+ .map(([model, modelEntities]) => modelEntities.map(({ ids, value: preValue }) => __awaiter(this, void 0, void 0, function* () { return this.checkPostUpdate(model, ids, tx, preValue); })))
622
+ .flat());
623
+ }
624
+ return result;
625
+ }));
626
+ }
627
+ });
628
+ }
629
+ transaction(db, action) {
630
+ if (db.__zenstack_tx) {
631
+ // already in transaction, don't nest
632
+ return action(db);
633
+ }
634
+ else {
635
+ return db.$transaction((tx) => action(tx));
636
+ }
637
+ }
638
+ deniedByPolicy(model, operation, extra, reason) {
639
+ return new runtime_1.PrismaClientKnownRequestError(`denied by policy: ${model} entities failed '${operation}' check${extra ? ', ' + extra : ''}`, { clientVersion: (0, version_1.getVersion)(), code: 'P2004', meta: { reason } });
640
+ }
641
+ notFound(model) {
642
+ return new runtime_1.PrismaClientKnownRequestError(`entity not found for model ${model}`, {
643
+ clientVersion: (0, version_1.getVersion)(),
644
+ code: 'P2025',
645
+ });
646
+ }
647
+ unknownError(message) {
648
+ return new runtime_1.PrismaClientUnknownRequestError(message, {
649
+ clientVersion: (0, version_1.getVersion)(),
650
+ });
651
+ }
652
+ /**
653
+ * Given a filter, check if applying access policy filtering will result
654
+ * in data being trimmed, and if so, throw an error.
655
+ */
656
+ checkPolicyForFilter(model, filter, operation, db) {
657
+ return __awaiter(this, void 0, void 0, function* () {
658
+ this.logger.info(`Checking policy for ${model}#${JSON.stringify(filter)} for ${operation}`);
659
+ const queryFilter = (0, deepcopy_1.default)(filter);
660
+ // query args will be used with findMany, so we need to
661
+ // translate unique constraint filters into a flat filter
662
+ // e.g.: { a_b: { a: '1', b: '1' } } => { a: '1', b: '1' }
663
+ yield this.flattenGeneratedUniqueField(model, queryFilter);
664
+ const count = (yield db[model].count({ where: queryFilter }));
665
+ const guard = yield this.getAuthGuard(model, operation);
666
+ // build a query condition with policy injected
667
+ const guardedQuery = { where: this.and(queryFilter, guard) };
668
+ const schema = (operation === 'create' || operation === 'update') && (yield this.getModelSchema(model));
669
+ if (schema) {
670
+ // we've got schemas, so have to fetch entities and validate them
671
+ const entities = yield db[model].findMany(guardedQuery);
672
+ if (entities.length < count) {
673
+ this.logger.info(`entity ${model} failed policy check for operation ${operation}`);
674
+ throw this.deniedByPolicy(model, operation, `${count - entities.length} ${(0, pluralize_1.default)('entity', count - entities.length)} failed policy check`);
675
+ }
676
+ // TODO: push down schema check to the database
677
+ const schemaCheckErrors = entities.map((entity) => schema.safeParse(entity)).filter((r) => !r.success);
678
+ if (schemaCheckErrors.length > 0) {
679
+ const error = schemaCheckErrors.map((r) => !r.success && (0, zod_validation_error_1.fromZodError)(r.error).message).join(', ');
680
+ this.logger.info(`entity ${model} failed schema check for operation ${operation}: ${error}`);
681
+ throw this.deniedByPolicy(model, operation, `entities failed schema check: [${error}]`);
682
+ }
683
+ }
684
+ else {
685
+ // count entities with policy injected and see if any of them are filtered out
686
+ const guardedCount = (yield db[model].count(guardedQuery));
687
+ if (guardedCount < count) {
688
+ this.logger.info(`entity ${model} failed policy check for operation ${operation}`);
689
+ throw this.deniedByPolicy(model, operation, `${count - guardedCount} ${(0, pluralize_1.default)('entity', count - guardedCount)} failed policy check`);
690
+ }
691
+ }
692
+ });
693
+ }
694
+ checkPostUpdate(model, ids, db, preValue) {
695
+ return __awaiter(this, void 0, void 0, function* () {
696
+ this.logger.info(`Checking post-update policy for ${model}#${ids}, preValue: ${(0, utils_1.formatObject)(preValue)}`);
697
+ const guard = yield this.getAuthGuard(model, 'postUpdate', preValue);
698
+ // build a query condition with policy injected
699
+ const guardedQuery = { where: this.and(ids, guard) };
700
+ // query with policy injected
701
+ const entity = yield db[model].findFirst(guardedQuery);
702
+ // see if we get fewer items with policy, if so, reject with an throw
703
+ if (!entity) {
704
+ this.logger.info(`entity ${model} failed policy check for operation postUpdate`);
705
+ throw this.deniedByPolicy(model, 'postUpdate');
706
+ }
707
+ // TODO: push down schema check to the database
708
+ const schema = yield this.getModelSchema(model);
709
+ if (schema) {
710
+ const schemaCheckResult = schema.safeParse(entity);
711
+ if (!schemaCheckResult.success) {
712
+ const error = (0, zod_validation_error_1.fromZodError)(schemaCheckResult.error).message;
713
+ this.logger.info(`entity ${model} failed schema check for operation postUpdate: ${error}`);
714
+ throw this.deniedByPolicy(model, 'postUpdate', `entity failed schema check: ${error}`);
715
+ }
716
+ }
717
+ });
718
+ }
719
+ isToOneRelation(field) {
720
+ return !!field && field.isDataModel && !field.isArray;
721
+ }
722
+ /**
723
+ * Clones an object and makes sure it's not empty.
724
+ */
725
+ clone(value) {
726
+ return value ? (0, deepcopy_1.default)(value) : {};
727
+ }
728
+ /**
729
+ * Gets "id" field for a given model.
730
+ */
731
+ getIdFields(model) {
732
+ const fields = this.modelMeta.fields[(0, change_case_1.camelCase)(model)];
733
+ if (!fields) {
734
+ throw this.unknownError(`Unable to load fields for ${model}`);
735
+ }
736
+ const result = Object.values(fields).filter((f) => f.isId);
737
+ if (result.length === 0) {
738
+ throw this.unknownError(`model ${model} does not have an id field`);
739
+ }
740
+ return result;
741
+ }
742
+ /**
743
+ * Gets id field value from an entity.
744
+ */
745
+ getEntityIds(model, entityData) {
746
+ const idFields = this.getIdFields(model);
747
+ const result = {};
748
+ for (const idField of idFields) {
749
+ result[idField.name] = entityData[idField.name];
750
+ }
751
+ return result;
752
+ }
753
+ }
754
+ exports.PolicyUtil = PolicyUtil;
755
+ //# sourceMappingURL=policy-utils.js.map