Feature: add permission plugin api (#57)
* fix: update method of permission * 页面访问权限 * feat: add tabs update in role * refactor: change main action permission check into single file * feat: add pluginLoaded event listener for init page permissions * refactor: change to can api * refactor: use constants for role types * refactor: can api * 只处理 actions 表里的权限,其余跳过 * 注释掉 fields * bugfix * add: 详情模块编辑按钮权限判断 * test: add cases * fix: add association permission judgment * fix: add disabled property to drawer select Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
eff2353954
commit
bd756a6a5c
@ -28,7 +28,7 @@ const api = Api.create({
|
||||
charset: 'utf8mb4',
|
||||
collate: 'utf8mb4_unicode_ci',
|
||||
},
|
||||
logging: false,
|
||||
logging: process.env.DB_LOG_SQL === 'on' ? console.log : false,
|
||||
define: {},
|
||||
sync,
|
||||
},
|
||||
|
@ -7,7 +7,7 @@ api.resourcer.use(async (ctx: actions.Context, next) => {
|
||||
// ctx.state.developerMode = {[Op.not]: null};
|
||||
ctx.state.developerMode = false;
|
||||
if (table && table.hasField('developerMode') && ctx.state.developerMode === false) {
|
||||
ctx.action.mergeParams({ filter: { developerMode: ctx.state.developerMode } }, { filter: 'and' });
|
||||
ctx.action.mergeParams({ filter: { "developerMode.$isFalsy": true } }, { filter: 'and' });
|
||||
}
|
||||
if (table && ['get', 'list'].includes(actionName)) {
|
||||
const except = [];
|
||||
|
36
packages/app/src/api/migrations/alter-permissions.ts
Normal file
36
packages/app/src/api/migrations/alter-permissions.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import api from '../app';
|
||||
import Database from '@nocobase/database';
|
||||
|
||||
(async () => {
|
||||
await api.loadPlugins();
|
||||
const database: Database = api.database;
|
||||
await api.database.sync({
|
||||
// tables: ['roles'],
|
||||
});
|
||||
|
||||
const [Collection, User, Role] = database.getModels(['collections', 'users', 'roles']);
|
||||
|
||||
const tables = database.getTables();
|
||||
|
||||
for (let table of tables) {
|
||||
console.log(table.getName());
|
||||
await Collection.import(table.getOptions(), { update: true, migrate: false });
|
||||
}
|
||||
|
||||
let user = await User.findOne({
|
||||
where: {
|
||||
username: "admin",
|
||||
},
|
||||
});
|
||||
|
||||
const role = await Role.findOne({
|
||||
where: {
|
||||
title: '系统开发组',
|
||||
}
|
||||
});
|
||||
|
||||
role.set('type', -1);
|
||||
|
||||
await role.save();
|
||||
await user.setRoles([role]);
|
||||
})();
|
@ -138,13 +138,13 @@ const data = [
|
||||
await Collection.import(table.getOptions(), { update: true, migrate: false });
|
||||
}
|
||||
await Page.import(data);
|
||||
const user = await User.findOne({
|
||||
let user = await User.findOne({
|
||||
where: {
|
||||
username: "admin",
|
||||
},
|
||||
});
|
||||
if (!user) {
|
||||
await User.create({
|
||||
user = await User.create({
|
||||
nickname: "超级管理员",
|
||||
password: "admin",
|
||||
username: "admin",
|
||||
@ -180,11 +180,18 @@ const data = [
|
||||
}
|
||||
const Role = database.getModel('roles');
|
||||
const roles = await Role.bulkCreate([
|
||||
{ title: '系统开发组' },
|
||||
{ title: '数据管理组' },
|
||||
{ title: '普通用户组' },
|
||||
{ title: '未登录用户组' },
|
||||
{ title: '系统开发组', type: -1 },
|
||||
{ title: '匿名用户组', type: 0 },
|
||||
]);
|
||||
await roles[0].updateAssociations({
|
||||
users: user
|
||||
});
|
||||
|
||||
const Action = database.getModel('actions');
|
||||
// 全局
|
||||
await Action.bulkCreate([
|
||||
]);
|
||||
|
||||
await database.getModel('collections').import(require('./collections/example').default);
|
||||
await database.getModel('collections').import(require('./collections/authors').default);
|
||||
await database.getModel('collections').import(require('./collections/books').default);
|
||||
|
@ -1,6 +1,7 @@
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
min-height: 32px;
|
||||
.action-button {
|
||||
margin-right: 8px;
|
||||
&:last-child {
|
||||
|
@ -28,7 +28,7 @@ function transform({value, multiple, labelField, valueField = 'id'}) {
|
||||
}
|
||||
|
||||
function DrawerSelectComponent(props) {
|
||||
const { target, multiple, associatedName, labelField, valueField = 'id', value, onChange } = props;
|
||||
const { disabled, target, multiple, associatedName, labelField, valueField = 'id', value, onChange } = props;
|
||||
const [selectedKeys, selectedValue] = transform({value, multiple, labelField, valueField });
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [selectedRowKeys, setSelectedRowKeys] = useState(multiple ? selectedKeys : [selectedKeys]);
|
||||
@ -38,6 +38,7 @@ function DrawerSelectComponent(props) {
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
disabled={disabled}
|
||||
open={false}
|
||||
mode={multiple ? 'tags' : undefined}
|
||||
labelInValue
|
||||
@ -61,7 +62,9 @@ function DrawerSelectComponent(props) {
|
||||
}
|
||||
}}
|
||||
onClick={() => {
|
||||
setVisible(true);
|
||||
if (!disabled) {
|
||||
setVisible(true);
|
||||
}
|
||||
}}
|
||||
></Select>
|
||||
<Drawer
|
||||
|
@ -85,10 +85,10 @@ Permissions.Actions = connect({
|
||||
if (index === -1) {
|
||||
values.push({
|
||||
name: `${resourceKey}:${record.name}`,
|
||||
scope: data,
|
||||
scope_id: data,
|
||||
});
|
||||
} else {
|
||||
set(values, [index, 'scope'], data);
|
||||
set(values, [index, 'scope_id'], data);
|
||||
}
|
||||
console.log('valvalvalvalval', {values})
|
||||
onChange(values);
|
||||
|
@ -101,8 +101,11 @@ export function BooleanField(props: any) {
|
||||
if (editable) {
|
||||
return <Checkbox defaultChecked={value} onChange={async (e) => {
|
||||
await api.resource(resource).update({
|
||||
associatedKey: data.id,
|
||||
associatedKey: data.associatedKey,
|
||||
resourceKey: data.id,
|
||||
tableName: data.tableName||'pages',
|
||||
values: {
|
||||
tableName: data.tableName||'pages',
|
||||
[name]: e.target.checked,
|
||||
},
|
||||
});
|
||||
|
@ -48,6 +48,7 @@ export default function ViewFactory(props: ViewProps) {
|
||||
associatedKey,
|
||||
resourceName,
|
||||
resourceTarget,
|
||||
resourceKey,
|
||||
resourceFieldName,
|
||||
viewName,
|
||||
mode,
|
||||
@ -59,6 +60,7 @@ export default function ViewFactory(props: ViewProps) {
|
||||
const params = {
|
||||
resourceKey: viewName,
|
||||
values: {
|
||||
resourceKey,
|
||||
associatedKey: associatedKey,
|
||||
associatedName: associatedName,
|
||||
resourceFieldName: resourceName,
|
||||
|
@ -7,6 +7,7 @@
|
||||
"@nocobase/server": "^0.3.0-alpha.0",
|
||||
"@nocobase/database": "^0.3.0-alpha.0",
|
||||
"@nocobase/resourcer": "^0.3.0-alpha.0",
|
||||
"@nocobase/actions": "^0.3.0-alpha.0",
|
||||
"@nocobase/client": "^0.3.0-alpha.0",
|
||||
"react": "16.14.0"
|
||||
}
|
||||
|
@ -9,6 +9,9 @@ const transforms = {
|
||||
if (!field.get('component.showInTable')) {
|
||||
continue;
|
||||
}
|
||||
if (!context.listFields.includes(field.id)) {
|
||||
continue;
|
||||
}
|
||||
arr.push({
|
||||
...field.get(),
|
||||
sorter: field.get('sortable'),
|
||||
@ -25,6 +28,9 @@ const transforms = {
|
||||
if (!field.get('component.showInForm')) {
|
||||
continue;
|
||||
}
|
||||
if (!ctx.listFields.includes(field.id)) {
|
||||
continue;
|
||||
}
|
||||
const interfaceType = field.get('interface');
|
||||
const type = field.get('component.type') || 'string';
|
||||
const prop: any = {
|
||||
@ -32,6 +38,13 @@ const transforms = {
|
||||
title: field.title||field.name,
|
||||
...(field.component||{}),
|
||||
}
|
||||
if (ctx.formMode === 'update') {
|
||||
if (!ctx.updateFields.includes(field.id)) {
|
||||
set(prop, 'x-component-props.disabled', true);
|
||||
}
|
||||
} else if (!ctx.createFields.includes(field.id)) {
|
||||
set(prop, 'x-component-props.disabled', true);
|
||||
}
|
||||
if (field.get('name') === 'interface' && ctx.state.developerMode === false) {
|
||||
const dataSource = field.get('dataSource').filter(item => item.key !== 'developerMode');
|
||||
field.set('dataSource', dataSource);
|
||||
@ -144,6 +157,9 @@ const transforms = {
|
||||
if (!get(field.component, 'showInDetail')) {
|
||||
continue;
|
||||
}
|
||||
if (!context.listFields.includes(field.id)) {
|
||||
continue;
|
||||
}
|
||||
const props = {};
|
||||
if (field.get('interface') === 'subTable') {
|
||||
const children = await field.getChildren({
|
||||
@ -163,7 +179,7 @@ const transforms = {
|
||||
filter: {
|
||||
type: 'filter',
|
||||
'x-component-props': {
|
||||
fields: fields.filter(field => field.get('filterable')),
|
||||
fields: fields.filter(field => ctx.listFields.includes(field.id) && field.get('filterable')),
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -184,7 +200,33 @@ export default async (ctx, next) => {
|
||||
// },
|
||||
}));
|
||||
let throughName;
|
||||
const { associatedName, resourceFieldName, associatedKey } = values;
|
||||
const { resourceKey: resourceKey2, associatedName, resourceFieldName, associatedKey } = values;
|
||||
const permissions = await ctx.can(resourceName).permissions();
|
||||
ctx.listFields = [];
|
||||
ctx.createFields = [];
|
||||
ctx.updateFields = [];
|
||||
ctx.allowedActions = [];
|
||||
for (const action of permissions.actions) {
|
||||
ctx.allowedActions.push(action.name);
|
||||
}
|
||||
console.log(ctx.allowedActions);
|
||||
for (const permissionField of permissions.fields) {
|
||||
const pfc = permissionField.actions;
|
||||
if (pfc.includes(`${resourceName}:list`)) {
|
||||
ctx.listFields.push(permissionField.field_id);
|
||||
}
|
||||
if (pfc.includes(`${resourceName}:create`)) {
|
||||
ctx.createFields.push(permissionField.field_id);
|
||||
}
|
||||
if (pfc.includes(`${resourceName}:update`)) {
|
||||
ctx.updateFields.push(permissionField.field_id);
|
||||
}
|
||||
}
|
||||
console.log({
|
||||
listFields: ctx.listFields,
|
||||
createFields: ctx.createFields,
|
||||
updateFields: ctx.updateFields,
|
||||
})
|
||||
if (associatedName) {
|
||||
const table = ctx.db.getTable(associatedName);
|
||||
const resourceField = table.getField(resourceFieldName);
|
||||
@ -260,6 +302,7 @@ export default async (ctx, next) => {
|
||||
// view.setDataValue('viewCollectionName', view.collection_name);
|
||||
let title = collection.get('title');
|
||||
const mode = get(ctx.action.params, ['values', 'mode'], ctx.action.params.mode);
|
||||
ctx.formMode = mode;
|
||||
if (mode === 'update') {
|
||||
title = `编辑${title}`;
|
||||
} else {
|
||||
@ -337,7 +380,7 @@ export default async (ctx, next) => {
|
||||
},
|
||||
{
|
||||
"title": "访问权限",
|
||||
"name": "accessable",
|
||||
"name": "accessible",
|
||||
"interface": "boolean",
|
||||
"type": "boolean",
|
||||
"parent_id": null,
|
||||
@ -349,7 +392,7 @@ export default async (ctx, next) => {
|
||||
"type": "boolean",
|
||||
"showInTable": true,
|
||||
},
|
||||
"dataIndex": ["accessable"]
|
||||
"dataIndex": ["accessible"]
|
||||
}
|
||||
],
|
||||
};
|
||||
@ -452,13 +495,22 @@ export default async (ctx, next) => {
|
||||
},
|
||||
};
|
||||
} else {
|
||||
let allowedUpdate = false;
|
||||
if (view.type === 'details' && await ctx.can(resourceName).act('update').one(resourceKey2)) {
|
||||
allowedUpdate = true;
|
||||
}
|
||||
ctx.body = {
|
||||
...view.get(),
|
||||
title,
|
||||
actionDefaultParams,
|
||||
original: fields,
|
||||
fields: await (transforms[view.type]||transforms.table)(fields, ctx),
|
||||
actions: actions.filter(action => actionNames.includes(action.name)).map(action => ({
|
||||
actions: actions.filter(action => {
|
||||
if (view.type === 'details' && action.name === 'update') {
|
||||
return allowedUpdate;
|
||||
}
|
||||
return actionNames.includes(action.name) && ctx.allowedActions.includes(`${resourceName}:${action.name}`);
|
||||
}).map(action => ({
|
||||
...action.toJSON(),
|
||||
...action.options,
|
||||
// viewCollectionName: action.collection_name,
|
||||
|
111
packages/plugin-pages/src/actions/roles.pages.ts
Normal file
111
packages/plugin-pages/src/actions/roles.pages.ts
Normal file
@ -0,0 +1,111 @@
|
||||
import { actions } from '@nocobase/actions';
|
||||
import Database from '@nocobase/database';
|
||||
import { flatToTree } from '../utils';
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
export async function list(ctx: actions.Context, next: actions.Next) {
|
||||
const database: Database = ctx.db;
|
||||
const { associatedKey } = ctx.action.params;
|
||||
const Page = database.getModel('pages');
|
||||
const Collection = database.getModel('collections');
|
||||
let pages = await Page.findAll(Page.parseApiJson(ctx.state.developerMode ? {
|
||||
filter: {
|
||||
'parent_id.$notNull': true,
|
||||
},
|
||||
sort: ['sort'],
|
||||
} : {
|
||||
filter: {
|
||||
'parent_id.$notNull': true,
|
||||
developerMode: {'$isFalsy': true},
|
||||
},
|
||||
sort: ['sort'],
|
||||
}));
|
||||
const items = [];
|
||||
for (const page of pages) {
|
||||
items.push({
|
||||
id: page.id,
|
||||
key: `page-${page.id}`,
|
||||
title: page.title,
|
||||
tableName: 'pages',
|
||||
parent_id: `page-${page.parent_id}`,
|
||||
associatedKey,
|
||||
accessible: false, // TODO 对接权限
|
||||
});
|
||||
if (page.get('path') === '/collections') {
|
||||
const collections = await Collection.findAll(Collection.parseApiJson(ctx.state.developerMode ? {
|
||||
filter: {
|
||||
showInDataMenu: true,
|
||||
},
|
||||
sort: ['sort'],
|
||||
}: {
|
||||
filter: {
|
||||
developerMode: {'$isFalsy': true},
|
||||
showInDataMenu: true,
|
||||
},
|
||||
sort: ['sort'],
|
||||
}));
|
||||
for (const collection of collections) {
|
||||
items.push({
|
||||
associatedKey,
|
||||
id: collection.id,
|
||||
key: `collection-${collection.id}`,
|
||||
tableName: 'collections',
|
||||
title: collection.get('title'),
|
||||
parent_id: `page-${page.id}`,
|
||||
accessible: false, // TODO 对接权限
|
||||
});
|
||||
const views = await collection.getViews({
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{ showInDataMenu: true },
|
||||
{ default: true }
|
||||
]
|
||||
},
|
||||
order: [['sort', 'asc']]
|
||||
});
|
||||
if (views.length > 1) {
|
||||
for (const view of views) {
|
||||
items.push({
|
||||
associatedKey,
|
||||
id: view.id,
|
||||
tableName: 'views',
|
||||
title: view.title,
|
||||
key: `view-${view.id}`,
|
||||
parent_id: `collection-${collection.id}`,
|
||||
accessible: false, // TODO 对接权限
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const data = flatToTree(items, {
|
||||
id: 'key',
|
||||
parentId: 'parent_id',
|
||||
children: 'children',
|
||||
});
|
||||
ctx.body = data;
|
||||
// TODO: 暂时 action 中间件就这么写了
|
||||
// ctx.action.mergeParams({associated: null});
|
||||
// const { associatedKey } = ctx.action.params;
|
||||
// ctx.action.mergeParams({
|
||||
// filter: {
|
||||
// 'parent_id.$notNull': true,
|
||||
// }
|
||||
// })
|
||||
// const done = async () => {
|
||||
// ctx.body.rows = ctx.body.rows.map(row => {
|
||||
// row.setDataValue('tableName', 'pages');
|
||||
// row.setDataValue('associatedKey', parseInt(associatedKey));
|
||||
// return row.get();
|
||||
// });
|
||||
// console.log(ctx.body.rows);
|
||||
// await next();
|
||||
// }
|
||||
// return actions.common.list(ctx, done);
|
||||
}
|
||||
|
||||
export async function update(ctx: actions.Context, next: actions.Next) {
|
||||
ctx.body = {};
|
||||
await next();
|
||||
}
|
@ -5,6 +5,7 @@ import getCollection from './actions/getCollection';
|
||||
import getView from './actions/getView';
|
||||
import getRoutes from './actions/getRoutes';
|
||||
import getPageInfo from './actions/getPageInfo';
|
||||
import * as rolesPagesActions from './actions/roles.pages';
|
||||
|
||||
export default async function (options = {}) {
|
||||
const database: Database = this.database;
|
||||
@ -18,6 +19,10 @@ export default async function (options = {}) {
|
||||
resourcer.registerActionHandler('getView', getView);
|
||||
resourcer.registerActionHandler('getPageInfo', getPageInfo);
|
||||
resourcer.registerActionHandler('pages:getRoutes', getRoutes);
|
||||
|
||||
Object.keys(rolesPagesActions).forEach(actionName => {
|
||||
resourcer.registerActionHandler(`roles.pages:${actionName}`, rolesPagesActions[actionName]);
|
||||
});
|
||||
/*
|
||||
const [Collection, Page, View] = database.getModels(['collections', 'pages', 'views']);
|
||||
|
||||
|
319
packages/plugin-permissions/src/AccessController.ts
Normal file
319
packages/plugin-permissions/src/AccessController.ts
Normal file
@ -0,0 +1,319 @@
|
||||
import { Op } from 'sequelize';
|
||||
|
||||
import { ROLE_TYPE_ANONYMOUS, ROLE_TYPE_ROOT, ROLE_TYPE_USER } from './constants';
|
||||
|
||||
|
||||
|
||||
function getPermissions(roles) {
|
||||
return roles.reduce((permissions, role) => permissions.concat(role.get('permissions')), []);
|
||||
}
|
||||
|
||||
function getActionPermissions(permissions) {
|
||||
const actionsMap = new Map<string, any>();
|
||||
|
||||
permissions.forEach(permission => {
|
||||
permission.get('actions').forEach(action => {
|
||||
// 如果没有同名 action
|
||||
if (!actionsMap.has(action.name)) {
|
||||
actionsMap.set(action.name, action);
|
||||
return;
|
||||
}
|
||||
|
||||
const existedScope = actionsMap.get(action.name).get('scope');
|
||||
// 如果之前的同名 action 没有 scope 或 filter 为空
|
||||
if (!existedScope || !existedScope.get('filter') || !Object.keys(existedScope.get('filter')).length) {
|
||||
actionsMap.set(action.name, action);
|
||||
return;
|
||||
}
|
||||
|
||||
const newScope = action.get('scope');
|
||||
// 如果新 action 没有 scope 或 filter 为空
|
||||
if (!newScope || !newScope.get('filter') || !Object.keys(newScope.get('filter')).length) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 以 or 关心合并两个 scope 中的 filter
|
||||
existedScope.set('filter', { 'or': [existedScope.get('filter'), newScope.get('filter')] });
|
||||
});
|
||||
});
|
||||
|
||||
return Array.from(actionsMap.values());
|
||||
}
|
||||
|
||||
function getFieldPermissions(permissions) {
|
||||
const fieldsMap = new Map<string, any>();
|
||||
permissions.forEach(permission => {
|
||||
permission.get('fields_permissions').forEach(field => {
|
||||
if (!fieldsMap.has(field.field_id)) {
|
||||
fieldsMap.set(field.field_id, field);
|
||||
return;
|
||||
}
|
||||
|
||||
const existedActions = fieldsMap.get(field.field_id).get('actions');
|
||||
if (!existedActions || !existedActions.length) {
|
||||
fieldsMap.set(field.field_id, field);
|
||||
return;
|
||||
}
|
||||
|
||||
const newActions = field.get('actions');
|
||||
if (!newActions || !newActions.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const actions = new Set(existedActions);
|
||||
newActions.forEach(item => actions.add(item));
|
||||
fieldsMap.get(field.field_id).set('actions', Array.from(actions));
|
||||
});
|
||||
});
|
||||
|
||||
return Array.from(fieldsMap.values());
|
||||
}
|
||||
|
||||
function getTabPemissions(permissions) {
|
||||
const tabs = new Set();
|
||||
permissions.forEach(permission => {
|
||||
permission.get('tabs_permissions').forEach(tabPermission => tabs.add(tabPermission.tab_id));
|
||||
});
|
||||
|
||||
return Array.from(tabs);
|
||||
}
|
||||
|
||||
export type CollectionPermissions = {
|
||||
actions: any[];
|
||||
fields: any[];
|
||||
tabs: any[]
|
||||
};
|
||||
|
||||
export type PermissionParams = true | null | {
|
||||
filter: any,
|
||||
fields: any[]
|
||||
};
|
||||
|
||||
// ctx.can = new AccessController(ctx).can;
|
||||
// ctx.can('collection').permissions()
|
||||
// => [{ name: 'list', scope: { id: 1, filter: {} } }]
|
||||
// ctx.can('collection').act('list').any()
|
||||
// => { filter: {}, fields: [] }
|
||||
// ctx.can('collection').act('update').any()
|
||||
// ctx.can('collection').act('update').one(resourceKey)
|
||||
// ctx.can('collection').act('get').one(resourceKey)
|
||||
|
||||
export default class AccessController {
|
||||
context;
|
||||
|
||||
resourceName: string | null = null;
|
||||
actionName: string | null = null;
|
||||
|
||||
constructor(ctx) {
|
||||
this.context = ctx;
|
||||
}
|
||||
|
||||
can = (resourceName: string | null) => {
|
||||
this.resourceName = resourceName;
|
||||
this.actionName = null;
|
||||
return this;
|
||||
};
|
||||
|
||||
act(name: string | null) {
|
||||
this.actionName = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
async permissions(): Promise<CollectionPermissions> {
|
||||
const roles = await this.getRolesWithPermissions();
|
||||
if (roles.some(role => role.type === ROLE_TYPE_ROOT)) {
|
||||
return this.getRootPermissions();
|
||||
}
|
||||
|
||||
const permissions = getPermissions(roles);
|
||||
|
||||
return {
|
||||
actions: getActionPermissions(permissions),
|
||||
fields: getFieldPermissions(permissions),
|
||||
tabs: getTabPemissions(permissions)
|
||||
};
|
||||
}
|
||||
|
||||
async any(): Promise<PermissionParams> {
|
||||
const roles = await this.getRolesWithPermissions();
|
||||
if (roles.some(role => role.type === ROLE_TYPE_ROOT)) {
|
||||
return true;
|
||||
}
|
||||
// 只处理 actions 表里的权限,其余跳过
|
||||
const getActionNames = await this.getActionNames();
|
||||
if (!getActionNames.includes(this.actionName)) {
|
||||
console.log(`skip ${this.resourceName}:${this.actionName}`);
|
||||
return true;
|
||||
}
|
||||
const permissions = getPermissions(roles);
|
||||
const actionPermissions = getActionPermissions(permissions);
|
||||
|
||||
if (!actionPermissions.length) {
|
||||
// 如果找不到可用的 action 记录
|
||||
// 则认为没有权限
|
||||
return null;
|
||||
}
|
||||
|
||||
const filters = actionPermissions
|
||||
.filter(item => Boolean(item.scope) && Object.keys(item.scope.filter).length)
|
||||
.map(item => item.scope.filter);
|
||||
|
||||
const fields = getFieldPermissions(permissions);
|
||||
|
||||
return {
|
||||
filter: filters.length
|
||||
? filters.length > 1 ? { or: filters } : filters[0]
|
||||
: {},
|
||||
fields
|
||||
};
|
||||
}
|
||||
|
||||
async one(resourceKey): Promise<PermissionParams> {
|
||||
const any = await this.any();
|
||||
|
||||
if (!any || any === true) {
|
||||
return any;
|
||||
}
|
||||
|
||||
const { filter } = any;
|
||||
|
||||
const Collection = this.context.db.getModel(this.resourceName);
|
||||
const existed = await Collection.count({
|
||||
where: {
|
||||
...Collection.parseApiJson({ filter }).where,
|
||||
[Collection.primaryKeyAttribute]: resourceKey
|
||||
}
|
||||
});
|
||||
|
||||
return existed ? any : null;
|
||||
}
|
||||
|
||||
async getRolesWithPermissions() {
|
||||
const { context, resourceName, actionName = null } = this;
|
||||
if (!resourceName) {
|
||||
throw new Error('resource name must be set first by `can(resourceName)`');
|
||||
}
|
||||
const Role = context.db.getModel('roles');
|
||||
const permissionInclusion = {
|
||||
association: 'permissions',
|
||||
where: {
|
||||
collection_name: resourceName
|
||||
},
|
||||
required: true,
|
||||
include: [
|
||||
{
|
||||
association: 'actions',
|
||||
where: actionName ? {
|
||||
name: `${resourceName}:${actionName}`
|
||||
} : {},
|
||||
required: true,
|
||||
// 对 hasMany 关系可以进行拆分查询,避免联表过多标识符超过 PG 的 64 字符限制
|
||||
separate: true,
|
||||
include: [
|
||||
{
|
||||
association: 'scope'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
association: 'fields_permissions',
|
||||
include: [
|
||||
{
|
||||
association: 'field'
|
||||
}
|
||||
],
|
||||
separate: true,
|
||||
},
|
||||
{
|
||||
association: 'tabs_permissions',
|
||||
separate: true,
|
||||
}
|
||||
],
|
||||
};
|
||||
|
||||
let userRoles = [];
|
||||
// 获取登入用户的角色及权限
|
||||
const { currentUser } = context.state;
|
||||
if (currentUser) {
|
||||
const rootRoles = await currentUser.getRoles({
|
||||
where: {
|
||||
type: ROLE_TYPE_ROOT
|
||||
}
|
||||
});
|
||||
if (rootRoles.length) {
|
||||
return rootRoles;
|
||||
}
|
||||
|
||||
userRoles = await currentUser.getRoles({
|
||||
where: {
|
||||
type: ROLE_TYPE_USER
|
||||
},
|
||||
include: [
|
||||
permissionInclusion
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
// 获取匿名用户的角色及权限
|
||||
const anonymousRoles = await Role.findAll({
|
||||
where: {
|
||||
type: ROLE_TYPE_ANONYMOUS
|
||||
},
|
||||
include: [
|
||||
permissionInclusion
|
||||
]
|
||||
});
|
||||
|
||||
return [...anonymousRoles, ...userRoles];
|
||||
}
|
||||
|
||||
async getActionNames() {
|
||||
const { context, resourceName = null } = this;
|
||||
const Action = context.db.getModel('actions');
|
||||
const actions = await Action.findAll({
|
||||
where: {
|
||||
collection_name: { [Op.or]: [resourceName, null] }
|
||||
}
|
||||
});
|
||||
return actions.map(action => action.name);
|
||||
}
|
||||
|
||||
async getRootPermissions(): Promise<CollectionPermissions> {
|
||||
const { context, resourceName = null } = this;
|
||||
const Action = context.db.getModel('actions');
|
||||
const actions = await Action.findAll({
|
||||
where: {
|
||||
collection_name: { [Op.or]: [resourceName, null] }
|
||||
}
|
||||
});
|
||||
|
||||
const Field = context.db.getModel('fields');
|
||||
const fields = await Field.findAll({
|
||||
where: {
|
||||
collection_name: resourceName
|
||||
}
|
||||
});
|
||||
|
||||
const Tab = context.db.getModel('tabs');
|
||||
const tabs = await Tab.findAll({
|
||||
where: {
|
||||
collection_name: resourceName
|
||||
},
|
||||
attribute: [Tab.primaryKeyAttribute]
|
||||
});
|
||||
|
||||
const actionsNames = actions.map(action => ({
|
||||
name: `${resourceName}:${action.name}`
|
||||
}));
|
||||
|
||||
return {
|
||||
actions: actionsNames,
|
||||
fields: fields.map(field => ({
|
||||
field,
|
||||
field_id: field.id,
|
||||
actions: actionsNames.map(({ name }) => name)
|
||||
})),
|
||||
tabs: tabs.map(tab => tab[Tab.primaryKeyAttribute])
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
import { getApp } from '.';
|
||||
import AccessController from '../AccessController';
|
||||
|
||||
describe('AccessController', () => {
|
||||
let app;
|
||||
let db;
|
||||
const ac: any = {};
|
||||
|
||||
beforeEach(async () => {
|
||||
app = await getApp();
|
||||
db = app.database;
|
||||
|
||||
ac.anonymous = new AccessController({
|
||||
state: {},
|
||||
db
|
||||
});
|
||||
|
||||
const User = db.getModel('users');
|
||||
const user = await User.findByPk(1);
|
||||
ac.user = new AccessController({
|
||||
state: { currentUser: user },
|
||||
db
|
||||
});
|
||||
|
||||
const root = await User.findByPk(4);
|
||||
ac.root = new AccessController({
|
||||
state: { currentUser: root },
|
||||
db
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => db.close());
|
||||
|
||||
describe('can().permissions()', () => {
|
||||
it('anonymous', async () => {
|
||||
const result = await ac.anonymous.can('posts').permissions();
|
||||
expect(result).toMatchObject({
|
||||
actions: [
|
||||
{ name: 'posts:list', scope: { filter: { status: 'published' }, collection_name: 'posts' } }
|
||||
],
|
||||
fields: [
|
||||
{ actions: ['posts:list'] }
|
||||
],
|
||||
tabs: []
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('can().act()', () => {
|
||||
it('normal user invoke can().permissions() invoked after .act()', async () => {
|
||||
const actionResult = await ac.user.can('posts').act('list').any();
|
||||
expect(actionResult.filter).toEqual({
|
||||
or: [
|
||||
{ status: 'published' },
|
||||
{ status: 'draft', 'created_by_id.$currentUser': true }
|
||||
]
|
||||
});
|
||||
|
||||
const permissionsResult = await ac.user.can('posts').permissions();
|
||||
expect(permissionsResult).toMatchObject({
|
||||
actions: [
|
||||
{
|
||||
name: 'posts:list',
|
||||
scope: { filter: { or: [{ status: 'published' }, { status: 'draft', 'created_by_id.$currentUser': true }] } }
|
||||
},
|
||||
{
|
||||
name: 'posts:update',
|
||||
scope: { filter: { status: 'draft', 'created_by_id.$currentUser': true }, collection_name: 'posts' },
|
||||
}
|
||||
],
|
||||
tabs: []
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -8,6 +8,7 @@ import { actions, middlewares } from '@nocobase/actions';
|
||||
import { Application } from '@nocobase/server';
|
||||
import middleware from '@nocobase/server/src/middleware';
|
||||
import plugin from '../server';
|
||||
import seed from './seed';
|
||||
|
||||
function getTestKey() {
|
||||
const { id } = require.main;
|
||||
@ -65,7 +66,7 @@ export async function getApp() {
|
||||
});
|
||||
try {
|
||||
await app.database.sync();
|
||||
}catch(err) {
|
||||
} catch(err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
@ -74,6 +75,9 @@ export async function getApp() {
|
||||
// 创建和更新里面仍会再次创建 fields,导致创建相关的数据重复,数据库报错。
|
||||
await app.database.getModel('collections').import(table.getOptions(), { update: true, migrate: false });
|
||||
}
|
||||
|
||||
await seed(app.database);
|
||||
|
||||
app.context.db = app.database;
|
||||
app.use(bodyParser());
|
||||
app.use(middleware({
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { getApp, getAgent, getAPI } from '.';
|
||||
import AccessController from '../AccessController';
|
||||
|
||||
describe.skip('list', () => {
|
||||
describe('list', () => {
|
||||
let app;
|
||||
let agent;
|
||||
let api;
|
||||
@ -13,12 +14,7 @@ describe.skip('list', () => {
|
||||
db = app.database;
|
||||
|
||||
const User = db.getModel('users');
|
||||
const users = await User.bulkCreate([
|
||||
{ username: 'user1', token: 'token1' },
|
||||
{ username: 'user2', token: 'token2' },
|
||||
{ username: 'user3', token: 'token3' },
|
||||
{ username: 'user4', token: 'token4' },
|
||||
]);
|
||||
const users = await User.findAll({ order: [['id', 'ASC']] });
|
||||
|
||||
userAgents = users.map(user => {
|
||||
const userAgent = getAgent(app);
|
||||
@ -26,142 +22,6 @@ describe.skip('list', () => {
|
||||
return userAgent;
|
||||
});
|
||||
|
||||
const Role = db.getModel('roles');
|
||||
const roles = await Role.bulkCreate([
|
||||
{ title: '匿名用户', type: 0 },
|
||||
{ title: '普通用户' },
|
||||
{ title: '编辑' },
|
||||
{ title: '管理员', type: -1 },
|
||||
]);
|
||||
|
||||
const Field = db.getModel('fields');
|
||||
const postTitleField = await Field.findOne({
|
||||
where: {
|
||||
name: 'title',
|
||||
collection_name: 'posts'
|
||||
}
|
||||
});
|
||||
const postStatusField = await Field.findOne({
|
||||
where: {
|
||||
name: 'status',
|
||||
collection_name: 'posts'
|
||||
}
|
||||
});
|
||||
const postCategoryField = await Field.findOne({
|
||||
where: {
|
||||
name: 'category',
|
||||
collection_name: 'posts'
|
||||
}
|
||||
});
|
||||
|
||||
// 匿名用户
|
||||
await roles[0].updateAssociations({
|
||||
permissions: [
|
||||
{
|
||||
collection_name: 'posts',
|
||||
actions_permissions: [
|
||||
{
|
||||
name: 'list',
|
||||
scope: { filter: { status: 'published' }, collection_name: 'posts' },
|
||||
}
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
id: postTitleField.id,
|
||||
fields_permissions: { actions: ['posts:list'] }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
collection_name: 'categories',
|
||||
actions_permissions: [
|
||||
{ name: 'list' }
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// 普通用户
|
||||
await roles[1].updateAssociations({
|
||||
users: [users[0], users[3]],
|
||||
permissions: [
|
||||
{
|
||||
collection_name: 'posts',
|
||||
actions_permissions: [
|
||||
{
|
||||
name: 'list',
|
||||
// TODO(bug): 字段应使用 'created_by' 名称,通过程序映射成外键
|
||||
scope: { filter: { status: 'draft', 'created_by_id.$currentUser': true }, collection_name: 'posts' },
|
||||
},
|
||||
{
|
||||
name: 'update',
|
||||
scope: { filter: { status: 'draft', 'created_by_id.$currentUser': true }, collection_name: 'posts' },
|
||||
}
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
id: postTitleField.id,
|
||||
fields_permissions: { actions: ['posts:list', 'posts:create', 'posts:update'] }
|
||||
},
|
||||
{
|
||||
id: postStatusField.id,
|
||||
fields_permissions: { actions: ['posts:list'] }
|
||||
},
|
||||
{
|
||||
id: postCategoryField.id,
|
||||
fields_permissions: { actions: ['posts:list'] }
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// 编辑
|
||||
await roles[2].updateAssociations({
|
||||
users: [users[1], users[3]],
|
||||
permissions: [
|
||||
{
|
||||
collection_name: 'posts',
|
||||
actions_permissions: [
|
||||
{
|
||||
name: 'update'
|
||||
}
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
id: postTitleField.id,
|
||||
fields_permissions: { actions: ['posts:update'] }
|
||||
},
|
||||
{
|
||||
id: postStatusField.id,
|
||||
fields_permissions: { actions: ['posts:update'] }
|
||||
},
|
||||
{
|
||||
id: postCategoryField.id,
|
||||
fields_permissions: { actions: ['posts:update'] }
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
collection_name: 'categories',
|
||||
actions_permissions: [
|
||||
{ name: 'create' },
|
||||
{ name: 'update' },
|
||||
{ name: 'destroy' },
|
||||
]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// 管理员
|
||||
|
||||
const Post = db.getModel('posts');
|
||||
await Post.bulkCreate([
|
||||
{ title: 'title1', created_by_id: users[0].id },
|
||||
{ title: 'title2', created_by_id: users[0].id },
|
||||
{ title: 'title3', created_by_id: users[1].id },
|
||||
{ title: 'title4', created_by_id: users[3].id },
|
||||
]);
|
||||
});
|
||||
|
||||
afterEach(() => db.close());
|
||||
@ -174,16 +34,15 @@ describe.skip('list', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// TODO(bug): 单独执行可以通过。
|
||||
// 由于 app.database.getModel('collections').import() 有 bug,无法正确的完成数据构造。
|
||||
describe('normal user', () => {
|
||||
it('user could get posts created by self and limited fields', async () => {
|
||||
const response = await userAgents[0].get('/api/posts?sort=title');
|
||||
expect(response.body.count).toBe(2);
|
||||
expect(response.body.rows).toEqual([
|
||||
{ title: 'title1', status: 'draft', category: null },
|
||||
{ title: 'title2', status: 'draft', category: null }
|
||||
]);
|
||||
// TODO(bug): fields 过滤暂时先跳过了
|
||||
// expect(response.body.rows).toEqual([
|
||||
// { title: 'title1', status: 'draft', category: null },
|
||||
// { title: 'title2', status: 'draft', category: null }
|
||||
// ]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
148
packages/plugin-permissions/src/__tests__/seed.ts
Normal file
148
packages/plugin-permissions/src/__tests__/seed.ts
Normal file
@ -0,0 +1,148 @@
|
||||
import { ROLE_TYPE_ANONYMOUS, ROLE_TYPE_ROOT, ROLE_TYPE_USER } from '../constants';
|
||||
|
||||
export default async function(db) {
|
||||
const User = db.getModel('users');
|
||||
const users = await User.bulkCreate([
|
||||
{ username: 'user1', token: 'token1' },
|
||||
{ username: 'user2', token: 'token2' },
|
||||
{ username: 'user3', token: 'token3' },
|
||||
{ username: 'user4', token: 'token4' },
|
||||
]);
|
||||
|
||||
const Role = db.getModel('roles');
|
||||
const roles = await Role.bulkCreate([
|
||||
{ title: '匿名用户', type: ROLE_TYPE_ANONYMOUS },
|
||||
{ title: '普通用户' },
|
||||
{ title: '编辑' },
|
||||
{ title: '管理员', type: ROLE_TYPE_ROOT },
|
||||
]);
|
||||
|
||||
const Field = db.getModel('fields');
|
||||
const postTitleField = await Field.findOne({
|
||||
where: {
|
||||
name: 'title',
|
||||
collection_name: 'posts'
|
||||
}
|
||||
});
|
||||
const postStatusField = await Field.findOne({
|
||||
where: {
|
||||
name: 'status',
|
||||
collection_name: 'posts'
|
||||
}
|
||||
});
|
||||
const postCategoryField = await Field.findOne({
|
||||
where: {
|
||||
name: 'category',
|
||||
collection_name: 'posts'
|
||||
}
|
||||
});
|
||||
|
||||
// 匿名用户
|
||||
await roles[0].updateAssociations({
|
||||
permissions: [
|
||||
{
|
||||
collection_name: 'posts',
|
||||
actions: [
|
||||
{
|
||||
name: 'posts:list',
|
||||
scope: { filter: { status: 'published' }, collection_name: 'posts' },
|
||||
}
|
||||
],
|
||||
fields_permissions: [
|
||||
{
|
||||
field_id: postTitleField[Field.primaryKeyAttribute],
|
||||
actions: ['posts:list']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
collection_name: 'categories',
|
||||
actions: [
|
||||
{ name: 'posts:list' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}, { migrate: false });
|
||||
|
||||
// 普通用户
|
||||
await roles[1].updateAssociations({
|
||||
users: [users[0], users[3]],
|
||||
permissions: [
|
||||
{
|
||||
collection_name: 'posts',
|
||||
actions: [
|
||||
{
|
||||
name: 'posts:list',
|
||||
// TODO(bug): 字段应使用 'created_by' 名称,通过程序映射成外键
|
||||
scope: { filter: { status: 'draft', 'created_by_id.$currentUser': true }, collection_name: 'posts' },
|
||||
},
|
||||
{
|
||||
name: 'posts:update',
|
||||
scope: { filter: { status: 'draft', 'created_by_id.$currentUser': true }, collection_name: 'posts' },
|
||||
}
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
id: postTitleField.id,
|
||||
fields_permissions: { actions: ['posts:list', 'posts:create', 'posts:update'] }
|
||||
},
|
||||
{
|
||||
id: postStatusField.id,
|
||||
fields_permissions: { actions: ['posts:list'] }
|
||||
},
|
||||
{
|
||||
id: postCategoryField.id,
|
||||
fields_permissions: { actions: ['posts:list'] }
|
||||
},
|
||||
]
|
||||
}
|
||||
]
|
||||
}, { migrate: false });
|
||||
|
||||
// 编辑
|
||||
await roles[2].updateAssociations({
|
||||
users: [users[1], users[3]],
|
||||
permissions: [
|
||||
{
|
||||
collection_name: 'posts',
|
||||
actions: [
|
||||
{
|
||||
name: 'posts:update'
|
||||
}
|
||||
],
|
||||
fields_permissions: [
|
||||
{
|
||||
field_id: postTitleField[Field.primaryKeyAttribute],
|
||||
actions: ['posts:update']
|
||||
},
|
||||
{
|
||||
field_id: postStatusField[Field.primaryKeyAttribute],
|
||||
actions: ['posts:update']
|
||||
},
|
||||
{
|
||||
field_id: postCategoryField[Field.primaryKeyAttribute],
|
||||
actions: ['posts:update']
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
collection_name: 'categories',
|
||||
actions: [
|
||||
{ name: 'posts:create' },
|
||||
{ name: 'posts:update' },
|
||||
{ name: 'posts:destroy' },
|
||||
]
|
||||
}
|
||||
]
|
||||
}, { migrate: false });
|
||||
|
||||
// 管理员
|
||||
|
||||
const Post = db.getModel('posts');
|
||||
await Post.bulkCreate([
|
||||
{ title: 'title1', created_by_id: users[0].id },
|
||||
{ title: 'title2', created_by_id: users[0].id },
|
||||
{ title: 'title3', created_by_id: users[1].id },
|
||||
{ title: 'title4', created_by_id: users[3].id },
|
||||
]);
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import { Op } from 'sequelize';
|
||||
import { actions } from '@nocobase/actions';
|
||||
|
||||
export async function list(ctx: actions.Context, next: actions.Next) {
|
||||
@ -18,29 +19,33 @@ export async function get(ctx: actions.Context, next: actions.Next) {
|
||||
},
|
||||
include: [
|
||||
{
|
||||
association: 'actions_permissions',
|
||||
association: 'actions',
|
||||
// 对 hasMany 关系可以进行拆分查询,避免联表过多标识符超过 PG 的 64 字符限制
|
||||
separate: true,
|
||||
include: [
|
||||
{
|
||||
association: 'scope',
|
||||
attribute: ['filter']
|
||||
},
|
||||
association: 'scope'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
association: 'fields_permissions',
|
||||
separate: true,
|
||||
},
|
||||
{
|
||||
association: 'tabs_permissions',
|
||||
separate: true,
|
||||
}
|
||||
],
|
||||
distinct: true,
|
||||
limit: 1
|
||||
});
|
||||
|
||||
const result = permission
|
||||
? {
|
||||
actions: permission.actions_permissions || [],
|
||||
actions: permission.actions || [],
|
||||
fields: permission.fields_permissions || [],
|
||||
tabs: permission.tabs_permissions || [],
|
||||
tabs: (permission.tabs_permissions || []).map(item => item.tab_id),
|
||||
}
|
||||
: {
|
||||
actions: [],
|
||||
@ -60,26 +65,114 @@ export async function update(ctx: actions.Context, next: actions.Next) {
|
||||
values
|
||||
} = ctx.action.params;
|
||||
|
||||
const transaction = await ctx.db.sequelize.transaction();
|
||||
|
||||
// role.getPermissions
|
||||
let [permission] = await associated.getPermissions({
|
||||
where: {
|
||||
collection_name: resourceKey
|
||||
}
|
||||
},
|
||||
transaction
|
||||
});
|
||||
|
||||
if (!permission) {
|
||||
// 不存在则创建
|
||||
permission = await associated.createPermission({
|
||||
collection_name: resourceKey,
|
||||
description: values.description
|
||||
});
|
||||
}, { transaction });
|
||||
} else {
|
||||
await permission.update({ description: values.description });
|
||||
// 存在则更新描述
|
||||
await permission.update({ description: values.description }, { transaction });
|
||||
}
|
||||
|
||||
await permission.updateAssociations({
|
||||
actions_permissions: values.actions,
|
||||
fields_permissions: values.fields,
|
||||
tabs: values.tabs
|
||||
// 查找对应 collection 下所有“可用”的 actions
|
||||
const ActionModel = ctx.db.getModel('actions');
|
||||
const availableActions = await ActionModel.findAll({
|
||||
where: {
|
||||
// null 代表全局 action
|
||||
collection_name: {
|
||||
[Op.or]: [resourceKey, null]
|
||||
},
|
||||
...(ctx.state.developerMode ? {} : { developerMode: false })
|
||||
},
|
||||
transaction
|
||||
});
|
||||
|
||||
// 需要移除的 = 所有可用的 - 要添加的
|
||||
const toRemoveActionNames = new Set<string>();
|
||||
availableActions.forEach(action => {
|
||||
const name = `${action.collection_name || resourceKey}:${action.name}`;
|
||||
if (!values.actions.find(item => item.name === name)) {
|
||||
toRemoveActionNames.add(name);
|
||||
}
|
||||
});
|
||||
|
||||
// 移除没有设置的
|
||||
// 因为只知道 name(不知道 id),所以只能从 Model 上调用移除
|
||||
const ActionPermission = ctx.db.getModel('actions_permissions');
|
||||
await ActionPermission.destroy({
|
||||
where: {
|
||||
permission_id: permission.id,
|
||||
name: Array.from(toRemoveActionNames)
|
||||
},
|
||||
transaction
|
||||
});
|
||||
// 更新或创建
|
||||
const existedActions = await permission.getActions({ transaction });
|
||||
for (const actionItem of values.actions) {
|
||||
const action = existedActions.find(item => item.name === actionItem.name);
|
||||
if (action) {
|
||||
await action.update(actionItem, { transaction });
|
||||
} else {
|
||||
await permission.createAction(actionItem, { transaction });
|
||||
}
|
||||
};
|
||||
|
||||
const fieldsValue = values.fields.filter(field => field.actions && field.actions.length);
|
||||
const existedFields = await permission.getFields_permissions({
|
||||
include: [
|
||||
{ association: 'field' }
|
||||
],
|
||||
transaction
|
||||
});
|
||||
const toRemoveFields = [];
|
||||
const fieldsLeft = [];
|
||||
existedFields.forEach(field => {
|
||||
if (
|
||||
!fieldsValue.find(({ field_id }) => field_id === field.field_id)
|
||||
&& !(field.field.developerMode ^ ctx.state.developerMode)
|
||||
) {
|
||||
toRemoveFields.push(field.field);
|
||||
} else {
|
||||
fieldsLeft.push(field);
|
||||
}
|
||||
});
|
||||
if (toRemoveFields.length) {
|
||||
await permission.removeFields(toRemoveFields, { transaction });
|
||||
}
|
||||
for (const fieldItem of fieldsValue) {
|
||||
const field = fieldsLeft.find(item => item.field_id === fieldItem.field_id);
|
||||
if (field) {
|
||||
await field.update(fieldItem, { transaction });
|
||||
} else {
|
||||
await permission.createFields_permission(fieldItem, { transaction });
|
||||
}
|
||||
}
|
||||
|
||||
const TabModel = ctx.db.getModel('tabs');
|
||||
const existedTabs = await permission.getTabs({ transaction });
|
||||
const toRemoveTabs = existedTabs.filter(tab => (
|
||||
// 如果没找到
|
||||
!values.tabs.find(id => tab[TabModel.primaryKeyAttribute] === id)
|
||||
// 且开发者模式匹配
|
||||
&& !(tab.developerMode ^ ctx.state.developerMode)));
|
||||
await permission.removeTabs(toRemoveTabs, { transaction });
|
||||
await permission.addTabs(values.tabs, { transaction });
|
||||
|
||||
await transaction.commit();
|
||||
|
||||
ctx.body = permission;
|
||||
|
||||
await next();
|
||||
}
|
||||
|
@ -3,7 +3,22 @@ import { actions } from '@nocobase/actions';
|
||||
export async function list(ctx: actions.Context, next: actions.Next) {
|
||||
// TODO: 暂时 action 中间件就这么写了
|
||||
ctx.action.mergeParams({associated: null});
|
||||
return actions.common.list(ctx, next);
|
||||
const { associatedKey } = ctx.action.params;
|
||||
ctx.action.mergeParams({
|
||||
filter: {
|
||||
'parent_id.$notNull': true,
|
||||
}
|
||||
})
|
||||
const done = async () => {
|
||||
ctx.body.rows = ctx.body.rows.map(row => {
|
||||
row.setDataValue('tableName', 'pages');
|
||||
row.setDataValue('associatedKey', parseInt(associatedKey));
|
||||
return row.get();
|
||||
});
|
||||
console.log(ctx.body.rows);
|
||||
await next();
|
||||
}
|
||||
return actions.common.list(ctx, done);
|
||||
}
|
||||
|
||||
export async function update(ctx: actions.Context, next: actions.Next) {
|
||||
|
@ -20,6 +20,7 @@ export default {
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'permission',
|
||||
onDelete: 'CASCADE'
|
||||
},
|
||||
],
|
||||
} as TableOptions;
|
||||
|
@ -33,7 +33,8 @@ export default {
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'collection',
|
||||
targetKey: 'name'
|
||||
targetKey: 'name',
|
||||
onDelete: 'CASCADE'
|
||||
}
|
||||
],
|
||||
} as TableOptions;
|
||||
|
@ -37,7 +37,8 @@ export default {
|
||||
{
|
||||
comment: '允许的操作集',
|
||||
type: 'hasMany',
|
||||
name: 'actions_permissions',
|
||||
name: 'actions',
|
||||
target: 'actions_permissions'
|
||||
},
|
||||
{
|
||||
type: 'belongsToMany',
|
||||
@ -59,17 +60,17 @@ export default {
|
||||
// type: 'hasMany',
|
||||
// name: 'views_permissions',
|
||||
// },
|
||||
// {
|
||||
// comment: '允许的 tabs 列表',
|
||||
// type: 'belongsToMany',
|
||||
// name: 'tabs',
|
||||
// through: 'tabs_permissions'
|
||||
// },
|
||||
// {
|
||||
// comment: '标签页集(方便访问)',
|
||||
// type: 'hasMany',
|
||||
// name: 'tabs_permissions',
|
||||
// },
|
||||
{
|
||||
comment: '允许的 tabs 列表',
|
||||
type: 'belongsToMany',
|
||||
name: 'tabs',
|
||||
through: 'tabs_permissions'
|
||||
},
|
||||
{
|
||||
comment: '标签页集(方便访问)',
|
||||
type: 'hasMany',
|
||||
name: 'tabs_permissions',
|
||||
},
|
||||
],
|
||||
views: [
|
||||
{
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { TableOptions } from '@nocobase/database';
|
||||
import { ROLE_TYPE_ROOT, ROLE_TYPE_USER, ROLE_TYPE_ANONYMOUS } from '../constants';
|
||||
|
||||
export default {
|
||||
name: 'roles',
|
||||
@ -19,11 +20,16 @@ export default {
|
||||
},
|
||||
},
|
||||
{
|
||||
interface: 'boolean',
|
||||
comment: '支持匿名用户',
|
||||
type: 'boolean',
|
||||
name: 'anonymous',
|
||||
defaultValue: false
|
||||
interface: 'select',
|
||||
title: '角色类型',
|
||||
type: 'integer',
|
||||
name: 'type',
|
||||
dataSource: [
|
||||
{ value: ROLE_TYPE_ROOT, label: '系统角色' },
|
||||
{ value: ROLE_TYPE_ANONYMOUS, label: '匿名角色' },
|
||||
{ value: ROLE_TYPE_USER, label: '自定义角色' },
|
||||
],
|
||||
defaultValue: ROLE_TYPE_USER
|
||||
},
|
||||
// TODO(feature): 用户组后续考虑
|
||||
// TODO(feature): 用户表应通过插件配置关联,考虑到今后会有多账户系统的情况
|
||||
|
@ -0,0 +1,18 @@
|
||||
import { TableOptions } from '@nocobase/database';
|
||||
|
||||
export default {
|
||||
name: 'tabs_permissions',
|
||||
title: '标签页访问权限',
|
||||
developerMode: true,
|
||||
internal: true,
|
||||
fields: [
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'permission',
|
||||
},
|
||||
{
|
||||
type: 'belongsTo',
|
||||
name: 'tab'
|
||||
},
|
||||
],
|
||||
} as TableOptions;
|
3
packages/plugin-permissions/src/constants.ts
Normal file
3
packages/plugin-permissions/src/constants.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export const ROLE_TYPE_ROOT = -1;
|
||||
export const ROLE_TYPE_ANONYMOUS = 0;
|
||||
export const ROLE_TYPE_USER = 1;
|
@ -1,40 +1,26 @@
|
||||
import path from 'path';
|
||||
import { Op } from 'sequelize';
|
||||
import { Application } from '@nocobase/server';
|
||||
import Database, { Operator } from '@nocobase/database';
|
||||
import Resourcer from '@nocobase/resourcer';
|
||||
import { Operator } from '@nocobase/database';
|
||||
import * as collectionsRolesActions from './actions/collections.roles';
|
||||
import * as rolesCollectionsActions from './actions/roles.collections';
|
||||
import * as rolesPagesActions from './actions/roles.pages';
|
||||
import AccessController from './AccessController';
|
||||
|
||||
// API
|
||||
// const permissions = ctx.app.getPluginInstance('permissions');
|
||||
// const result: boolean = permissions.check(ctx);
|
||||
// const result: boolean = permissions.can(key, options);
|
||||
|
||||
class Permissions {
|
||||
|
||||
|
||||
export class Permissions {
|
||||
readonly app: Application;
|
||||
readonly options: any;
|
||||
|
||||
static check(roles) {
|
||||
return Boolean(this.getActionPermissions(roles).length);
|
||||
}
|
||||
|
||||
static getActionPermissions(roles) {
|
||||
const permissions = roles.reduce((permissions, role) => permissions.concat(role.get('permissions')), []);
|
||||
return permissions.reduce((actions, permission) => actions.concat(permission.get('actions_permissions')), []);
|
||||
}
|
||||
|
||||
static getFieldPermissions(roles) {
|
||||
const permissions = roles.reduce((permissions, role) => permissions.concat(role.get('permissions')), []);
|
||||
return permissions.reduce((fields, permission) => fields.concat(permission.get('fields_permissions')), []);
|
||||
}
|
||||
|
||||
constructor(app: Application, options) {
|
||||
this.app = app;
|
||||
this.options = options;
|
||||
|
||||
const database: Database = app.database;
|
||||
const resourcer: Resourcer = app.resourcer;
|
||||
const { database, resourcer } = app;
|
||||
|
||||
database.import({
|
||||
directory: path.resolve(__dirname, 'collections'),
|
||||
@ -48,10 +34,6 @@ class Permissions {
|
||||
resourcer.registerActionHandler(`roles.collections:${actionName}`, rolesCollectionsActions[actionName]);
|
||||
});
|
||||
|
||||
Object.keys(rolesPagesActions).forEach(actionName => {
|
||||
resourcer.registerActionHandler(`roles.pages:${actionName}`, rolesPagesActions[actionName]);
|
||||
});
|
||||
|
||||
const defaultScopes = [
|
||||
{
|
||||
title: '全部数据',
|
||||
@ -84,120 +66,57 @@ class Permissions {
|
||||
// 针对“自己创建的” scope 添加特殊的操作符以生成查询条件
|
||||
if (!Operator.has('$currentUser')) {
|
||||
Operator.register('$currentUser', (value, { ctx }) => {
|
||||
const user = this.getCurrentUser(ctx);
|
||||
const user = ctx.state.currentUser;
|
||||
return { [Op.eq]: user[user.constructor.primaryKeyAttribute] };
|
||||
});
|
||||
}
|
||||
|
||||
// resourcer.use(this.middleware());
|
||||
resourcer.use(this.injection);
|
||||
resourcer.use(this.middleware);
|
||||
}
|
||||
|
||||
middleware() {
|
||||
return async (ctx, next) => {
|
||||
const {
|
||||
resourceName,
|
||||
actionName
|
||||
} = ctx.action.params;
|
||||
injection = async (ctx, next) => {
|
||||
ctx.can = new AccessController(ctx).can;
|
||||
|
||||
const roles = await this.getRolesWithPermissions(ctx);
|
||||
const actionPermissions = Permissions.getActionPermissions(roles);
|
||||
return next();
|
||||
};
|
||||
|
||||
if (!actionPermissions.length) {
|
||||
return ctx.throw(404);
|
||||
}
|
||||
|
||||
const filters = actionPermissions
|
||||
.filter(item => Boolean(item.scope) && Object.keys(item.scope.filter).length)
|
||||
.map(item => item.scope.filter);
|
||||
|
||||
const fields = new Set();
|
||||
const fieldPermissions = Permissions.getFieldPermissions(roles);
|
||||
fieldPermissions.forEach(item => {
|
||||
const actions = item.get('actions');
|
||||
if (actions && actions.includes(`${resourceName}:${actionName}`)) {
|
||||
fields.add(item.get('field').get('name'));
|
||||
}
|
||||
});
|
||||
|
||||
ctx.action.mergeParams({
|
||||
...(filters.length
|
||||
? { filter: filters.length > 1 ? { or: filters } : filters[0] }
|
||||
: {}),
|
||||
fields: Array.from(fields)
|
||||
});
|
||||
|
||||
return next();
|
||||
};
|
||||
}
|
||||
|
||||
getCurrentUser(ctx) {
|
||||
// TODO: 获取当前用户应调用当前依赖用户插件的相关方法
|
||||
return ctx.state.currentUser;
|
||||
}
|
||||
|
||||
async getRolesWithPermissions(ctx) {
|
||||
// TODO: 还未定义关联数据的权限如何表达
|
||||
middleware = async (ctx, next) => {
|
||||
const {
|
||||
resourceName,
|
||||
associated,
|
||||
associatedName,
|
||||
associatedKey,
|
||||
resourceField,
|
||||
resourceName,
|
||||
actionName
|
||||
} = ctx.action.params;
|
||||
|
||||
const Role = ctx.db.getModel('roles');
|
||||
const permissionInclusion = {
|
||||
association: 'permissions',
|
||||
where: {
|
||||
collection_name: resourceName
|
||||
},
|
||||
required: true,
|
||||
include: [
|
||||
{
|
||||
association: 'actions_permissions',
|
||||
where: {
|
||||
name: actionName
|
||||
},
|
||||
required: true,
|
||||
// 对 hasMany 关系可以进行拆分查询,避免联表过多标识符超过 PG 的 64 字符限制
|
||||
separate: true,
|
||||
include: [
|
||||
{
|
||||
association: 'scope',
|
||||
attribute: ['filter']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
association: 'fields_permissions',
|
||||
include: [
|
||||
{
|
||||
association: 'field',
|
||||
attributes: ['name']
|
||||
}
|
||||
],
|
||||
separate: true,
|
||||
}
|
||||
],
|
||||
};
|
||||
// 获取匿名用户的角色及权限
|
||||
const anonymousRoles = await Role.findAll({
|
||||
where: {
|
||||
anonymous: true
|
||||
},
|
||||
include: [
|
||||
permissionInclusion
|
||||
]
|
||||
});
|
||||
// 获取登入用户的角色及权限
|
||||
const currentUser = this.getCurrentUser(ctx);
|
||||
const userRoles = currentUser ? await currentUser.getRoles({
|
||||
include: [
|
||||
permissionInclusion
|
||||
]
|
||||
}) : [];
|
||||
let result = null;
|
||||
|
||||
return [...anonymousRoles, ...userRoles];
|
||||
// 关系数据的权限
|
||||
if (associatedName && resourceField) {
|
||||
result = await ctx.can(resourceField.options.target).act(actionName).any();
|
||||
} else {
|
||||
result = await ctx.can(resourceName).act(actionName).any();
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
return this.reject(ctx);
|
||||
}
|
||||
|
||||
if (result === true) {
|
||||
return next();
|
||||
}
|
||||
|
||||
ctx.action.mergeParams({
|
||||
filter: result.filter,
|
||||
// TODO: 在 fields 改进之前,先注释掉
|
||||
// fields: result.fields.map(item => item.get('field').get('name'))
|
||||
});
|
||||
|
||||
return next();
|
||||
};
|
||||
|
||||
reject(ctx) {
|
||||
ctx.throw(404);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,8 +3,8 @@ import { TableOptions } from '@nocobase/database';
|
||||
export default {
|
||||
name: 'users',
|
||||
title: '用户',
|
||||
developerMode: true,
|
||||
internal: true,
|
||||
// developerMode: true,
|
||||
// internal: true,
|
||||
fields: [
|
||||
{
|
||||
interface: 'string',
|
||||
|
@ -8,6 +8,7 @@ export interface ApplicationOptions {
|
||||
}
|
||||
|
||||
export class Application extends Koa {
|
||||
// static const EVENT_PLUGINS_LOADED = Symbol('pluginsLoaded');
|
||||
|
||||
public readonly database: Database;
|
||||
|
||||
@ -45,7 +46,8 @@ export class Application extends Koa {
|
||||
}
|
||||
|
||||
async loadPlugins() {
|
||||
for (const plugin of this.plugins.values()) {
|
||||
const allPlugins = this.plugins.values();
|
||||
for (const plugin of allPlugins) {
|
||||
plugin.instance = await this.loadPlugin(plugin);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user