/** * 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 = { 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>(); 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(); 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]; } } }