zenstack 2.1.2 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/language-server/validator/datamodel-validator.d.ts +1 -0
- package/language-server/validator/datamodel-validator.js +12 -1
- package/language-server/validator/datamodel-validator.js.map +1 -1
- package/language-server/validator/expression-validator.js +10 -9
- package/language-server/validator/expression-validator.js.map +1 -1
- package/package.json +8 -8
- package/plugins/enhancer/policy/index.js +1 -1
- package/plugins/enhancer/policy/index.js.map +1 -1
- package/plugins/enhancer/policy/policy-guard-generator.d.ts +26 -17
- package/plugins/enhancer/policy/policy-guard-generator.js +374 -599
- package/plugins/enhancer/policy/policy-guard-generator.js.map +1 -1
- package/plugins/enhancer/policy/utils.d.ts +25 -0
- package/plugins/enhancer/policy/utils.js +433 -0
- package/plugins/enhancer/policy/utils.js.map +1 -0
- package/plugins/prisma/schema-generator.d.ts +5 -0
- package/plugins/prisma/schema-generator.js +145 -30
- package/plugins/prisma/schema-generator.js.map +1 -1
- package/plugins/zod/utils/schema-gen.js +8 -1
- package/plugins/zod/utils/schema-gen.js.map +1 -1
- package/utils/ast-utils.d.ts +4 -0
- package/utils/ast-utils.js +17 -1
- package/utils/ast-utils.js.map +1 -1
|
@@ -14,60 +14,27 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
15
|
exports.PolicyGenerator = void 0;
|
|
16
16
|
const ast_1 = require("@zenstackhq/language/ast");
|
|
17
|
-
const runtime_1 = require("@zenstackhq/runtime");
|
|
18
17
|
const sdk_1 = require("@zenstackhq/sdk");
|
|
19
18
|
const prisma_1 = require("@zenstackhq/sdk/prisma");
|
|
20
19
|
const langium_1 = require("langium");
|
|
21
20
|
const lower_case_first_1 = require("lower-case-first");
|
|
22
21
|
const path_1 = __importDefault(require("path"));
|
|
23
22
|
const ts_morph_1 = require("ts-morph");
|
|
24
|
-
const __1 = require("..");
|
|
25
|
-
const ast_utils_1 = require("../../../utils/ast-utils");
|
|
26
|
-
const plugin_utils_1 = require("../../plugin-utils");
|
|
27
23
|
const constraint_transformer_1 = require("./constraint-transformer");
|
|
28
|
-
const
|
|
24
|
+
const utils_1 = require("./utils");
|
|
29
25
|
/**
|
|
30
26
|
* Generates source file that contains Prisma query guard objects used for injecting database queries
|
|
31
27
|
*/
|
|
32
28
|
class PolicyGenerator {
|
|
33
|
-
|
|
29
|
+
constructor(options) {
|
|
30
|
+
this.options = options;
|
|
31
|
+
}
|
|
32
|
+
generate(project, model, output) {
|
|
34
33
|
return __awaiter(this, void 0, void 0, function* () {
|
|
35
34
|
const sf = project.createSourceFile(path_1.default.join(output, 'policy.ts'), undefined, { overwrite: true });
|
|
36
35
|
sf.addStatements('/* eslint-disable */');
|
|
37
|
-
|
|
38
|
-
namedImports: [
|
|
39
|
-
{ name: 'type QueryContext' },
|
|
40
|
-
{ name: 'type CrudContract' },
|
|
41
|
-
{ name: 'allFieldsEqual' },
|
|
42
|
-
{ name: 'type PolicyDef' },
|
|
43
|
-
{ name: 'type CheckerContext' },
|
|
44
|
-
{ name: 'type CheckerConstraint' },
|
|
45
|
-
],
|
|
46
|
-
moduleSpecifier: `${sdk_1.RUNTIME_PACKAGE}`,
|
|
47
|
-
});
|
|
48
|
-
// import enums
|
|
49
|
-
const prismaImport = (0, prisma_1.getPrismaClientImportSpec)(output, options);
|
|
50
|
-
for (const e of model.declarations.filter((d) => (0, ast_1.isEnum)(d) && this.isEnumReferenced(model, d))) {
|
|
51
|
-
sf.addImportDeclaration({
|
|
52
|
-
namedImports: [{ name: e.name }],
|
|
53
|
-
moduleSpecifier: prismaImport,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
36
|
+
this.writeImports(model, output, sf);
|
|
56
37
|
const models = (0, sdk_1.getDataModels)(model);
|
|
57
|
-
// policy guard functions
|
|
58
|
-
const policyMap = {};
|
|
59
|
-
for (const model of models) {
|
|
60
|
-
policyMap[model.name] = yield this.generateQueryGuardForModel(model, sf);
|
|
61
|
-
}
|
|
62
|
-
const generatePermissionChecker = options.generatePermissionChecker === true;
|
|
63
|
-
// CRUD checker functions
|
|
64
|
-
const checkerMap = {};
|
|
65
|
-
if (generatePermissionChecker) {
|
|
66
|
-
for (const model of models) {
|
|
67
|
-
checkerMap[model.name] = yield this.generateCheckerForModel(model, sf);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
const authSelector = this.generateAuthSelector(models);
|
|
71
38
|
sf.addVariableStatement({
|
|
72
39
|
declarationKind: ts_morph_1.VariableDeclarationKind.Const,
|
|
73
40
|
declarations: [
|
|
@@ -76,53 +43,9 @@ class PolicyGenerator {
|
|
|
76
43
|
type: 'PolicyDef',
|
|
77
44
|
initializer: (writer) => {
|
|
78
45
|
writer.block(() => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
writer.write(`${(0, lower_case_first_1.lowerCaseFirst)(model)}:`);
|
|
83
|
-
writer.inlineBlock(() => {
|
|
84
|
-
for (const [op, func] of Object.entries(map)) {
|
|
85
|
-
if (typeof func === 'object') {
|
|
86
|
-
writer.write(`${op}: ${JSON.stringify(func)},`);
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
writer.write(`${op}: ${func},`);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
writer.write(',');
|
|
94
|
-
}
|
|
95
|
-
});
|
|
96
|
-
writer.writeLine(',');
|
|
97
|
-
if (generatePermissionChecker) {
|
|
98
|
-
writer.write('checker:');
|
|
99
|
-
writer.inlineBlock(() => {
|
|
100
|
-
for (const [model, map] of Object.entries(checkerMap)) {
|
|
101
|
-
writer.write(`${(0, lower_case_first_1.lowerCaseFirst)(model)}:`);
|
|
102
|
-
writer.inlineBlock(() => {
|
|
103
|
-
Object.entries(map).forEach(([op, func]) => {
|
|
104
|
-
writer.write(`${op}: ${func},`);
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
writer.writeLine(',');
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
writer.writeLine(',');
|
|
111
|
-
}
|
|
112
|
-
writer.write('validation:');
|
|
113
|
-
writer.inlineBlock(() => {
|
|
114
|
-
for (const model of models) {
|
|
115
|
-
writer.write(`${(0, lower_case_first_1.lowerCaseFirst)(model.name)}:`);
|
|
116
|
-
writer.inlineBlock(() => {
|
|
117
|
-
writer.write(`hasValidation: ${(0, sdk_1.hasValidationAttributes)(model)}`);
|
|
118
|
-
});
|
|
119
|
-
writer.writeLine(',');
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
if (authSelector) {
|
|
123
|
-
writer.writeLine(',');
|
|
124
|
-
writer.write(`authSelector: ${JSON.stringify(authSelector)}`);
|
|
125
|
-
}
|
|
46
|
+
this.writePolicy(writer, models, sf);
|
|
47
|
+
this.writeValidationMeta(writer, models);
|
|
48
|
+
this.writeAuthSelector(models, writer);
|
|
126
49
|
});
|
|
127
50
|
},
|
|
128
51
|
},
|
|
@@ -130,200 +53,138 @@ class PolicyGenerator {
|
|
|
130
53
|
});
|
|
131
54
|
sf.addStatements('export default policy');
|
|
132
55
|
// save ts files if requested explicitly or the user provided
|
|
133
|
-
const preserveTsFiles = options.preserveTsFiles === true || !!options.output;
|
|
56
|
+
const preserveTsFiles = this.options.preserveTsFiles === true || !!this.options.output;
|
|
134
57
|
if (preserveTsFiles) {
|
|
135
58
|
yield sf.save();
|
|
136
59
|
}
|
|
137
60
|
});
|
|
138
61
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const allExpressions = [...modelPolicyAttrs, ...fieldPolicyAttrs]
|
|
151
|
-
.filter((attr) => attr.args.length > 1)
|
|
152
|
-
.map((attr) => attr.args[1].value);
|
|
153
|
-
// collect `auth()` member access
|
|
154
|
-
allExpressions.forEach((rule) => {
|
|
155
|
-
(0, langium_1.streamAst)(rule).forEach((node) => {
|
|
156
|
-
if ((0, ast_1.isMemberAccessExpr)(node) && (0, sdk_1.isAuthInvocation)(node.operand)) {
|
|
157
|
-
authRules.push(node);
|
|
158
|
-
}
|
|
159
|
-
});
|
|
160
|
-
});
|
|
62
|
+
writeImports(model, output, sf) {
|
|
63
|
+
sf.addImportDeclaration({
|
|
64
|
+
namedImports: [
|
|
65
|
+
{ name: 'type QueryContext' },
|
|
66
|
+
{ name: 'type CrudContract' },
|
|
67
|
+
{ name: 'allFieldsEqual' },
|
|
68
|
+
{ name: 'type PolicyDef' },
|
|
69
|
+
{ name: 'type PermissionCheckerContext' },
|
|
70
|
+
{ name: 'type PermissionCheckerConstraint' },
|
|
71
|
+
],
|
|
72
|
+
moduleSpecifier: `${sdk_1.RUNTIME_PACKAGE}`,
|
|
161
73
|
});
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
74
|
+
// import enums
|
|
75
|
+
const prismaImport = (0, prisma_1.getPrismaClientImportSpec)(output, this.options);
|
|
76
|
+
for (const e of model.declarations.filter((d) => (0, ast_1.isEnum)(d) && (0, utils_1.isEnumReferenced)(model, d))) {
|
|
77
|
+
sf.addImportDeclaration({
|
|
78
|
+
namedImports: [{ name: e.name }],
|
|
79
|
+
moduleSpecifier: prismaImport,
|
|
80
|
+
});
|
|
167
81
|
}
|
|
168
82
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
83
|
+
writePolicy(writer, models, sourceFile) {
|
|
84
|
+
writer.write('policy:');
|
|
85
|
+
writer.inlineBlock(() => {
|
|
86
|
+
for (const model of models) {
|
|
87
|
+
writer.write(`${(0, lower_case_first_1.lowerCaseFirst)(model.name)}:`);
|
|
88
|
+
writer.block(() => {
|
|
89
|
+
// model-level guards
|
|
90
|
+
this.writeModelLevelDefs(model, writer, sourceFile);
|
|
91
|
+
// field-level guards
|
|
92
|
+
this.writeFieldLevelDefs(model, writer, sourceFile);
|
|
93
|
+
});
|
|
94
|
+
writer.writeLine(',');
|
|
179
95
|
}
|
|
180
|
-
return false;
|
|
181
96
|
});
|
|
97
|
+
writer.writeLine(',');
|
|
182
98
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
else {
|
|
196
|
-
return true;
|
|
197
|
-
}
|
|
99
|
+
// #region Model-level definitions
|
|
100
|
+
// writes model-level policy def for each operation kind for a model
|
|
101
|
+
// `[modelName]: { [operationKind]: [funcName] },`
|
|
102
|
+
writeModelLevelDefs(model, writer, sourceFile) {
|
|
103
|
+
const policies = (0, sdk_1.analyzePolicies)(model);
|
|
104
|
+
writer.write('modelLevel:');
|
|
105
|
+
writer.inlineBlock(() => {
|
|
106
|
+
this.writeModelReadDef(model, policies, writer, sourceFile);
|
|
107
|
+
this.writeModelCreateDef(model, policies, writer, sourceFile);
|
|
108
|
+
this.writeModelUpdateDef(model, policies, writer, sourceFile);
|
|
109
|
+
this.writeModelPostUpdateDef(model, policies, writer, sourceFile);
|
|
110
|
+
this.writeModelDeleteDef(model, policies, writer, sourceFile);
|
|
198
111
|
});
|
|
199
|
-
|
|
200
|
-
let result = attrs
|
|
201
|
-
.filter((attr) => {
|
|
202
|
-
const opsValue = (0, sdk_1.getLiteral)(attr.args[0].value);
|
|
203
|
-
if (!opsValue) {
|
|
204
|
-
return false;
|
|
205
|
-
}
|
|
206
|
-
const ops = opsValue.split(',').map((s) => s.trim());
|
|
207
|
-
return ops.includes(checkOperation) || ops.includes('all');
|
|
208
|
-
})
|
|
209
|
-
.map((attr) => attr.args[1].value);
|
|
210
|
-
if (operation === 'update') {
|
|
211
|
-
result = this.processUpdatePolicies(result, false);
|
|
212
|
-
}
|
|
213
|
-
else if (operation === 'postUpdate') {
|
|
214
|
-
result = this.processUpdatePolicies(result, true);
|
|
215
|
-
}
|
|
216
|
-
return result;
|
|
112
|
+
writer.writeLine(',');
|
|
217
113
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
else {
|
|
226
|
-
// when compiling pre-update rules, if any rule contains `future()` reference,
|
|
227
|
-
// we completely skip pre-update check and defer them to post-update
|
|
228
|
-
return hasFutureReference ? [] : expressions;
|
|
229
|
-
}
|
|
114
|
+
// writes `read: ...` for a given model
|
|
115
|
+
writeModelReadDef(model, policies, writer, sourceFile) {
|
|
116
|
+
writer.write(`read:`);
|
|
117
|
+
writer.inlineBlock(() => {
|
|
118
|
+
this.writeCommonModelDef(model, 'read', policies, writer, sourceFile);
|
|
119
|
+
});
|
|
120
|
+
writer.writeLine(',');
|
|
230
121
|
}
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
122
|
+
// writes `create: ...` for a given model
|
|
123
|
+
writeModelCreateDef(model, policies, writer, sourceFile) {
|
|
124
|
+
writer.write(`create:`);
|
|
125
|
+
writer.inlineBlock(() => {
|
|
126
|
+
this.writeCommonModelDef(model, 'create', policies, writer, sourceFile);
|
|
127
|
+
// create policy has an additional input checker for validating the payload
|
|
128
|
+
this.writeCreateInputChecker(model, writer, sourceFile);
|
|
129
|
+
});
|
|
130
|
+
writer.writeLine(',');
|
|
131
|
+
}
|
|
132
|
+
// writes `inputChecker: [funcName]` for a given model
|
|
133
|
+
writeCreateInputChecker(model, writer, sourceFile) {
|
|
134
|
+
if (this.canCheckCreateBasedOnInput(model)) {
|
|
135
|
+
const inputCheckFunc = this.generateCreateInputCheckerFunction(model, sourceFile);
|
|
136
|
+
writer.write(`inputChecker: ${inputCheckFunc.getName()},`);
|
|
237
137
|
}
|
|
238
|
-
return false;
|
|
239
138
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (
|
|
247
|
-
|
|
248
|
-
if (kind === 'create') {
|
|
249
|
-
result[kind + '_input'] = policies[kind];
|
|
250
|
-
}
|
|
251
|
-
continue;
|
|
139
|
+
canCheckCreateBasedOnInput(model) {
|
|
140
|
+
const allows = (0, utils_1.getPolicyExpressions)(model, 'allow', 'create', false, 'all');
|
|
141
|
+
const denies = (0, utils_1.getPolicyExpressions)(model, 'deny', 'create', false, 'all');
|
|
142
|
+
return [...allows, ...denies].every((rule) => {
|
|
143
|
+
return (0, langium_1.streamAst)(rule).every((expr) => {
|
|
144
|
+
var _a;
|
|
145
|
+
if ((0, ast_1.isThisExpr)(expr)) {
|
|
146
|
+
return false;
|
|
252
147
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if (this.getPolicyExpressions(model, 'allow', 'postUpdate').length === 0) {
|
|
259
|
-
result[kind] = false;
|
|
260
|
-
continue;
|
|
148
|
+
if ((0, ast_1.isReferenceExpr)(expr)) {
|
|
149
|
+
if ((0, ast_1.isDataModel)((_a = expr.$resolvedType) === null || _a === void 0 ? void 0 : _a.decl)) {
|
|
150
|
+
// if policy rules uses relation fields,
|
|
151
|
+
// we can't check based on create input
|
|
152
|
+
return false;
|
|
261
153
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
154
|
+
if ((0, ast_1.isDataModelField)(expr.target.ref) &&
|
|
155
|
+
expr.target.ref.$container === model &&
|
|
156
|
+
(0, sdk_1.hasAttribute)(expr.target.ref, '@default')) {
|
|
157
|
+
// reference to field of current model
|
|
158
|
+
// if it has default value, we can't check
|
|
159
|
+
// based on create input
|
|
160
|
+
return false;
|
|
265
161
|
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
continue;
|
|
271
|
-
}
|
|
272
|
-
const guardFunc = this.generateQueryGuardFunction(sourceFile, model, kind, allows, denies);
|
|
273
|
-
result[kind] = guardFunc.getName();
|
|
274
|
-
if (kind === 'postUpdate') {
|
|
275
|
-
const preValueSelect = this.generateSelectForRules([...allows, ...denies]);
|
|
276
|
-
if (preValueSelect) {
|
|
277
|
-
result[runtime_1.PRE_UPDATE_VALUE_SELECTOR] = preValueSelect;
|
|
162
|
+
if ((0, ast_1.isDataModelField)(expr.target.ref) && (0, sdk_1.isForeignKeyField)(expr.target.ref)) {
|
|
163
|
+
// reference to foreign key field
|
|
164
|
+
// we can't check based on create input
|
|
165
|
+
return false;
|
|
278
166
|
}
|
|
279
167
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
result[kind + '_input'] = inputCheckFunc.getName();
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
// generate field read checkers
|
|
286
|
-
this.generateReadFieldsCheckers(model, sourceFile, result);
|
|
287
|
-
// generate field read override guards
|
|
288
|
-
this.generateReadFieldsOverrideGuards(model, sourceFile, result);
|
|
289
|
-
// generate field update guards
|
|
290
|
-
this.generateUpdateFieldsGuards(model, sourceFile, result);
|
|
291
|
-
return result;
|
|
168
|
+
return true;
|
|
169
|
+
});
|
|
292
170
|
});
|
|
293
171
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const allFieldsDenies = [];
|
|
297
|
-
for (const field of model.fields) {
|
|
298
|
-
const allows = this.getPolicyExpressions(field, 'allow', 'read');
|
|
299
|
-
const denies = this.getPolicyExpressions(field, 'deny', 'read');
|
|
300
|
-
if (denies.length === 0 && allows.length === 0) {
|
|
301
|
-
continue;
|
|
302
|
-
}
|
|
303
|
-
allFieldsAllows.push(...allows);
|
|
304
|
-
allFieldsDenies.push(...denies);
|
|
305
|
-
const guardFunc = this.generateReadFieldCheckerFunction(sourceFile, field, allows, denies);
|
|
306
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
307
|
-
result[`${runtime_1.FIELD_LEVEL_READ_CHECKER_PREFIX}${field.name}`] = guardFunc.getName();
|
|
308
|
-
}
|
|
309
|
-
if (allFieldsAllows.length > 0 || allFieldsDenies.length > 0) {
|
|
310
|
-
result[runtime_1.HAS_FIELD_LEVEL_POLICY_FLAG] = true;
|
|
311
|
-
const readFieldCheckSelect = this.generateSelectForRules([...allFieldsAllows, ...allFieldsDenies]);
|
|
312
|
-
if (readFieldCheckSelect) {
|
|
313
|
-
result[runtime_1.FIELD_LEVEL_READ_CHECKER_SELECTOR] = readFieldCheckSelect;
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
generateReadFieldCheckerFunction(sourceFile, field, allows, denies) {
|
|
172
|
+
// generates a function for checking "create" input
|
|
173
|
+
generateCreateInputCheckerFunction(model, sourceFile) {
|
|
318
174
|
const statements = [];
|
|
319
|
-
|
|
320
|
-
|
|
175
|
+
const allows = (0, utils_1.getPolicyExpressions)(model, 'allow', 'create');
|
|
176
|
+
const denies = (0, utils_1.getPolicyExpressions)(model, 'deny', 'create');
|
|
177
|
+
(0, utils_1.generateNormalizedAuthRef)(model, allows, denies, statements);
|
|
321
178
|
statements.push((writer) => {
|
|
179
|
+
if (allows.length === 0) {
|
|
180
|
+
writer.write('return false;');
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
322
183
|
const transformer = new sdk_1.TypeScriptExpressionTransformer({
|
|
323
184
|
context: sdk_1.ExpressionContext.AccessPolicy,
|
|
324
185
|
fieldReferenceContext: 'input',
|
|
325
186
|
});
|
|
326
|
-
|
|
187
|
+
let expr = denies.length > 0
|
|
327
188
|
? '!(' +
|
|
328
189
|
denies
|
|
329
190
|
.map((deny) => {
|
|
@@ -332,32 +193,16 @@ class PolicyGenerator {
|
|
|
332
193
|
.join(' || ') +
|
|
333
194
|
')'
|
|
334
195
|
: undefined;
|
|
335
|
-
const allowStmt = allows
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
.join(' || ') +
|
|
342
|
-
')'
|
|
343
|
-
: undefined;
|
|
344
|
-
let expr;
|
|
345
|
-
if (denyStmt && allowStmt) {
|
|
346
|
-
expr = `${denyStmt} && ${allowStmt}`;
|
|
347
|
-
}
|
|
348
|
-
else if (denyStmt) {
|
|
349
|
-
expr = denyStmt;
|
|
350
|
-
}
|
|
351
|
-
else if (allowStmt) {
|
|
352
|
-
expr = allowStmt;
|
|
353
|
-
}
|
|
354
|
-
else {
|
|
355
|
-
throw new Error('should not happen');
|
|
356
|
-
}
|
|
196
|
+
const allowStmt = allows
|
|
197
|
+
.map((allow) => {
|
|
198
|
+
return transformer.transform(allow);
|
|
199
|
+
})
|
|
200
|
+
.join(' || ');
|
|
201
|
+
expr = expr ? `${expr} && (${allowStmt})` : allowStmt;
|
|
357
202
|
writer.write('return ' + expr);
|
|
358
203
|
});
|
|
359
204
|
const func = sourceFile.addFunction({
|
|
360
|
-
name:
|
|
205
|
+
name: model.name + '_create_input',
|
|
361
206
|
returnType: 'boolean',
|
|
362
207
|
parameters: [
|
|
363
208
|
{
|
|
@@ -373,294 +218,221 @@ class PolicyGenerator {
|
|
|
373
218
|
});
|
|
374
219
|
return func;
|
|
375
220
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
}
|
|
384
|
-
}
|
|
221
|
+
// writes `update: ...` for a given model
|
|
222
|
+
writeModelUpdateDef(model, policies, writer, sourceFile) {
|
|
223
|
+
writer.write(`update:`);
|
|
224
|
+
writer.inlineBlock(() => {
|
|
225
|
+
this.writeCommonModelDef(model, 'update', policies, writer, sourceFile);
|
|
226
|
+
});
|
|
227
|
+
writer.writeLine(',');
|
|
385
228
|
}
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
229
|
+
// writes `postUpdate: ...` for a given model
|
|
230
|
+
writeModelPostUpdateDef(model, policies, writer, sourceFile) {
|
|
231
|
+
writer.write(`postUpdate:`);
|
|
232
|
+
writer.inlineBlock(() => {
|
|
233
|
+
this.writeCommonModelDef(model, 'postUpdate', policies, writer, sourceFile);
|
|
234
|
+
// post-update policy has an additional selector for reading the pre-update entity data
|
|
235
|
+
this.writePostUpdatePreValueSelector(model, writer);
|
|
236
|
+
});
|
|
237
|
+
writer.writeLine(',');
|
|
238
|
+
}
|
|
239
|
+
writePostUpdatePreValueSelector(model, writer) {
|
|
240
|
+
const allows = (0, utils_1.getPolicyExpressions)(model, 'allow', 'postUpdate');
|
|
241
|
+
const denies = (0, utils_1.getPolicyExpressions)(model, 'deny', 'postUpdate');
|
|
242
|
+
const preValueSelect = (0, utils_1.generateSelectForRules)([...allows, ...denies]);
|
|
243
|
+
if (preValueSelect) {
|
|
244
|
+
writer.writeLine(`preUpdateSelector: ${JSON.stringify(preValueSelect)},`);
|
|
401
245
|
}
|
|
402
246
|
}
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
return false;
|
|
409
|
-
}
|
|
410
|
-
if ((0, ast_1.isReferenceExpr)(expr)) {
|
|
411
|
-
if ((0, ast_1.isDataModel)((_a = expr.$resolvedType) === null || _a === void 0 ? void 0 : _a.decl)) {
|
|
412
|
-
// if policy rules uses relation fields,
|
|
413
|
-
// we can't check based on create input
|
|
414
|
-
return false;
|
|
415
|
-
}
|
|
416
|
-
if ((0, ast_1.isDataModelField)(expr.target.ref) &&
|
|
417
|
-
expr.target.ref.$container === model &&
|
|
418
|
-
(0, sdk_1.hasAttribute)(expr.target.ref, '@default')) {
|
|
419
|
-
// reference to field of current model
|
|
420
|
-
// if it has default value, we can't check
|
|
421
|
-
// based on create input
|
|
422
|
-
return false;
|
|
423
|
-
}
|
|
424
|
-
if ((0, ast_1.isDataModelField)(expr.target.ref) && (0, sdk_1.isForeignKeyField)(expr.target.ref)) {
|
|
425
|
-
// reference to foreign key field
|
|
426
|
-
// we can't check based on create input
|
|
427
|
-
return false;
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
return true;
|
|
431
|
-
});
|
|
247
|
+
// writes `delete: ...` for a given model
|
|
248
|
+
writeModelDeleteDef(model, policies, writer, sourceFile) {
|
|
249
|
+
writer.write(`delete:`);
|
|
250
|
+
writer.inlineBlock(() => {
|
|
251
|
+
this.writeCommonModelDef(model, 'delete', policies, writer, sourceFile);
|
|
432
252
|
});
|
|
433
253
|
}
|
|
434
|
-
//
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
if ((0,
|
|
467
|
-
|
|
468
|
-
return [node.member.$refText];
|
|
469
|
-
}
|
|
470
|
-
if ((0, sdk_1.isFutureExpr)(node.operand)) {
|
|
471
|
-
// future().field is not subject to pre-update select
|
|
472
|
-
return undefined;
|
|
473
|
-
}
|
|
474
|
-
// build a selection path inside-out for chained member access
|
|
475
|
-
const inner = visit(node.operand);
|
|
476
|
-
if (inner) {
|
|
477
|
-
return [...inner, node.member.$refText];
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
return undefined;
|
|
481
|
-
};
|
|
482
|
-
// collect selection paths from the given expression
|
|
483
|
-
const collectReferencePaths = (expr) => {
|
|
484
|
-
var _a, _b, _c;
|
|
485
|
-
if ((0, ast_1.isThisExpr)(expr) && !(0, ast_1.isMemberAccessExpr)(expr.$container)) {
|
|
486
|
-
// a standalone `this` expression, include all id fields
|
|
487
|
-
const model = (_a = expr.$resolvedType) === null || _a === void 0 ? void 0 : _a.decl;
|
|
488
|
-
const idFields = (0, sdk_1.getIdFields)(model);
|
|
489
|
-
return idFields.map((field) => [field.name]);
|
|
490
|
-
}
|
|
491
|
-
if ((0, ast_1.isMemberAccessExpr)(expr) || (0, ast_1.isReferenceExpr)(expr)) {
|
|
492
|
-
const path = visit(expr);
|
|
493
|
-
if (path) {
|
|
494
|
-
if ((0, ast_1.isDataModel)((_b = expr.$resolvedType) === null || _b === void 0 ? void 0 : _b.decl)) {
|
|
495
|
-
// member selection ended at a data model field, include its id fields
|
|
496
|
-
const idFields = (0, sdk_1.getIdFields)((_c = expr.$resolvedType) === null || _c === void 0 ? void 0 : _c.decl);
|
|
497
|
-
return idFields.map((field) => [...path, field.name]);
|
|
498
|
-
}
|
|
499
|
-
else {
|
|
500
|
-
return [path];
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
else {
|
|
504
|
-
return [];
|
|
505
|
-
}
|
|
254
|
+
// writes `[kind]: ...` for a given model
|
|
255
|
+
writeCommonModelDef(model, kind, policies, writer, sourceFile) {
|
|
256
|
+
const allows = (0, utils_1.getPolicyExpressions)(model, 'allow', kind);
|
|
257
|
+
const denies = (0, utils_1.getPolicyExpressions)(model, 'deny', kind);
|
|
258
|
+
// policy guard
|
|
259
|
+
this.writePolicyGuard(model, kind, policies, allows, denies, writer, sourceFile);
|
|
260
|
+
// permission checker
|
|
261
|
+
if (kind !== 'postUpdate') {
|
|
262
|
+
this.writePermissionChecker(model, kind, policies, allows, denies, writer, sourceFile);
|
|
263
|
+
}
|
|
264
|
+
// write cross-model comparison rules as entity checker functions
|
|
265
|
+
// because they cannot be checked inside Prisma
|
|
266
|
+
this.writeEntityChecker(model, kind, writer, sourceFile, true);
|
|
267
|
+
}
|
|
268
|
+
writeEntityChecker(target, kind, writer, sourceFile, onlyCrossModelComparison = false, forOverride = false) {
|
|
269
|
+
var _a;
|
|
270
|
+
const allows = (0, utils_1.getPolicyExpressions)(target, 'allow', kind, forOverride, onlyCrossModelComparison ? 'onlyCrossModelComparison' : 'all');
|
|
271
|
+
const denies = (0, utils_1.getPolicyExpressions)(target, 'deny', kind, forOverride, onlyCrossModelComparison ? 'onlyCrossModelComparison' : 'all');
|
|
272
|
+
if (allows.length === 0 && denies.length === 0) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const model = (0, ast_1.isDataModel)(target) ? target : target.$container;
|
|
276
|
+
const func = (0, utils_1.generateEntityCheckerFunction)(sourceFile, model, kind, allows, denies, (0, ast_1.isDataModelField)(target) ? target : undefined, forOverride);
|
|
277
|
+
const selector = (_a = (0, utils_1.generateSelectForRules)([...allows, ...denies], false, kind !== 'postUpdate')) !== null && _a !== void 0 ? _a : {};
|
|
278
|
+
const key = forOverride ? 'overrideEntityChecker' : 'entityChecker';
|
|
279
|
+
writer.write(`${key}: { func: ${func.getName()}, selector: ${JSON.stringify(selector)} },`);
|
|
280
|
+
}
|
|
281
|
+
// writes `guard: ...` for a given policy operation kind
|
|
282
|
+
writePolicyGuard(model, kind, policies, allows, denies, writer, sourceFile) {
|
|
283
|
+
if (kind === 'update' && allows.length === 0) {
|
|
284
|
+
// no allow rule for 'update', policy is constant based on if there's
|
|
285
|
+
// post-update counterpart
|
|
286
|
+
if ((0, utils_1.getPolicyExpressions)(model, 'allow', 'postUpdate').length === 0) {
|
|
287
|
+
writer.write(`guard: false,`);
|
|
506
288
|
}
|
|
507
|
-
else
|
|
508
|
-
|
|
509
|
-
if (path) {
|
|
510
|
-
// recurse into RHS
|
|
511
|
-
const rhs = collectReferencePaths(expr.right);
|
|
512
|
-
// combine path of LHS and RHS
|
|
513
|
-
return rhs.map((r) => [...path, ...r]);
|
|
514
|
-
}
|
|
515
|
-
else {
|
|
516
|
-
return [];
|
|
517
|
-
}
|
|
289
|
+
else {
|
|
290
|
+
writer.write(`guard: true,`);
|
|
518
291
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (kind === 'postUpdate' && allows.length === 0 && denies.length === 0) {
|
|
295
|
+
// no 'postUpdate' rule, always allow
|
|
296
|
+
writer.write(`guard: true,`);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (kind in policies && typeof policies[kind] === 'boolean') {
|
|
300
|
+
// constant policy
|
|
301
|
+
writer.write(`guard: ${policies[kind]},`);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
// generate a policy function that evaluates a partial prisma query
|
|
305
|
+
const guardFunc = (0, utils_1.generateQueryGuardFunction)(sourceFile, model, kind, allows, denies);
|
|
306
|
+
writer.write(`guard: ${guardFunc.getName()},`);
|
|
307
|
+
}
|
|
308
|
+
// writes `permissionChecker: ...` for a given policy operation kind
|
|
309
|
+
writePermissionChecker(model, kind, policies, allows, denies, writer, sourceFile) {
|
|
310
|
+
if (this.options.generatePermissionChecker !== true) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (policies[kind] === true || policies[kind] === false) {
|
|
314
|
+
// constant policy
|
|
315
|
+
writer.write(`permissionChecker: ${policies[kind]},`);
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (kind === 'update' && allows.length === 0) {
|
|
319
|
+
// no allow rule for 'update', policy is constant based on if there's
|
|
320
|
+
// post-update counterpart
|
|
321
|
+
if ((0, utils_1.getPolicyExpressions)(model, 'allow', 'postUpdate').length === 0) {
|
|
322
|
+
writer.write(`permissionChecker: false,`);
|
|
522
323
|
}
|
|
523
324
|
else {
|
|
524
|
-
|
|
525
|
-
const children = (0, langium_1.streamContents)(expr)
|
|
526
|
-
.filter((child) => (0, ast_1.isExpression)(child))
|
|
527
|
-
.toArray();
|
|
528
|
-
return children.flatMap((child) => collectReferencePaths(child));
|
|
325
|
+
writer.write(`permissionChecker: true,`);
|
|
529
326
|
}
|
|
530
|
-
|
|
531
|
-
for (const rule of rules) {
|
|
532
|
-
const paths = collectReferencePaths(rule);
|
|
533
|
-
paths.forEach((p) => addPath(p));
|
|
327
|
+
return;
|
|
534
328
|
}
|
|
535
|
-
|
|
329
|
+
const guardFunc = this.generatePermissionCheckerFunction(model, kind, allows, denies, sourceFile);
|
|
330
|
+
writer.write(`permissionChecker: ${guardFunc.getName()},`);
|
|
536
331
|
}
|
|
537
|
-
|
|
332
|
+
generatePermissionCheckerFunction(model, kind, allows, denies, sourceFile) {
|
|
538
333
|
const statements = [];
|
|
539
|
-
|
|
540
|
-
const
|
|
541
|
-
|
|
542
|
-
(
|
|
543
|
-
|
|
544
|
-
(0, sdk_1.isFutureExpr)(child) ||
|
|
545
|
-
// field reference
|
|
546
|
-
((0, ast_1.isReferenceExpr)(child) && (0, ast_1.isDataModelField)(child.target.ref))));
|
|
547
|
-
if (!hasFieldAccess) {
|
|
548
|
-
// none of the rules reference model fields, we can compile down to a plain boolean
|
|
549
|
-
// function in this case (so we can skip doing SQL queries when validating)
|
|
550
|
-
statements.push((writer) => {
|
|
551
|
-
const transformer = new sdk_1.TypeScriptExpressionTransformer({
|
|
552
|
-
context: sdk_1.ExpressionContext.AccessPolicy,
|
|
553
|
-
isPostGuard: kind === 'postUpdate',
|
|
554
|
-
});
|
|
555
|
-
try {
|
|
556
|
-
denies.forEach((rule) => {
|
|
557
|
-
writer.write(`if (${transformer.transform(rule, false)}) { return ${expression_writer_1.FALSE}; }`);
|
|
558
|
-
});
|
|
559
|
-
allows.forEach((rule) => {
|
|
560
|
-
writer.write(`if (${transformer.transform(rule, false)}) { return ${expression_writer_1.TRUE}; }`);
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
catch (err) {
|
|
564
|
-
if (err instanceof sdk_1.TypeScriptExpressionTransformerError) {
|
|
565
|
-
throw new sdk_1.PluginError(__1.name, err.message);
|
|
566
|
-
}
|
|
567
|
-
else {
|
|
568
|
-
throw err;
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
if (forField) {
|
|
572
|
-
if (allows.length === 0) {
|
|
573
|
-
// if there's no allow rule, for field-level rules, by default we allow
|
|
574
|
-
writer.write(`return ${expression_writer_1.TRUE};`);
|
|
575
|
-
}
|
|
576
|
-
else {
|
|
577
|
-
// if there's any allow rule, we deny unless any allow rule evaluates to true
|
|
578
|
-
writer.write(`return ${expression_writer_1.FALSE};`);
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
else {
|
|
582
|
-
// for model-level rules, the default is always deny
|
|
583
|
-
writer.write(`return ${expression_writer_1.FALSE};`);
|
|
584
|
-
}
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
else {
|
|
588
|
-
statements.push((writer) => {
|
|
589
|
-
writer.write('return ');
|
|
590
|
-
const exprWriter = new expression_writer_1.ExpressionWriter(writer, kind === 'postUpdate');
|
|
591
|
-
const writeDenies = () => {
|
|
592
|
-
writer.conditionalWrite(denies.length > 1, '{ AND: [');
|
|
593
|
-
denies.forEach((expr, i) => {
|
|
594
|
-
writer.inlineBlock(() => {
|
|
595
|
-
writer.write('NOT: ');
|
|
596
|
-
exprWriter.write(expr);
|
|
597
|
-
});
|
|
598
|
-
writer.conditionalWrite(i !== denies.length - 1, ',');
|
|
599
|
-
});
|
|
600
|
-
writer.conditionalWrite(denies.length > 1, ']}');
|
|
601
|
-
};
|
|
602
|
-
const writeAllows = () => {
|
|
603
|
-
writer.conditionalWrite(allows.length > 1, '{ OR: [');
|
|
604
|
-
allows.forEach((expr, i) => {
|
|
605
|
-
exprWriter.write(expr);
|
|
606
|
-
writer.conditionalWrite(i !== allows.length - 1, ',');
|
|
607
|
-
});
|
|
608
|
-
writer.conditionalWrite(allows.length > 1, ']}');
|
|
609
|
-
};
|
|
610
|
-
if (allows.length > 0 && denies.length > 0) {
|
|
611
|
-
// include both allow and deny rules
|
|
612
|
-
writer.write('{ AND: [');
|
|
613
|
-
writeDenies();
|
|
614
|
-
writer.write(',');
|
|
615
|
-
writeAllows();
|
|
616
|
-
writer.write(']}');
|
|
617
|
-
}
|
|
618
|
-
else if (denies.length > 0) {
|
|
619
|
-
// only deny rules
|
|
620
|
-
writeDenies();
|
|
621
|
-
}
|
|
622
|
-
else if (allows.length > 0) {
|
|
623
|
-
// only allow rules
|
|
624
|
-
writeAllows();
|
|
625
|
-
}
|
|
626
|
-
else {
|
|
627
|
-
// disallow any operation
|
|
628
|
-
writer.write(`{ OR: [] }`);
|
|
629
|
-
}
|
|
630
|
-
writer.write(';');
|
|
631
|
-
});
|
|
632
|
-
}
|
|
334
|
+
(0, utils_1.generateNormalizedAuthRef)(model, allows, denies, statements);
|
|
335
|
+
const transformed = new constraint_transformer_1.ConstraintTransformer({
|
|
336
|
+
authAccessor: 'user',
|
|
337
|
+
}).transformRules(allows, denies);
|
|
338
|
+
statements.push(`return ${transformed};`);
|
|
633
339
|
const func = sourceFile.addFunction({
|
|
634
|
-
name: `${model.name}${
|
|
635
|
-
returnType: '
|
|
340
|
+
name: `${model.name}$checker$${kind}`,
|
|
341
|
+
returnType: 'PermissionCheckerConstraint',
|
|
636
342
|
parameters: [
|
|
637
343
|
{
|
|
638
344
|
name: 'context',
|
|
639
|
-
type: '
|
|
640
|
-
},
|
|
641
|
-
{
|
|
642
|
-
// for generating field references used by field comparison in the same model
|
|
643
|
-
name: 'db',
|
|
644
|
-
type: 'CrudContract',
|
|
345
|
+
type: 'PermissionCheckerContext',
|
|
645
346
|
},
|
|
646
347
|
],
|
|
647
348
|
statements,
|
|
648
349
|
});
|
|
649
350
|
return func;
|
|
650
351
|
}
|
|
651
|
-
|
|
352
|
+
// #endregion
|
|
353
|
+
// #region Field-level definitions
|
|
354
|
+
writeFieldLevelDefs(model, writer, sf) {
|
|
355
|
+
writer.write('fieldLevel:');
|
|
356
|
+
writer.inlineBlock(() => {
|
|
357
|
+
this.writeFieldReadDef(model, writer, sf);
|
|
358
|
+
this.writeFieldUpdateDef(model, writer, sf);
|
|
359
|
+
});
|
|
360
|
+
writer.writeLine(',');
|
|
361
|
+
}
|
|
362
|
+
writeFieldReadDef(model, writer, sourceFile) {
|
|
363
|
+
writer.writeLine('read:');
|
|
364
|
+
writer.block(() => {
|
|
365
|
+
for (const field of model.fields) {
|
|
366
|
+
const allows = (0, utils_1.getPolicyExpressions)(field, 'allow', 'read');
|
|
367
|
+
const denies = (0, utils_1.getPolicyExpressions)(field, 'deny', 'read');
|
|
368
|
+
const overrideAllows = (0, utils_1.getPolicyExpressions)(field, 'allow', 'read', true);
|
|
369
|
+
if (allows.length === 0 && denies.length === 0 && overrideAllows.length === 0) {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
writer.write(`${field.name}:`);
|
|
373
|
+
writer.block(() => {
|
|
374
|
+
// guard
|
|
375
|
+
const guardFunc = (0, utils_1.generateQueryGuardFunction)(sourceFile, model, 'read', allows, denies, field);
|
|
376
|
+
writer.write(`guard: ${guardFunc.getName()},`);
|
|
377
|
+
// checker function
|
|
378
|
+
// write all field-level rules as entity checker function
|
|
379
|
+
this.writeEntityChecker(field, 'read', writer, sourceFile, false, false);
|
|
380
|
+
if (overrideAllows.length > 0) {
|
|
381
|
+
// override guard function
|
|
382
|
+
const denies = (0, utils_1.getPolicyExpressions)(field, 'deny', 'read');
|
|
383
|
+
const overrideGuardFunc = (0, utils_1.generateQueryGuardFunction)(sourceFile, model, 'read', overrideAllows, denies, field, true);
|
|
384
|
+
writer.write(`overrideGuard: ${overrideGuardFunc.getName()},`);
|
|
385
|
+
// additional entity checker for override
|
|
386
|
+
this.writeEntityChecker(field, 'read', writer, sourceFile, false, true);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
writer.writeLine(',');
|
|
390
|
+
}
|
|
391
|
+
});
|
|
392
|
+
writer.writeLine(',');
|
|
393
|
+
}
|
|
394
|
+
writeFieldUpdateDef(model, writer, sourceFile) {
|
|
395
|
+
writer.writeLine('update:');
|
|
396
|
+
writer.block(() => {
|
|
397
|
+
for (const field of model.fields) {
|
|
398
|
+
const allows = (0, utils_1.getPolicyExpressions)(field, 'allow', 'update');
|
|
399
|
+
const denies = (0, utils_1.getPolicyExpressions)(field, 'deny', 'update');
|
|
400
|
+
const overrideAllows = (0, utils_1.getPolicyExpressions)(field, 'allow', 'update', true);
|
|
401
|
+
if (allows.length === 0 && denies.length === 0 && overrideAllows.length === 0) {
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
writer.write(`${field.name}:`);
|
|
405
|
+
writer.block(() => {
|
|
406
|
+
// guard
|
|
407
|
+
const guardFunc = (0, utils_1.generateQueryGuardFunction)(sourceFile, model, 'update', allows, denies, field);
|
|
408
|
+
writer.write(`guard: ${guardFunc.getName()},`);
|
|
409
|
+
// write cross-model comparison rules as entity checker functions
|
|
410
|
+
// because they cannot be checked inside Prisma
|
|
411
|
+
this.writeEntityChecker(field, 'update', writer, sourceFile, true, false);
|
|
412
|
+
if (overrideAllows.length > 0) {
|
|
413
|
+
// override guard
|
|
414
|
+
const overrideGuardFunc = (0, utils_1.generateQueryGuardFunction)(sourceFile, model, 'update', overrideAllows, denies, field, true);
|
|
415
|
+
writer.write(`overrideGuard: ${overrideGuardFunc.getName()},`);
|
|
416
|
+
// write cross-model comparison override rules as entity checker functions
|
|
417
|
+
// because they cannot be checked inside Prisma
|
|
418
|
+
this.writeEntityChecker(field, 'update', writer, sourceFile, true, true);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
writer.writeLine(',');
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
writer.writeLine(',');
|
|
425
|
+
}
|
|
426
|
+
generateFieldReadCheckerFunction(sourceFile, field, allows, denies) {
|
|
652
427
|
const statements = [];
|
|
653
|
-
|
|
428
|
+
(0, utils_1.generateNormalizedAuthRef)(field.$container, allows, denies, statements);
|
|
429
|
+
// compile rules down to typescript expressions
|
|
654
430
|
statements.push((writer) => {
|
|
655
|
-
if (allows.length === 0) {
|
|
656
|
-
writer.write('return false;');
|
|
657
|
-
return;
|
|
658
|
-
}
|
|
659
431
|
const transformer = new sdk_1.TypeScriptExpressionTransformer({
|
|
660
432
|
context: sdk_1.ExpressionContext.AccessPolicy,
|
|
661
433
|
fieldReferenceContext: 'input',
|
|
662
434
|
});
|
|
663
|
-
|
|
435
|
+
const denyStmt = denies.length > 0
|
|
664
436
|
? '!(' +
|
|
665
437
|
denies
|
|
666
438
|
.map((deny) => {
|
|
@@ -669,16 +441,32 @@ class PolicyGenerator {
|
|
|
669
441
|
.join(' || ') +
|
|
670
442
|
')'
|
|
671
443
|
: undefined;
|
|
672
|
-
const allowStmt = allows
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
444
|
+
const allowStmt = allows.length > 0
|
|
445
|
+
? '(' +
|
|
446
|
+
allows
|
|
447
|
+
.map((allow) => {
|
|
448
|
+
return transformer.transform(allow);
|
|
449
|
+
})
|
|
450
|
+
.join(' || ') +
|
|
451
|
+
')'
|
|
452
|
+
: undefined;
|
|
453
|
+
let expr;
|
|
454
|
+
if (denyStmt && allowStmt) {
|
|
455
|
+
expr = `${denyStmt} && ${allowStmt}`;
|
|
456
|
+
}
|
|
457
|
+
else if (denyStmt) {
|
|
458
|
+
expr = denyStmt;
|
|
459
|
+
}
|
|
460
|
+
else if (allowStmt) {
|
|
461
|
+
expr = allowStmt;
|
|
462
|
+
}
|
|
463
|
+
else {
|
|
464
|
+
throw new Error('should not happen');
|
|
465
|
+
}
|
|
678
466
|
writer.write('return ' + expr);
|
|
679
467
|
});
|
|
680
468
|
const func = sourceFile.addFunction({
|
|
681
|
-
name:
|
|
469
|
+
name: `${field.$container.name}$${field.name}_read`,
|
|
682
470
|
returnType: 'boolean',
|
|
683
471
|
parameters: [
|
|
684
472
|
{
|
|
@@ -694,71 +482,58 @@ class PolicyGenerator {
|
|
|
694
482
|
});
|
|
695
483
|
return func;
|
|
696
484
|
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
throw new sdk_1.PluginError(__1.name, 'Auth model not found');
|
|
704
|
-
}
|
|
705
|
-
const userIdFields = (0, sdk_1.getIdFields)(authModel);
|
|
706
|
-
if (!userIdFields || userIdFields.length === 0) {
|
|
707
|
-
throw new sdk_1.PluginError(__1.name, 'User model does not have an id field');
|
|
708
|
-
}
|
|
709
|
-
// normalize user to null to avoid accidentally use undefined in filter
|
|
710
|
-
statements.push(`const user: any = context.user ?? null;`);
|
|
485
|
+
// #endregion
|
|
486
|
+
//#region Auth selector
|
|
487
|
+
writeAuthSelector(models, writer) {
|
|
488
|
+
const authSelector = this.generateAuthSelector(models);
|
|
489
|
+
if (authSelector) {
|
|
490
|
+
writer.write(`authSelector: ${JSON.stringify(authSelector)},`);
|
|
711
491
|
}
|
|
712
492
|
}
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
else {
|
|
733
|
-
result[kind] = true;
|
|
734
|
-
continue;
|
|
493
|
+
// Generates a { select: ... } object to select `auth()` fields used in policy rules
|
|
494
|
+
generateAuthSelector(models) {
|
|
495
|
+
const authRules = [];
|
|
496
|
+
models.forEach((model) => {
|
|
497
|
+
// model-level rules
|
|
498
|
+
const modelPolicyAttrs = model.attributes.filter((attr) => ['@@allow', '@@deny'].includes(attr.decl.$refText));
|
|
499
|
+
// field-level rules
|
|
500
|
+
const fieldPolicyAttrs = model.fields
|
|
501
|
+
.flatMap((f) => f.attributes)
|
|
502
|
+
.filter((attr) => ['@allow', '@deny'].includes(attr.decl.$refText));
|
|
503
|
+
// all rule expression
|
|
504
|
+
const allExpressions = [...modelPolicyAttrs, ...fieldPolicyAttrs]
|
|
505
|
+
.filter((attr) => attr.args.length > 1)
|
|
506
|
+
.map((attr) => attr.args[1].value);
|
|
507
|
+
// collect `auth()` member access
|
|
508
|
+
allExpressions.forEach((rule) => {
|
|
509
|
+
(0, langium_1.streamAst)(rule).forEach((node) => {
|
|
510
|
+
if ((0, ast_1.isMemberAccessExpr)(node) && (0, sdk_1.isAuthInvocation)(node.operand)) {
|
|
511
|
+
authRules.push(node);
|
|
735
512
|
}
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
result[kind] = guardFunc.getName();
|
|
739
|
-
}
|
|
740
|
-
return result;
|
|
513
|
+
});
|
|
514
|
+
});
|
|
741
515
|
});
|
|
516
|
+
if (authRules.length > 0) {
|
|
517
|
+
return (0, utils_1.generateSelectForRules)(authRules, true);
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
return undefined;
|
|
521
|
+
}
|
|
742
522
|
}
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
name: 'context',
|
|
756
|
-
type: 'CheckerContext',
|
|
757
|
-
},
|
|
758
|
-
],
|
|
759
|
-
statements,
|
|
523
|
+
// #endregion
|
|
524
|
+
// #region Validation meta
|
|
525
|
+
writeValidationMeta(writer, models) {
|
|
526
|
+
writer.write('validation:');
|
|
527
|
+
writer.inlineBlock(() => {
|
|
528
|
+
for (const model of models) {
|
|
529
|
+
writer.write(`${(0, lower_case_first_1.lowerCaseFirst)(model.name)}:`);
|
|
530
|
+
writer.inlineBlock(() => {
|
|
531
|
+
writer.write(`hasValidation: ${(0, sdk_1.hasValidationAttributes)(model)}`);
|
|
532
|
+
});
|
|
533
|
+
writer.writeLine(',');
|
|
534
|
+
}
|
|
760
535
|
});
|
|
761
|
-
|
|
536
|
+
writer.writeLine(',');
|
|
762
537
|
}
|
|
763
538
|
}
|
|
764
539
|
exports.PolicyGenerator = PolicyGenerator;
|