214 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { assign } from '@nocobase/utils';
import _ from 'lodash';
import { ACLRole } from '../acl-role';
export function mergeRole(roles: ACLRole[]) {
const result: Record<string, any> = {
roles: [],
strategy: {},
actions: null,
snippets: [],
resources: null,
};
const allSnippets: string[][] = [];
for (const role of roles) {
const jsonRole = role.toJSON();
result.roles = mergeRoleNames(result.roles, jsonRole.role);
result.strategy = mergeRoleStrategy(result.strategy, jsonRole.strategy);
result.actions = mergeRoleActions(result.actions, jsonRole.actions);
result.resources = mergeRoleResources(result.resources, [...role.resources.keys()]);
if (_.isArray(jsonRole.snippets)) {
allSnippets.push(jsonRole.snippets);
}
}
result.snippets = mergeRoleSnippets(allSnippets);
return result;
}
function mergeRoleNames(sourceRoleNames, newRoleName) {
return newRoleName ? sourceRoleNames.concat(newRoleName) : sourceRoleNames;
}
function mergeRoleStrategy(sourceStrategy, newStrategy) {
if (!newStrategy) {
return sourceStrategy;
}
if (_.isArray(newStrategy.actions)) {
if (!sourceStrategy.actions) {
sourceStrategy.actions = newStrategy.actions;
} else {
const actions = sourceStrategy.actions.concat(newStrategy.actions);
return {
...sourceStrategy,
actions: [...new Set(actions)],
};
}
}
return sourceStrategy;
}
function mergeRoleActions(sourceActions, newActions) {
if (_.isEmpty(sourceActions)) return newActions;
if (_.isEmpty(newActions)) return sourceActions;
const result = {};
[...new Set(Reflect.ownKeys(sourceActions).concat(Reflect.ownKeys(newActions)))].forEach((key) => {
if (_.has(sourceActions, key) && _.has(newActions, key)) {
result[key] = mergeAclActionParams(sourceActions[key], newActions[key]);
return;
}
result[key] = _.has(sourceActions, key) ? sourceActions[key] : newActions[key];
});
return result;
}
function mergeRoleSnippets(allRoleSnippets: string[][]): string[] {
if (!allRoleSnippets.length) {
return [];
}
const allSnippets = allRoleSnippets.flat();
const isExclusion = (value) => value.startsWith('!');
const includes = new Set(allSnippets.filter((x) => !isExclusion(x)));
const excludes = new Set(allSnippets.filter(isExclusion));
// 统计 xxx.* 在多少个角色中存在
const domainRoleMap = new Map<string, Set<number>>();
allRoleSnippets.forEach((roleSnippets, i) => {
roleSnippets
.filter((x) => x.endsWith('.*') && !isExclusion(x))
.forEach((include) => {
const domain = include.slice(0, -1);
if (!domainRoleMap.has(domain)) {
domainRoleMap.set(domain, new Set());
}
domainRoleMap.get(domain).add(i);
});
});
// 处理黑名单交集(只有所有角色都有 `!xxx` 才保留)
const excludesSet = new Set<string>();
for (const snippet of excludes) {
if (allRoleSnippets.every((x) => x.includes(snippet))) {
excludesSet.add(snippet);
}
}
for (const [domain, indexes] of domainRoleMap.entries()) {
const fullDomain = `${domain}.*`;
// xxx.* 存在时,覆盖 !xxx.*
if (includes.has(fullDomain)) {
excludesSet.delete(`!${fullDomain}`);
}
// 计算 !xxx.yyy当所有 xxx.* 角色都包含 !xxx.yyy 时才保留
for (const roleIndex of indexes) {
for (const exclude of allRoleSnippets[roleIndex]) {
if (exclude.startsWith(`!${domain}`) && exclude !== `!${fullDomain}`) {
if ([...indexes].every((i) => allRoleSnippets[i].includes(exclude))) {
excludesSet.add(exclude);
}
}
}
}
}
// 确保 !xxx.yyy 只有在 xxx.* 存在时才有效,同时解决 [xxx] 和 [!xxx] 冲突
if (includes.size > 0) {
for (const x of [...excludesSet]) {
const exactMatch = x.slice(1);
const segments = exactMatch.split('.');
if (segments.length > 1 && segments[1] !== '*') {
const parentDomain = segments[0] + '.*';
if (!includes.has(parentDomain)) {
excludesSet.delete(x);
}
}
}
}
return [...includes, ...excludesSet];
}
function mergeRoleResources(sourceResources, newResources) {
if (sourceResources === null) {
return newResources;
}
return [...new Set(sourceResources.concat(newResources))];
}
export function mergeAclActionParams(sourceParams, targetParams) {
if (_.isEmpty(sourceParams) || _.isEmpty(targetParams)) {
return {};
}
// source 和 target 其中之一没有 fields 字段时, 最终希望没有此字段
removeUnmatchedParams(sourceParams, targetParams, ['fields', 'whitelist', 'appends']);
const andMerge = (x, y) => {
if (_.isEmpty(x) || _.isEmpty(y)) {
return [];
}
return _.uniq(x.concat(y)).filter(Boolean);
};
const mergedParams = assign(targetParams, sourceParams, {
own: (x, y) => x || y,
filter: (x, y) => {
if (_.isEmpty(x) || _.isEmpty(y)) {
return {};
}
const xHasOr = _.has(x, '$or'),
yHasOr = _.has(y, '$or');
let $or = [x, y];
if (xHasOr && !yHasOr) {
$or = [...x.$or, y];
} else if (!xHasOr && yHasOr) {
$or = [x, ...y.$or];
} else if (xHasOr && yHasOr) {
$or = [...x.$or, ...y.$or];
}
return { $or: _.uniqWith($or, _.isEqual) };
},
fields: andMerge,
whitelist: andMerge,
appends: 'union',
});
removeEmptyParams(mergedParams);
return mergedParams;
}
export function removeEmptyParams(params) {
if (!_.isObject(params)) {
return;
}
Object.keys(params).forEach((key) => {
if (_.isEmpty(params[key])) {
delete params[key];
}
});
}
function removeUnmatchedParams(source, target, keys: string[]) {
for (const key of keys) {
if (_.has(source, key) && !_.has(target, key)) {
delete source[key];
}
if (!_.has(source, key) && _.has(target, key)) {
delete target[key];
}
}
}