@nocobase/plugin-acl 2.1.0-alpha.2 → 2.1.0-alpha.20
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/LICENSE +201 -661
- package/README.md +79 -10
- package/client-v2.d.ts +2 -0
- package/client-v2.js +1 -0
- package/dist/client/949.7ad4ad3b554e5452.js +10 -0
- package/dist/client/971.50ecf7b6ac572080.js +10 -0
- package/dist/client/index.js +1 -1
- package/dist/client-v2/139.929bc11d582ef7d4.js +10 -0
- package/dist/client-v2/193.3245b23f17b4c9f8.js +10 -0
- package/dist/client-v2/366.069b6cf12cfb9a67.js +10 -0
- package/dist/client-v2/627.ce101823deb86dd6.js +10 -0
- package/dist/client-v2/index.d.ts +9 -0
- package/dist/client-v2/index.js +10 -0
- package/dist/client-v2/plugin.d.ts +5 -0
- package/dist/client-v2/routes/AppInfoDemoRoute.d.ts +10 -0
- package/dist/client-v2/routes/DemoHomepageRoute.d.ts +10 -0
- package/dist/client-v2/routes/FlowSettingsComponentLoaderDemoRoute.d.ts +2 -0
- package/dist/client-v2/settings/DemoFlowSettingsLazyField.d.ts +10 -0
- package/dist/externalVersion.js +11 -9
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +2 -0
- package/dist/server/middlewares/check-association-operate.js +14 -5
- package/dist/server/middlewares/check-change-with-association.d.ts +21 -0
- package/dist/server/middlewares/check-change-with-association.js +327 -246
- package/dist/server/middlewares/check-query-permission.d.ts +10 -0
- package/dist/server/middlewares/check-query-permission.js +64 -0
- package/dist/server/middlewares/with-acl-meta.js +7 -2
- package/dist/server/migrations/20251119225252-update-member-default-permission.js +1 -1
- package/dist/server/query/apply-query-permission.d.ts +27 -0
- package/dist/server/query/apply-query-permission.js +242 -0
- package/dist/server/server.d.ts +6 -1
- package/dist/server/server.js +8 -1
- package/dist/swagger/index.d.ts +962 -143
- package/dist/swagger/index.js +854 -183
- package/package.json +5 -3
- package/dist/client/0655d5ded9f45bb1.js +0 -10
- package/dist/client/50204a14518b3a0d.js +0 -10
|
@@ -36,42 +36,260 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
36
36
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
37
37
|
var check_change_with_association_exports = {};
|
|
38
38
|
__export(check_change_with_association_exports, {
|
|
39
|
-
checkChangesWithAssociation: () => checkChangesWithAssociation
|
|
39
|
+
checkChangesWithAssociation: () => checkChangesWithAssociation,
|
|
40
|
+
sanitizeAssociationValues: () => sanitizeAssociationValues
|
|
40
41
|
});
|
|
41
42
|
module.exports = __toCommonJS(check_change_with_association_exports);
|
|
42
43
|
var import_acl = require("@nocobase/acl");
|
|
43
44
|
var import_lodash = __toESM(require("lodash"));
|
|
45
|
+
async function sanitizeAssociationValues(options) {
|
|
46
|
+
var _a, _b, _c;
|
|
47
|
+
const {
|
|
48
|
+
acl,
|
|
49
|
+
resourceName,
|
|
50
|
+
actionName,
|
|
51
|
+
values,
|
|
52
|
+
updateAssociationValues = [],
|
|
53
|
+
protectedKeys = [],
|
|
54
|
+
aclParams
|
|
55
|
+
} = options;
|
|
56
|
+
if (import_lodash.default.isEmpty(values)) {
|
|
57
|
+
return values;
|
|
58
|
+
}
|
|
59
|
+
const collection = options.collection ?? ((_b = (_a = options.database ?? options.db) == null ? void 0 : _a.getCollection) == null ? void 0 : _b.call(_a, resourceName));
|
|
60
|
+
if (!collection) {
|
|
61
|
+
return values;
|
|
62
|
+
}
|
|
63
|
+
const params = aclParams ?? (acl ? (_c = acl.fixedParamsManager) == null ? void 0 : _c.getParams(resourceName, actionName) : void 0);
|
|
64
|
+
const roles = options.roles;
|
|
65
|
+
const can = (canOptions) => (acl == null ? void 0 : acl.can({ roles: (roles == null ? void 0 : roles.length) ? roles : ["anonymous"], ...canOptions })) ?? null;
|
|
66
|
+
const parseOptions = {
|
|
67
|
+
timezone: options.timezone,
|
|
68
|
+
userProvider: options.userProvider,
|
|
69
|
+
state: {
|
|
70
|
+
...options.state || {},
|
|
71
|
+
currentRole: options.currentRole,
|
|
72
|
+
currentRoles: options.roles,
|
|
73
|
+
currentUser: options.currentUser
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
return await processValues({
|
|
77
|
+
values,
|
|
78
|
+
updateAssociationValues,
|
|
79
|
+
aclParams: params,
|
|
80
|
+
collection,
|
|
81
|
+
lastFieldPath: "",
|
|
82
|
+
protectedKeys,
|
|
83
|
+
can,
|
|
84
|
+
parseOptions
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const checkChangesWithAssociation = async (ctx, next) => {
|
|
88
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
|
|
89
|
+
const timezone = ((_b = (_a = ctx.request) == null ? void 0 : _a.get) == null ? void 0 : _b.call(_a, "x-timezone")) ?? ((_d = (_c = ctx.request) == null ? void 0 : _c.header) == null ? void 0 : _d["x-timezone"]) ?? ((_f = (_e = ctx.req) == null ? void 0 : _e.headers) == null ? void 0 : _f["x-timezone"]);
|
|
90
|
+
const { resourceName, actionName } = ctx.action;
|
|
91
|
+
if (!["create", "firstOrCreate", "updateOrCreate", "update"].includes(actionName)) {
|
|
92
|
+
return next();
|
|
93
|
+
}
|
|
94
|
+
if ((_g = ctx.permission) == null ? void 0 : _g.skip) {
|
|
95
|
+
return next();
|
|
96
|
+
}
|
|
97
|
+
const roles = ctx.state.currentRoles;
|
|
98
|
+
if (roles.includes("root")) {
|
|
99
|
+
return next();
|
|
100
|
+
}
|
|
101
|
+
const acl = ctx.acl;
|
|
102
|
+
for (const role of roles) {
|
|
103
|
+
const aclRole = acl.getRole(role);
|
|
104
|
+
if (aclRole == null ? void 0 : aclRole.snippetAllowed(`${resourceName}:${actionName}`)) {
|
|
105
|
+
return next();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const params = ctx.action.params || {};
|
|
109
|
+
const rawValues = params.values;
|
|
110
|
+
if (import_lodash.default.isEmpty(rawValues)) {
|
|
111
|
+
return next();
|
|
112
|
+
}
|
|
113
|
+
const protectedKeys = ["firstOrCreate", "updateOrCreate"].includes(actionName) ? params.filterKeys || [] : [];
|
|
114
|
+
const collection = (_i = (_h = ctx.database ?? ctx.db) == null ? void 0 : _h.getCollection) == null ? void 0 : _i.call(_h, resourceName);
|
|
115
|
+
const processed = await sanitizeAssociationValues({
|
|
116
|
+
acl,
|
|
117
|
+
collection,
|
|
118
|
+
resourceName,
|
|
119
|
+
actionName,
|
|
120
|
+
values: rawValues,
|
|
121
|
+
updateAssociationValues: params.updateAssociationValues || [],
|
|
122
|
+
protectedKeys,
|
|
123
|
+
roles,
|
|
124
|
+
currentRole: ctx.state.currentRole,
|
|
125
|
+
currentUser: ctx.state.currentUser,
|
|
126
|
+
state: import_lodash.default.clone(ctx.state),
|
|
127
|
+
aclParams: (_k = (_j = ctx.permission) == null ? void 0 : _j.can) == null ? void 0 : _k.params,
|
|
128
|
+
timezone,
|
|
129
|
+
userProvider: (0, import_acl.createUserProvider)({
|
|
130
|
+
dataSourceManager: (_l = ctx.app) == null ? void 0 : _l.dataSourceManager,
|
|
131
|
+
currentUser: (_m = ctx.state) == null ? void 0 : _m.currentUser
|
|
132
|
+
})
|
|
133
|
+
});
|
|
134
|
+
ctx.action.params.values = processed;
|
|
135
|
+
await next();
|
|
136
|
+
};
|
|
137
|
+
async function processValues(options) {
|
|
138
|
+
var _a, _b;
|
|
139
|
+
const {
|
|
140
|
+
values,
|
|
141
|
+
updateAssociationValues,
|
|
142
|
+
aclParams,
|
|
143
|
+
collection,
|
|
144
|
+
lastFieldPath = "",
|
|
145
|
+
protectedKeys = [],
|
|
146
|
+
can,
|
|
147
|
+
parseOptions
|
|
148
|
+
} = options;
|
|
149
|
+
if (Array.isArray(values)) {
|
|
150
|
+
const result = [];
|
|
151
|
+
for (const item of values) {
|
|
152
|
+
if (!import_lodash.default.isPlainObject(item)) {
|
|
153
|
+
result.push(item);
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const processed = await processValues({
|
|
157
|
+
values: item,
|
|
158
|
+
updateAssociationValues,
|
|
159
|
+
aclParams,
|
|
160
|
+
collection,
|
|
161
|
+
lastFieldPath,
|
|
162
|
+
protectedKeys,
|
|
163
|
+
can,
|
|
164
|
+
parseOptions
|
|
165
|
+
});
|
|
166
|
+
if (processed !== null && processed !== void 0) {
|
|
167
|
+
result.push(processed);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return result;
|
|
171
|
+
}
|
|
172
|
+
if (!values || !import_lodash.default.isPlainObject(values)) {
|
|
173
|
+
return values;
|
|
174
|
+
}
|
|
175
|
+
if (!collection) {
|
|
176
|
+
return values;
|
|
177
|
+
}
|
|
178
|
+
let v = values;
|
|
179
|
+
if (aclParams == null ? void 0 : aclParams.whitelist) {
|
|
180
|
+
const combined = import_lodash.default.uniq([...aclParams.whitelist, ...protectedKeys]);
|
|
181
|
+
v = import_lodash.default.pick(values, combined);
|
|
182
|
+
}
|
|
183
|
+
for (const [fieldName, fieldValue] of Object.entries(v)) {
|
|
184
|
+
if (protectedKeys.includes(fieldName)) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
const field = collection.getField(fieldName);
|
|
188
|
+
const isAssociation = field && ["hasOne", "hasMany", "belongsTo", "belongsToMany", "belongsToArray"].includes(field.type);
|
|
189
|
+
if (!isAssociation) {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
const targetCollection = collection.db.getCollection(field.target);
|
|
193
|
+
if (!targetCollection) {
|
|
194
|
+
delete v[fieldName];
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const fieldPath = lastFieldPath ? `${lastFieldPath}.${fieldName}` : fieldName;
|
|
198
|
+
const recordKey = field.type === "hasOne" ? targetCollection.model.primaryKeyAttribute : field.targetKey;
|
|
199
|
+
const canUpdateAssociation = updateAssociationValues.includes(fieldPath);
|
|
200
|
+
if (!canUpdateAssociation) {
|
|
201
|
+
const normalized = normalizeAssociationValue(fieldValue, recordKey);
|
|
202
|
+
if (normalized === void 0 && !protectedKeys.includes(fieldName)) {
|
|
203
|
+
delete v[fieldName];
|
|
204
|
+
} else {
|
|
205
|
+
v[fieldName] = normalized;
|
|
206
|
+
}
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const createParams = can == null ? void 0 : can({
|
|
210
|
+
resource: field.target,
|
|
211
|
+
action: "create"
|
|
212
|
+
});
|
|
213
|
+
const updateParams = can == null ? void 0 : can({
|
|
214
|
+
resource: field.target,
|
|
215
|
+
action: "update"
|
|
216
|
+
});
|
|
217
|
+
if (Array.isArray(fieldValue)) {
|
|
218
|
+
const processed = [];
|
|
219
|
+
let allowedRecordKeys;
|
|
220
|
+
let existingRecordKeys;
|
|
221
|
+
if (updateParams) {
|
|
222
|
+
const allowedResult = await collectAllowedRecordKeys(
|
|
223
|
+
fieldValue,
|
|
224
|
+
recordKey,
|
|
225
|
+
(_a = updateParams == null ? void 0 : updateParams.params) == null ? void 0 : _a.filter,
|
|
226
|
+
targetCollection,
|
|
227
|
+
parseOptions
|
|
228
|
+
);
|
|
229
|
+
allowedRecordKeys = allowedResult == null ? void 0 : allowedResult.allowedKeys;
|
|
230
|
+
if (createParams && ((_b = allowedResult == null ? void 0 : allowedResult.missingKeys) == null ? void 0 : _b.size)) {
|
|
231
|
+
existingRecordKeys = await collectExistingRecordKeys(recordKey, targetCollection, allowedResult.missingKeys);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
for (const item of fieldValue) {
|
|
235
|
+
const r2 = await processAssociationChild({
|
|
236
|
+
value: item,
|
|
237
|
+
recordKey,
|
|
238
|
+
updateAssociationValues,
|
|
239
|
+
createParams,
|
|
240
|
+
updateParams,
|
|
241
|
+
target: targetCollection,
|
|
242
|
+
fieldPath,
|
|
243
|
+
allowedRecordKeys,
|
|
244
|
+
existingRecordKeys,
|
|
245
|
+
can,
|
|
246
|
+
parseOptions
|
|
247
|
+
});
|
|
248
|
+
if (r2 !== null && r2 !== void 0) {
|
|
249
|
+
processed.push(r2);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
v[fieldName] = processed;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
const r = await processAssociationChild({
|
|
256
|
+
value: fieldValue,
|
|
257
|
+
recordKey,
|
|
258
|
+
updateAssociationValues,
|
|
259
|
+
createParams,
|
|
260
|
+
updateParams,
|
|
261
|
+
target: targetCollection,
|
|
262
|
+
fieldPath,
|
|
263
|
+
can,
|
|
264
|
+
parseOptions
|
|
265
|
+
});
|
|
266
|
+
v[fieldName] = r;
|
|
267
|
+
}
|
|
268
|
+
return v;
|
|
269
|
+
}
|
|
44
270
|
function normalizeAssociationValue(value, recordKey) {
|
|
45
271
|
if (!value) {
|
|
46
272
|
return value;
|
|
47
273
|
}
|
|
48
274
|
if (Array.isArray(value)) {
|
|
49
275
|
const result = value.map((v) => typeof v === "number" || typeof v === "string" ? v : v[recordKey]).filter((v) => v !== null && v !== void 0);
|
|
50
|
-
return result
|
|
51
|
-
} else {
|
|
52
|
-
return typeof value === "number" || typeof value === "string" ? value : value[recordKey];
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
async function resolveScopeFilter(ctx, target, params) {
|
|
56
|
-
if (!params) {
|
|
57
|
-
return {};
|
|
276
|
+
return result;
|
|
58
277
|
}
|
|
59
|
-
|
|
60
|
-
const parsedParams = await ctx.acl.parseJsonTemplate(filteredParams, ctx);
|
|
61
|
-
return parsedParams.filter || {};
|
|
278
|
+
return typeof value === "number" || typeof value === "string" ? value : value[recordKey];
|
|
62
279
|
}
|
|
63
|
-
async function collectAllowedRecordKeys(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return void 0;
|
|
280
|
+
async function collectAllowedRecordKeys(items, recordKey, filter, collection, parseOptions) {
|
|
281
|
+
if (!collection) {
|
|
282
|
+
return;
|
|
67
283
|
}
|
|
284
|
+
const { repository } = collection;
|
|
68
285
|
const keys = items.map((item) => import_lodash.default.isPlainObject(item) ? item[recordKey] : void 0).filter((key) => key !== void 0 && key !== null);
|
|
69
286
|
if (!keys.length) {
|
|
70
|
-
return
|
|
287
|
+
return;
|
|
71
288
|
}
|
|
72
289
|
try {
|
|
73
|
-
|
|
74
|
-
const
|
|
290
|
+
(0, import_acl.checkFilterParams)(collection, filter);
|
|
291
|
+
const scopedFilter = filter ? await (0, import_acl.parseJsonTemplate)(filter, parseOptions) : {};
|
|
292
|
+
const records = await repository.find({
|
|
75
293
|
filter: {
|
|
76
294
|
...scopedFilter,
|
|
77
295
|
[`${recordKey}.$in`]: keys
|
|
@@ -96,16 +314,16 @@ async function collectAllowedRecordKeys(ctx, items, recordKey, updateParams, tar
|
|
|
96
314
|
throw e;
|
|
97
315
|
}
|
|
98
316
|
}
|
|
99
|
-
async function collectExistingRecordKeys(
|
|
100
|
-
const
|
|
101
|
-
if (!
|
|
317
|
+
async function collectExistingRecordKeys(recordKey, collection, keys) {
|
|
318
|
+
const { repository } = collection;
|
|
319
|
+
if (!repository) {
|
|
102
320
|
return /* @__PURE__ */ new Set();
|
|
103
321
|
}
|
|
104
322
|
const keyList = Array.from(keys);
|
|
105
323
|
if (!keyList.length) {
|
|
106
324
|
return /* @__PURE__ */ new Set();
|
|
107
325
|
}
|
|
108
|
-
const records = await
|
|
326
|
+
const records = await repository.find({
|
|
109
327
|
filter: {
|
|
110
328
|
[`${recordKey}.$in`]: keyList
|
|
111
329
|
}
|
|
@@ -119,43 +337,55 @@ async function collectExistingRecordKeys(ctx, recordKey, target, keys) {
|
|
|
119
337
|
}
|
|
120
338
|
return existingKeys;
|
|
121
339
|
}
|
|
122
|
-
async function recordExistsWithoutScope(
|
|
123
|
-
const
|
|
124
|
-
if (!
|
|
340
|
+
async function recordExistsWithoutScope(collection, recordKey, keyValue) {
|
|
341
|
+
const { repository } = collection;
|
|
342
|
+
if (!repository) {
|
|
125
343
|
return false;
|
|
126
344
|
}
|
|
127
|
-
const record = await
|
|
345
|
+
const record = await repository.findOne({
|
|
128
346
|
filter: {
|
|
129
347
|
[recordKey]: keyValue
|
|
130
348
|
}
|
|
131
349
|
});
|
|
132
350
|
return Boolean(record);
|
|
133
351
|
}
|
|
134
|
-
async function processAssociationChild(
|
|
352
|
+
async function processAssociationChild(options) {
|
|
353
|
+
var _a, _b;
|
|
354
|
+
const {
|
|
355
|
+
value,
|
|
356
|
+
recordKey,
|
|
357
|
+
updateAssociationValues,
|
|
358
|
+
createParams,
|
|
359
|
+
updateParams,
|
|
360
|
+
target,
|
|
361
|
+
fieldPath,
|
|
362
|
+
allowedRecordKeys,
|
|
363
|
+
existingRecordKeys,
|
|
364
|
+
can,
|
|
365
|
+
parseOptions
|
|
366
|
+
} = options;
|
|
135
367
|
const keyValue = value == null ? void 0 : value[recordKey];
|
|
136
368
|
const fallbackToCreate = async () => {
|
|
137
369
|
if (!createParams) {
|
|
138
370
|
return keyValue;
|
|
139
371
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
372
|
+
return await processValues({
|
|
373
|
+
values: value,
|
|
374
|
+
updateAssociationValues,
|
|
375
|
+
aclParams: createParams.params,
|
|
376
|
+
collection: target,
|
|
377
|
+
lastFieldPath: fieldPath,
|
|
378
|
+
protectedKeys: [],
|
|
379
|
+
can,
|
|
380
|
+
parseOptions
|
|
144
381
|
});
|
|
145
|
-
return await processValues(ctx, value, updateAssociationValues, createParams.params, target, fieldPath, []);
|
|
146
382
|
};
|
|
147
383
|
const tryFallbackToCreate = async (reason, knownExists) => {
|
|
148
384
|
if (!createParams) {
|
|
149
385
|
return void 0;
|
|
150
386
|
}
|
|
151
|
-
const recordExists = typeof knownExists === "boolean" ? knownExists : await recordExistsWithoutScope(
|
|
387
|
+
const recordExists = typeof knownExists === "boolean" ? knownExists : await recordExistsWithoutScope(target, recordKey, keyValue);
|
|
152
388
|
if (!recordExists) {
|
|
153
|
-
ctx.log.debug(reason, {
|
|
154
|
-
fieldPath,
|
|
155
|
-
value,
|
|
156
|
-
createParams,
|
|
157
|
-
updateParams
|
|
158
|
-
});
|
|
159
389
|
return await fallbackToCreate();
|
|
160
390
|
}
|
|
161
391
|
return void 0;
|
|
@@ -166,225 +396,76 @@ async function processAssociationChild(ctx, value, recordKey, updateAssociationV
|
|
|
166
396
|
if (created !== void 0) {
|
|
167
397
|
return created;
|
|
168
398
|
}
|
|
169
|
-
ctx.log.debug(`No permission to update association`, { fieldPath, value, updateParams });
|
|
170
399
|
return keyValue;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (allowedRecordKeys) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
return created;
|
|
186
|
-
}
|
|
187
|
-
ctx.log.debug(`No permission to update association due to scope`, { fieldPath, value, updateParams });
|
|
188
|
-
return keyValue;
|
|
189
|
-
}
|
|
190
|
-
} else {
|
|
191
|
-
const filter = await resolveScopeFilter(ctx, target, updateParams.params);
|
|
192
|
-
const record = await repo.findOne({
|
|
193
|
-
filter: {
|
|
194
|
-
...filter,
|
|
195
|
-
[recordKey]: keyValue
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
if (!record) {
|
|
199
|
-
const created = await tryFallbackToCreate(
|
|
200
|
-
`No permission to update association due to scope, try create not exist record`
|
|
201
|
-
);
|
|
202
|
-
if (created !== void 0) {
|
|
203
|
-
return created;
|
|
204
|
-
}
|
|
205
|
-
ctx.log.debug(`No permission to update association due to scope`, { fieldPath, value, updateParams });
|
|
206
|
-
return keyValue;
|
|
400
|
+
}
|
|
401
|
+
const { repository } = target;
|
|
402
|
+
if (!repository) {
|
|
403
|
+
return keyValue;
|
|
404
|
+
}
|
|
405
|
+
try {
|
|
406
|
+
if (allowedRecordKeys) {
|
|
407
|
+
if (!allowedRecordKeys.has(keyValue)) {
|
|
408
|
+
const created = await tryFallbackToCreate(
|
|
409
|
+
`No permission to update association due to scope, try create not exist record`,
|
|
410
|
+
existingRecordKeys ? existingRecordKeys.has(keyValue) : void 0
|
|
411
|
+
);
|
|
412
|
+
if (created !== void 0) {
|
|
413
|
+
return created;
|
|
207
414
|
}
|
|
208
|
-
}
|
|
209
|
-
return await processValues(ctx, value, updateAssociationValues, updateParams.params, target, fieldPath, []);
|
|
210
|
-
} catch (e) {
|
|
211
|
-
if (e instanceof import_acl.NoPermissionError) {
|
|
212
415
|
return keyValue;
|
|
213
416
|
}
|
|
214
|
-
throw e;
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
if (createParams) {
|
|
219
|
-
return await processValues(ctx, value, updateAssociationValues, createParams.params, target, fieldPath, []);
|
|
220
|
-
}
|
|
221
|
-
ctx.log.debug(`No permission to create association`, { fieldPath, value, createParams });
|
|
222
|
-
return null;
|
|
223
|
-
}
|
|
224
|
-
async function processValues(ctx, values, updateAssociationValues, aclParams, collectionName, lastFieldPath = "", protectedKeys = []) {
|
|
225
|
-
var _a;
|
|
226
|
-
if (Array.isArray(values)) {
|
|
227
|
-
const result = [];
|
|
228
|
-
for (const item of values) {
|
|
229
|
-
if (!import_lodash.default.isPlainObject(item)) {
|
|
230
|
-
result.push(item);
|
|
231
|
-
continue;
|
|
232
|
-
}
|
|
233
|
-
const processed = await processValues(
|
|
234
|
-
ctx,
|
|
235
|
-
item,
|
|
236
|
-
updateAssociationValues,
|
|
237
|
-
aclParams,
|
|
238
|
-
collectionName,
|
|
239
|
-
lastFieldPath,
|
|
240
|
-
protectedKeys
|
|
241
|
-
);
|
|
242
|
-
if (processed !== null && processed !== void 0) {
|
|
243
|
-
result.push(processed);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
return result;
|
|
247
|
-
}
|
|
248
|
-
if (!values || !import_lodash.default.isPlainObject(values)) {
|
|
249
|
-
return values;
|
|
250
|
-
}
|
|
251
|
-
const db = ctx.database;
|
|
252
|
-
const collection = db.getCollection(collectionName);
|
|
253
|
-
if (!collection) {
|
|
254
|
-
return values;
|
|
255
|
-
}
|
|
256
|
-
if (aclParams == null ? void 0 : aclParams.whitelist) {
|
|
257
|
-
const combined = import_lodash.default.uniq([...aclParams.whitelist, ...protectedKeys]);
|
|
258
|
-
values = import_lodash.default.pick(values, combined);
|
|
259
|
-
}
|
|
260
|
-
for (const [fieldName, fieldValue] of Object.entries(values)) {
|
|
261
|
-
if (protectedKeys.includes(fieldName)) {
|
|
262
|
-
continue;
|
|
263
|
-
}
|
|
264
|
-
const field = collection.getField(fieldName);
|
|
265
|
-
const isAssociation = field && ["hasOne", "hasMany", "belongsTo", "belongsToMany", "belongsToArray"].includes(field.type);
|
|
266
|
-
if (!isAssociation) {
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
const targetCollection = db.getCollection(field.target);
|
|
270
|
-
if (!targetCollection) {
|
|
271
|
-
delete values[fieldName];
|
|
272
|
-
continue;
|
|
273
|
-
}
|
|
274
|
-
const fieldPath = lastFieldPath ? `${lastFieldPath}.${fieldName}` : fieldName;
|
|
275
|
-
const recordKey = field.type === "hasOne" ? targetCollection.model.primaryKeyAttribute : field.targetKey;
|
|
276
|
-
const canUpdateAssociation = updateAssociationValues.includes(fieldPath);
|
|
277
|
-
if (!canUpdateAssociation) {
|
|
278
|
-
const normalized = normalizeAssociationValue(fieldValue, recordKey);
|
|
279
|
-
if (normalized === void 0 && !protectedKeys.includes(fieldName)) {
|
|
280
|
-
delete values[fieldName];
|
|
281
417
|
} else {
|
|
282
|
-
|
|
418
|
+
(0, import_acl.checkFilterParams)(target, (_a = updateParams.params) == null ? void 0 : _a.filter);
|
|
419
|
+
const filter = await (0, import_acl.parseJsonTemplate)((_b = updateParams.params) == null ? void 0 : _b.filter, parseOptions);
|
|
420
|
+
const record = await repository.findOne({
|
|
421
|
+
filter: {
|
|
422
|
+
...filter,
|
|
423
|
+
[recordKey]: keyValue
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
if (!record) {
|
|
427
|
+
const created = await tryFallbackToCreate(
|
|
428
|
+
`No permission to update association due to scope, try create not exist record`
|
|
429
|
+
);
|
|
430
|
+
if (created !== void 0) {
|
|
431
|
+
return created;
|
|
432
|
+
}
|
|
433
|
+
return keyValue;
|
|
434
|
+
}
|
|
283
435
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
fieldValue,
|
|
436
|
+
return await processValues({
|
|
437
|
+
values: value,
|
|
287
438
|
updateAssociationValues,
|
|
288
|
-
|
|
289
|
-
|
|
439
|
+
aclParams: updateParams.params,
|
|
440
|
+
collection: target,
|
|
441
|
+
lastFieldPath: fieldPath,
|
|
442
|
+
protectedKeys: [],
|
|
443
|
+
can,
|
|
444
|
+
parseOptions
|
|
290
445
|
});
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
roles: ctx.state.currentRoles,
|
|
295
|
-
resource: field.target,
|
|
296
|
-
action: "create"
|
|
297
|
-
});
|
|
298
|
-
const updateParams = ctx.can({
|
|
299
|
-
roles: ctx.state.currentRoles,
|
|
300
|
-
resource: field.target,
|
|
301
|
-
action: "update"
|
|
302
|
-
});
|
|
303
|
-
if (Array.isArray(fieldValue)) {
|
|
304
|
-
const processed = [];
|
|
305
|
-
let allowedRecordKeys;
|
|
306
|
-
let existingRecordKeys;
|
|
307
|
-
if (updateParams) {
|
|
308
|
-
const allowedResult = await collectAllowedRecordKeys(ctx, fieldValue, recordKey, updateParams, field.target);
|
|
309
|
-
allowedRecordKeys = allowedResult == null ? void 0 : allowedResult.allowedKeys;
|
|
310
|
-
if (createParams && ((_a = allowedResult == null ? void 0 : allowedResult.missingKeys) == null ? void 0 : _a.size)) {
|
|
311
|
-
existingRecordKeys = await collectExistingRecordKeys(ctx, recordKey, field.target, allowedResult.missingKeys);
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
for (const item of fieldValue) {
|
|
315
|
-
const r2 = await processAssociationChild(
|
|
316
|
-
ctx,
|
|
317
|
-
item,
|
|
318
|
-
recordKey,
|
|
319
|
-
updateAssociationValues,
|
|
320
|
-
createParams,
|
|
321
|
-
updateParams,
|
|
322
|
-
field.target,
|
|
323
|
-
fieldPath,
|
|
324
|
-
allowedRecordKeys,
|
|
325
|
-
existingRecordKeys
|
|
326
|
-
);
|
|
327
|
-
if (r2 !== null && r2 !== void 0) {
|
|
328
|
-
processed.push(r2);
|
|
329
|
-
}
|
|
446
|
+
} catch (e) {
|
|
447
|
+
if (e instanceof import_acl.NoPermissionError) {
|
|
448
|
+
return keyValue;
|
|
330
449
|
}
|
|
331
|
-
|
|
332
|
-
continue;
|
|
450
|
+
throw e;
|
|
333
451
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
452
|
+
}
|
|
453
|
+
if (createParams) {
|
|
454
|
+
return await processValues({
|
|
455
|
+
values: value,
|
|
338
456
|
updateAssociationValues,
|
|
339
|
-
createParams,
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
457
|
+
aclParams: createParams.params,
|
|
458
|
+
collection: target,
|
|
459
|
+
lastFieldPath: fieldPath,
|
|
460
|
+
protectedKeys: [],
|
|
461
|
+
can,
|
|
462
|
+
parseOptions
|
|
463
|
+
});
|
|
345
464
|
}
|
|
346
|
-
return
|
|
465
|
+
return null;
|
|
347
466
|
}
|
|
348
|
-
const checkChangesWithAssociation = async (ctx, next) => {
|
|
349
|
-
var _a, _b;
|
|
350
|
-
const { resourceName, actionName } = ctx.action;
|
|
351
|
-
if (!["create", "firstOrCreate", "updateOrCreate", "update"].includes(actionName)) {
|
|
352
|
-
return next();
|
|
353
|
-
}
|
|
354
|
-
if ((_a = ctx.permission) == null ? void 0 : _a.skip) {
|
|
355
|
-
return next();
|
|
356
|
-
}
|
|
357
|
-
const roles = ctx.state.currentRoles;
|
|
358
|
-
if (roles.includes("root")) {
|
|
359
|
-
return next();
|
|
360
|
-
}
|
|
361
|
-
const acl = ctx.acl;
|
|
362
|
-
for (const role of roles) {
|
|
363
|
-
const aclRole = acl.getRole(role);
|
|
364
|
-
if (aclRole.snippetAllowed(`${resourceName}:${actionName}`)) {
|
|
365
|
-
return next();
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
const params = ctx.action.params || {};
|
|
369
|
-
const rawValues = params.values;
|
|
370
|
-
if (import_lodash.default.isEmpty(rawValues)) {
|
|
371
|
-
return next();
|
|
372
|
-
}
|
|
373
|
-
const protectedKeys = ["firstOrCreate", "updateOrCreate"].includes(actionName) ? params.filterKeys || [] : [];
|
|
374
|
-
const aclParams = ((_b = ctx.permission.can) == null ? void 0 : _b.params) || ctx.acl.fixedParamsManager.getParams(resourceName, actionName);
|
|
375
|
-
const processed = await processValues(
|
|
376
|
-
ctx,
|
|
377
|
-
rawValues,
|
|
378
|
-
params.updateAssociationValues || [],
|
|
379
|
-
aclParams,
|
|
380
|
-
resourceName,
|
|
381
|
-
"",
|
|
382
|
-
protectedKeys
|
|
383
|
-
);
|
|
384
|
-
ctx.action.params.values = processed;
|
|
385
|
-
await next();
|
|
386
|
-
};
|
|
387
467
|
// Annotate the CommonJS export names for ESM import in node:
|
|
388
468
|
0 && (module.exports = {
|
|
389
|
-
checkChangesWithAssociation
|
|
469
|
+
checkChangesWithAssociation,
|
|
470
|
+
sanitizeAssociationValues
|
|
390
471
|
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This file is part of the NocoBase (R) project.
|
|
3
|
+
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
|
|
4
|
+
* Authors: NocoBase Team.
|
|
5
|
+
*
|
|
6
|
+
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
|
+
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
|
+
*/
|
|
9
|
+
import type { Next } from '@nocobase/actions';
|
|
10
|
+
export declare function checkQueryPermission(ctx: any, next: Next): Promise<void>;
|