feat: route permissions (#58)

* feat: routes permissions

* fix: try to fix roles.pages action

* 多态关联

* bugfix

* fix: auto generate when option value is undefined or null

* feat: add pages' permissions saving/listing

* feat: add permission filter for getRoutes

* roles description

* feat: get root permissions all true and create user with default role

* feat: roles.collections list output with permission

* add permissions description

* fix: add context to parseApiJson

* fix: typo

* 小细节补充

Co-authored-by: mytharcher <mytharcher@gmail.com>
This commit is contained in:
chenos 2021-01-22 10:18:02 +08:00 committed by GitHub
parent bd756a6a5c
commit 301229ef88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 501 additions and 124 deletions

View File

@ -1,3 +1,11 @@
// @ts-ignore
global.sync = {
force: true,
alter: {
drop: true,
},
};
import api from '../app'; import api from '../app';
import Database from '@nocobase/database'; import Database from '@nocobase/database';
@ -135,6 +143,9 @@ const data = [
const tables = database.getTables([]); const tables = database.getTables([]);
for (let table of tables) { for (let table of tables) {
console.log(table.getName()); console.log(table.getName());
if (table.getName() === 'roles') {
console.log('roles', table.getOptions())
}
await Collection.import(table.getOptions(), { update: true, migrate: false }); await Collection.import(table.getOptions(), { update: true, migrate: false });
} }
await Page.import(data); await Page.import(data);
@ -182,6 +193,7 @@ const data = [
const roles = await Role.bulkCreate([ const roles = await Role.bulkCreate([
{ title: '系统开发组', type: -1 }, { title: '系统开发组', type: -1 },
{ title: '匿名用户组', type: 0 }, { title: '匿名用户组', type: 0 },
{ title: '普通用户组', default: true },
]); ]);
await roles[0].updateAssociations({ await roles[0].updateAssociations({
users: user users: user

View File

@ -8,6 +8,8 @@ import get from 'lodash/get';
import './style.less'; import './style.less';
import Field from '../Field'; import Field from '../Field';
import cloneDeep from 'lodash/cloneDeep'; import cloneDeep from 'lodash/cloneDeep';
import { Checkbox, message } from 'antd';
import api from '@/api-client';
export const SortableItem = sortableElement(props => <tr {...props} />); export const SortableItem = sortableElement(props => <tr {...props} />);
export const SortableContainer = sortableContainer(props => <tbody {...props} />); export const SortableContainer = sortableContainer(props => <tbody {...props} />);
@ -58,11 +60,38 @@ export const components = ({data = {}, rowKey, mutate, onMoved, isFieldComponent
}; };
}; };
export function fields2columns(fields) { export function fields2columns(fields, ctx: any = {}) {
const columns: any[] = fields.map(item => { const columns: any[] = fields.map(item => {
const field = cloneDeep(item); const field = cloneDeep(item);
field.render = (value, record) => field.interface === 'sort' ? <DragHandle/> : <Field data={record} viewType={'table'} schema={field} value={value}/>; field.render = (value, record) => field.interface === 'sort' ? <DragHandle/> : <Field data={record} viewType={'table'} schema={field} value={value}/>;
field.className = `${field.className||''} noco-field-${field.interface}`; field.className = `${field.className||''} noco-field-${field.interface}`;
if (field.editable && field.interface === 'boolean') {
field.title = (
<span>
<Checkbox onChange={async (e) => {
try {
await api.resource(field.resource).update({
associatedKey: ctx.associatedKey,
// resourceKey: data.id,
// tableName: data.tableName||'pages',
values: {
accessible: e.target.checked,
},
});
message.success('保存成功');
if (ctx.refresh) {
ctx.refresh();
}
} catch (error) {
message.error('保存失败');
}
}}/>
{' '}
{field.title}
</span>
);
}
return { return {
...field, ...field,
...(field.component||{}), ...(field.component||{}),

View File

@ -154,7 +154,7 @@ export function Table(props: TableProps) {
indicator: icon, indicator: icon,
// className: 'spinning--absolute m32', // className: 'spinning--absolute m32',
}} }}
columns={fields2columns(fields)} columns={fields2columns(fields, {associatedKey, refresh})}
dataSource={data?.list||(data as any)} dataSource={data?.list||(data as any)}
onChange={(pagination, filters, sorter, extra) => { onChange={(pagination, filters, sorter, extra) => {
run({...params[0], sorter}); run({...params[0], sorter});

View File

@ -68,6 +68,8 @@ export default class Database {
protected hooks = {}; protected hooks = {};
protected extTableOptions = new Map<string, any>();
constructor(options: DatabaseOptions) { constructor(options: DatabaseOptions) {
this.options = options; this.options = options;
this.sequelize = new Sequelize(options); this.sequelize = new Sequelize(options);
@ -94,10 +96,21 @@ export default class Database {
files.forEach((file: string) => { files.forEach((file: string) => {
const result = requireModule(file); const result = requireModule(file);
if (result instanceof Extend) { if (result instanceof Extend) {
const table = this.extend(result.tableOptions, result.mergeOptions); // 如果还没初始化extend 的先暂存起来,后续处理
tables.set(table.getName(), table); if (!this.tables.has(result.tableOptions.name)) {
this.extTableOptions.set(result.tableOptions.name, result);
} else {
const table = this.extend(result.tableOptions, result.mergeOptions);
tables.set(table.getName(), table);
}
} else { } else {
const table = this.extend(typeof result === 'function' ? result(this) : result); let table = this.extend(typeof result === 'function' ? result(this) : result);
// 如果有未处理的 extend 取回来合并
if (this.extTableOptions.has(table.getName())) {
const result = this.extTableOptions.get(table.getName());
table = this.extend(result.tableOptions, result.mergeOptions);
this.extTableOptions.delete(table.getName());
}
tables.set(table.getName(), table); tables.set(table.getName(), table);
} }
}); });

View File

@ -18,7 +18,7 @@ export default async function (model: FieldModel, options) {
const dataSource = model.get('dataSource'); const dataSource = model.get('dataSource');
if (Array.isArray(dataSource)) { if (Array.isArray(dataSource)) {
model.set('dataSource', dataSource.map(item => { model.set('dataSource', dataSource.map(item => {
if (!item.value) { if (item.value === null || typeof item.value === 'undefined') {
item.value = generateValueName(); item.value = generateValueName();
} }
return {...item}; return {...item};

View File

@ -40,6 +40,21 @@ export default async function getRoutes(ctx, next) {
const database: Database = ctx.database; const database: Database = ctx.database;
const Page = database.getModel('pages'); const Page = database.getModel('pages');
const Collection = database.getModel('collections'); const Collection = database.getModel('collections');
const RoutePermission = database.getModel('routes_permissions');
const roles = await ctx.ac.getRoles();
// TODO(optimize): isRoot 的判断需要在内部完成,尽量不要交给调用者
const isRoot = ctx.ac.constructor.isRoot(roles);
const routesPermissionsMap = new Map();
if (!isRoot) {
const routesPermissions = await RoutePermission.findAll({
where: {
role_id: roles.map(({ id }) => id)
}
});
routesPermissions.forEach(permission => {
routesPermissionsMap.set(`${permission.routable_type}:${permission.routable_id}`, permission);
});
}
let pages = await Page.findAll(Page.parseApiJson(ctx.state.developerMode ? { let pages = await Page.findAll(Page.parseApiJson(ctx.state.developerMode ? {
filter: { filter: {
}, },
@ -52,6 +67,15 @@ export default async function getRoutes(ctx, next) {
})); }));
const items = []; const items = [];
for (const page of pages) { for (const page of pages) {
if (!isRoot
&& !routesPermissionsMap.has(`pages:${page.id}`)
// 以下路径先临时处理
&& page.get('path') !== '/'
&& page.get('path') !== '/register'
&& page.get('path') !== '/login'
) {
continue;
}
items.push(page.toJSON()); items.push(page.toJSON());
if (page.get('path') === '/collections') { if (page.get('path') === '/collections') {
const collections = await Collection.findAll(Collection.parseApiJson(ctx.state.developerMode ? { const collections = await Collection.findAll(Collection.parseApiJson(ctx.state.developerMode ? {
@ -67,6 +91,9 @@ export default async function getRoutes(ctx, next) {
sort: ['sort'], sort: ['sort'],
})); }));
for (const collection of collections) { for (const collection of collections) {
if (!isRoot && !routesPermissionsMap.has(`collections:${collection.id}`)) {
continue;
}
const pageId = `collection-${collection.id}`; const pageId = `collection-${collection.id}`;
items.push({ items.push({
id: pageId, id: pageId,
@ -90,6 +117,9 @@ export default async function getRoutes(ctx, next) {
}); });
if (views.length > 1) { if (views.length > 1) {
for (const view of views) { for (const view of views) {
if (!isRoot && !routesPermissionsMap.has(`views:${view.id}`)) {
continue;
}
items.push({ items.push({
id: `view-${view.get('id')}`, id: `view-${view.get('id')}`,
type: 'collection', type: 'collection',

View File

@ -201,7 +201,7 @@ export default async (ctx, next) => {
})); }));
let throughName; let throughName;
const { resourceKey: resourceKey2, associatedName, resourceFieldName, associatedKey } = values; const { resourceKey: resourceKey2, associatedName, resourceFieldName, associatedKey } = values;
const permissions = await ctx.can(resourceName).permissions(); const permissions = await ctx.ac.can(resourceName).permissions();
ctx.listFields = []; ctx.listFields = [];
ctx.createFields = []; ctx.createFields = [];
ctx.updateFields = []; ctx.updateFields = [];
@ -209,7 +209,7 @@ export default async (ctx, next) => {
for (const action of permissions.actions) { for (const action of permissions.actions) {
ctx.allowedActions.push(action.name); ctx.allowedActions.push(action.name);
} }
console.log(ctx.allowedActions); // console.log(ctx.allowedActions);
for (const permissionField of permissions.fields) { for (const permissionField of permissions.fields) {
const pfc = permissionField.actions; const pfc = permissionField.actions;
if (pfc.includes(`${resourceName}:list`)) { if (pfc.includes(`${resourceName}:list`)) {
@ -222,11 +222,11 @@ export default async (ctx, next) => {
ctx.updateFields.push(permissionField.field_id); ctx.updateFields.push(permissionField.field_id);
} }
} }
console.log({ // console.log({
listFields: ctx.listFields, // listFields: ctx.listFields,
createFields: ctx.createFields, // createFields: ctx.createFields,
updateFields: ctx.updateFields, // updateFields: ctx.updateFields,
}) // })
if (associatedName) { if (associatedName) {
const table = ctx.db.getTable(associatedName); const table = ctx.db.getTable(associatedName);
const resourceField = table.getField(resourceFieldName); const resourceField = table.getField(resourceFieldName);
@ -420,6 +420,23 @@ export default async (ctx, next) => {
"showInDetail": true "showInDetail": true
}, },
"dataIndex": ["title"] "dataIndex": ["title"]
},
{
"title": "描述",
"name": "permissions[0].description",
"interface": "string",
"type": "string",
"parent_id": null,
"required": true,
"developerMode": false,
"component": {
"type": "string",
"className": "drag-visible",
"showInForm": true,
"showInTable": true,
"showInDetail": true
},
"dataIndex": ["permissions", 0, 'description']
} }
], ],
}; };
@ -496,7 +513,7 @@ export default async (ctx, next) => {
}; };
} else { } else {
let allowedUpdate = false; let allowedUpdate = false;
if (view.type === 'details' && await ctx.can(resourceName).act('update').one(resourceKey2)) { if (view.type === 'details' && await ctx.ac.can(resourceName).act('update').one(resourceKey2)) {
allowedUpdate = true; allowedUpdate = true;
} }
ctx.body = { ctx.body = {

View File

@ -3,9 +3,8 @@ import Database from '@nocobase/database';
import { flatToTree } from '../utils'; import { flatToTree } from '../utils';
import { Op } from 'sequelize'; import { Op } from 'sequelize';
export async function list(ctx: actions.Context, next: actions.Next) { async function getRoutes(ctx) {
const database: Database = ctx.db; const database: Database = ctx.db;
const { associatedKey } = ctx.action.params;
const Page = database.getModel('pages'); const Page = database.getModel('pages');
const Collection = database.getModel('collections'); const Collection = database.getModel('collections');
let pages = await Page.findAll(Page.parseApiJson(ctx.state.developerMode ? { let pages = await Page.findAll(Page.parseApiJson(ctx.state.developerMode ? {
@ -21,6 +20,80 @@ export async function list(ctx: actions.Context, next: actions.Next) {
sort: ['sort'], sort: ['sort'],
})); }));
const items = []; const items = [];
for (const page of pages) {
items.push({
routable_type: 'pages',
routable_id: page.id,
});
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({
routable_type: 'collections',
routable_id: collection.id,
});
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({
routable_id: view.id,
routable_type: 'views',
});
}
}
}
}
}
return items;
}
export async function list(ctx: actions.Context, next: actions.Next) {
const database: Database = ctx.db;
const { associatedKey, associated } = ctx.action.params;
const Page = database.getModel('pages');
const Collection = database.getModel('collections');
// TODO(optimize): isRoot 的判断需要在内部完成,尽量不要交给调用者
const isRoot = ctx.ac.constructor.isRoot(associated);
const routesPermissionsMap = new Map();
if (!isRoot) {
const routesPermissions = await associated.getRoutes();
routesPermissions.forEach(permission => {
routesPermissionsMap.set(`${permission.routable_type}:${permission.routable_id}`, permission);
});
}
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) { for (const page of pages) {
items.push({ items.push({
id: page.id, id: page.id,
@ -29,7 +102,7 @@ export async function list(ctx: actions.Context, next: actions.Next) {
tableName: 'pages', tableName: 'pages',
parent_id: `page-${page.parent_id}`, parent_id: `page-${page.parent_id}`,
associatedKey, associatedKey,
accessible: false, // TODO 对接权限 accessible: isRoot || routesPermissionsMap.has(`pages:${page.id}`), // TODO 对接权限
}); });
if (page.get('path') === '/collections') { if (page.get('path') === '/collections') {
const collections = await Collection.findAll(Collection.parseApiJson(ctx.state.developerMode ? { const collections = await Collection.findAll(Collection.parseApiJson(ctx.state.developerMode ? {
@ -52,7 +125,7 @@ export async function list(ctx: actions.Context, next: actions.Next) {
tableName: 'collections', tableName: 'collections',
title: collection.get('title'), title: collection.get('title'),
parent_id: `page-${page.id}`, parent_id: `page-${page.id}`,
accessible: false, // TODO 对接权限 accessible: isRoot || routesPermissionsMap.has(`collections:${collection.id}`), // TODO 对接权限
}); });
const views = await collection.getViews({ const views = await collection.getViews({
where: { where: {
@ -72,7 +145,7 @@ export async function list(ctx: actions.Context, next: actions.Next) {
title: view.title, title: view.title,
key: `view-${view.id}`, key: `view-${view.id}`,
parent_id: `collection-${collection.id}`, parent_id: `collection-${collection.id}`,
accessible: false, // TODO 对接权限 accessible: isRoot || routesPermissionsMap.has(`views:${view.id}`), // TODO 对接权限
}); });
} }
} }
@ -106,6 +179,46 @@ export async function list(ctx: actions.Context, next: actions.Next) {
} }
export async function update(ctx: actions.Context, next: actions.Next) { export async function update(ctx: actions.Context, next: actions.Next) {
ctx.body = {}; const {
associated,
resourceKey,
values: {
tableName,
accessible
}
} = ctx.action.params;
if (!resourceKey) {
if (accessible === false) {
await associated.updateAssociations({
routes: [],
});
} else if (accessible === true) {
const routes = await getRoutes(ctx);
// console.log(routes);
await associated.updateAssociations({
routes,
});
}
ctx.body = {};
return next();
}
console.log(ctx.action.params, { routable_type: tableName, routable_id: resourceKey });
let [route] = await associated.getRoutes({
where: { routable_type: tableName, routable_id: resourceKey },
limit: 1
});
if (accessible) {
if (!route) {
route = await associated.createRoute({ routable_type: tableName, routable_id: resourceKey });
}
ctx.body = route;
} else {
if (route) {
await route.destroy();
}
}
await next(); await next();
} }

View File

@ -192,6 +192,15 @@ export default {
type: 'number', type: 'number',
}, },
}, },
// {
// type: 'belongsToMany',
// name: 'roles',
// through: 'routes_permissions',
// foreignKey: 'routable_id',
// otherKey: 'role_id',
// morphType: 'routable',
// constraints: false,
// },
{ {
interface: 'json', interface: 'json',
type: 'json', type: 'json',

View File

@ -0,0 +1,43 @@
import { extend } from '@nocobase/database';
export default extend({
name: 'roles',
fields: [
{
type: 'hasMany',
name: 'routes',
target: 'routes_permissions',
},
{
interface: 'linkTo',
type: 'belongsToMany',
name: 'pages',
title: '可访问的页面',
through: 'routes_permissions',
foreignKey: 'role_id',
otherKey: 'routable_id',
morphType: 'routable', // 现在没有多态关联的设置,暂时先这么写了
constraints: false, // 多态关联建立外键约束会有问题
}
],
tabs: [
{
type: 'association',
name: 'pages',
title: '系统菜单权限',
association: 'pages',
viewName: 'permissionTable',
},
]
}, {
customMerge(key) {
if (['tabs'].includes(key)) {
return (x = [], y = []) => {
const last = x.pop();
const tabs = x.concat(y);
tabs.push(last);
return tabs;
};
}
}
});

View File

@ -0,0 +1,30 @@
import { TableOptions } from '@nocobase/database';
export default {
name: 'routes_permissions',
title: '页面权限',
developerMode: true,
internal: true,
fields: [
{
type: 'integer',
name: 'id',
primaryKey: true,
autoIncrement: true,
},
{
type: 'string',
name: 'routable_type',
title: '关联的表', // 仅 pages 和 collections
},
{
type: 'integer',
name: 'routable_id',
title: '关联的对象'
},
{
type: 'belongsTo',
name: 'role'
},
],
} as TableOptions;

View File

@ -5,7 +5,7 @@ import { ROLE_TYPE_ANONYMOUS, ROLE_TYPE_ROOT, ROLE_TYPE_USER } from './constants
function getPermissions(roles) { function getPermissions(roles) {
return roles.reduce((permissions, role) => permissions.concat(role.get('permissions')), []); return roles.reduce((permissions, role) => permissions.concat(role.permissions), []);
} }
function getActionPermissions(permissions) { function getActionPermissions(permissions) {
@ -97,10 +97,16 @@ export type PermissionParams = true | null | {
// ctx.can('collection').act('update').any() // ctx.can('collection').act('update').any()
// ctx.can('collection').act('update').one(resourceKey) // ctx.can('collection').act('update').one(resourceKey)
// ctx.can('collection').act('get').one(resourceKey) // ctx.can('collection').act('get').one(resourceKey)
// ctx.as(roles).can('collection').permissions()
// TODO(optimize): 需要优化链式调用结构和上下文,以避免顺序或重置等问题
export default class AccessController<T extends typeof AccessController = typeof AccessController> {
static isRoot(roles): boolean {
return (Array.isArray(roles) ? roles : [roles]).some(role => role.type === ROLE_TYPE_ROOT);
}
export default class AccessController {
context; context;
roles;
resourceName: string | null = null; resourceName: string | null = null;
actionName: string | null = null; actionName: string | null = null;
@ -108,11 +114,21 @@ export default class AccessController {
this.context = ctx; this.context = ctx;
} }
can = (resourceName: string | null) => { /**
*
* @param roles Array
*/
as(roles) {
const instance = new (this.constructor as T)(this.context);
instance.roles = Array.isArray(roles) ? roles : [roles];
return instance;
}
can(resourceName: string | null) {
this.resourceName = resourceName; this.resourceName = resourceName;
this.actionName = null; this.actionName = null;
return this; return this;
}; }
act(name: string | null) { act(name: string | null) {
this.actionName = name; this.actionName = name;
@ -121,7 +137,7 @@ export default class AccessController {
async permissions(): Promise<CollectionPermissions> { async permissions(): Promise<CollectionPermissions> {
const roles = await this.getRolesWithPermissions(); const roles = await this.getRolesWithPermissions();
if (roles.some(role => role.type === ROLE_TYPE_ROOT)) { if ((this.constructor as T).isRoot(roles)) {
return this.getRootPermissions(); return this.getRootPermissions();
} }
@ -136,7 +152,7 @@ export default class AccessController {
async any(): Promise<PermissionParams> { async any(): Promise<PermissionParams> {
const roles = await this.getRolesWithPermissions(); const roles = await this.getRolesWithPermissions();
if (roles.some(role => role.type === ROLE_TYPE_ROOT)) { if ((this.constructor as T).isRoot(roles)) {
return true; return true;
} }
// 只处理 actions 表里的权限,其余跳过 // 只处理 actions 表里的权限,其余跳过
@ -180,7 +196,7 @@ export default class AccessController {
const Collection = this.context.db.getModel(this.resourceName); const Collection = this.context.db.getModel(this.resourceName);
const existed = await Collection.count({ const existed = await Collection.count({
where: { where: {
...Collection.parseApiJson({ filter }).where, ...Collection.parseApiJson({ filter, context: this.context }).where,
[Collection.primaryKeyAttribute]: resourceKey [Collection.primaryKeyAttribute]: resourceKey
} }
}); });
@ -188,25 +204,78 @@ export default class AccessController {
return existed ? any : null; return existed ? any : null;
} }
async isRoot(): Promise<boolean> {
const { context } = this;
const { currentUser } = context.state;
if (!currentUser) {
return false;
}
const rootRoles = await currentUser.countRoles({
where: {
type: ROLE_TYPE_ROOT
}
});
if (!rootRoles.length) {
return false;
}
return true;
}
async getRoles() {
if (this.roles) {
return this.roles;
}
const { context } = this;
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
}
});
}
const Role = context.db.getModel('roles');
const anonymousRoles = await Role.findAll({
where: {
type: ROLE_TYPE_ANONYMOUS
}
});
return [...userRoles, ...anonymousRoles];
}
async getRolesWithPermissions() { async getRolesWithPermissions() {
const { context, resourceName, actionName = null } = this; const { context, resourceName, actionName = null } = this;
if (!resourceName) { if (!resourceName) {
throw new Error('resource name must be set first by `can(resourceName)`'); throw new Error('resource name must be set first by `can(resourceName)`');
} }
const Role = context.db.getModel('roles');
const permissionInclusion = { const permissionOptions = {
association: 'permissions',
where: { where: {
collection_name: resourceName collection_name: resourceName
}, },
required: true,
include: [ include: [
{ {
association: 'actions', association: 'actions',
where: actionName ? { ...(actionName ? {
name: `${resourceName}:${actionName}` where: {
} : {}, name: `${resourceName}:${actionName}`
required: true, },
required: true,
} : {}),
// 对 hasMany 关系可以进行拆分查询,避免联表过多标识符超过 PG 的 64 字符限制 // 对 hasMany 关系可以进行拆分查询,避免联表过多标识符超过 PG 的 64 字符限制
separate: true, separate: true,
include: [ include: [
@ -228,8 +297,24 @@ export default class AccessController {
association: 'tabs_permissions', association: 'tabs_permissions',
separate: true, separate: true,
} }
], ]
}; };
const permissionInclusion = {
...permissionOptions,
association: 'permissions',
required: true
};
if (this.roles) {
if ((this.constructor as T).isRoot(this.roles)) {
return this.roles;
}
for (const role of this.roles) {
role.permissions = await role.getPermissions(permissionOptions);
role.set('permissions', role.permissions);
}
return this.roles;
}
let userRoles = []; let userRoles = [];
// 获取登入用户的角色及权限 // 获取登入用户的角色及权限
@ -255,6 +340,7 @@ export default class AccessController {
} }
// 获取匿名用户的角色及权限 // 获取匿名用户的角色及权限
const Role = context.db.getModel('roles');
const anonymousRoles = await Role.findAll({ const anonymousRoles = await Role.findAll({
where: { where: {
type: ROLE_TYPE_ANONYMOUS type: ROLE_TYPE_ANONYMOUS

View File

@ -1,10 +1,24 @@
import { Op } from 'sequelize'; import { Op } from 'sequelize';
import { actions } from '@nocobase/actions'; import { actions } from '@nocobase/actions';
import _ from 'lodash';
export async function list(ctx: actions.Context, next: actions.Next) { export async function list(ctx: actions.Context, next: actions.Next) {
const { associated } = ctx.action.params;
// TODO: 暂时 action 中间件就这么写了 // TODO: 暂时 action 中间件就这么写了
ctx.action.mergeParams({associated: null}); ctx.action.mergeParams({
return actions.common.list(ctx, next); associated: null
});
await actions.common.list(ctx, async () => {
const permissions = await associated.getPermissions();
ctx.body.rows.forEach(item => {
const permission = permissions.find(p => p.collection_name === item.get('name'));
if (permission) {
// item.permissions = [permission]; // 不输出
item.set('permissions', [permission]); // 输出
}
});
});
await next();
} }
export async function get(ctx: actions.Context, next: actions.Next) { export async function get(ctx: actions.Context, next: actions.Next) {
@ -13,47 +27,22 @@ export async function get(ctx: actions.Context, next: actions.Next) {
associated associated
} = ctx.action.params; } = ctx.action.params;
const [permission] = await associated.getPermissions({ const permissions = await ctx.ac.as(associated).can(resourceKey).permissions();
where: {
collection_name: resourceKey
},
include: [
{
association: 'actions',
// 对 hasMany 关系可以进行拆分查询,避免联表过多标识符超过 PG 的 64 字符限制
separate: true,
include: [
{
association: 'scope'
}
]
},
{
association: 'fields_permissions',
separate: true,
},
{
association: 'tabs_permissions',
separate: true,
}
],
distinct: true,
limit: 1
});
const result = permission
? {
actions: permission.actions || [],
fields: permission.fields_permissions || [],
tabs: (permission.tabs_permissions || []).map(item => item.tab_id),
}
: {
actions: [],
fields: [],
tabs: []
};
ctx.body = result; const permission = await associated.getPermissions({
where: {
collection_name: resourceKey,
},
plain: true,
limit: 1,
});
console.log(permission);
ctx.body = {
...permissions,
description: _.get(permission, 'description'),
};
await next(); await next();
} }

View File

@ -1,27 +0,0 @@
import { actions } from '@nocobase/actions';
export async function list(ctx: actions.Context, next: actions.Next) {
// 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();
}

View File

@ -27,6 +27,12 @@ export default extend({
through: 'permissions', through: 'permissions',
sourceKey: 'name' sourceKey: 'name'
}, },
// {
// type: 'hasMany',
// name: 'permissions',
// sourceKey: 'name',
// foreignKey: 'collection_name'
// }
], ],
views: [ views: [
{ {

View File

@ -21,7 +21,7 @@ export default {
}, },
{ {
type: 'string', type: 'string',
name: 'desctiption', name: 'description',
}, },
{ {
comment: '关联的角色', comment: '关联的角色',

View File

@ -24,12 +24,39 @@ export default {
title: '角色类型', title: '角色类型',
type: 'integer', type: 'integer',
name: 'type', name: 'type',
developerMode: true,
dataSource: [ dataSource: [
{ value: ROLE_TYPE_ROOT, label: '系统角色' }, { value: ROLE_TYPE_ROOT, label: '系统角色' },
{ value: ROLE_TYPE_ANONYMOUS, label: '匿名角色' }, { value: ROLE_TYPE_ANONYMOUS, label: '匿名角色' },
{ value: ROLE_TYPE_USER, label: '自定义角色' }, { value: ROLE_TYPE_USER, label: '自定义角色' },
], ],
defaultValue: ROLE_TYPE_USER 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): 用户组后续考虑
// TODO(feature): 用户表应通过插件配置关联,考虑到今后会有多账户系统的情况 // TODO(feature): 用户表应通过插件配置关联,考虑到今后会有多账户系统的情况
@ -51,12 +78,6 @@ export default {
through: 'permissions', through: 'permissions',
targetKey: 'name' targetKey: 'name'
}, },
{
interface: 'linkTo',
title: '页面',
type: 'belongsToMany',
name: 'pages',
},
{ {
comment: '权限集(方便访问)', comment: '权限集(方便访问)',
type: 'hasMany', type: 'hasMany',
@ -134,13 +155,6 @@ export default {
association: 'collections', association: 'collections',
viewName: 'permissionTable', viewName: 'permissionTable',
}, },
{
type: 'association',
name: 'pages',
title: '系统菜单权限',
association: 'pages',
viewName: 'permissionTable',
},
{ {
type: 'association', type: 'association',
name: 'users', name: 'users',

View File

@ -63,6 +63,19 @@ export class Permissions {
} }
}); });
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 添加特殊的操作符以生成查询条件 // 针对“自己创建的” scope 添加特殊的操作符以生成查询条件
if (!Operator.has('$currentUser')) { if (!Operator.has('$currentUser')) {
Operator.register('$currentUser', (value, { ctx }) => { Operator.register('$currentUser', (value, { ctx }) => {
@ -76,7 +89,7 @@ export class Permissions {
} }
injection = async (ctx, next) => { injection = async (ctx, next) => {
ctx.can = new AccessController(ctx).can; ctx.ac = new AccessController(ctx);
return next(); return next();
}; };
@ -93,9 +106,9 @@ export class Permissions {
// 关系数据的权限 // 关系数据的权限
if (associatedName && resourceField) { if (associatedName && resourceField) {
result = await ctx.can(resourceField.options.target).act(actionName).any(); result = await ctx.ac.can(resourceField.options.target).act(actionName).any();
} else { } else {
result = await ctx.can(resourceName).act(actionName).any(); result = await ctx.ac.can(resourceName).act(actionName).any();
} }
if (!result) { if (!result) {