需求:
项目中的es有很多表,每个表有很多不同的数据。需求是把数据权限的粒度做到单条数据层面。
比如:用户查询10个ip,其中3个有权限,显示所有内容,7个无权限,只显示部分内容并且标识出来,便于前端展示。
方案:
- 通过egg的middleware中间件,在接口返回出对返回数据进行隐私处理。
- 建立数据权限组机制,数据导入时需要选择数据所属的权限组,并且在每条数据上标记权限组id
- 用户可以动态的增删数据权限组,拥有这个数据权限组,就拥有了该数据权限组下的所有数据
- 接口返回数据经过中间件时,根据用户所属的用户权限组,对数据进行处理。有权限的直接返回,无权限的返回最低限度的可展示数据。
直接上代码
这波代码结构清晰注释明了,不会看不懂吧=w=
import { Context } from 'egg';
import * as _ from 'lodash';
// 需要校验数据权限的接口,其他的直接过
const dataAuthUrl = [
'/api/ip/port',
'/api/ip/domain',
// ...仅保留示例,多余接口删除
];
// 无权限的数据可以预览,预览数据所需要返回的通用字段
// data_permission就是数据所属的数据权限组id
const previewCommonKeys = ['id', '_id', 'data_permission'];
// 不同数据库所返回的预览数据字段
const previewKeyMap = {
ip: ['ip'], // ip指纹库
domain: ['domain'], // 域名库
// ...仅保留示例,其余表所对应的预览字段删除
};
// 获取所需要展示的所有预览字段
// dataType keyof previewKeyMap
const getPreviewKeys = dataType => [...previewCommonKeys, ...previewKeyMap[dataType]];
// 根据数据权限过滤数据
// data 所需要处理的es数据
// dataAuth 用户拥有的数据权限组id数组
// previewKeys 预览数据所需要展示的字段
const dataTrans = (data: any, dataAuth: string[], previewKeys: string[]) => {
// 该条数据 存在数据权限字段 且 数据权限字段不为空 且 值不在登录用户所包含的数据权限组中
if (
data &&
data.data_permission &&
data.data_permission !== '' &&
!dataAuth.includes(data.data_permission)
) {
// 处理数据,只返回可预览的部分数据
const previewData = {};
previewKeys.forEach(k => {
if (data[k]) {
previewData[k] = data[k];
}
});
return { _dataAuthAccessDenied: true, ...previewData };
}
// 否则直接返回
return data;
};
// 通用dataList结构处理: 对 data.list进行处理
const dataListTransformer = (dataType: string) => {
return (ctx: Context, body: any, dataAuth: string[]) => {
// 过滤数据
const { data, ...rest } = body;
const previewKeys = getPreviewKeys(dataType);
return {
...rest,
data: {
...data,
// 对列表数据进行过滤
list: data.list.map(item => dataTrans(item, dataAuth, previewKeys)),
},
};
};
};
// 通用dataArray结构处理: 对data本身进行处理,data是数组
const dataArrayTransformer = (dataType: string) => {
return (ctx: Context, body: any, dataAuth: string[]) => {
const { data, ...rest } = body;
const previewKeys = getPreviewKeys(dataType);
return {
...rest,
data: data.map(item => dataTrans(item, dataAuth, previewKeys)),
};
};
};
// 通用data结构处理,对data本身进行处理,data是单个数据对象
const dataTransformer = (dataType: string) => {
return (ctx: Context, body: any, dataAuth: string[]) => {
const { data, ...rest } = body;
const previewKeys = getPreviewKeys(dataType);
return {
...rest,
data: dataTrans(data, dataAuth, previewKeys),
};
};
};
// 针对接口对其返回数据 根据 数据权限过滤
const url2Transformer = {
'/api/domain/vul': dataListTransformer('vul'),
'/api/domain/whois': dataTransformer('domainWhois'),
'/api/ip/visit': dataArrayTransformer('netRecord'),
// ...仅保留示例,其余接口对应的处理删除
};
export default () => async (ctx: Context, next) => {
// 开发模式跳过权限检查
const { config } = ctx.app;
if (config.skipAuthentication) {
await next();
return true;
}
const reqUrl = ctx.request.url;
// 不处理的直接结束
if (!dataAuthUrl.includes(reqUrl)) {
await next();
return true;
}
// 获取用户的数据权限组,用户页面分配的永久权限
const userDoc = await ctx.model.AuthUser.findById(ctx.user.id);
if (!userDoc) {
await next();
return true;
}
const { dataAuth, account } = userDoc;
// 获取用户申请的数据权限组,个人中心 我的数据权限 中申请的有期限的权限
const dataPermission = await ctx.model.DataPermission.aggregate([
{
$match: {
founder: account,
approveStatus: 'adopt',
},
},
{
$lookup: {
from: 'data_auth_groups',
localField: 'dataPermissionName',
foreignField: 'name',
as: 'dataAuthGroup',
},
},
{
$unwind: '$dataAuthGroup',
},
]);
const applyDataAuth = dataPermission.map(item => item.dataAuthGroup._id);
// 组合在一起才是完整的数据权限
const allDataAuth = _.uniq(
[...dataAuth, ...applyDataAuth].map(ObjectId => ObjectId.toString()),
);
await next();
// 处理返回数据
const { status, body } = ctx.response;
if (status === 200) {
const newData = url2Transformer[reqUrl](ctx, body, allDataAuth);
ctx.response.body = newData;
}
return true;
};