mirror of
https://github.com/nocobase/nocobase
synced 2024-11-17 09:25:54 +00:00
7a7ab2ef41
* feat: getRepository * getRepository return type * export action * add: acl * feat: setResourceAction * feat: action alias * chore: code struct * feat: removeResourceAction * chore: file name * ignorecase * remove ACL * feat: ACL * feat: role toJSON * using emit * chore: test * feat: plugin-acl * feat: acl with predicate * grant universal action test * grant action test * update resource action test * revoke resource action * usingActionsConfig switch * plugin-ui-schema-storage * remove global acl instance * fix: collection manager with sqlite * add own action listener * add acl middleware * add acl allowConfigure strategy option * add plugin-acl allowConfigure * change acl resourceName * add acl middleware merge params * bugfix * append fields on acl action params * acl middleware parse template * fix: collection-manager migrate * add acl association field test * feat(plugin-acl): grant association field actions * chore(plugin-acl): type name * feat(plugin-acl): regrant actions on resource action update * feat(plugin-acl): regrant action on field destroy * fix(plugin-acl): test * fix(plugin-acl): test run * feat(plugin-acl): set default role * feat(plugin-users): set user default role * test(plugin-users): create user with role * feat(plugin-users): create user with role Co-authored-by: chenos <chenlinxh@gmail.com>
229 lines
5.7 KiB
TypeScript
229 lines
5.7 KiB
TypeScript
import lodash from 'lodash';
|
|
import { ACLAvailableStrategy, AvailableStrategyOptions, predicate } from './acl-available-strategy';
|
|
import { ACLRole, RoleActionParams } from './acl-role';
|
|
import { AclAvailableAction, AvailableActionOptions } from './acl-available-action';
|
|
import EventEmitter from 'events';
|
|
import { Action } from '@nocobase/resourcer';
|
|
const parse = require('json-templates');
|
|
|
|
interface CanResult {
|
|
role: string;
|
|
resource: string;
|
|
action: string;
|
|
params?: any;
|
|
}
|
|
|
|
export interface DefineOptions {
|
|
role: string;
|
|
allowConfigure?: boolean;
|
|
strategy?: string | Omit<AvailableStrategyOptions, 'acl'>;
|
|
actions?: {
|
|
[key: string]: RoleActionParams;
|
|
};
|
|
routes?: any;
|
|
}
|
|
|
|
export interface ListenerContext {
|
|
acl: ACL;
|
|
role: ACLRole;
|
|
path: string;
|
|
actionName: string;
|
|
resourceName: string;
|
|
params: RoleActionParams;
|
|
}
|
|
|
|
type Listener = (ctx: ListenerContext) => void;
|
|
|
|
interface CanArgs {
|
|
role: string;
|
|
resource: string;
|
|
action: string;
|
|
}
|
|
|
|
export class ACL extends EventEmitter {
|
|
protected availableActions = new Map<string, AclAvailableAction>();
|
|
protected availableStrategy = new Map<string, ACLAvailableStrategy>();
|
|
|
|
roles = new Map<string, ACLRole>();
|
|
|
|
actionAlias = new Map<string, string>();
|
|
|
|
configResources: string[] = [];
|
|
|
|
constructor() {
|
|
super();
|
|
|
|
this.beforeGrantAction((ctx) => {
|
|
if (lodash.isPlainObject(ctx.params) && ctx.params.own) {
|
|
ctx.params = lodash.merge(ctx.params, predicate.own);
|
|
}
|
|
});
|
|
|
|
this.beforeGrantAction((ctx) => {
|
|
const actionName = this.resolveActionAlias(ctx.actionName);
|
|
|
|
if (lodash.isPlainObject(ctx.params)) {
|
|
if ((actionName === 'create' || actionName === 'update') && ctx.params.fields) {
|
|
ctx.params = {
|
|
...lodash.omit(ctx.params, 'fields'),
|
|
whitelist: ctx.params.fields,
|
|
};
|
|
}
|
|
|
|
if (actionName === 'view' && ctx.params.fields) {
|
|
const appendFields = ['id', 'createdAt', 'updatedAt'];
|
|
ctx.params = {
|
|
...lodash.omit(ctx.params, 'fields'),
|
|
fields: [...ctx.params.fields, ...appendFields],
|
|
};
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
define(options: DefineOptions): ACLRole {
|
|
const roleName = options.role;
|
|
const role = new ACLRole(this, roleName);
|
|
|
|
if (options.strategy) {
|
|
role.strategy = options.strategy;
|
|
}
|
|
|
|
const actions = options.actions || {};
|
|
|
|
for (const [actionName, actionParams] of Object.entries(actions)) {
|
|
role.grantAction(actionName, actionParams);
|
|
}
|
|
|
|
this.roles.set(roleName, role);
|
|
|
|
return role;
|
|
}
|
|
|
|
getRole(name: string): ACLRole {
|
|
return this.roles.get(name);
|
|
}
|
|
|
|
removeRole(name: string) {
|
|
return this.roles.delete(name);
|
|
}
|
|
|
|
registerConfigResources(names: string[]) {
|
|
names.forEach((name) => this.registerConfigResource(name));
|
|
}
|
|
|
|
registerConfigResource(name: string) {
|
|
this.configResources.push(name);
|
|
}
|
|
|
|
isConfigResource(name: string) {
|
|
return this.configResources.includes(name);
|
|
}
|
|
|
|
setAvailableAction(name: string, options: AvailableActionOptions) {
|
|
this.availableActions.set(name, new AclAvailableAction(name, options));
|
|
|
|
if (options.aliases) {
|
|
const aliases = lodash.isArray(options.aliases) ? options.aliases : [options.aliases];
|
|
for (const alias of aliases) {
|
|
this.actionAlias.set(alias, name);
|
|
}
|
|
}
|
|
}
|
|
|
|
getAvailableActions() {
|
|
return this.availableActions;
|
|
}
|
|
|
|
setAvailableStrategy(name: string, options: Omit<AvailableStrategyOptions, 'acl'>) {
|
|
this.availableStrategy.set(name, new ACLAvailableStrategy(this, options));
|
|
}
|
|
|
|
beforeGrantAction(listener?: Listener) {
|
|
this.addListener('beforeGrantAction', listener);
|
|
}
|
|
|
|
can({ role, resource, action }: CanArgs): CanResult | null {
|
|
const aclRole = this.roles.get(role);
|
|
const aclResource = aclRole.getResource(resource);
|
|
|
|
if (aclResource) {
|
|
const actionParams = aclResource.getAction(action);
|
|
|
|
if (actionParams) {
|
|
// handle single action config
|
|
return {
|
|
role,
|
|
resource,
|
|
action,
|
|
params: actionParams,
|
|
};
|
|
}
|
|
}
|
|
|
|
if (!aclRole.strategy) {
|
|
return null;
|
|
}
|
|
|
|
const roleStrategy = lodash.isString(aclRole.strategy)
|
|
? this.availableStrategy.get(aclRole.strategy)
|
|
: new ACLAvailableStrategy(this, aclRole.strategy);
|
|
|
|
if (!roleStrategy) {
|
|
return null;
|
|
}
|
|
|
|
const roleStrategyParams = roleStrategy.allow(resource, this.resolveActionAlias(action));
|
|
|
|
if (roleStrategyParams) {
|
|
const result = { role, resource, action };
|
|
|
|
if (lodash.isPlainObject(roleStrategyParams)) {
|
|
result['params'] = roleStrategyParams;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
protected isAvailableAction(actionName: string) {
|
|
return this.availableActions.has(this.resolveActionAlias(actionName));
|
|
}
|
|
|
|
public resolveActionAlias(action: string) {
|
|
return this.actionAlias.get(action) ? this.actionAlias.get(action) : action;
|
|
}
|
|
|
|
middleware() {
|
|
const aclInstance = this;
|
|
|
|
return async function ACLMiddleware(ctx, next) {
|
|
const roleName = ctx.state.currentRole;
|
|
const { resourceName, actionName } = ctx.action;
|
|
|
|
const resourcerAction: Action = ctx.action;
|
|
|
|
ctx.can = (options: Omit<CanArgs, 'role'>) => {
|
|
return aclInstance.can({ role: roleName, ...options });
|
|
};
|
|
|
|
const canResult = ctx.can({ resource: resourceName, action: actionName });
|
|
|
|
if (!canResult) {
|
|
ctx.throw(403, 'no permission');
|
|
return;
|
|
}
|
|
|
|
if (lodash.get(canResult, 'params')) {
|
|
const template = parse(canResult.params);
|
|
|
|
resourcerAction.mergeParams(template({ ctx }));
|
|
}
|
|
|
|
await next();
|
|
};
|
|
}
|
|
}
|