@nocobase/plugin-acl 2.1.0-beta.5 → 2.1.0-beta.6
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/dist/externalVersion.js +9 -9
- package/dist/server/middlewares/check-association-operate.js +14 -5
- package/dist/server/middlewares/check-change-with-association.d.ts +20 -0
- package/dist/server/middlewares/check-change-with-association.js +324 -245
- package/dist/server/server.d.ts +6 -1
- package/dist/server/server.js +6 -1
- package/package.json +2 -2
package/dist/externalVersion.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
module.exports = {
|
|
11
|
-
"@nocobase/client": "2.1.0-beta.
|
|
11
|
+
"@nocobase/client": "2.1.0-beta.6",
|
|
12
12
|
"antd": "5.24.2",
|
|
13
13
|
"react": "18.2.0",
|
|
14
14
|
"react-i18next": "11.18.6",
|
|
@@ -17,14 +17,14 @@ module.exports = {
|
|
|
17
17
|
"@formily/react": "2.3.7",
|
|
18
18
|
"@ant-design/icons": "5.6.1",
|
|
19
19
|
"lodash": "4.17.21",
|
|
20
|
-
"@nocobase/utils": "2.1.0-beta.
|
|
21
|
-
"@nocobase/actions": "2.1.0-beta.
|
|
22
|
-
"@nocobase/cache": "2.1.0-beta.
|
|
23
|
-
"@nocobase/database": "2.1.0-beta.
|
|
24
|
-
"@nocobase/server": "2.1.0-beta.
|
|
25
|
-
"@nocobase/
|
|
20
|
+
"@nocobase/utils": "2.1.0-beta.6",
|
|
21
|
+
"@nocobase/actions": "2.1.0-beta.6",
|
|
22
|
+
"@nocobase/cache": "2.1.0-beta.6",
|
|
23
|
+
"@nocobase/database": "2.1.0-beta.6",
|
|
24
|
+
"@nocobase/server": "2.1.0-beta.6",
|
|
25
|
+
"@nocobase/acl": "2.1.0-beta.6",
|
|
26
|
+
"@nocobase/test": "2.1.0-beta.6",
|
|
26
27
|
"@formily/core": "2.3.7",
|
|
27
28
|
"@formily/antd-v5": "1.2.3",
|
|
28
|
-
"antd-style": "3.7.1"
|
|
29
|
-
"@nocobase/acl": "2.1.0-beta.5"
|
|
29
|
+
"antd-style": "3.7.1"
|
|
30
30
|
};
|
|
@@ -31,7 +31,7 @@ __export(check_association_operate_exports, {
|
|
|
31
31
|
module.exports = __toCommonJS(check_association_operate_exports);
|
|
32
32
|
var import_acl = require("@nocobase/acl");
|
|
33
33
|
async function checkAssociationOperate(ctx, next) {
|
|
34
|
-
var _a;
|
|
34
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j;
|
|
35
35
|
const { actionName, resourceName, sourceId } = ctx.action;
|
|
36
36
|
if (!(resourceName.includes(".") && ["add", "set", "remove", "toggle"].includes(actionName))) {
|
|
37
37
|
return next();
|
|
@@ -59,12 +59,21 @@ async function checkAssociationOperate(ctx, next) {
|
|
|
59
59
|
}
|
|
60
60
|
if (params.filter) {
|
|
61
61
|
try {
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
|
|
62
|
+
const timezone = ((_c = (_b = ctx.request) == null ? void 0 : _b.get) == null ? void 0 : _c.call(_b, "x-timezone")) ?? ((_e = (_d = ctx.request) == null ? void 0 : _d.header) == null ? void 0 : _e["x-timezone"]) ?? ((_g = (_f = ctx.req) == null ? void 0 : _f.headers) == null ? void 0 : _g["x-timezone"]);
|
|
63
|
+
const collection = (_i = (_h = ctx.database) == null ? void 0 : _h.getCollection) == null ? void 0 : _i.call(_h, resource);
|
|
64
|
+
(0, import_acl.checkFilterParams)(collection, params.filter);
|
|
65
|
+
const parsedFilter = await (0, import_acl.parseJsonTemplate)(params.filter, {
|
|
66
|
+
state: ctx.state,
|
|
67
|
+
timezone,
|
|
68
|
+
userProvider: (0, import_acl.createUserProvider)({
|
|
69
|
+
db: ctx.db,
|
|
70
|
+
currentUser: (_j = ctx.state) == null ? void 0 : _j.currentUser
|
|
71
|
+
})
|
|
72
|
+
});
|
|
73
|
+
const repo = ctx.database.getRepository(resource);
|
|
65
74
|
const record = await repo.findOne({
|
|
66
75
|
filterByTk: sourceId,
|
|
67
|
-
filter:
|
|
76
|
+
filter: parsedFilter ?? params.filter
|
|
68
77
|
});
|
|
69
78
|
if (!record) {
|
|
70
79
|
ctx.throw(403, "No permissions");
|
|
@@ -6,5 +6,25 @@
|
|
|
6
6
|
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
|
|
7
7
|
* For more information, please refer to: https://www.nocobase.com/agreement.
|
|
8
8
|
*/
|
|
9
|
+
import { ACL, UserProvider } from '@nocobase/acl';
|
|
9
10
|
import { Context, Next } from '@nocobase/actions';
|
|
11
|
+
import { Collection } from '@nocobase/database';
|
|
12
|
+
export type SanitizeAssociationValuesOptions = {
|
|
13
|
+
acl?: ACL;
|
|
14
|
+
resourceName: string;
|
|
15
|
+
actionName: string;
|
|
16
|
+
values: any;
|
|
17
|
+
updateAssociationValues?: string[];
|
|
18
|
+
protectedKeys?: string[];
|
|
19
|
+
aclParams?: any;
|
|
20
|
+
roles?: string[];
|
|
21
|
+
currentRole?: string;
|
|
22
|
+
currentUser?: any;
|
|
23
|
+
collection?: Collection;
|
|
24
|
+
db?: any;
|
|
25
|
+
database?: any;
|
|
26
|
+
timezone?: string;
|
|
27
|
+
userProvider?: UserProvider;
|
|
28
|
+
};
|
|
29
|
+
export declare function sanitizeAssociationValues(options: SanitizeAssociationValuesOptions): Promise<any>;
|
|
10
30
|
export declare const checkChangesWithAssociation: (ctx: Context, next: Next) => Promise<any>;
|
|
@@ -36,11 +36,235 @@ 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
|
+
currentRole: options.currentRole,
|
|
71
|
+
currentRoles: options.roles,
|
|
72
|
+
currentUser: options.currentUser
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
return await processValues({
|
|
76
|
+
values,
|
|
77
|
+
updateAssociationValues,
|
|
78
|
+
aclParams: params,
|
|
79
|
+
collection,
|
|
80
|
+
lastFieldPath: "",
|
|
81
|
+
protectedKeys,
|
|
82
|
+
can,
|
|
83
|
+
parseOptions
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const checkChangesWithAssociation = async (ctx, next) => {
|
|
87
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m;
|
|
88
|
+
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"]);
|
|
89
|
+
const { resourceName, actionName } = ctx.action;
|
|
90
|
+
if (!["create", "firstOrCreate", "updateOrCreate", "update"].includes(actionName)) {
|
|
91
|
+
return next();
|
|
92
|
+
}
|
|
93
|
+
if ((_g = ctx.permission) == null ? void 0 : _g.skip) {
|
|
94
|
+
return next();
|
|
95
|
+
}
|
|
96
|
+
const roles = ctx.state.currentRoles;
|
|
97
|
+
if (roles.includes("root")) {
|
|
98
|
+
return next();
|
|
99
|
+
}
|
|
100
|
+
const acl = ctx.acl;
|
|
101
|
+
for (const role of roles) {
|
|
102
|
+
const aclRole = acl.getRole(role);
|
|
103
|
+
if (aclRole.snippetAllowed(`${resourceName}:${actionName}`)) {
|
|
104
|
+
return next();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const params = ctx.action.params || {};
|
|
108
|
+
const rawValues = params.values;
|
|
109
|
+
if (import_lodash.default.isEmpty(rawValues)) {
|
|
110
|
+
return next();
|
|
111
|
+
}
|
|
112
|
+
const protectedKeys = ["firstOrCreate", "updateOrCreate"].includes(actionName) ? params.filterKeys || [] : [];
|
|
113
|
+
const collection = (_i = (_h = ctx.database ?? ctx.db) == null ? void 0 : _h.getCollection) == null ? void 0 : _i.call(_h, resourceName);
|
|
114
|
+
const processed = await sanitizeAssociationValues({
|
|
115
|
+
acl,
|
|
116
|
+
collection,
|
|
117
|
+
resourceName,
|
|
118
|
+
actionName,
|
|
119
|
+
values: rawValues,
|
|
120
|
+
updateAssociationValues: params.updateAssociationValues || [],
|
|
121
|
+
protectedKeys,
|
|
122
|
+
roles,
|
|
123
|
+
currentRole: ctx.state.currentRole,
|
|
124
|
+
currentUser: ctx.state.currentUser,
|
|
125
|
+
aclParams: (_k = (_j = ctx.permission) == null ? void 0 : _j.can) == null ? void 0 : _k.params,
|
|
126
|
+
timezone,
|
|
127
|
+
userProvider: (0, import_acl.createUserProvider)({
|
|
128
|
+
dataSourceManager: (_l = ctx.app) == null ? void 0 : _l.dataSourceManager,
|
|
129
|
+
currentUser: (_m = ctx.state) == null ? void 0 : _m.currentUser
|
|
130
|
+
})
|
|
131
|
+
});
|
|
132
|
+
ctx.action.params.values = processed;
|
|
133
|
+
await next();
|
|
134
|
+
};
|
|
135
|
+
async function processValues(options) {
|
|
136
|
+
var _a, _b;
|
|
137
|
+
const {
|
|
138
|
+
values,
|
|
139
|
+
updateAssociationValues,
|
|
140
|
+
aclParams,
|
|
141
|
+
collection,
|
|
142
|
+
lastFieldPath = "",
|
|
143
|
+
protectedKeys = [],
|
|
144
|
+
can,
|
|
145
|
+
parseOptions
|
|
146
|
+
} = options;
|
|
147
|
+
if (Array.isArray(values)) {
|
|
148
|
+
const result = [];
|
|
149
|
+
for (const item of values) {
|
|
150
|
+
if (!import_lodash.default.isPlainObject(item)) {
|
|
151
|
+
result.push(item);
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
const processed = await processValues({
|
|
155
|
+
values: item,
|
|
156
|
+
updateAssociationValues,
|
|
157
|
+
aclParams,
|
|
158
|
+
collection,
|
|
159
|
+
lastFieldPath,
|
|
160
|
+
protectedKeys,
|
|
161
|
+
can,
|
|
162
|
+
parseOptions
|
|
163
|
+
});
|
|
164
|
+
if (processed !== null && processed !== void 0) {
|
|
165
|
+
result.push(processed);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return result;
|
|
169
|
+
}
|
|
170
|
+
if (!values || !import_lodash.default.isPlainObject(values)) {
|
|
171
|
+
return values;
|
|
172
|
+
}
|
|
173
|
+
if (!collection) {
|
|
174
|
+
return values;
|
|
175
|
+
}
|
|
176
|
+
let v = values;
|
|
177
|
+
if (aclParams == null ? void 0 : aclParams.whitelist) {
|
|
178
|
+
const combined = import_lodash.default.uniq([...aclParams.whitelist, ...protectedKeys]);
|
|
179
|
+
v = import_lodash.default.pick(values, combined);
|
|
180
|
+
}
|
|
181
|
+
for (const [fieldName, fieldValue] of Object.entries(v)) {
|
|
182
|
+
if (protectedKeys.includes(fieldName)) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const field = collection.getField(fieldName);
|
|
186
|
+
const isAssociation = field && ["hasOne", "hasMany", "belongsTo", "belongsToMany", "belongsToArray"].includes(field.type);
|
|
187
|
+
if (!isAssociation) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const targetCollection = collection.db.getCollection(field.target);
|
|
191
|
+
if (!targetCollection) {
|
|
192
|
+
delete v[fieldName];
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const fieldPath = lastFieldPath ? `${lastFieldPath}.${fieldName}` : fieldName;
|
|
196
|
+
const recordKey = field.type === "hasOne" ? targetCollection.model.primaryKeyAttribute : field.targetKey;
|
|
197
|
+
const canUpdateAssociation = updateAssociationValues.includes(fieldPath);
|
|
198
|
+
if (!canUpdateAssociation) {
|
|
199
|
+
const normalized = normalizeAssociationValue(fieldValue, recordKey);
|
|
200
|
+
if (normalized === void 0 && !protectedKeys.includes(fieldName)) {
|
|
201
|
+
delete v[fieldName];
|
|
202
|
+
} else {
|
|
203
|
+
v[fieldName] = normalized;
|
|
204
|
+
}
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
const createParams = can == null ? void 0 : can({
|
|
208
|
+
resource: field.target,
|
|
209
|
+
action: "create"
|
|
210
|
+
});
|
|
211
|
+
const updateParams = can == null ? void 0 : can({
|
|
212
|
+
resource: field.target,
|
|
213
|
+
action: "update"
|
|
214
|
+
});
|
|
215
|
+
if (Array.isArray(fieldValue)) {
|
|
216
|
+
const processed = [];
|
|
217
|
+
let allowedRecordKeys;
|
|
218
|
+
let existingRecordKeys;
|
|
219
|
+
if (updateParams) {
|
|
220
|
+
const allowedResult = await collectAllowedRecordKeys(
|
|
221
|
+
fieldValue,
|
|
222
|
+
recordKey,
|
|
223
|
+
(_a = updateParams == null ? void 0 : updateParams.params) == null ? void 0 : _a.filter,
|
|
224
|
+
targetCollection,
|
|
225
|
+
parseOptions
|
|
226
|
+
);
|
|
227
|
+
allowedRecordKeys = allowedResult == null ? void 0 : allowedResult.allowedKeys;
|
|
228
|
+
if (createParams && ((_b = allowedResult == null ? void 0 : allowedResult.missingKeys) == null ? void 0 : _b.size)) {
|
|
229
|
+
existingRecordKeys = await collectExistingRecordKeys(recordKey, targetCollection, allowedResult.missingKeys);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
for (const item of fieldValue) {
|
|
233
|
+
const r2 = await processAssociationChild({
|
|
234
|
+
value: item,
|
|
235
|
+
recordKey,
|
|
236
|
+
updateAssociationValues,
|
|
237
|
+
createParams,
|
|
238
|
+
updateParams,
|
|
239
|
+
target: targetCollection,
|
|
240
|
+
fieldPath,
|
|
241
|
+
allowedRecordKeys,
|
|
242
|
+
existingRecordKeys,
|
|
243
|
+
can,
|
|
244
|
+
parseOptions
|
|
245
|
+
});
|
|
246
|
+
if (r2 !== null && r2 !== void 0) {
|
|
247
|
+
processed.push(r2);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
v[fieldName] = processed;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const r = await processAssociationChild({
|
|
254
|
+
value: fieldValue,
|
|
255
|
+
recordKey,
|
|
256
|
+
updateAssociationValues,
|
|
257
|
+
createParams,
|
|
258
|
+
updateParams,
|
|
259
|
+
target: targetCollection,
|
|
260
|
+
fieldPath,
|
|
261
|
+
can,
|
|
262
|
+
parseOptions
|
|
263
|
+
});
|
|
264
|
+
v[fieldName] = r;
|
|
265
|
+
}
|
|
266
|
+
return v;
|
|
267
|
+
}
|
|
44
268
|
function normalizeAssociationValue(value, recordKey) {
|
|
45
269
|
if (!value) {
|
|
46
270
|
return value;
|
|
@@ -48,30 +272,22 @@ function normalizeAssociationValue(value, recordKey) {
|
|
|
48
272
|
if (Array.isArray(value)) {
|
|
49
273
|
const result = value.map((v) => typeof v === "number" || typeof v === "string" ? v : v[recordKey]).filter((v) => v !== null && v !== void 0);
|
|
50
274
|
return result.length > 0 ? result : void 0;
|
|
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 {};
|
|
58
275
|
}
|
|
59
|
-
|
|
60
|
-
const parsedParams = await ctx.acl.parseJsonTemplate(filteredParams, ctx);
|
|
61
|
-
return parsedParams.filter || {};
|
|
276
|
+
return typeof value === "number" || typeof value === "string" ? value : value[recordKey];
|
|
62
277
|
}
|
|
63
|
-
async function collectAllowedRecordKeys(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return void 0;
|
|
278
|
+
async function collectAllowedRecordKeys(items, recordKey, filter, collection, parseOptions) {
|
|
279
|
+
if (!collection) {
|
|
280
|
+
return;
|
|
67
281
|
}
|
|
282
|
+
const { repository } = collection;
|
|
68
283
|
const keys = items.map((item) => import_lodash.default.isPlainObject(item) ? item[recordKey] : void 0).filter((key) => key !== void 0 && key !== null);
|
|
69
284
|
if (!keys.length) {
|
|
70
|
-
return
|
|
285
|
+
return;
|
|
71
286
|
}
|
|
72
287
|
try {
|
|
73
|
-
|
|
74
|
-
const
|
|
288
|
+
(0, import_acl.checkFilterParams)(collection, filter);
|
|
289
|
+
const scopedFilter = filter ? await (0, import_acl.parseJsonTemplate)(filter, parseOptions) : {};
|
|
290
|
+
const records = await repository.find({
|
|
75
291
|
filter: {
|
|
76
292
|
...scopedFilter,
|
|
77
293
|
[`${recordKey}.$in`]: keys
|
|
@@ -96,16 +312,16 @@ async function collectAllowedRecordKeys(ctx, items, recordKey, updateParams, tar
|
|
|
96
312
|
throw e;
|
|
97
313
|
}
|
|
98
314
|
}
|
|
99
|
-
async function collectExistingRecordKeys(
|
|
100
|
-
const
|
|
101
|
-
if (!
|
|
315
|
+
async function collectExistingRecordKeys(recordKey, collection, keys) {
|
|
316
|
+
const { repository } = collection;
|
|
317
|
+
if (!repository) {
|
|
102
318
|
return /* @__PURE__ */ new Set();
|
|
103
319
|
}
|
|
104
320
|
const keyList = Array.from(keys);
|
|
105
321
|
if (!keyList.length) {
|
|
106
322
|
return /* @__PURE__ */ new Set();
|
|
107
323
|
}
|
|
108
|
-
const records = await
|
|
324
|
+
const records = await repository.find({
|
|
109
325
|
filter: {
|
|
110
326
|
[`${recordKey}.$in`]: keyList
|
|
111
327
|
}
|
|
@@ -119,43 +335,55 @@ async function collectExistingRecordKeys(ctx, recordKey, target, keys) {
|
|
|
119
335
|
}
|
|
120
336
|
return existingKeys;
|
|
121
337
|
}
|
|
122
|
-
async function recordExistsWithoutScope(
|
|
123
|
-
const
|
|
124
|
-
if (!
|
|
338
|
+
async function recordExistsWithoutScope(collection, recordKey, keyValue) {
|
|
339
|
+
const { repository } = collection;
|
|
340
|
+
if (!repository) {
|
|
125
341
|
return false;
|
|
126
342
|
}
|
|
127
|
-
const record = await
|
|
343
|
+
const record = await repository.findOne({
|
|
128
344
|
filter: {
|
|
129
345
|
[recordKey]: keyValue
|
|
130
346
|
}
|
|
131
347
|
});
|
|
132
348
|
return Boolean(record);
|
|
133
349
|
}
|
|
134
|
-
async function processAssociationChild(
|
|
350
|
+
async function processAssociationChild(options) {
|
|
351
|
+
var _a, _b;
|
|
352
|
+
const {
|
|
353
|
+
value,
|
|
354
|
+
recordKey,
|
|
355
|
+
updateAssociationValues,
|
|
356
|
+
createParams,
|
|
357
|
+
updateParams,
|
|
358
|
+
target,
|
|
359
|
+
fieldPath,
|
|
360
|
+
allowedRecordKeys,
|
|
361
|
+
existingRecordKeys,
|
|
362
|
+
can,
|
|
363
|
+
parseOptions
|
|
364
|
+
} = options;
|
|
135
365
|
const keyValue = value == null ? void 0 : value[recordKey];
|
|
136
366
|
const fallbackToCreate = async () => {
|
|
137
367
|
if (!createParams) {
|
|
138
368
|
return keyValue;
|
|
139
369
|
}
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
370
|
+
return await processValues({
|
|
371
|
+
values: value,
|
|
372
|
+
updateAssociationValues,
|
|
373
|
+
aclParams: createParams.params,
|
|
374
|
+
collection: target,
|
|
375
|
+
lastFieldPath: fieldPath,
|
|
376
|
+
protectedKeys: [],
|
|
377
|
+
can,
|
|
378
|
+
parseOptions
|
|
144
379
|
});
|
|
145
|
-
return await processValues(ctx, value, updateAssociationValues, createParams.params, target, fieldPath, []);
|
|
146
380
|
};
|
|
147
381
|
const tryFallbackToCreate = async (reason, knownExists) => {
|
|
148
382
|
if (!createParams) {
|
|
149
383
|
return void 0;
|
|
150
384
|
}
|
|
151
|
-
const recordExists = typeof knownExists === "boolean" ? knownExists : await recordExistsWithoutScope(
|
|
385
|
+
const recordExists = typeof knownExists === "boolean" ? knownExists : await recordExistsWithoutScope(target, recordKey, keyValue);
|
|
152
386
|
if (!recordExists) {
|
|
153
|
-
ctx.log.debug(reason, {
|
|
154
|
-
fieldPath,
|
|
155
|
-
value,
|
|
156
|
-
createParams,
|
|
157
|
-
updateParams
|
|
158
|
-
});
|
|
159
387
|
return await fallbackToCreate();
|
|
160
388
|
}
|
|
161
389
|
return void 0;
|
|
@@ -166,225 +394,76 @@ async function processAssociationChild(ctx, value, recordKey, updateAssociationV
|
|
|
166
394
|
if (created !== void 0) {
|
|
167
395
|
return created;
|
|
168
396
|
}
|
|
169
|
-
ctx.log.debug(`No permission to update association`, { fieldPath, value, updateParams });
|
|
170
397
|
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;
|
|
398
|
+
}
|
|
399
|
+
const { repository } = target;
|
|
400
|
+
if (!repository) {
|
|
401
|
+
return keyValue;
|
|
402
|
+
}
|
|
403
|
+
try {
|
|
404
|
+
if (allowedRecordKeys) {
|
|
405
|
+
if (!allowedRecordKeys.has(keyValue)) {
|
|
406
|
+
const created = await tryFallbackToCreate(
|
|
407
|
+
`No permission to update association due to scope, try create not exist record`,
|
|
408
|
+
existingRecordKeys ? existingRecordKeys.has(keyValue) : void 0
|
|
409
|
+
);
|
|
410
|
+
if (created !== void 0) {
|
|
411
|
+
return created;
|
|
207
412
|
}
|
|
208
|
-
}
|
|
209
|
-
return await processValues(ctx, value, updateAssociationValues, updateParams.params, target, fieldPath, []);
|
|
210
|
-
} catch (e) {
|
|
211
|
-
if (e instanceof import_acl.NoPermissionError) {
|
|
212
413
|
return keyValue;
|
|
213
414
|
}
|
|
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
415
|
} else {
|
|
282
|
-
|
|
416
|
+
(0, import_acl.checkFilterParams)(target, (_a = updateParams.params) == null ? void 0 : _a.filter);
|
|
417
|
+
const filter = await (0, import_acl.parseJsonTemplate)((_b = updateParams.params) == null ? void 0 : _b.filter, parseOptions);
|
|
418
|
+
const record = await repository.findOne({
|
|
419
|
+
filter: {
|
|
420
|
+
...filter,
|
|
421
|
+
[recordKey]: keyValue
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
if (!record) {
|
|
425
|
+
const created = await tryFallbackToCreate(
|
|
426
|
+
`No permission to update association due to scope, try create not exist record`
|
|
427
|
+
);
|
|
428
|
+
if (created !== void 0) {
|
|
429
|
+
return created;
|
|
430
|
+
}
|
|
431
|
+
return keyValue;
|
|
432
|
+
}
|
|
283
433
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
fieldValue,
|
|
434
|
+
return await processValues({
|
|
435
|
+
values: value,
|
|
287
436
|
updateAssociationValues,
|
|
288
|
-
|
|
289
|
-
|
|
437
|
+
aclParams: updateParams.params,
|
|
438
|
+
collection: target,
|
|
439
|
+
lastFieldPath: fieldPath,
|
|
440
|
+
protectedKeys: [],
|
|
441
|
+
can,
|
|
442
|
+
parseOptions
|
|
290
443
|
});
|
|
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
|
-
}
|
|
444
|
+
} catch (e) {
|
|
445
|
+
if (e instanceof import_acl.NoPermissionError) {
|
|
446
|
+
return keyValue;
|
|
330
447
|
}
|
|
331
|
-
|
|
332
|
-
continue;
|
|
448
|
+
throw e;
|
|
333
449
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
450
|
+
}
|
|
451
|
+
if (createParams) {
|
|
452
|
+
return await processValues({
|
|
453
|
+
values: value,
|
|
338
454
|
updateAssociationValues,
|
|
339
|
-
createParams,
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
455
|
+
aclParams: createParams.params,
|
|
456
|
+
collection: target,
|
|
457
|
+
lastFieldPath: fieldPath,
|
|
458
|
+
protectedKeys: [],
|
|
459
|
+
can,
|
|
460
|
+
parseOptions
|
|
461
|
+
});
|
|
345
462
|
}
|
|
346
|
-
return
|
|
463
|
+
return null;
|
|
347
464
|
}
|
|
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
465
|
// Annotate the CommonJS export names for ESM import in node:
|
|
388
466
|
0 && (module.exports = {
|
|
389
|
-
checkChangesWithAssociation
|
|
467
|
+
checkChangesWithAssociation,
|
|
468
|
+
sanitizeAssociationValues
|
|
390
469
|
});
|
package/dist/server/server.d.ts
CHANGED
|
@@ -11,8 +11,13 @@ import { Plugin } from '@nocobase/server';
|
|
|
11
11
|
import { RoleModel } from './model/RoleModel';
|
|
12
12
|
import { RoleResourceActionModel } from './model/RoleResourceActionModel';
|
|
13
13
|
import { RoleResourceModel } from './model/RoleResourceModel';
|
|
14
|
+
import { SanitizeAssociationValuesOptions } from './middlewares/check-change-with-association';
|
|
15
|
+
import type { ACL } from '@nocobase/acl';
|
|
14
16
|
export declare class PluginACLServer extends Plugin {
|
|
15
|
-
get acl():
|
|
17
|
+
get acl(): ACL;
|
|
18
|
+
sanitizeAssociationValues(options: SanitizeAssociationValuesOptions & {
|
|
19
|
+
acl?: ACL;
|
|
20
|
+
}): Promise<any>;
|
|
16
21
|
writeResourceToACL(resourceModel: RoleResourceModel, transaction: Transaction): Promise<void>;
|
|
17
22
|
writeActionToACL(actionModel: RoleResourceActionModel, transaction: Transaction): Promise<void>;
|
|
18
23
|
handleSyncMessage(message: any): Promise<void>;
|
package/dist/server/server.js
CHANGED
|
@@ -61,6 +61,12 @@ class PluginACLServer extends import_server.Plugin {
|
|
|
61
61
|
get acl() {
|
|
62
62
|
return this.app.acl;
|
|
63
63
|
}
|
|
64
|
+
async sanitizeAssociationValues(options) {
|
|
65
|
+
return (0, import_check_change_with_association.sanitizeAssociationValues)({
|
|
66
|
+
...options,
|
|
67
|
+
acl: options.acl ?? this.acl
|
|
68
|
+
});
|
|
69
|
+
}
|
|
64
70
|
async writeResourceToACL(resourceModel, transaction) {
|
|
65
71
|
await resourceModel.writeToACL({
|
|
66
72
|
acl: this.acl,
|
|
@@ -505,7 +511,6 @@ class PluginACLServer extends import_server.Plugin {
|
|
|
505
511
|
}
|
|
506
512
|
return next();
|
|
507
513
|
});
|
|
508
|
-
const parseJsonTemplate = this.app.acl.parseJsonTemplate;
|
|
509
514
|
this.app.acl.beforeGrantAction(async (ctx) => {
|
|
510
515
|
const actionName = this.app.acl.resolveActionAlias(ctx.actionName);
|
|
511
516
|
if (import_lodash.default.isPlainObject(ctx.params)) {
|
package/package.json
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"description": "Based on roles, resources, and actions, access control can precisely manage interface configuration permissions, data operation permissions, menu access permissions, and plugin permissions.",
|
|
7
7
|
"description.ru-RU": "На основе ролей, ресурсов и действий система контроля доступа может точно управлять разрешениями на изменение интерфейса, работу с данными, доступ к меню и разрешениями для подключаемых модулей.",
|
|
8
8
|
"description.zh-CN": "基于角色、资源和操作的权限控制,可以精确控制界面配置权限、数据操作权限、菜单访问权限、插件权限。",
|
|
9
|
-
"version": "2.1.0-beta.
|
|
9
|
+
"version": "2.1.0-beta.6",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"main": "./dist/server/index.js",
|
|
12
12
|
"homepage": "https://docs.nocobase.com/handbook/acl",
|
|
@@ -44,5 +44,5 @@
|
|
|
44
44
|
"url": "git+https://github.com/nocobase/nocobase.git",
|
|
45
45
|
"directory": "packages/plugins/acl"
|
|
46
46
|
},
|
|
47
|
-
"gitHead": "
|
|
47
|
+
"gitHead": "439e45f32ee5f34d771e7f4751ef57eb7d4a82a8"
|
|
48
48
|
}
|