YANG QIA 414ec3c817
fix(acl): incorrect data records when using m2m fields in collection permission data scope (#6304)
* fix(acl): incorrect data records when using m2m fields in collection permission data scope

* fix: duplicate
2025-02-26 18:38:26 +08:00

300 lines
7.8 KiB
TypeScript

/**
* 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 lodash from 'lodash';
import { snakeCase } from '@nocobase/database';
import { NoPermissionError } from '@nocobase/acl';
function createWithACLMetaMiddleware() {
return async (ctx: any, next) => {
await next();
const dataSourceKey = ctx.get('x-data-source');
const dataSource = ctx.app.dataSourceManager.dataSources.get(dataSourceKey);
const db = dataSource ? dataSource.collectionManager.db : ctx.db;
if (!db) {
return;
}
const acl = dataSource ? dataSource.acl : ctx.app.acl;
if (!ctx.action || !ctx.get('X-With-ACL-Meta') || ctx.status !== 200) {
return;
}
const { resourceName, actionName } = ctx.permission;
if (!['list', 'get'].includes(actionName)) {
return;
}
const collection = db.getCollection(resourceName);
if (!collection) {
return;
}
const Model = collection.model;
// skip if collection is multi filter target key
if (collection.isMultiFilterTargetKey()) {
return;
}
// @ts-ignore
const primaryKeyField = Model.primaryKeyField || Model.primaryKeyAttribute;
let listData;
if (ctx.body?.data) {
listData = ctx.data;
} else if (ctx.body?.rows) {
listData = ctx.body.rows;
} else if (ctx.body) {
listData = ctx.body;
}
if (!listData) {
return;
}
if (actionName == 'get') {
listData = lodash.castArray(listData);
}
const inspectActions = ['view', 'update', 'destroy'];
const actionsParams = [];
for (const action of inspectActions) {
const actionCtx: any = {
db,
get: () => {
return undefined;
},
app: {
getDb() {
return db;
},
},
getCurrentRepository: ctx.getCurrentRepository,
action: {
actionName: action,
name: action,
params: {},
resourceName: ctx.action.resourceName,
resourceOf: ctx.action.resourceOf,
mergeParams() {},
},
state: {
currentRole: ctx.state.currentRole,
currentUser: (() => {
if (!ctx.state.currentUser) {
return null;
}
if (ctx.state.currentUser.toJSON) {
return ctx.state.currentUser?.toJSON();
}
return ctx.state.currentUser;
})(),
},
permission: {},
throw(...args) {
throw new NoPermissionError(...args);
},
};
try {
await acl.getActionParams(actionCtx);
} catch (e) {
if (e instanceof NoPermissionError) {
continue;
}
throw e;
}
actionsParams.push([
action,
actionCtx.permission?.can === null && !actionCtx.permission.skip
? null
: actionCtx.permission?.parsedParams || {},
actionCtx,
]);
}
const ids = (() => {
if (collection.options.tree) {
if (listData.length == 0) return [];
const getAllNodeIds = (data) => [data[primaryKeyField], ...(data.children || []).flatMap(getAllNodeIds)];
return listData.map((tree) => getAllNodeIds(tree.toJSON())).flat();
}
return listData.map((item) => item[primaryKeyField]);
})();
// if all ids are empty, skip
if (ids.filter(Boolean).length == 0) {
return;
}
const conditions = [];
const allAllowed = [];
for (const [action, params, actionCtx] of actionsParams) {
if (!params) {
continue;
}
if (lodash.isEmpty(params) || lodash.isEmpty(params.filter)) {
allAllowed.push(action);
continue;
}
const queryParams = collection.repository.buildQueryOptions({
...params,
context: actionCtx,
});
const actionSql = ctx.db.sequelize.queryInterface.queryGenerator.selectQuery(
Model.getTableName(),
{
where: (() => {
const filterObj = queryParams.where;
if (!db.options.underscored) {
return filterObj;
}
const isAssociationKey = (key) => {
return key.startsWith('$') && key.endsWith('$');
};
// change camelCase to snake_case
const iterate = (rootObj, path = []) => {
const obj = path.length == 0 ? rootObj : lodash.get(rootObj, path);
if (Array.isArray(obj)) {
for (let i = 0; i < obj.length; i++) {
if (obj[i] === null) {
continue;
}
if (typeof obj[i] === 'object') {
iterate(rootObj, [...path, i]);
}
}
return;
}
Reflect.ownKeys(obj).forEach((key) => {
if (Array.isArray(obj) && key == 'length') {
return;
}
if ((typeof obj[key] === 'object' && obj[key] !== null) || typeof obj[key] === 'symbol') {
iterate(rootObj, [...path, key]);
}
if (typeof key === 'string' && key !== snakeCase(key)) {
const setKey = isAssociationKey(key)
? (() => {
const parts = key.split('.');
parts[parts.length - 1] = lodash.snakeCase(parts[parts.length - 1]);
const result = parts.join('.');
return result.endsWith('$') ? result : `${result}$`;
})()
: snakeCase(key);
const setValue = lodash.cloneDeep(obj[key]);
lodash.unset(rootObj, [...path, key]);
lodash.set(rootObj, [...path, setKey], setValue);
}
});
};
iterate(filterObj);
return filterObj;
})(),
attributes: [primaryKeyField],
includeIgnoreAttributes: false,
},
Model,
);
const whereCaseMatch = actionSql.match(/WHERE (.*?);/);
if (!whereCaseMatch) {
conditions.push({
whereCase: '1=1',
action,
include: queryParams.include,
});
} else {
const whereCase = actionSql.match(/WHERE (.*?);/)[1];
conditions.push({
whereCase,
action,
include: queryParams.include,
});
}
}
const results = await collection.model.findAll({
where: {
[primaryKeyField]: ids,
},
attributes: [
primaryKeyField,
...conditions.map((condition) => {
return [ctx.db.sequelize.literal(`CASE WHEN ${condition.whereCase} THEN 1 ELSE 0 END`), condition.action];
}),
],
include: conditions.map((condition) => condition.include).flat(),
raw: true,
});
const allowedActions = inspectActions
.map((action) => {
if (allAllowed.includes(action)) {
return [action, ids];
}
let actionIds = results.filter((item) => Boolean(item[action])).map((item) => item[primaryKeyField]);
actionIds = Array.from(new Set(actionIds));
return [action, actionIds];
})
.reduce((acc, [action, ids]) => {
acc[action] = ids;
return acc;
}, {});
if (actionName == 'get') {
ctx.bodyMeta = {
...(ctx.bodyMeta || {}),
allowedActions: allowedActions,
};
}
if (actionName == 'list') {
ctx.body.allowedActions = allowedActions;
}
};
}
export { createWithACLMetaMiddleware };