tachybase_todo/packages/core/acl/src/acl.ts
ChengLei Shao 24ea83f0ff
Feat/create nocobase app (#273)
* create-nocobase-app template from [develop]

* change create-nocobase-app package.json config

* feat: load configuration from directory

* feat: configuration repository toObject

* feat: create application from configuration dir

* feat: application factory with plugins options

* export type

* feat: read application config &  application with plugins options

* feat: release command

* fix: database release

* chore: workflow package.json

* feat: nocobase cli package

* feat: console command

* chore: load application in command

* fix: load packages from process.cwd

* feat: cli load env file

* feat: create-nocobase-app

* fix: gitignore create-nocobase-app lib

* fix: sqlite path

* feat: create plugin

* chore: plugin files template

* chore: move cli into application

* chore: create-nocobase-app

* fix: create plugin

* chore: app-client && app-server

* chore: package.json

* feat: create-nocobase-app download template from npm

* chore: create-nocobase-app template

* fix: config of plugin-users

* fix: yarn.lock

* fix: database build error

* fix: yarn.lock

* fix: resourcer config

* chore: cross-env

* chore: app-client dependents

* fix: env

* chore: v0.6.0-alpha.1

* chore: verdaccio

* chore(versions): 😊 publish v0.6.0

* chore(versions): 😊 publish v0.6.1-alpha.0

* chore(versions): 😊 publish v0.6.2-alpha.0

* chore(versions): 😊 publish v0.6.2-alpha.1

* chore: 0.6.2-alpha.2

* feat: workspaces

* chore(versions): 😊 publish v0.6.2-alpha.3

* chore(versions): 😊 publish v0.6.2-alpha.4

* chore: create-nocobase-app

* chore: create-nocobase-app lib

* fix: update tsconfig.jest.json

* chore: .env

* chore(versions): 😊 publish v0.6.2-alpha.5

* chore(versions): 😊 publish v0.6.2-alpha.6

* feat: improve code

* chore(versions): 😊 publish v0.6.2-alpha.7

* fix: cleanup

* chore(versions): 😊 publish v0.6.2-alpha.8

* chore: tsconfig for app server package

* fix: move files

* fix: move files

Co-authored-by: chenos <chenlinxh@gmail.com>
2022-04-17 10:00:42 +08:00

291 lines
7.2 KiB
TypeScript

import { Action } from '@nocobase/resourcer';
import EventEmitter from 'events';
import compose from 'koa-compose';
import lodash from 'lodash';
import { AclAvailableAction, AvailableActionOptions } from './acl-available-action';
import { ACLAvailableStrategy, AvailableStrategyOptions, predicate } from './acl-available-strategy';
import { ACLRole, RoleActionParams } from './acl-role';
import { SkipManager } from './skip-manager';
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>();
protected middlewares = [];
public skipManager = new SkipManager(this);
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],
};
}
}
});
this.middlewares.push(this.skipManager.aclMiddleware());
}
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);
}
}
}
getAvailableAction(name: string) {
const actionName = this.actionAlias.get(name) || name;
return this.availableActions.get(actionName);
}
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);
if (!aclRole) {
return null;
}
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;
}
use(fn: any) {
this.middlewares.push(fn);
}
skip(resourceName: string, actionName: string, condition?: any) {
this.skipManager.skip(resourceName, actionName, condition);
}
parseJsonTemplate(json: any, ctx: any) {
return parse(json)({
ctx: {
state: JSON.parse(JSON.stringify(ctx.state)),
},
});
}
middleware() {
const acl = this;
const filterParams = (ctx, resourceName, params) => {
if (params?.filter?.createdById) {
const collection = ctx.db.getCollection(resourceName);
if (collection && !collection.getField('createdById')) {
return lodash.omit(params, 'filter.createdById');
}
}
return params;
};
const ctxToObject = (ctx) => {
return {
state: JSON.parse(JSON.stringify(ctx.state)),
};
};
return async function ACLMiddleware(ctx, next) {
const roleName = ctx.state.currentRole || 'anonymous';
const { resourceName, actionName } = ctx.action;
const resourcerAction: Action = ctx.action;
ctx.can = (options: Omit<CanArgs, 'role'>) => {
return acl.can({ role: roleName, ...options });
};
ctx.permission = {
can: ctx.can({ resource: resourceName, action: actionName }),
};
return compose(acl.middlewares)(ctx, async () => {
const permission = ctx.permission;
if (permission.skip) {
return next();
}
if (!permission.can || typeof permission.can !== 'object') {
ctx.throw(403, 'No permissions');
return;
}
const { params } = permission.can;
if (params) {
const filteredParams = filterParams(ctx, resourceName, params);
const parsedParams = acl.parseJsonTemplate(filteredParams, ctx);
resourcerAction.mergeParams(parsedParams);
}
await next();
});
};
}
}