2022-02-11 19:31:53 +08:00
|
|
|
import { Action } from '@nocobase/resourcer';
|
2023-04-21 11:13:43 +08:00
|
|
|
import { assign, parseFilter, Toposort, ToposortOptions } from '@nocobase/utils';
|
2022-02-11 19:31:53 +08:00
|
|
|
import EventEmitter from 'events';
|
|
|
|
import compose from 'koa-compose';
|
2022-01-18 16:38:03 +08:00
|
|
|
import lodash from 'lodash';
|
2022-10-06 10:29:53 +08:00
|
|
|
import { ACLAvailableAction, AvailableActionOptions } from './acl-available-action';
|
2022-01-24 14:10:35 +08:00
|
|
|
import { ACLAvailableStrategy, AvailableStrategyOptions, predicate } from './acl-available-strategy';
|
2022-10-06 10:29:53 +08:00
|
|
|
import { ACLRole, ResourceActionsOptions, RoleActionParams } from './acl-role';
|
|
|
|
import { AllowManager, ConditionFunc } from './allow-manager';
|
2023-01-09 07:35:48 +08:00
|
|
|
import FixedParamsManager, { Merger } from './fixed-params-manager';
|
|
|
|
import SnippetManager, { SnippetOptions } from './snippet-manager';
|
2022-01-18 16:38:03 +08:00
|
|
|
|
|
|
|
interface CanResult {
|
|
|
|
role: string;
|
|
|
|
resource: string;
|
|
|
|
action: string;
|
|
|
|
params?: any;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface DefineOptions {
|
|
|
|
role: string;
|
2022-01-24 14:10:35 +08:00
|
|
|
allowConfigure?: boolean;
|
2022-10-06 10:29:53 +08:00
|
|
|
strategy?: string | AvailableStrategyOptions;
|
|
|
|
actions?: ResourceActionsOptions;
|
2022-01-18 16:38:03 +08:00
|
|
|
routes?: any;
|
2023-01-09 07:35:48 +08:00
|
|
|
snippets?: string[];
|
2022-01-18 16:38:03 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
export interface ListenerContext {
|
|
|
|
acl: ACL;
|
|
|
|
role: ACLRole;
|
2022-01-24 14:10:35 +08:00
|
|
|
path: string;
|
|
|
|
actionName: string;
|
|
|
|
resourceName: string;
|
2022-01-18 16:38:03 +08:00
|
|
|
params: RoleActionParams;
|
|
|
|
}
|
|
|
|
|
|
|
|
type Listener = (ctx: ListenerContext) => void;
|
|
|
|
|
2022-01-24 14:10:35 +08:00
|
|
|
interface CanArgs {
|
|
|
|
role: string;
|
|
|
|
resource: string;
|
|
|
|
action: string;
|
2023-01-09 07:35:48 +08:00
|
|
|
ctx?: any;
|
2022-01-24 14:10:35 +08:00
|
|
|
}
|
|
|
|
|
2022-01-18 16:38:03 +08:00
|
|
|
export class ACL extends EventEmitter {
|
2023-01-09 07:35:48 +08:00
|
|
|
public availableStrategy = new Map<string, ACLAvailableStrategy>();
|
2022-04-24 10:14:46 +08:00
|
|
|
public allowManager = new AllowManager(this);
|
2023-01-09 07:35:48 +08:00
|
|
|
public snippetManager = new SnippetManager();
|
2022-01-18 16:38:03 +08:00
|
|
|
roles = new Map<string, ACLRole>();
|
|
|
|
actionAlias = new Map<string, string>();
|
2022-01-24 14:10:35 +08:00
|
|
|
configResources: string[] = [];
|
2023-05-24 21:31:12 +08:00
|
|
|
protected availableActions = new Map<string, ACLAvailableAction>();
|
|
|
|
protected fixedParamsManager = new FixedParamsManager();
|
|
|
|
protected middlewares: Toposort<any>;
|
2022-01-24 14:10:35 +08:00
|
|
|
|
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
|
2022-09-29 21:05:31 +08:00
|
|
|
this.middlewares = new Toposort<any>();
|
|
|
|
|
2022-01-24 14:10:35 +08:00
|
|
|
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,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
2022-03-11 10:10:57 +08:00
|
|
|
|
2023-01-09 07:35:48 +08:00
|
|
|
this.use(this.allowManager.aclMiddleware(), {
|
|
|
|
tag: 'allow-manager',
|
|
|
|
before: 'core',
|
|
|
|
});
|
|
|
|
|
|
|
|
this.addCoreMiddleware();
|
|
|
|
}
|
|
|
|
|
2022-01-18 16:38:03 +08:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2022-01-24 14:10:35 +08:00
|
|
|
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);
|
|
|
|
}
|
|
|
|
|
2022-08-16 14:41:29 +08:00
|
|
|
setAvailableAction(name: string, options: AvailableActionOptions = {}) {
|
2022-10-06 10:29:53 +08:00
|
|
|
this.availableActions.set(name, new ACLAvailableAction(name, options));
|
2022-01-18 16:38:03 +08:00
|
|
|
|
|
|
|
if (options.aliases) {
|
|
|
|
const aliases = lodash.isArray(options.aliases) ? options.aliases : [options.aliases];
|
|
|
|
for (const alias of aliases) {
|
|
|
|
this.actionAlias.set(alias, name);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-04-12 12:02:58 +08:00
|
|
|
getAvailableAction(name: string) {
|
|
|
|
const actionName = this.actionAlias.get(name) || name;
|
|
|
|
return this.availableActions.get(actionName);
|
|
|
|
}
|
|
|
|
|
2022-01-24 14:10:35 +08:00
|
|
|
getAvailableActions() {
|
|
|
|
return this.availableActions;
|
|
|
|
}
|
|
|
|
|
2022-10-06 10:29:53 +08:00
|
|
|
setAvailableStrategy(name: string, options: AvailableStrategyOptions) {
|
2022-01-24 14:10:35 +08:00
|
|
|
this.availableStrategy.set(name, new ACLAvailableStrategy(this, options));
|
2022-01-18 16:38:03 +08:00
|
|
|
}
|
|
|
|
|
2022-01-24 14:10:35 +08:00
|
|
|
beforeGrantAction(listener?: Listener) {
|
|
|
|
this.addListener('beforeGrantAction', listener);
|
2022-01-18 16:38:03 +08:00
|
|
|
}
|
|
|
|
|
2022-10-16 19:16:14 +08:00
|
|
|
can(options: CanArgs): CanResult | null {
|
|
|
|
const { role, resource, action } = options;
|
2022-01-18 16:38:03 +08:00
|
|
|
const aclRole = this.roles.get(role);
|
2022-02-11 19:31:53 +08:00
|
|
|
|
|
|
|
if (!aclRole) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2023-01-09 07:35:48 +08:00
|
|
|
const snippetAllowed = aclRole.snippetAllowed(`${resource}:${action}`);
|
|
|
|
|
|
|
|
if (snippetAllowed === false) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
const fixedParams = this.fixedParamsManager.getParams(resource, action);
|
|
|
|
|
|
|
|
const mergeParams = (result: CanResult) => {
|
|
|
|
const params = result['params'] || {};
|
|
|
|
|
|
|
|
const mergedParams = assign(params, fixedParams);
|
|
|
|
|
|
|
|
if (Object.keys(mergedParams).length) {
|
|
|
|
result['params'] = mergedParams;
|
|
|
|
} else {
|
|
|
|
delete result['params'];
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
};
|
|
|
|
|
2022-01-18 16:38:03 +08:00
|
|
|
const aclResource = aclRole.getResource(resource);
|
|
|
|
|
|
|
|
if (aclResource) {
|
2022-01-18 20:29:41 +08:00
|
|
|
const actionParams = aclResource.getAction(action);
|
2022-01-18 16:38:03 +08:00
|
|
|
|
2022-01-18 20:29:41 +08:00
|
|
|
if (actionParams) {
|
2022-01-18 16:38:03 +08:00
|
|
|
// handle single action config
|
2023-01-09 07:35:48 +08:00
|
|
|
return mergeParams({
|
2022-01-18 16:38:03 +08:00
|
|
|
role,
|
|
|
|
resource,
|
|
|
|
action,
|
2022-01-18 20:29:41 +08:00
|
|
|
params: actionParams,
|
2023-01-09 07:35:48 +08:00
|
|
|
});
|
2022-05-04 10:16:53 +08:00
|
|
|
} else {
|
|
|
|
return null;
|
2022-01-18 16:38:03 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-09 07:35:48 +08:00
|
|
|
const roleStrategy = aclRole.getStrategy();
|
|
|
|
|
|
|
|
if (!roleStrategy && !snippetAllowed) {
|
2022-01-24 14:10:35 +08:00
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2023-01-09 07:35:48 +08:00
|
|
|
let roleStrategyParams = roleStrategy?.allow(resource, this.resolveActionAlias(action));
|
2022-01-18 16:38:03 +08:00
|
|
|
|
2023-01-09 07:35:48 +08:00
|
|
|
if (!roleStrategyParams && snippetAllowed) {
|
|
|
|
roleStrategyParams = {};
|
2022-01-18 16:38:03 +08:00
|
|
|
}
|
|
|
|
|
2022-01-24 14:10:35 +08:00
|
|
|
if (roleStrategyParams) {
|
2023-01-09 07:35:48 +08:00
|
|
|
const result = { role, resource, action, params: {} };
|
2022-01-24 14:10:35 +08:00
|
|
|
|
|
|
|
if (lodash.isPlainObject(roleStrategyParams)) {
|
|
|
|
result['params'] = roleStrategyParams;
|
|
|
|
}
|
|
|
|
|
2023-01-09 07:35:48 +08:00
|
|
|
return mergeParams(result);
|
2022-01-18 16:38:03 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
2022-01-18 20:29:41 +08:00
|
|
|
public resolveActionAlias(action: string) {
|
2022-01-18 16:38:03 +08:00
|
|
|
return this.actionAlias.get(action) ? this.actionAlias.get(action) : action;
|
|
|
|
}
|
2022-01-24 14:10:35 +08:00
|
|
|
|
2022-09-29 21:05:31 +08:00
|
|
|
use(fn: any, options?: ToposortOptions) {
|
2023-01-09 07:35:48 +08:00
|
|
|
this.middlewares.add(fn, {
|
|
|
|
group: 'prep',
|
|
|
|
...options,
|
|
|
|
});
|
2022-02-11 19:31:53 +08:00
|
|
|
}
|
|
|
|
|
2022-10-06 10:29:53 +08:00
|
|
|
allow(resourceName: string, actionNames: string[] | string, condition?: string | ConditionFunc) {
|
2023-01-09 07:35:48 +08:00
|
|
|
return this.skip(resourceName, actionNames, condition);
|
|
|
|
}
|
|
|
|
|
|
|
|
skip(resourceName: string, actionNames: string[] | string, condition?: string | ConditionFunc) {
|
2022-04-24 10:14:46 +08:00
|
|
|
if (!Array.isArray(actionNames)) {
|
|
|
|
actionNames = [actionNames];
|
|
|
|
}
|
|
|
|
|
|
|
|
for (const actionName of actionNames) {
|
|
|
|
this.allowManager.allow(resourceName, actionName, condition);
|
|
|
|
}
|
2022-03-11 10:10:57 +08:00
|
|
|
}
|
|
|
|
|
2023-04-21 11:13:43 +08:00
|
|
|
async parseJsonTemplate(json: any, ctx: any) {
|
|
|
|
if (json.filter) {
|
|
|
|
ctx.logger?.info?.('parseJsonTemplate.raw', JSON.parse(JSON.stringify(json.filter)));
|
|
|
|
const timezone = ctx?.get?.('x-timezone');
|
|
|
|
const state = JSON.parse(JSON.stringify(ctx.state));
|
|
|
|
const filter = await parseFilter(json.filter, {
|
|
|
|
timezone,
|
|
|
|
now: new Date().toISOString(),
|
|
|
|
vars: {
|
|
|
|
ctx: {
|
|
|
|
state,
|
|
|
|
},
|
|
|
|
$user: async () => state.currentUser,
|
|
|
|
},
|
|
|
|
});
|
|
|
|
json.filter = filter;
|
|
|
|
ctx.logger?.info?.('parseJsonTemplate.parsed', filter);
|
|
|
|
}
|
|
|
|
return json;
|
2022-04-12 12:02:58 +08:00
|
|
|
}
|
|
|
|
|
2022-01-24 14:10:35 +08:00
|
|
|
middleware() {
|
2022-02-11 19:31:53 +08:00
|
|
|
const acl = this;
|
2022-01-24 14:10:35 +08:00
|
|
|
|
|
|
|
return async function ACLMiddleware(ctx, next) {
|
2022-02-11 19:31:53 +08:00
|
|
|
const roleName = ctx.state.currentRole || 'anonymous';
|
2022-01-24 14:10:35 +08:00
|
|
|
const { resourceName, actionName } = ctx.action;
|
|
|
|
|
|
|
|
ctx.can = (options: Omit<CanArgs, 'role'>) => {
|
2023-05-24 21:31:12 +08:00
|
|
|
const canResult = acl.can({ role: roleName, ...options });
|
|
|
|
|
|
|
|
return canResult;
|
2022-01-24 14:10:35 +08:00
|
|
|
};
|
|
|
|
|
2022-02-11 19:31:53 +08:00
|
|
|
ctx.permission = {
|
|
|
|
can: ctx.can({ resource: resourceName, action: actionName }),
|
|
|
|
};
|
2022-01-24 14:10:35 +08:00
|
|
|
|
2023-01-09 07:35:48 +08:00
|
|
|
return compose(acl.middlewares.nodes)(ctx, next);
|
|
|
|
};
|
|
|
|
}
|
2022-02-11 19:31:53 +08:00
|
|
|
|
2023-01-09 07:35:48 +08:00
|
|
|
async getActionParams(ctx) {
|
|
|
|
const roleName = ctx.state.currentRole || 'anonymous';
|
|
|
|
const { resourceName, actionName } = ctx.action;
|
2022-01-24 14:10:35 +08:00
|
|
|
|
2023-01-09 07:35:48 +08:00
|
|
|
ctx.can = (options: Omit<CanArgs, 'role'>) => {
|
2023-05-16 20:33:05 +08:00
|
|
|
const can = this.can({ role: roleName, ...options });
|
|
|
|
if (!can) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return lodash.cloneDeep(can);
|
2023-01-09 07:35:48 +08:00
|
|
|
};
|
2022-01-24 14:10:35 +08:00
|
|
|
|
2023-01-09 07:35:48 +08:00
|
|
|
ctx.permission = {
|
|
|
|
can: ctx.can({ resource: resourceName, action: actionName }),
|
|
|
|
};
|
2022-02-11 19:31:53 +08:00
|
|
|
|
2023-01-09 07:35:48 +08:00
|
|
|
await compose(this.middlewares.nodes)(ctx, async () => {});
|
|
|
|
}
|
2022-01-24 14:10:35 +08:00
|
|
|
|
2023-01-09 07:35:48 +08:00
|
|
|
addFixedParams(resource: string, action: string, merger: Merger) {
|
|
|
|
this.fixedParamsManager.addParams(resource, action, merger);
|
|
|
|
}
|
|
|
|
|
|
|
|
registerSnippet(snippet: SnippetOptions) {
|
|
|
|
this.snippetManager.register(snippet);
|
2022-01-24 14:10:35 +08:00
|
|
|
}
|
2023-05-24 21:31:12 +08:00
|
|
|
|
2023-09-06 09:19:10 +08:00
|
|
|
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');
|
2023-05-24 21:31:12 +08:00
|
|
|
}
|
2023-09-06 09:19:10 +08:00
|
|
|
}
|
2023-05-24 21:31:12 +08:00
|
|
|
|
2023-09-06 09:19:10 +08:00
|
|
|
return params;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected addCoreMiddleware() {
|
|
|
|
const acl = this;
|
2023-05-24 21:31:12 +08:00
|
|
|
|
|
|
|
this.middlewares.add(
|
|
|
|
async (ctx, next) => {
|
|
|
|
const resourcerAction: Action = ctx.action;
|
|
|
|
const { resourceName, actionName } = ctx.action;
|
|
|
|
|
|
|
|
const permission = ctx.permission;
|
|
|
|
|
|
|
|
ctx.log?.info && ctx.log.info('ctx permission', permission);
|
|
|
|
|
|
|
|
if ((!permission.can || typeof permission.can !== 'object') && !permission.skip) {
|
|
|
|
ctx.throw(403, 'No permissions');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const params = permission.can?.params || acl.fixedParamsManager.getParams(resourceName, actionName);
|
|
|
|
|
|
|
|
ctx.log?.info && ctx.log.info('acl params', params);
|
|
|
|
|
|
|
|
if (params && resourcerAction.mergeParams) {
|
2023-09-06 09:19:10 +08:00
|
|
|
const filteredParams = acl.filterParams(ctx, resourceName, params);
|
2023-05-24 21:31:12 +08:00
|
|
|
const parsedParams = await acl.parseJsonTemplate(filteredParams, ctx);
|
|
|
|
|
|
|
|
ctx.permission.parsedParams = parsedParams;
|
|
|
|
ctx.log?.info && ctx.log.info('acl parsedParams', parsedParams);
|
|
|
|
ctx.permission.rawParams = lodash.cloneDeep(resourcerAction.params);
|
|
|
|
resourcerAction.mergeParams(parsedParams, {
|
|
|
|
appends: (x, y) => {
|
|
|
|
if (!x) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
if (!y) {
|
|
|
|
return x;
|
|
|
|
}
|
|
|
|
return (x as any[]).filter((i) => y.includes(i.split('.').shift()));
|
|
|
|
},
|
|
|
|
});
|
|
|
|
ctx.permission.mergedParams = lodash.cloneDeep(resourcerAction.params);
|
|
|
|
}
|
|
|
|
|
|
|
|
await next();
|
|
|
|
},
|
|
|
|
{
|
|
|
|
tag: 'core',
|
|
|
|
group: 'core',
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
protected isAvailableAction(actionName: string) {
|
|
|
|
return this.availableActions.has(this.resolveActionAlias(actionName));
|
|
|
|
}
|
2022-01-18 16:38:03 +08:00
|
|
|
}
|