refactor: permissions plugin

This commit is contained in:
chenos 2021-08-11 00:02:37 +08:00
parent 8f58e3ec27
commit fe12c67ba6
26 changed files with 454 additions and 340 deletions

View File

@ -0,0 +1,7 @@
node_modules
*.log
docs
__tests__
tsconfig.json
src
.fatherrc.ts

View File

@ -0,0 +1,13 @@
{
"name": "@nocobase/plugin-permissions-v04",
"version": "0.4.0-alpha.7",
"main": "lib/index.js",
"license": "MIT",
"dependencies": {
"@nocobase/actions": "^0.4.0-alpha.7",
"@nocobase/database": "^0.4.0-alpha.7",
"@nocobase/resourcer": "^0.4.0-alpha.7",
"@nocobase/server": "^0.4.0-alpha.7"
},
"gitHead": "f0b335ac30f29f25c95d7d137655fa64d8d67f1e"
}

View File

@ -0,0 +1,194 @@
import { TableOptions } from '@nocobase/database';
import { ROLE_TYPE_ROOT, ROLE_TYPE_USER, ROLE_TYPE_ANONYMOUS } from '../constants';
export default {
name: 'roles',
title: '权限组配置',
developerMode: true,
internal: true,
fields: [
{
interface: 'string',
title: '权限组名称',
comment: '角色名称',
type: 'string',
name: 'title',
component: {
showInTable: true,
showInForm: true,
showInDetail: true,
},
},
{
interface: 'select',
title: '角色类型',
type: 'integer',
name: 'type',
developerMode: true,
dataSource: [
{ value: ROLE_TYPE_ROOT, label: '系统角色' },
{ value: ROLE_TYPE_ANONYMOUS, label: '匿名角色' },
{ value: ROLE_TYPE_USER, label: '自定义角色' },
],
defaultValue: ROLE_TYPE_USER,
component: {
showInTable: true,
showInDetail: true,
}
},
{
interface: 'boolean',
title: '默认权限组',
type: 'radio',
name: 'default',
component: {
showInTable: true,
showInForm: true,
showInDetail: true,
}
},
{
interface: 'textarea',
title: '描述',
type: 'text',
name: 'description',
component: {
showInTable: true,
showInForm: true,
showInDetail: true,
},
},
// TODO(feature): 用户组后续考虑
// TODO(feature): 用户表应通过插件配置关联,考虑到今后会有多账户系统的情况
{
interface: 'linkTo',
title: '用户',
comment: '关联的用户表',
type: 'belongsToMany',
name: 'users',
target: 'users',
through: 'users_roles'
},
{
interface: 'linkTo',
title: '数据表',
comment: '包含的以数据表分组的权限集',
type: 'belongsToMany',
name: 'collections',
through: 'permissions',
targetKey: 'name'
},
{
interface: 'linkTo',
title: '页面',
type: 'belongsToMany',
name: 'menus',
target: 'menus',
through: 'menus_permissions',
},
{
comment: '权限集(方便访问)',
type: 'hasMany',
name: 'permissions'
},
],
actions: [
{
type: 'list',
name: 'list',
title: '查看',
},
// {
// type: 'get',
// name: 'get',
// title: '详情',
// },
{
type: 'create',
name: 'create',
title: '新增',
viewName: 'form',
},
{
type: 'update',
name: 'update',
title: '编辑',
viewName: 'form',
},
{
type: 'destroy',
name: 'destroy',
title: '删除',
filter: {
type: ROLE_TYPE_USER,
default: false
}
},
],
views_v2: [
{
developerMode: true,
type: 'table',
name: 'table',
title: '全部数据',
labelField: 'title',
actions: [
{
name: 'create',
type: 'create',
title: '新增',
viewName: 'form',
},
{
name: 'destroy',
type: 'destroy',
title: '删除',
},
],
fields: ['title', 'description', 'default'],
detailsOpenMode: 'drawer', // window
details: ['descriptions', 'collections', 'pages'],
sort: ['id'],
},
{
developerMode: true,
type: 'descriptions',
name: 'descriptions',
title: '详情',
fields: ['title', 'description', 'default'],
actions: [
{
name: 'update',
type: 'update',
title: '编辑',
viewName: 'form',
},
],
},
{
developerMode: true,
type: 'form',
name: 'form',
title: '表单',
fields: ['title', 'description', 'default'],
},
{
developerMode: true,
type: 'table',
dataSourceType: 'association',
name: 'collections',
title: '数据操作权限',
targetViewName: 'permissions_table',
targetFieldName: 'collections',
},
{
developerMode: true,
type: 'table',
dataSourceType: 'association',
name: 'pages',
title: '菜单和页面权限',
targetViewName: 'permissions_table',
targetFieldName: 'menus',
},
],
} as TableOptions;

