@nocobase/plugin-acl 2.0.0-alpha.9 → 2.0.0-beta.10
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/client/index.js +1 -1
- package/dist/externalVersion.js +8 -8
- package/dist/locale/de-DE.json +18 -7
- package/dist/locale/en-US.json +17 -13
- package/dist/locale/es-ES.json +22 -0
- package/dist/locale/fr-FR.json +22 -0
- package/dist/locale/hu-HU.json +22 -0
- package/dist/locale/id-ID.json +22 -0
- package/dist/locale/it-IT.json +18 -7
- package/dist/locale/ja-JP.json +18 -6
- package/dist/locale/ko-KR.json +21 -4
- package/dist/locale/nl-NL.json +20 -9
- package/dist/locale/pt-BR.json +22 -0
- package/dist/locale/ru-RU.json +22 -0
- package/dist/locale/tr-TR.json +22 -0
- package/dist/locale/uk-UA.json +22 -0
- package/dist/locale/vi-VN.json +22 -0
- package/dist/locale/zh-CN.json +15 -15
- package/dist/locale/zh-TW.json +22 -0
- package/dist/server/actions/user-setDefaultRole.js +5 -2
- package/dist/server/collections/roles.js +1 -0
- package/dist/server/middlewares/check-association-operate.d.ts +10 -0
- package/dist/server/middlewares/check-association-operate.js +88 -0
- package/dist/server/middlewares/check-change-with-association.d.ts +10 -0
- package/dist/server/middlewares/check-change-with-association.js +390 -0
- package/dist/server/middlewares/with-acl-meta.js +3 -0
- package/dist/server/migrations/20251119225252-update-member-default-permission.d.ts +14 -0
- package/dist/server/migrations/20251119225252-update-member-default-permission.js +59 -0
- package/dist/server/server.js +28 -3
- package/package.json +8 -2
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Allow roles union": "Allow roles union",
|
|
3
|
+
"Allow users to use role union, which means they can use permissions from all their roles simultaneously, or switch between individual roles.": "Allow users to use role union, which means they can use permissions from all their roles simultaneously, or switch between individual roles.",
|
|
4
|
+
"Data sources": "Data sources",
|
|
5
|
+
"Desktop menu": "Desktop menu",
|
|
6
|
+
"Do not use role union. Users need to switch between their roles individually.": "Do not use role union. Users need to switch between their roles individually.",
|
|
7
|
+
"Force users to use only role union. They cannot switch between individual roles.": "Force users to use only role union. They cannot switch between individual roles.",
|
|
8
|
+
"Full permissions": "Full permissions",
|
|
9
|
+
"General": "General",
|
|
10
|
+
"Independent roles": "Independent roles",
|
|
11
|
+
"New role": "New role",
|
|
12
|
+
"Permissions": "Permissions",
|
|
13
|
+
"Please select role mode": "Please select role mode",
|
|
14
|
+
"Plugin settings": "Plugin settings",
|
|
15
|
+
"Role mode": "Role mode",
|
|
16
|
+
"Role mode doc": "https://docs.nocobase.com/handbook/acl/manual",
|
|
17
|
+
"Roles & Permissions": "Roles & Permissions",
|
|
18
|
+
"Roles union only": "Roles union only",
|
|
19
|
+
"Saved successfully": "Saved successfully",
|
|
20
|
+
"The current user has no roles. Please try another account.": "The current user has no roles. Please try another account.",
|
|
21
|
+
"The user role does not exist. Please try signing in again": "The user role does not exist. Please try signing in again"
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Allow roles union": "Allow roles union",
|
|
3
|
+
"Allow users to use role union, which means they can use permissions from all their roles simultaneously, or switch between individual roles.": "Allow users to use role union, which means they can use permissions from all their roles simultaneously, or switch between individual roles.",
|
|
4
|
+
"Data sources": "Data sources",
|
|
5
|
+
"Desktop menu": "Desktop menu",
|
|
6
|
+
"Do not use role union. Users need to switch between their roles individually.": "Do not use role union. Users need to switch between their roles individually.",
|
|
7
|
+
"Force users to use only role union. They cannot switch between individual roles.": "Force users to use only role union. They cannot switch between individual roles.",
|
|
8
|
+
"Full permissions": "Full permissions",
|
|
9
|
+
"General": "Tổng quát",
|
|
10
|
+
"Independent roles": "Independent roles",
|
|
11
|
+
"New role": "New role",
|
|
12
|
+
"Permissions": "Quyền",
|
|
13
|
+
"Please select role mode": "Please select role mode",
|
|
14
|
+
"Plugin settings": "Plugin settings",
|
|
15
|
+
"Role mode": "Role mode",
|
|
16
|
+
"Role mode doc": "https://docs.nocobase.com/handbook/acl/manual",
|
|
17
|
+
"Roles & Permissions": "Roles & Permissions",
|
|
18
|
+
"Roles union only": "Roles union only",
|
|
19
|
+
"Saved successfully": "Saved successfully",
|
|
20
|
+
"The current user has no roles. Please try another account.": "The current user has no roles. Please try another account.",
|
|
21
|
+
"The user role does not exist. Please try signing in again": "The user role does not exist. Please try signing in again"
|
|
22
|
+
}
|
package/dist/locale/zh-CN.json
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
{
|
|
2
|
-
"
|
|
3
|
-
"
|
|
2
|
+
"Allow roles union": "允许角色并集",
|
|
3
|
+
"Allow users to use role union, which means they can use permissions from all their roles simultaneously, or switch between individual roles.": "允许用户使用角色并集,即可以同时使用自己拥有的所有角色的权限,也允许用户逐个切换自己的角色。",
|
|
4
|
+
"Data sources": "数据源",
|
|
5
|
+
"Desktop menu": "桌面端菜单",
|
|
6
|
+
"Do not use role union. Users need to switch between their roles individually.": "不使用角色并集,用户需要逐个切换自己拥有的角色。",
|
|
7
|
+
"Force users to use only role union. They cannot switch between individual roles.": "强制用户仅能使用角色并集,不能逐个切换角色。",
|
|
8
|
+
"Full permissions": "全部权限",
|
|
9
|
+
"General": "通用",
|
|
10
|
+
"Independent roles": "独立角色",
|
|
4
11
|
"New role": "新建角色",
|
|
5
12
|
"Permissions": "权限",
|
|
6
|
-
"
|
|
7
|
-
"General": "通用",
|
|
8
|
-
"Desktop menu": "桌面端菜单",
|
|
13
|
+
"Please select role mode": "请选择角色模式",
|
|
9
14
|
"Plugin settings": "插件设置",
|
|
10
|
-
"Data sources": "数据源",
|
|
11
|
-
"Independent roles": "独立角色",
|
|
12
|
-
"Allow roles union": "允许角色并集",
|
|
13
|
-
"Roles union only": "仅角色并集",
|
|
14
15
|
"Role mode": "角色模式",
|
|
15
|
-
"Saved successfully": "保存成功",
|
|
16
|
-
"Please select role mode": "请选择角色模式",
|
|
17
|
-
"Full permissions": "全部权限",
|
|
18
16
|
"Role mode doc": "https://docs-cn.nocobase.com/handbook/acl/manual",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
17
|
+
"Roles & Permissions": "角色和权限",
|
|
18
|
+
"Roles union only": "仅角色并集",
|
|
19
|
+
"Saved successfully": "保存成功",
|
|
20
|
+
"The current user has no roles. Please try another account.": "当前用户没有角色,请使用其他账号。",
|
|
21
|
+
"The user role does not exist. Please try signing in again": "用户角色不存在,请尝试重新登录。"
|
|
22
22
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"Allow roles union": "Allow roles union",
|
|
3
|
+
"Allow users to use role union, which means they can use permissions from all their roles simultaneously, or switch between individual roles.": "Allow users to use role union, which means they can use permissions from all their roles simultaneously, or switch between individual roles.",
|
|
4
|
+
"Data sources": "Data sources",
|
|
5
|
+
"Desktop menu": "Desktop menu",
|
|
6
|
+
"Do not use role union. Users need to switch between their roles individually.": "Do not use role union. Users need to switch between their roles individually.",
|
|
7
|
+
"Force users to use only role union. They cannot switch between individual roles.": "Force users to use only role union. They cannot switch between individual roles.",
|
|
8
|
+
"Full permissions": "Full permissions",
|
|
9
|
+
"General": "General",
|
|
10
|
+
"Independent roles": "Independent roles",
|
|
11
|
+
"New role": "New role",
|
|
12
|
+
"Permissions": "Permissions",
|
|
13
|
+
"Please select role mode": "Please select role mode",
|
|
14
|
+
"Plugin settings": "Plugin settings",
|
|
15
|
+
"Role mode": "Role mode",
|
|
16
|
+
"Role mode doc": "https://docs.nocobase.com/handbook/acl/manual",
|
|
17
|
+
"Roles & Permissions": "Roles & Permissions",
|
|
18
|
+
"Roles union only": "Roles union only",
|
|
19
|
+
"Saved successfully": "Saved successfully",
|
|
20
|
+
"The current user has no roles. Please try another account.": "The current user has no roles. Please try another account.",
|
|
21
|
+
"The user role does not exist. Please try signing in again": "The user role does not exist. Please try signing in again"
|
|
22
|
+
}
|
|
@@ -29,6 +29,7 @@ __export(user_setDefaultRole_exports, {
|
|
|
29
29
|
setDefaultRole: () => setDefaultRole
|
|
30
30
|
});
|
|
31
31
|
module.exports = __toCommonJS(user_setDefaultRole_exports);
|
|
32
|
+
var import_constants = require("../constants");
|
|
32
33
|
async function setDefaultRole(ctx, next) {
|
|
33
34
|
const {
|
|
34
35
|
values: { roleName }
|
|
@@ -72,7 +73,7 @@ async function setDefaultRole(ctx, next) {
|
|
|
72
73
|
if (targetUserRole) {
|
|
73
74
|
await repository.model.update({ default: true }, { where: { userId: currentUser.id, roleName }, transaction });
|
|
74
75
|
model = targetUserRole.set("default", true);
|
|
75
|
-
} else {
|
|
76
|
+
} else if (roleName === import_constants.UNION_ROLE_KEY) {
|
|
76
77
|
model = await repository.create({
|
|
77
78
|
values: {
|
|
78
79
|
userId: currentUser.id,
|
|
@@ -82,7 +83,9 @@ async function setDefaultRole(ctx, next) {
|
|
|
82
83
|
transaction
|
|
83
84
|
});
|
|
84
85
|
}
|
|
85
|
-
|
|
86
|
+
if (model) {
|
|
87
|
+
db.emitAsync("rolesUsers.afterSave", model);
|
|
88
|
+
}
|
|
86
89
|
});
|
|
87
90
|
ctx.body = "ok";
|
|
88
91
|
await next();
|
|
@@ -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 { Context, Next } from '@nocobase/actions';
|
|
10
|
+
export declare function checkAssociationOperate(ctx: Context, next: Next): Promise<any>;
|
|
@@ -0,0 +1,88 @@
|
|
|
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
|
+
|
|
10
|
+
var __defProp = Object.defineProperty;
|
|
11
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
12
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __export = (target, all) => {
|
|
15
|
+
for (var name in all)
|
|
16
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
17
|
+
};
|
|
18
|
+
var __copyProps = (to, from, except, desc) => {
|
|
19
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
20
|
+
for (let key of __getOwnPropNames(from))
|
|
21
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
22
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
23
|
+
}
|
|
24
|
+
return to;
|
|
25
|
+
};
|
|
26
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
27
|
+
var check_association_operate_exports = {};
|
|
28
|
+
__export(check_association_operate_exports, {
|
|
29
|
+
checkAssociationOperate: () => checkAssociationOperate
|
|
30
|
+
});
|
|
31
|
+
module.exports = __toCommonJS(check_association_operate_exports);
|
|
32
|
+
var import_acl = require("@nocobase/acl");
|
|
33
|
+
async function checkAssociationOperate(ctx, next) {
|
|
34
|
+
var _a;
|
|
35
|
+
const { actionName, resourceName, sourceId } = ctx.action;
|
|
36
|
+
if (!(resourceName.includes(".") && ["add", "set", "remove", "toggle"].includes(actionName))) {
|
|
37
|
+
return next();
|
|
38
|
+
}
|
|
39
|
+
const acl = ctx.acl;
|
|
40
|
+
const roles = ctx.state.currentRoles;
|
|
41
|
+
for (const role of roles) {
|
|
42
|
+
const aclRole = acl.getRole(role);
|
|
43
|
+
if (aclRole.snippetAllowed(`${resourceName}:${actionName}`)) {
|
|
44
|
+
return next();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const [resource, association] = resourceName.split(".");
|
|
48
|
+
const result = ctx.can({
|
|
49
|
+
roles,
|
|
50
|
+
resource,
|
|
51
|
+
action: "update"
|
|
52
|
+
});
|
|
53
|
+
if (!result) {
|
|
54
|
+
ctx.throw(403, "No permissions");
|
|
55
|
+
}
|
|
56
|
+
const params = result.params || ctx.acl.fixedParamsManager.getParams(resourceName, actionName);
|
|
57
|
+
if (params.whitelist && !((_a = params.whitelist) == null ? void 0 : _a.includes(association))) {
|
|
58
|
+
ctx.throw(403, "No permissions");
|
|
59
|
+
}
|
|
60
|
+
if (params.filter) {
|
|
61
|
+
try {
|
|
62
|
+
const filteredParams = ctx.acl.filterParams(ctx, resource, params);
|
|
63
|
+
const parsedParams = await ctx.acl.parseJsonTemplate(filteredParams, ctx);
|
|
64
|
+
const repo = ctx.db.getRepository(resource);
|
|
65
|
+
const record = await repo.findOne({
|
|
66
|
+
filterByTk: sourceId,
|
|
67
|
+
filter: parsedParams.filter
|
|
68
|
+
});
|
|
69
|
+
if (!record) {
|
|
70
|
+
ctx.throw(403, "No permissions");
|
|
71
|
+
}
|
|
72
|
+
} catch (e) {
|
|
73
|
+
if (e instanceof import_acl.NoPermissionError) {
|
|
74
|
+
ctx.throw(403, "No permissions");
|
|
75
|
+
}
|
|
76
|
+
throw e;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
ctx.permission = {
|
|
80
|
+
...ctx.permission,
|
|
81
|
+
skip: true
|
|
82
|
+
};
|
|
83
|
+
await next();
|
|
84
|
+
}
|
|
85
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
86
|
+
0 && (module.exports = {
|
|
87
|
+
checkAssociationOperate
|
|
88
|
+
});
|
|
@@ -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 { Context, Next } from '@nocobase/actions';
|
|
10
|
+
export declare const checkChangesWithAssociation: (ctx: Context, next: Next) => Promise<any>;
|
|
@@ -0,0 +1,390 @@
|
|
|
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
|
+
|
|
10
|
+
var __create = Object.create;
|
|
11
|
+
var __defProp = Object.defineProperty;
|
|
12
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
13
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
14
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
15
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
16
|
+
var __export = (target, all) => {
|
|
17
|
+
for (var name in all)
|
|
18
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
19
|
+
};
|
|
20
|
+
var __copyProps = (to, from, except, desc) => {
|
|
21
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
22
|
+
for (let key of __getOwnPropNames(from))
|
|
23
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
24
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
25
|
+
}
|
|
26
|
+
return to;
|
|
27
|
+
};
|
|
28
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
29
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
30
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
31
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
32
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
33
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
34
|
+
mod
|
|
35
|
+
));
|
|
36
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
37
|
+
var check_change_with_association_exports = {};
|
|
38
|
+
__export(check_change_with_association_exports, {
|
|
39
|
+
checkChangesWithAssociation: () => checkChangesWithAssociation
|
|
40
|
+
});
|
|
41
|
+
module.exports = __toCommonJS(check_change_with_association_exports);
|
|
42
|
+
var import_acl = require("@nocobase/acl");
|
|
43
|
+
var import_lodash = __toESM(require("lodash"));
|
|
44
|
+
function normalizeAssociationValue(value, recordKey) {
|
|
45
|
+
if (!value) {
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
if (Array.isArray(value)) {
|
|
49
|
+
const result = value.map((v) => typeof v === "number" || typeof v === "string" ? v : v[recordKey]).filter((v) => v !== null && v !== void 0);
|
|
50
|
+
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
|
+
}
|
|
59
|
+
const filteredParams = ctx.acl.filterParams(ctx, target, params);
|
|
60
|
+
const parsedParams = await ctx.acl.parseJsonTemplate(filteredParams, ctx);
|
|
61
|
+
return parsedParams.filter || {};
|
|
62
|
+
}
|
|
63
|
+
async function collectAllowedRecordKeys(ctx, items, recordKey, updateParams, target) {
|
|
64
|
+
const repo = ctx.database.getRepository(target);
|
|
65
|
+
if (!repo) {
|
|
66
|
+
return void 0;
|
|
67
|
+
}
|
|
68
|
+
const keys = items.map((item) => import_lodash.default.isPlainObject(item) ? item[recordKey] : void 0).filter((key) => key !== void 0 && key !== null);
|
|
69
|
+
if (!keys.length) {
|
|
70
|
+
return void 0;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const scopedFilter = await resolveScopeFilter(ctx, target, updateParams == null ? void 0 : updateParams.params);
|
|
74
|
+
const records = await repo.find({
|
|
75
|
+
filter: {
|
|
76
|
+
...scopedFilter,
|
|
77
|
+
[`${recordKey}.$in`]: keys
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
const allowedKeys = /* @__PURE__ */ new Set();
|
|
81
|
+
for (const record of records) {
|
|
82
|
+
const key = typeof record.get === "function" ? record.get(recordKey) : record[recordKey];
|
|
83
|
+
if (key !== void 0 && key !== null) {
|
|
84
|
+
allowedKeys.add(key);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const missingKeys = new Set(keys.filter((key) => !allowedKeys.has(key)));
|
|
88
|
+
return { allowedKeys, missingKeys };
|
|
89
|
+
} catch (e) {
|
|
90
|
+
if (e instanceof import_acl.NoPermissionError) {
|
|
91
|
+
return {
|
|
92
|
+
allowedKeys: /* @__PURE__ */ new Set(),
|
|
93
|
+
missingKeys: new Set(keys)
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
throw e;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
async function collectExistingRecordKeys(ctx, recordKey, target, keys) {
|
|
100
|
+
const repo = ctx.database.getRepository(target);
|
|
101
|
+
if (!repo) {
|
|
102
|
+
return /* @__PURE__ */ new Set();
|
|
103
|
+
}
|
|
104
|
+
const keyList = Array.from(keys);
|
|
105
|
+
if (!keyList.length) {
|
|
106
|
+
return /* @__PURE__ */ new Set();
|
|
107
|
+
}
|
|
108
|
+
const records = await repo.find({
|
|
109
|
+
filter: {
|
|
110
|
+
[`${recordKey}.$in`]: keyList
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
const existingKeys = /* @__PURE__ */ new Set();
|
|
114
|
+
for (const record of records) {
|
|
115
|
+
const key = typeof record.get === "function" ? record.get(recordKey) : record[recordKey];
|
|
116
|
+
if (key !== void 0 && key !== null) {
|
|
117
|
+
existingKeys.add(key);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return existingKeys;
|
|
121
|
+
}
|
|
122
|
+
async function recordExistsWithoutScope(ctx, target, recordKey, keyValue) {
|
|
123
|
+
const repo = ctx.database.getRepository(target);
|
|
124
|
+
if (!repo) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
const record = await repo.findOne({
|
|
128
|
+
filter: {
|
|
129
|
+
[recordKey]: keyValue
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
return Boolean(record);
|
|
133
|
+
}
|
|
134
|
+
async function processAssociationChild(ctx, value, recordKey, updateAssociationValues, createParams, updateParams, target, fieldPath, allowedRecordKeys, existingRecordKeys) {
|
|
135
|
+
const keyValue = value == null ? void 0 : value[recordKey];
|
|
136
|
+
const fallbackToCreate = async () => {
|
|
137
|
+
if (!createParams) {
|
|
138
|
+
return keyValue;
|
|
139
|
+
}
|
|
140
|
+
ctx.log.debug(`Association record missing, fallback to create`, {
|
|
141
|
+
fieldPath,
|
|
142
|
+
value,
|
|
143
|
+
target
|
|
144
|
+
});
|
|
145
|
+
return await processValues(ctx, value, updateAssociationValues, createParams.params, target, fieldPath, []);
|
|
146
|
+
};
|
|
147
|
+
const tryFallbackToCreate = async (reason, knownExists) => {
|
|
148
|
+
if (!createParams) {
|
|
149
|
+
return void 0;
|
|
150
|
+
}
|
|
151
|
+
const recordExists = typeof knownExists === "boolean" ? knownExists : await recordExistsWithoutScope(ctx, target, recordKey, keyValue);
|
|
152
|
+
if (!recordExists) {
|
|
153
|
+
ctx.log.debug(reason, {
|
|
154
|
+
fieldPath,
|
|
155
|
+
value,
|
|
156
|
+
createParams,
|
|
157
|
+
updateParams
|
|
158
|
+
});
|
|
159
|
+
return await fallbackToCreate();
|
|
160
|
+
}
|
|
161
|
+
return void 0;
|
|
162
|
+
};
|
|
163
|
+
if (keyValue !== void 0 && keyValue !== null) {
|
|
164
|
+
if (!updateParams) {
|
|
165
|
+
const created = await tryFallbackToCreate(`No permission to update association, try create not exist record`);
|
|
166
|
+
if (created !== void 0) {
|
|
167
|
+
return created;
|
|
168
|
+
}
|
|
169
|
+
ctx.log.debug(`No permission to update association`, { fieldPath, value, updateParams });
|
|
170
|
+
return keyValue;
|
|
171
|
+
} else {
|
|
172
|
+
const repo = ctx.database.getRepository(target);
|
|
173
|
+
if (!repo) {
|
|
174
|
+
ctx.log.debug(`Repository not found for association target`, { fieldPath, target });
|
|
175
|
+
return keyValue;
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
if (allowedRecordKeys) {
|
|
179
|
+
if (!allowedRecordKeys.has(keyValue)) {
|
|
180
|
+
const created = await tryFallbackToCreate(
|
|
181
|
+
`No permission to update association due to scope, try create not exist record`,
|
|
182
|
+
existingRecordKeys ? existingRecordKeys.has(keyValue) : void 0
|
|
183
|
+
);
|
|
184
|
+
if (created !== void 0) {
|
|
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;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return await processValues(ctx, value, updateAssociationValues, updateParams.params, target, fieldPath, []);
|
|
210
|
+
} catch (e) {
|
|
211
|
+
if (e instanceof import_acl.NoPermissionError) {
|
|
212
|
+
return keyValue;
|
|
213
|
+
}
|
|
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
|
+
} else {
|
|
282
|
+
values[fieldName] = normalized;
|
|
283
|
+
}
|
|
284
|
+
ctx.log.debug(`Not allow to update association, only keep keys`, {
|
|
285
|
+
fieldPath,
|
|
286
|
+
fieldValue,
|
|
287
|
+
updateAssociationValues,
|
|
288
|
+
recordKey,
|
|
289
|
+
normalizedValue: values[fieldName]
|
|
290
|
+
});
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
const createParams = ctx.can({
|
|
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
|
+
}
|
|
330
|
+
}
|
|
331
|
+
values[fieldName] = processed;
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
const r = await processAssociationChild(
|
|
335
|
+
ctx,
|
|
336
|
+
fieldValue,
|
|
337
|
+
recordKey,
|
|
338
|
+
updateAssociationValues,
|
|
339
|
+
createParams,
|
|
340
|
+
updateParams,
|
|
341
|
+
field.target,
|
|
342
|
+
fieldPath
|
|
343
|
+
);
|
|
344
|
+
values[fieldName] = r;
|
|
345
|
+
}
|
|
346
|
+
return values;
|
|
347
|
+
}
|
|
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
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
388
|
+
0 && (module.exports = {
|
|
389
|
+
checkChangesWithAssociation
|
|
390
|
+
});
|
|
@@ -95,10 +95,13 @@ function createWithACLMetaMiddleware() {
|
|
|
95
95
|
return void 0;
|
|
96
96
|
},
|
|
97
97
|
app: {
|
|
98
|
+
dataSourceManager: ctx.app.dataSourceManager,
|
|
98
99
|
getDb() {
|
|
99
100
|
return db;
|
|
100
101
|
}
|
|
101
102
|
},
|
|
103
|
+
log: ctx.log,
|
|
104
|
+
logger: ctx.logger,
|
|
102
105
|
getCurrentRepository: ctx.getCurrentRepository,
|
|
103
106
|
action: {
|
|
104
107
|
actionName: action,
|
|
@@ -0,0 +1,14 @@
|
|
|
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 { Migration } from '@nocobase/server';
|
|
10
|
+
export default class extends Migration {
|
|
11
|
+
on: string;
|
|
12
|
+
appVersion: string;
|
|
13
|
+
up(): Promise<void>;
|
|
14
|
+
}
|