zenstack 1.0.0-alpha.23 → 1.0.0-alpha.25
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/README.md +79 -9
- package/cli/cli-error.js +3 -5
- package/cli/cli-error.js.map +1 -1
- package/cli/cli-util.js +123 -105
- package/cli/cli-util.js.map +1 -1
- package/cli/index.js +105 -63
- package/cli/index.js.map +1 -1
- package/cli/plugin-runner.js +130 -127
- package/cli/plugin-runner.js.map +1 -1
- package/language-server/constants.js +5 -13
- package/language-server/constants.js.map +1 -1
- package/language-server/main.js +8 -15
- package/language-server/main.js.map +1 -1
- package/language-server/types.js +3 -1
- package/language-server/types.js.map +1 -1
- package/language-server/utils.js +13 -16
- package/language-server/utils.js.map +1 -1
- package/language-server/validator/attribute-validator.js +3 -7
- package/language-server/validator/attribute-validator.js.map +1 -1
- package/language-server/validator/datamodel-validator.js +293 -347
- package/language-server/validator/datamodel-validator.js.map +1 -1
- package/language-server/validator/datasource-validator.js +61 -71
- package/language-server/validator/datasource-validator.js.map +1 -1
- package/language-server/validator/enum-validator.js +6 -10
- package/language-server/validator/enum-validator.js.map +1 -1
- package/language-server/validator/expression-validator.js +25 -31
- package/language-server/validator/expression-validator.js.map +1 -1
- package/language-server/validator/schema-validator.js +18 -25
- package/language-server/validator/schema-validator.js.map +1 -1
- package/language-server/validator/utils.js +86 -85
- package/language-server/validator/utils.js.map +1 -1
- package/language-server/validator/zmodel-validator.js +55 -58
- package/language-server/validator/zmodel-validator.js.map +1 -1
- package/language-server/zmodel-formatter.js +40 -21
- package/language-server/zmodel-formatter.js.map +1 -1
- package/language-server/zmodel-linker.js +328 -331
- package/language-server/zmodel-linker.js.map +1 -1
- package/language-server/zmodel-module.js +50 -59
- package/language-server/zmodel-module.js.map +1 -1
- package/language-server/zmodel-scope.js +35 -25
- package/language-server/zmodel-scope.js.map +1 -1
- package/language-server/zmodel-workspace-manager.js +30 -18
- package/language-server/zmodel-workspace-manager.js.map +1 -1
- package/package.json +8 -12
- package/plugins/access-policy/expression-writer.js +301 -292
- package/plugins/access-policy/expression-writer.js.map +1 -1
- package/plugins/access-policy/index.js +20 -11
- package/plugins/access-policy/index.js.map +1 -1
- package/plugins/access-policy/policy-guard-generator.js +327 -321
- package/plugins/access-policy/policy-guard-generator.js.map +1 -1
- package/plugins/access-policy/typescript-expression-transformer.js +94 -95
- package/plugins/access-policy/typescript-expression-transformer.js.map +1 -1
- package/plugins/access-policy/utils.js +7 -9
- package/plugins/access-policy/utils.js.map +1 -1
- package/plugins/access-policy/zod-schema-generator.js +143 -159
- package/plugins/access-policy/zod-schema-generator.js.map +1 -1
- package/plugins/model-meta/index.js +97 -102
- package/plugins/model-meta/index.js.map +1 -1
- package/plugins/plugin-utils.js +34 -40
- package/plugins/plugin-utils.js.map +1 -1
- package/plugins/prisma/indent-string.js +4 -8
- package/plugins/prisma/indent-string.js.map +1 -1
- package/plugins/prisma/index.js +20 -11
- package/plugins/prisma/index.js.map +1 -1
- package/plugins/prisma/prisma-builder.js +235 -213
- package/plugins/prisma/prisma-builder.js.map +1 -1
- package/plugins/prisma/schema-generator.js +205 -192
- package/plugins/prisma/schema-generator.js.map +1 -1
- package/plugins/prisma/zmodel-code-generator.js +109 -114
- package/plugins/prisma/zmodel-code-generator.js.map +1 -1
- package/telemetry.js +107 -90
- package/telemetry.js.map +1 -1
- package/types.js +3 -1
- package/types.js.map +1 -1
- package/utils/ast-utils.js +67 -67
- package/utils/ast-utils.js.map +1 -1
- package/utils/exec-utils.js +6 -15
- package/utils/exec-utils.js.map +1 -1
- package/utils/pkg-utils.js +38 -35
- package/utils/pkg-utils.js.map +1 -1
- package/utils/version-utils.js +9 -10
- package/utils/version-utils.js.map +1 -1
- package/global.d.js +0 -1
- package/global.d.js.map +0 -1
|
@@ -1,345 +1,351 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
});
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
var
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
const ast_1 = require("@zenstackhq/language/ast");
|
|
16
|
+
const sdk_1 = require("@zenstackhq/sdk");
|
|
17
|
+
const change_case_1 = require("change-case");
|
|
18
|
+
const langium_1 = require("langium");
|
|
19
|
+
const path_1 = __importDefault(require("path"));
|
|
20
|
+
const ts_morph_1 = require("ts-morph");
|
|
21
|
+
const _1 = require(".");
|
|
22
|
+
const utils_1 = require("../../language-server/utils");
|
|
23
|
+
const ast_utils_1 = require("../../utils/ast-utils");
|
|
24
|
+
const plugin_utils_1 = require("../plugin-utils");
|
|
25
|
+
const expression_writer_1 = require("./expression-writer");
|
|
26
|
+
const utils_2 = require("./utils");
|
|
27
|
+
const zod_schema_generator_1 = require("./zod-schema-generator");
|
|
21
28
|
const UNKNOWN_USER_ID = 'zenstack_unknown_user';
|
|
22
|
-
|
|
23
29
|
/**
|
|
24
30
|
* Generates source file that contains Prisma query guard objects used for injecting database queries
|
|
25
31
|
*/
|
|
26
32
|
class PolicyGenerator {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
// import enums
|
|
52
|
-
for (const e of model.declarations.filter(d => (0, _ast.isEnum)(d))) {
|
|
53
|
-
sf.addImportDeclaration({
|
|
54
|
-
namedImports: [{
|
|
55
|
-
name: e.name
|
|
56
|
-
}],
|
|
57
|
-
moduleSpecifier: '@prisma/client'
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
const models = model.declarations.filter(d => (0, _ast.isDataModel)(d));
|
|
61
|
-
const policyMap = {};
|
|
62
|
-
for (const model of models) {
|
|
63
|
-
policyMap[model.name] = await this.generateQueryGuardForModel(model, sf);
|
|
64
|
-
}
|
|
65
|
-
const zodGenerator = new _zodSchemaGenerator.ZodSchemaGenerator();
|
|
66
|
-
sf.addVariableStatement({
|
|
67
|
-
declarationKind: _tsMorph.VariableDeclarationKind.Const,
|
|
68
|
-
declarations: [{
|
|
69
|
-
name: 'policy',
|
|
70
|
-
initializer: writer => {
|
|
71
|
-
writer.block(() => {
|
|
72
|
-
writer.write('guard:');
|
|
73
|
-
writer.inlineBlock(() => {
|
|
74
|
-
for (const [model, map] of Object.entries(policyMap)) {
|
|
75
|
-
writer.write(`${(0, _changeCase.camelCase)(model)}:`);
|
|
76
|
-
writer.inlineBlock(() => {
|
|
77
|
-
for (const [op, func] of Object.entries(map)) {
|
|
78
|
-
if (typeof func === 'object') {
|
|
79
|
-
writer.write(`${op}: ${JSON.stringify(func)},`);
|
|
80
|
-
} else {
|
|
81
|
-
writer.write(`${op}: ${func},`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
33
|
+
generate(model, options) {
|
|
34
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
35
|
+
const output = options.output ? options.output : (0, plugin_utils_1.getDefaultOutputFolder)();
|
|
36
|
+
if (!output) {
|
|
37
|
+
console.error(`Unable to determine output path, not running plugin ${_1.name}`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const project = new ts_morph_1.Project();
|
|
41
|
+
const sf = project.createSourceFile(path_1.default.join(output, 'policy.ts'), undefined, { overwrite: true });
|
|
42
|
+
sf.addImportDeclaration({
|
|
43
|
+
namedImports: [{ name: 'QueryContext' }],
|
|
44
|
+
moduleSpecifier: `${plugin_utils_1.RUNTIME_PACKAGE}`,
|
|
45
|
+
isTypeOnly: true,
|
|
46
|
+
});
|
|
47
|
+
sf.addImportDeclaration({
|
|
48
|
+
namedImports: [{ name: 'z' }],
|
|
49
|
+
moduleSpecifier: 'zod',
|
|
50
|
+
});
|
|
51
|
+
// import enums
|
|
52
|
+
for (const e of model.declarations.filter((d) => (0, ast_1.isEnum)(d))) {
|
|
53
|
+
sf.addImportDeclaration({
|
|
54
|
+
namedImports: [{ name: e.name }],
|
|
55
|
+
moduleSpecifier: '@prisma/client',
|
|
84
56
|
});
|
|
85
|
-
|
|
86
|
-
|
|
57
|
+
}
|
|
58
|
+
const models = model.declarations.filter((d) => (0, ast_1.isDataModel)(d));
|
|
59
|
+
const policyMap = {};
|
|
60
|
+
for (const model of models) {
|
|
61
|
+
policyMap[model.name] = yield this.generateQueryGuardForModel(model, sf);
|
|
62
|
+
}
|
|
63
|
+
const zodGenerator = new zod_schema_generator_1.ZodSchemaGenerator();
|
|
64
|
+
sf.addVariableStatement({
|
|
65
|
+
declarationKind: ts_morph_1.VariableDeclarationKind.Const,
|
|
66
|
+
declarations: [
|
|
67
|
+
{
|
|
68
|
+
name: 'policy',
|
|
69
|
+
initializer: (writer) => {
|
|
70
|
+
writer.block(() => {
|
|
71
|
+
writer.write('guard:');
|
|
72
|
+
writer.inlineBlock(() => {
|
|
73
|
+
for (const [model, map] of Object.entries(policyMap)) {
|
|
74
|
+
writer.write(`${(0, change_case_1.camelCase)(model)}:`);
|
|
75
|
+
writer.inlineBlock(() => {
|
|
76
|
+
for (const [op, func] of Object.entries(map)) {
|
|
77
|
+
if (typeof func === 'object') {
|
|
78
|
+
writer.write(`${op}: ${JSON.stringify(func)},`);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
writer.write(`${op}: ${func},`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
writer.write(',');
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
writer.writeLine(',');
|
|
89
|
+
writer.write('schema:');
|
|
90
|
+
zodGenerator.generate(writer, models);
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
],
|
|
87
95
|
});
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
}]
|
|
94
|
-
});
|
|
95
|
-
sf.addStatements('export default policy');
|
|
96
|
-
sf.formatText();
|
|
97
|
-
await project.save();
|
|
98
|
-
await project.emit();
|
|
99
|
-
}
|
|
100
|
-
getPolicyExpressions(model, kind, operation) {
|
|
101
|
-
const attrs = model.attributes.filter(attr => {
|
|
102
|
-
var _attr$decl$ref;
|
|
103
|
-
return ((_attr$decl$ref = attr.decl.ref) === null || _attr$decl$ref === void 0 ? void 0 : _attr$decl$ref.name) === `@@${kind}`;
|
|
104
|
-
});
|
|
105
|
-
const checkOperation = operation === 'postUpdate' ? 'update' : operation;
|
|
106
|
-
let result = attrs.filter(attr => {
|
|
107
|
-
const opsValue = (0, _sdk.getLiteral)(attr.args[0].value);
|
|
108
|
-
if (!opsValue) {
|
|
109
|
-
return false;
|
|
110
|
-
}
|
|
111
|
-
const ops = opsValue.split(',').map(s => s.trim());
|
|
112
|
-
return ops.includes(checkOperation) || ops.includes('all');
|
|
113
|
-
}).map(attr => attr.args[1].value);
|
|
114
|
-
if (operation === 'update') {
|
|
115
|
-
result = this.processUpdatePolicies(result, false);
|
|
116
|
-
} else if (operation === 'postUpdate') {
|
|
117
|
-
result = this.processUpdatePolicies(result, true);
|
|
118
|
-
}
|
|
119
|
-
return result;
|
|
120
|
-
}
|
|
121
|
-
processUpdatePolicies(expressions, postUpdate) {
|
|
122
|
-
return expressions.map(expr => this.visitPolicyExpression(expr, postUpdate)).filter(e => !!e);
|
|
123
|
-
}
|
|
124
|
-
visitPolicyExpression(expr, postUpdate) {
|
|
125
|
-
if ((0, _ast.isBinaryExpr)(expr) && (expr.operator === '&&' || expr.operator === '||')) {
|
|
126
|
-
const left = this.visitPolicyExpression(expr.left, postUpdate);
|
|
127
|
-
const right = this.visitPolicyExpression(expr.right, postUpdate);
|
|
128
|
-
if (!left) return right;
|
|
129
|
-
if (!right) return left;
|
|
130
|
-
return {
|
|
131
|
-
...expr,
|
|
132
|
-
left,
|
|
133
|
-
right
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
if ((0, _ast.isUnaryExpr)(expr) && expr.operator === '!') {
|
|
137
|
-
const operand = this.visitPolicyExpression(expr.operand, postUpdate);
|
|
138
|
-
if (!operand) return undefined;
|
|
139
|
-
return {
|
|
140
|
-
...expr,
|
|
141
|
-
operand
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
if (postUpdate && !this.hasFutureReference(expr)) {
|
|
145
|
-
return undefined;
|
|
146
|
-
} else if (!postUpdate && this.hasFutureReference(expr)) {
|
|
147
|
-
return undefined;
|
|
148
|
-
}
|
|
149
|
-
return expr;
|
|
150
|
-
}
|
|
151
|
-
hasFutureReference(expr) {
|
|
152
|
-
for (const node of (0, _langium.streamAllContents)(expr)) {
|
|
153
|
-
var _node$function$ref;
|
|
154
|
-
if ((0, _ast.isInvocationExpr)(node) && ((_node$function$ref = node.function.ref) === null || _node$function$ref === void 0 ? void 0 : _node$function$ref.name) === 'future' && (0, _utils.isFromStdlib)(node.function.ref)) {
|
|
155
|
-
return true;
|
|
156
|
-
}
|
|
96
|
+
sf.addStatements('export default policy');
|
|
97
|
+
sf.formatText();
|
|
98
|
+
yield project.save();
|
|
99
|
+
yield project.emit();
|
|
100
|
+
});
|
|
157
101
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
// no allow rule for 'update', policy is constant based on if there's
|
|
174
|
-
// post-update counterpart
|
|
175
|
-
if (this.getPolicyExpressions(model, 'allow', 'postUpdate').length === 0) {
|
|
176
|
-
result[kind] = false;
|
|
177
|
-
continue;
|
|
178
|
-
} else {
|
|
179
|
-
result[kind] = true;
|
|
180
|
-
continue;
|
|
102
|
+
getPolicyExpressions(model, kind, operation) {
|
|
103
|
+
const attrs = model.attributes.filter((attr) => { var _a; return ((_a = attr.decl.ref) === null || _a === void 0 ? void 0 : _a.name) === `@@${kind}`; });
|
|
104
|
+
const checkOperation = operation === 'postUpdate' ? 'update' : operation;
|
|
105
|
+
let result = attrs
|
|
106
|
+
.filter((attr) => {
|
|
107
|
+
const opsValue = (0, sdk_1.getLiteral)(attr.args[0].value);
|
|
108
|
+
if (!opsValue) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
const ops = opsValue.split(',').map((s) => s.trim());
|
|
112
|
+
return ops.includes(checkOperation) || ops.includes('all');
|
|
113
|
+
})
|
|
114
|
+
.map((attr) => attr.args[1].value);
|
|
115
|
+
if (operation === 'update') {
|
|
116
|
+
result = this.processUpdatePolicies(result, false);
|
|
181
117
|
}
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
// no rule 'postUpdate', always allow
|
|
185
|
-
result[kind] = true;
|
|
186
|
-
continue;
|
|
187
|
-
}
|
|
188
|
-
const func = this.generateQueryGuardFunction(sourceFile, model, kind, allows, denies);
|
|
189
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
190
|
-
result[kind] = func.getName();
|
|
191
|
-
if (kind === 'postUpdate') {
|
|
192
|
-
const preValueSelect = this.generatePreValueSelect(model, allows, denies);
|
|
193
|
-
if (preValueSelect) {
|
|
194
|
-
result['preValueSelect'] = preValueSelect;
|
|
118
|
+
else if (operation === 'postUpdate') {
|
|
119
|
+
result = this.processUpdatePolicies(result, true);
|
|
195
120
|
}
|
|
196
|
-
|
|
121
|
+
return result;
|
|
197
122
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
if (!curr[seg]) {
|
|
213
|
-
curr[seg] = {
|
|
214
|
-
select: {}
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
curr = curr[seg].select;
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
};
|
|
221
|
-
const visit = node => {
|
|
222
|
-
if ((0, _ast.isReferenceExpr)(node)) {
|
|
223
|
-
const target = (0, _sdk.resolved)(node.target);
|
|
224
|
-
if ((0, _ast.isDataModelField)(target)) {
|
|
225
|
-
// a field selection, it's a terminal
|
|
226
|
-
return [target.name];
|
|
227
|
-
}
|
|
228
|
-
} else if ((0, _ast.isMemberAccessExpr)(node)) {
|
|
229
|
-
if ((0, _utils2.isFutureExpr)(node.operand)) {
|
|
230
|
-
// future().field is not subject to pre-update select
|
|
231
|
-
return undefined;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// build a selection path inside-out for chained member access
|
|
235
|
-
const inner = visit(node.operand);
|
|
236
|
-
if (inner) {
|
|
237
|
-
return [...inner, node.member.$refText];
|
|
123
|
+
processUpdatePolicies(expressions, postUpdate) {
|
|
124
|
+
return expressions
|
|
125
|
+
.map((expr) => this.visitPolicyExpression(expr, postUpdate))
|
|
126
|
+
.filter((e) => !!e);
|
|
127
|
+
}
|
|
128
|
+
visitPolicyExpression(expr, postUpdate) {
|
|
129
|
+
if ((0, ast_1.isBinaryExpr)(expr) && (expr.operator === '&&' || expr.operator === '||')) {
|
|
130
|
+
const left = this.visitPolicyExpression(expr.left, postUpdate);
|
|
131
|
+
const right = this.visitPolicyExpression(expr.right, postUpdate);
|
|
132
|
+
if (!left)
|
|
133
|
+
return right;
|
|
134
|
+
if (!right)
|
|
135
|
+
return left;
|
|
136
|
+
return Object.assign(Object.assign({}, expr), { left, right });
|
|
238
137
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
// only care about member access and reference expressions
|
|
245
|
-
if (!(0, _ast.isMemberAccessExpr)(expr) && !(0, _ast.isReferenceExpr)(expr)) {
|
|
246
|
-
continue;
|
|
138
|
+
if ((0, ast_1.isUnaryExpr)(expr) && expr.operator === '!') {
|
|
139
|
+
const operand = this.visitPolicyExpression(expr.operand, postUpdate);
|
|
140
|
+
if (!operand)
|
|
141
|
+
return undefined;
|
|
142
|
+
return Object.assign(Object.assign({}, expr), { operand });
|
|
247
143
|
}
|
|
248
|
-
if (
|
|
249
|
-
|
|
250
|
-
continue;
|
|
144
|
+
if (postUpdate && !this.hasFutureReference(expr)) {
|
|
145
|
+
return undefined;
|
|
251
146
|
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
var _expr$$resolvedType;
|
|
255
|
-
if ((0, _ast.isDataModel)((_expr$$resolvedType = expr.$resolvedType) === null || _expr$$resolvedType === void 0 ? void 0 : _expr$$resolvedType.decl)) {
|
|
256
|
-
// member selection ended at a data model field, include its 'id'
|
|
257
|
-
path.push('id');
|
|
258
|
-
}
|
|
259
|
-
addPath(path);
|
|
147
|
+
else if (!postUpdate && this.hasFutureReference(expr)) {
|
|
148
|
+
return undefined;
|
|
260
149
|
}
|
|
261
|
-
|
|
150
|
+
return expr;
|
|
262
151
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
parameters: [{
|
|
270
|
-
name: 'context',
|
|
271
|
-
type: 'QueryContext'
|
|
272
|
-
}]
|
|
273
|
-
}).addBody();
|
|
274
|
-
|
|
275
|
-
// check if any allow or deny rule contains 'auth()' invocation
|
|
276
|
-
let hasAuthRef = false;
|
|
277
|
-
for (const node of [...denies, ...allows]) {
|
|
278
|
-
for (const child of (0, _langium.streamAllContents)(node)) {
|
|
279
|
-
if ((0, _ast.isInvocationExpr)(child) && (0, _sdk.resolved)(child.function).name === 'auth') {
|
|
280
|
-
hasAuthRef = true;
|
|
281
|
-
break;
|
|
152
|
+
hasFutureReference(expr) {
|
|
153
|
+
var _a;
|
|
154
|
+
for (const node of (0, langium_1.streamAllContents)(expr)) {
|
|
155
|
+
if ((0, ast_1.isInvocationExpr)(node) && ((_a = node.function.ref) === null || _a === void 0 ? void 0 : _a.name) === 'future' && (0, utils_1.isFromStdlib)(node.function.ref)) {
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
282
158
|
}
|
|
283
|
-
|
|
284
|
-
if (hasAuthRef) {
|
|
285
|
-
break;
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
if (hasAuthRef) {
|
|
289
|
-
const userModel = model.$container.declarations.find(decl => (0, _ast.isDataModel)(decl) && decl.name === 'User');
|
|
290
|
-
if (!userModel) {
|
|
291
|
-
throw new _sdk.PluginError('User model not found');
|
|
292
|
-
}
|
|
293
|
-
const userIdField = (0, _astUtils.getIdField)(userModel);
|
|
294
|
-
if (!userIdField) {
|
|
295
|
-
throw new _sdk.PluginError('User model does not have an id field');
|
|
296
|
-
}
|
|
297
|
-
func.addStatements(
|
|
298
|
-
// make sure user id is always available
|
|
299
|
-
`const user = context.user?.${userIdField.name} ? context.user : { ...context.user, ${userIdField.name}: '${UNKNOWN_USER_ID}' };`);
|
|
159
|
+
return false;
|
|
300
160
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
161
|
+
generateQueryGuardForModel(model, sourceFile) {
|
|
162
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
163
|
+
const result = {};
|
|
164
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
165
|
+
const policies = (0, ast_utils_1.analyzePolicies)(model);
|
|
166
|
+
for (const kind of plugin_utils_1.ALL_OPERATION_KINDS) {
|
|
167
|
+
if (policies[kind] === true || policies[kind] === false) {
|
|
168
|
+
result[kind] = policies[kind];
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const denies = this.getPolicyExpressions(model, 'deny', kind);
|
|
172
|
+
const allows = this.getPolicyExpressions(model, 'allow', kind);
|
|
173
|
+
if (kind === 'update' && allows.length === 0) {
|
|
174
|
+
// no allow rule for 'update', policy is constant based on if there's
|
|
175
|
+
// post-update counterpart
|
|
176
|
+
if (this.getPolicyExpressions(model, 'allow', 'postUpdate').length === 0) {
|
|
177
|
+
result[kind] = false;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
result[kind] = true;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (kind === 'postUpdate' && allows.length === 0 && denies.length === 0) {
|
|
186
|
+
// no rule 'postUpdate', always allow
|
|
187
|
+
result[kind] = true;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const func = this.generateQueryGuardFunction(sourceFile, model, kind, allows, denies);
|
|
191
|
+
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
192
|
+
result[kind] = func.getName();
|
|
193
|
+
if (kind === 'postUpdate') {
|
|
194
|
+
const preValueSelect = this.generatePreValueSelect(model, allows, denies);
|
|
195
|
+
if (preValueSelect) {
|
|
196
|
+
result['preValueSelect'] = preValueSelect;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return result;
|
|
314
201
|
});
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
202
|
+
}
|
|
203
|
+
// generates an object that can be used as the 'select' argument when fetching pre-update
|
|
204
|
+
// entity value
|
|
205
|
+
generatePreValueSelect(model, allows, denies) {
|
|
206
|
+
var _a;
|
|
207
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
208
|
+
const result = {};
|
|
209
|
+
const addPath = (path) => {
|
|
210
|
+
let curr = result;
|
|
211
|
+
path.forEach((seg, i) => {
|
|
212
|
+
if (i === path.length - 1) {
|
|
213
|
+
curr[seg] = true;
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
if (!curr[seg]) {
|
|
217
|
+
curr[seg] = { select: {} };
|
|
218
|
+
}
|
|
219
|
+
curr = curr[seg].select;
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
};
|
|
223
|
+
const visit = (node) => {
|
|
224
|
+
if ((0, ast_1.isReferenceExpr)(node)) {
|
|
225
|
+
const target = (0, sdk_1.resolved)(node.target);
|
|
226
|
+
if ((0, ast_1.isDataModelField)(target)) {
|
|
227
|
+
// a field selection, it's a terminal
|
|
228
|
+
return [target.name];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
else if ((0, ast_1.isMemberAccessExpr)(node)) {
|
|
232
|
+
if ((0, utils_2.isFutureExpr)(node.operand)) {
|
|
233
|
+
// future().field is not subject to pre-update select
|
|
234
|
+
return undefined;
|
|
235
|
+
}
|
|
236
|
+
// build a selection path inside-out for chained member access
|
|
237
|
+
const inner = visit(node.operand);
|
|
238
|
+
if (inner) {
|
|
239
|
+
return [...inner, node.member.$refText];
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return undefined;
|
|
243
|
+
};
|
|
244
|
+
for (const rule of [...allows, ...denies]) {
|
|
245
|
+
for (const expr of (0, langium_1.streamAllContents)(rule).filter((node) => (0, ast_1.isExpression)(node))) {
|
|
246
|
+
// only care about member access and reference expressions
|
|
247
|
+
if (!(0, ast_1.isMemberAccessExpr)(expr) && !(0, ast_1.isReferenceExpr)(expr)) {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (expr.$container.$type === ast_1.MemberAccessExpr) {
|
|
251
|
+
// only visit top-level member access
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const path = visit(expr);
|
|
255
|
+
if (path) {
|
|
256
|
+
if ((0, ast_1.isDataModel)((_a = expr.$resolvedType) === null || _a === void 0 ? void 0 : _a.decl)) {
|
|
257
|
+
// member selection ended at a data model field, include its 'id'
|
|
258
|
+
path.push('id');
|
|
259
|
+
}
|
|
260
|
+
addPath(path);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return Object.keys(result).length === 0 ? null : result;
|
|
265
|
+
}
|
|
266
|
+
generateQueryGuardFunction(sourceFile, model, kind, allows, denies) {
|
|
267
|
+
const func = sourceFile
|
|
268
|
+
.addFunction({
|
|
269
|
+
name: model.name + '_' + kind,
|
|
270
|
+
returnType: 'any',
|
|
271
|
+
parameters: [
|
|
272
|
+
{
|
|
273
|
+
name: 'context',
|
|
274
|
+
type: 'QueryContext',
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
})
|
|
278
|
+
.addBody();
|
|
279
|
+
// check if any allow or deny rule contains 'auth()' invocation
|
|
280
|
+
let hasAuthRef = false;
|
|
281
|
+
for (const node of [...denies, ...allows]) {
|
|
282
|
+
for (const child of (0, langium_1.streamAllContents)(node)) {
|
|
283
|
+
if ((0, ast_1.isInvocationExpr)(child) && (0, sdk_1.resolved)(child.function).name === 'auth') {
|
|
284
|
+
hasAuthRef = true;
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (hasAuthRef) {
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (hasAuthRef) {
|
|
293
|
+
const userModel = model.$container.declarations.find((decl) => (0, ast_1.isDataModel)(decl) && decl.name === 'User');
|
|
294
|
+
if (!userModel) {
|
|
295
|
+
throw new sdk_1.PluginError('User model not found');
|
|
296
|
+
}
|
|
297
|
+
const userIdField = (0, ast_utils_1.getIdField)(userModel);
|
|
298
|
+
if (!userIdField) {
|
|
299
|
+
throw new sdk_1.PluginError('User model does not have an id field');
|
|
300
|
+
}
|
|
301
|
+
func.addStatements(
|
|
302
|
+
// make sure user id is always available
|
|
303
|
+
`const user = context.user?.${userIdField.name} ? context.user : { ...context.user, ${userIdField.name}: '${UNKNOWN_USER_ID}' };`);
|
|
304
|
+
}
|
|
305
|
+
// r = <guard object>;
|
|
306
|
+
func.addStatements((writer) => {
|
|
307
|
+
writer.write('return ');
|
|
308
|
+
const exprWriter = new expression_writer_1.ExpressionWriter(writer, kind === 'postUpdate');
|
|
309
|
+
const writeDenies = () => {
|
|
310
|
+
writer.conditionalWrite(denies.length > 1, '{ AND: [');
|
|
311
|
+
denies.forEach((expr, i) => {
|
|
312
|
+
writer.inlineBlock(() => {
|
|
313
|
+
writer.write('NOT: ');
|
|
314
|
+
exprWriter.write(expr);
|
|
315
|
+
});
|
|
316
|
+
writer.conditionalWrite(i !== denies.length - 1, ',');
|
|
317
|
+
});
|
|
318
|
+
writer.conditionalWrite(denies.length > 1, ']}');
|
|
319
|
+
};
|
|
320
|
+
const writeAllows = () => {
|
|
321
|
+
writer.conditionalWrite(allows.length > 1, '{ OR: [');
|
|
322
|
+
allows.forEach((expr, i) => {
|
|
323
|
+
exprWriter.write(expr);
|
|
324
|
+
writer.conditionalWrite(i !== allows.length - 1, ',');
|
|
325
|
+
});
|
|
326
|
+
writer.conditionalWrite(allows.length > 1, ']}');
|
|
327
|
+
};
|
|
328
|
+
if (allows.length > 0 && denies.length > 0) {
|
|
329
|
+
writer.write('{ AND: [');
|
|
330
|
+
writeDenies();
|
|
331
|
+
writer.write(',');
|
|
332
|
+
writeAllows();
|
|
333
|
+
writer.write(']}');
|
|
334
|
+
}
|
|
335
|
+
else if (denies.length > 0) {
|
|
336
|
+
writeDenies();
|
|
337
|
+
}
|
|
338
|
+
else if (allows.length > 0) {
|
|
339
|
+
writeAllows();
|
|
340
|
+
}
|
|
341
|
+
else {
|
|
342
|
+
// disallow any operation
|
|
343
|
+
writer.write(`{ ${sdk_1.GUARD_FIELD_NAME}: false }`);
|
|
344
|
+
}
|
|
345
|
+
writer.write(';');
|
|
322
346
|
});
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (allows.length > 0 && denies.length > 0) {
|
|
326
|
-
writer.write('{ AND: [');
|
|
327
|
-
writeDenies();
|
|
328
|
-
writer.write(',');
|
|
329
|
-
writeAllows();
|
|
330
|
-
writer.write(']}');
|
|
331
|
-
} else if (denies.length > 0) {
|
|
332
|
-
writeDenies();
|
|
333
|
-
} else if (allows.length > 0) {
|
|
334
|
-
writeAllows();
|
|
335
|
-
} else {
|
|
336
|
-
// disallow any operation
|
|
337
|
-
writer.write(`{ ${_sdk.GUARD_FIELD_NAME}: false }`);
|
|
338
|
-
}
|
|
339
|
-
writer.write(';');
|
|
340
|
-
});
|
|
341
|
-
return func;
|
|
342
|
-
}
|
|
347
|
+
return func;
|
|
348
|
+
}
|
|
343
349
|
}
|
|
344
350
|
exports.default = PolicyGenerator;
|
|
345
351
|
//# sourceMappingURL=policy-guard-generator.js.map
|