View File

@ -0,0 +1,166 @@
import path from 'path';
import { Op } from 'sequelize';
import { Application } from '@nocobase/server';
import { Operator } from '@nocobase/database';
import * as collectionsRolesActions from './actions/collections.roles';
import * as rolesCollectionsActions from './actions/roles.collections';
import AccessController, { PermissionParams } from './AccessController';
// API
// const permissions = ctx.app.getPluginInstance('permissions');
// const result: boolean = permissions.can(key, options);
export class Permissions {
readonly app: Application;
readonly options: any;
constructor(app: Application, options) {
this.app = app;
this.options = options;
const { database, resourcer } = app;
database.import({
directory: path.resolve(__dirname, 'collections'),
});
Object.keys(collectionsRolesActions).forEach(actionName => {
resourcer.registerActionHandler(`collections.roles:${actionName}`, collectionsRolesActions[actionName]);
});
Object.keys(rolesCollectionsActions).forEach(actionName => {
resourcer.registerActionHandler(`roles.collections:${actionName}`, rolesCollectionsActions[actionName]);
});
database.getModel('collections').addHook('afterCreate', async (model: any, options) => {
await model.updateAssociations({
scopes: [
{
title: '全部数据',
filter: {},
locked: true
},
{
title: '用户自己的数据',
filter: {
"created_by_id.$currentUser": true,
},
locked: true
},
]
}, options);
});
database.getModel('users').addHook('afterCreate', async(model, options) => {
const { transaction = await database.sequelize.transaction() } = options;
const Role = database.getModel('roles');
const defaultRole = await Role.findOne({ where: { default: true }, transaction });
if (defaultRole) {
// @ts-ignore
await model.addRole(defaultRole, { transaction });
}
if (!options.transaction) {
await transaction.commit();
}
});
// 针对“自己创建的” scope 添加特殊的操作符以生成查询条件
if (!Operator.has('$currentUser')) {
Operator.register('$currentUser', (value, { ctx }) => {
const user = ctx.state.currentUser;
return { [Op.eq]: user[user.constructor.primaryKeyAttribute] };
});
}
resourcer.use(this.injection);
resourcer.use(this.middleware);
resourcer.use(async (ctx, next) => {
const { resourceName } = ctx.action.params;
if (resourceName === 'action_logs' && !await ctx.ac.isRoot()) {
const collections = await ctx.ac.getCollections();
ctx.action.mergeParams({
filter: {
'collection_name.in': collections
},
});
}
await next();
});
}
injection = async (ctx, next) => {
ctx.ac = new AccessController(ctx);
return next();
};
middleware = async (ctx, next) => {
const {
associatedName,
resourceField,
resourceName,
actionName
} = ctx.action.params;
let result: PermissionParams = false;
// 关系数据的权限
if (associatedName && resourceField) {
if (resourceField.options.id && resourceField.options.interface === 'subTable') {
if (await ctx.ac.isRoot()) {
return next();
}
const permissions = await ctx.ac.getPermissions();
const FieldPermission = ctx.db.getModel('fields_permissions');
const fps = await FieldPermission.findAll({
where: {
field_id: resourceField.options.id,
permission_id: {
[Op.in]: permissions.map(p => p.id),
}
},
});
if (fps.length) {
for (const fp of fps) {
if (Array.isArray(fp.actions) && fp.actions.includes(`${resourceField.options.collection_name}:${actionName}`)) {
return next();
}
}
return this.reject(ctx);
}
}
result = await ctx.ac.can(resourceField.options.target).act(actionName).any();
} else {
result = await ctx.ac.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);
}
}
export default async function (options = {}) {
const instance = new Permissions(this, options);
return instance;
}

View File

@ -0,0 +1,38 @@
import { TableOptions } from '@nocobase/database';
export default {
name: 'action_permissions',
fields: [
{
type: 'string',
name: 'actionName',
},
{
type: 'belongsTo',
name: 'scope',
},
{
type: 'belongsTo',
name: 'collection',
},
{
type: 'belongsTo',
name: 'role',
},
{
type: 'belongsTo',
name: 'user',
},
{
type: 'hasMany',
name: 'fieldPermissions',
target: 'field_permissions',
foreignKey: 'action_permission_id',
},
{
type: 'belongsToMany',
name: 'fields',
through: 'field_permissions',
},
],
} as TableOptions;

View File

@ -1,194 +1,24 @@
import { TableOptions } from '@nocobase/database'; import { TableOptions } from '@nocobase/database';
import { ROLE_TYPE_ROOT, ROLE_TYPE_USER, ROLE_TYPE_ANONYMOUS } from '../constants';
export default { export default {
name: 'roles', name: 'roles',
title: '权限组配置', sortable: 'sort',
developerMode: true,
internal: true,
fields: [ fields: [
{ {
interface: 'string', type: 'uid',
title: '权限组名称', name: 'name',
comment: '角色名称', primaryKey: true,
prefix: 't_',
},
{
type: 'string', type: 'string',
name: 'title', name: 'title',
component: {
showInTable: true,
showInForm: true,
showInDetail: true,
},
}, },
{ {
interface: 'select',
title: '角色类型',
type: 'integer',
name: 'type',
developerMode: true,
dataSource: [
{ value: ROLE_TYPE_ROOT, label: '系统角色' },
{ value: ROLE_TYPE_ANONYMOUS, label: '匿名角色' },
{ value: ROLE_TYPE_USER, label: '自定义角色' },
],
defaultValue: ROLE_TYPE_USER,
component: {
showInTable: true,
showInDetail: true,
}
},
{
interface: 'boolean',
title: '默认权限组',
type: 'radio',
name: 'default',
component: {
showInTable: true,
showInForm: true,
showInDetail: true,
}
},
{
interface: 'textarea',
title: '描述',
type: 'text',
name: 'description',
component: {
showInTable: true,
showInForm: true,
showInDetail: true,
},
},
// TODO(feature): 用户组后续考虑
// TODO(feature): 用户表应通过插件配置关联,考虑到今后会有多账户系统的情况
{
interface: 'linkTo',
title: '用户',
comment: '关联的用户表',
type: 'belongsToMany', type: 'belongsToMany',
name: 'users', name: 'users',
target: 'users', target: 'users',
through: 'users_roles' through: 'roles_users'
},
{
interface: 'linkTo',
title: '数据表',
comment: '包含的以数据表分组的权限集',
type: 'belongsToMany',
name: 'collections',
through: 'permissions',
targetKey: 'name'
},
{
interface: 'linkTo',
title: '页面',
type: 'belongsToMany',
name: 'menus',
target: 'menus',
through: 'menus_permissions',
},
{
comment: '权限集(方便访问)',
type: 'hasMany',
name: 'permissions'
},
],
actions: [
{
type: 'list',
name: 'list',
title: '查看',
},
// {
// type: 'get',
// name: 'get',
// title: '详情',
// },
{
type: 'create',
name: 'create',
title: '新增',
viewName: 'form',
},
{
type: 'update',
name: 'update',
title: '编辑',
viewName: 'form',
},
{
type: 'destroy',
name: 'destroy',
title: '删除',
filter: {
type: ROLE_TYPE_USER,
default: false
}
},
],
views_v2: [
{
developerMode: true,
type: 'table',
name: 'table',
title: '全部数据',
labelField: 'title',
actions: [
{
name: 'create',
type: 'create',
title: '新增',
viewName: 'form',
},
{
name: 'destroy',
type: 'destroy',
title: '删除',
},
],
fields: ['title', 'description', 'default'],
detailsOpenMode: 'drawer', // window
details: ['descriptions', 'collections', 'pages'],
sort: ['id'],
},
{
developerMode: true,
type: 'descriptions',
name: 'descriptions',
title: '详情',
fields: ['title', 'description', 'default'],
actions: [
{
name: 'update',
type: 'update',
title: '编辑',
viewName: 'form',
},
],
},
{
developerMode: true,
type: 'form',
name: 'form',
title: '表单',
fields: ['title', 'description', 'default'],
},
{
developerMode: true,
type: 'table',
dataSourceType: 'association',
name: 'collections',
title: '数据操作权限',
targetViewName: 'permissions_table',
targetFieldName: 'collections',
},
{
developerMode: true,
type: 'table',
dataSourceType: 'association',
name: 'pages',
title: '菜单和页面权限',
targetViewName: 'permissions_table',
targetFieldName: 'menus',
}, },
], ],
} as TableOptions; } as TableOptions;

View File

@ -0,0 +1,20 @@
import { TableOptions } from '@nocobase/database';
export default {
name: 'scopes',
sortable: 'sort',
fields: [
{
type: 'string',
name: 'title',
},
{
type: 'json',
name: 'filter',
},
{
type: 'belongsTo',
name: 'collection',
},
],
} as TableOptions;

View File

@ -1,166 +1,12 @@
import path from 'path'; import path from 'path';
import { Op } from 'sequelize'; import Database from '@nocobase/database';
import { Application } from '@nocobase/server'; 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 AccessController, { PermissionParams } from './AccessController';
// API export default async function () {
// const permissions = ctx.app.getPluginInstance('permissions'); const database: Database = this.database;
// const result: boolean = permissions.can(key, options); const resourcer: Resourcer = this.resourcer;
export class Permissions {
readonly app: Application;
readonly options: any;
constructor(app: Application, options) {
this.app = app;
this.options = options;
const { database, resourcer } = app;
database.import({ database.import({
directory: path.resolve(__dirname, 'collections'), directory: path.resolve(__dirname, 'collections'),
}); });
Object.keys(collectionsRolesActions).forEach(actionName => {
resourcer.registerActionHandler(`collections.roles:${actionName}`, collectionsRolesActions[actionName]);
});
Object.keys(rolesCollectionsActions).forEach(actionName => {
resourcer.registerActionHandler(`roles.collections:${actionName}`, rolesCollectionsActions[actionName]);
});
database.getModel('collections').addHook('afterCreate', async (model: any, options) => {
await model.updateAssociations({
scopes: [
{
title: '全部数据',
filter: {},
locked: true
},
{
title: '用户自己的数据',
filter: {
"created_by_id.$currentUser": true,
},
locked: true
},
]
}, options);
});
database.getModel('users').addHook('afterCreate', async(model, options) => {
const { transaction = await database.sequelize.transaction() } = options;
const Role = database.getModel('roles');
const defaultRole = await Role.findOne({ where: { default: true }, transaction });
if (defaultRole) {
// @ts-ignore
await model.addRole(defaultRole, { transaction });
}
if (!options.transaction) {
await transaction.commit();
}
});
// 针对“自己创建的” scope 添加特殊的操作符以生成查询条件
if (!Operator.has('$currentUser')) {
Operator.register('$currentUser', (value, { ctx }) => {
const user = ctx.state.currentUser;
return { [Op.eq]: user[user.constructor.primaryKeyAttribute] };
});
}
resourcer.use(this.injection);
resourcer.use(this.middleware);
resourcer.use(async (ctx, next) => {
const { resourceName } = ctx.action.params;
if (resourceName === 'action_logs' && !await ctx.ac.isRoot()) {
const collections = await ctx.ac.getCollections();
ctx.action.mergeParams({
filter: {
'collection_name.in': collections
},
});
}
await next();
});
}
injection = async (ctx, next) => {
ctx.ac = new AccessController(ctx);
return next();
};
middleware = async (ctx, next) => {
const {
associatedName,
resourceField,
resourceName,
actionName
} = ctx.action.params;
let result: PermissionParams = false;
// 关系数据的权限
if (associatedName && resourceField) {
if (resourceField.options.id && resourceField.options.interface === 'subTable') {
if (await ctx.ac.isRoot()) {
return next();
}
const permissions = await ctx.ac.getPermissions();
const FieldPermission = ctx.db.getModel('fields_permissions');
const fps = await FieldPermission.findAll({
where: {
field_id: resourceField.options.id,
permission_id: {
[Op.in]: permissions.map(p => p.id),
}
},
});
if (fps.length) {
for (const fp of fps) {
if (Array.isArray(fp.actions) && fp.actions.includes(`${resourceField.options.collection_name}:${actionName}`)) {
return next();
}
}
return this.reject(ctx);
}
}
result = await ctx.ac.can(resourceField.options.target).act(actionName).any();
} else {
result = await ctx.ac.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);
}
}
export default async function (options = {}) {
const instance = new Permissions(this, options);
return instance;
} }