feat: acl (#153)
* 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 Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
parent
c9742c1710
commit
fd32705954
8
packages/acl/.npmignore
Normal file
8
packages/acl/.npmignore
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
node_modules
|
||||||
|
*.log
|
||||||
|
docs
|
||||||
|
__tests__
|
||||||
|
jest.config.js
|
||||||
|
tsconfig.json
|
||||||
|
src
|
||||||
|
.fatherrc.ts
|
20
packages/acl/package.json
Normal file
20
packages/acl/package.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "@nocobase/acl",
|
||||||
|
"version": "0.6.0-alpha.0",
|
||||||
|
"description": "",
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "./lib/index.js",
|
||||||
|
"types": "./lib/index.d.ts",
|
||||||
|
"scripts": {
|
||||||
|
"build": "rimraf -rf lib esm dist && npm run build:cjs && npm run build:esm",
|
||||||
|
"build:cjs": "tsc --project tsconfig.build.json",
|
||||||
|
"build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir esm"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/nocobase/nocobase.git",
|
||||||
|
"directory": "packages/acl"
|
||||||
|
}
|
||||||
|
}
|
287
packages/acl/src/__tests__/acl.test.ts
Normal file
287
packages/acl/src/__tests__/acl.test.ts
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
import { ACL } from '..';
|
||||||
|
|
||||||
|
describe('acl', () => {
|
||||||
|
let acl: ACL;
|
||||||
|
beforeEach(() => {
|
||||||
|
acl = new ACL();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow all', () => {
|
||||||
|
acl.setAvailableAction('create', {
|
||||||
|
type: 'new-data',
|
||||||
|
});
|
||||||
|
|
||||||
|
acl.setAvailableAction('edit', {
|
||||||
|
type: 'old-data',
|
||||||
|
});
|
||||||
|
|
||||||
|
acl.setAvailableStrategy('s1', {
|
||||||
|
displayName: 's1',
|
||||||
|
actions: '*',
|
||||||
|
});
|
||||||
|
|
||||||
|
acl.define({
|
||||||
|
role: 'admin',
|
||||||
|
strategy: 's1',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(acl.can({ role: 'admin', resource: 'posts', action: 'create' })).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deny all', () => {
|
||||||
|
acl.setAvailableStrategy('s1', {
|
||||||
|
displayName: 'test',
|
||||||
|
actions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
acl.define({
|
||||||
|
role: 'admin',
|
||||||
|
strategy: 's1',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(acl.can({ role: 'admin', resource: 'posts', action: 'create' })).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deny when action is not available action', () => {
|
||||||
|
acl.setAvailableStrategy('s1', {
|
||||||
|
displayName: 'test',
|
||||||
|
actions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const role = acl.define({
|
||||||
|
role: 'admin',
|
||||||
|
strategy: 's1',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(acl.can({ role: 'admin', resource: 'posts', action: 'create' })).toBeNull();
|
||||||
|
|
||||||
|
role.grantAction('posts:create', {});
|
||||||
|
|
||||||
|
expect(acl.can({ role: 'admin', resource: 'posts', action: 'create' })).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should grant action when define role', () => {
|
||||||
|
acl.setAvailableAction('create', {
|
||||||
|
displayName: 'create',
|
||||||
|
type: 'new-data',
|
||||||
|
});
|
||||||
|
|
||||||
|
acl.setAvailableStrategy('s1', {
|
||||||
|
displayName: 'test',
|
||||||
|
actions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const role = acl.define({
|
||||||
|
role: 'admin',
|
||||||
|
strategy: 's1',
|
||||||
|
actions: {
|
||||||
|
'posts:create': {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(acl.can({ role: 'admin', resource: 'posts', action: 'create' })).not.toBeNull();
|
||||||
|
});
|
||||||
|
it('should grant action', function () {
|
||||||
|
acl.setAvailableAction('create', {
|
||||||
|
displayName: 'create',
|
||||||
|
type: 'new-data',
|
||||||
|
});
|
||||||
|
|
||||||
|
acl.setAvailableStrategy('s1', {
|
||||||
|
displayName: 'test',
|
||||||
|
actions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const role = acl.define({
|
||||||
|
role: 'admin',
|
||||||
|
strategy: 's1',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(acl.can({ role: 'admin', resource: 'posts', action: 'create' })).toBeNull();
|
||||||
|
|
||||||
|
role.grantAction('posts:create', {});
|
||||||
|
|
||||||
|
expect(acl.can({ role: 'admin', resource: 'posts', action: 'create' })).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should works with alias action', () => {
|
||||||
|
acl.setAvailableAction('view', {
|
||||||
|
displayName: 'view',
|
||||||
|
type: 'new-data',
|
||||||
|
aliases: ['get', 'list'],
|
||||||
|
});
|
||||||
|
|
||||||
|
acl.setAvailableStrategy('s1', {
|
||||||
|
displayName: 'test',
|
||||||
|
actions: ['view'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const role = acl.define({
|
||||||
|
role: 'admin',
|
||||||
|
strategy: 's1',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(acl.can({ role: 'admin', resource: 'posts', action: 'get' })).not.toBeNull();
|
||||||
|
expect(acl.can({ role: 'admin', resource: 'posts', action: 'list' })).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return action params when check permission', () => {
|
||||||
|
acl.setAvailableStrategy('s2', {
|
||||||
|
displayName: 'view create update',
|
||||||
|
actions: ['view', 'create', 'update'],
|
||||||
|
});
|
||||||
|
|
||||||
|
acl.setAvailableAction('view', { type: 'new-data' });
|
||||||
|
acl.setAvailableAction('create', { type: 'new-data' });
|
||||||
|
acl.setAvailableAction('update', { type: 'new-data' });
|
||||||
|
|
||||||
|
acl.define({
|
||||||
|
role: 'admin',
|
||||||
|
strategy: 's2',
|
||||||
|
actions: {
|
||||||
|
'posts:view': {
|
||||||
|
filter: {
|
||||||
|
createdById: '{{ ctx.state.currentUser.id }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const canResult = acl.can({ role: 'admin', resource: 'posts', action: 'view' });
|
||||||
|
|
||||||
|
expect(canResult).toMatchObject({
|
||||||
|
role: 'admin',
|
||||||
|
resource: 'posts',
|
||||||
|
action: 'view',
|
||||||
|
params: {
|
||||||
|
filter: {
|
||||||
|
createdById: '{{ ctx.state.currentUser.id }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should getActionParams', () => {
|
||||||
|
acl.setAvailableStrategy('s2', {
|
||||||
|
displayName: 'view create update',
|
||||||
|
actions: ['view', 'create', 'update'],
|
||||||
|
});
|
||||||
|
|
||||||
|
acl.setAvailableAction('view', { type: 'new-data' });
|
||||||
|
acl.setAvailableAction('create', { type: 'new-data' });
|
||||||
|
acl.setAvailableAction('update', { type: 'new-data' });
|
||||||
|
|
||||||
|
const role = acl.define({
|
||||||
|
role: 'admin',
|
||||||
|
strategy: 's2',
|
||||||
|
actions: {
|
||||||
|
'posts:view': {
|
||||||
|
filter: {
|
||||||
|
createdById: '{{ ctx.state.currentUser.id }}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const params = role.getActionParams('posts:view');
|
||||||
|
|
||||||
|
expect(params).toMatchObject({
|
||||||
|
filter: {
|
||||||
|
createdById: '{{ ctx.state.currentUser.id }}',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should revoke action', () => {
|
||||||
|
acl.setAvailableAction('create', {
|
||||||
|
displayName: 'create',
|
||||||
|
type: 'new-data',
|
||||||
|
});
|
||||||
|
|
||||||
|
acl.setAvailableStrategy('s1', {
|
||||||
|
displayName: 'test',
|
||||||
|
actions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const role = acl.define({
|
||||||
|
role: 'admin',
|
||||||
|
strategy: 's1',
|
||||||
|
});
|
||||||
|
|
||||||
|
role.grantAction('posts:create', {});
|
||||||
|
|
||||||
|
expect(acl.can({ role: 'admin', resource: 'posts', action: 'create' })).not.toBeNull();
|
||||||
|
|
||||||
|
role.revokeAction('posts:create');
|
||||||
|
|
||||||
|
expect(acl.can({ role: 'admin', resource: 'posts', action: 'create' })).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call beforeGrantAction', () => {
|
||||||
|
acl.setAvailableAction('create', {
|
||||||
|
type: 'old-data',
|
||||||
|
});
|
||||||
|
|
||||||
|
acl.beforeGrantAction('posts:create', (ctx) => {
|
||||||
|
ctx.params = {
|
||||||
|
filter: {
|
||||||
|
status: 'publish',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(acl.listenerCount('posts:create.beforeGrantAction')).toEqual(1);
|
||||||
|
|
||||||
|
acl.define({
|
||||||
|
role: 'admin',
|
||||||
|
actions: {
|
||||||
|
'posts:create': {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const results = acl.can({ role: 'admin', resource: 'posts', action: 'create' });
|
||||||
|
|
||||||
|
expect(results).toMatchObject({
|
||||||
|
role: 'admin',
|
||||||
|
resource: 'posts',
|
||||||
|
action: 'create',
|
||||||
|
params: {
|
||||||
|
filter: {
|
||||||
|
status: 'publish',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should to JSON', () => {
|
||||||
|
acl.setAvailableAction('create', {
|
||||||
|
displayName: 'create',
|
||||||
|
type: 'new-data',
|
||||||
|
});
|
||||||
|
|
||||||
|
acl.setAvailableStrategy('s1', {
|
||||||
|
displayName: 'test',
|
||||||
|
actions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const role = acl.define({
|
||||||
|
role: 'admin',
|
||||||
|
strategy: 's1',
|
||||||
|
actions: {
|
||||||
|
'posts:create': {
|
||||||
|
filter: { a: 'b' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const roleJSON = role.toJSON();
|
||||||
|
|
||||||
|
expect(roleJSON).toMatchObject({
|
||||||
|
role: 'admin',
|
||||||
|
strategy: 's1',
|
||||||
|
actions: {
|
||||||
|
'posts:create': {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
10
packages/acl/src/acl-available-action.ts
Normal file
10
packages/acl/src/acl-available-action.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
export interface AvailableActionOptions {
|
||||||
|
aliases?: string[] | string;
|
||||||
|
type: 'new-data' | 'old-data';
|
||||||
|
displayName?: string;
|
||||||
|
resource?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AclAvailableAction {
|
||||||
|
constructor(private name: string, private options: AvailableActionOptions) {}
|
||||||
|
}
|
40
packages/acl/src/acl-available-strategy.ts
Normal file
40
packages/acl/src/acl-available-strategy.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import lodash from 'lodash';
|
||||||
|
type StrategyValue = false | '*' | string | string[];
|
||||||
|
|
||||||
|
export interface AvailableStrategyOptions {
|
||||||
|
displayName?: string;
|
||||||
|
actions: false | string | string[];
|
||||||
|
resource?: '*';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function strategyValueMatched(strategy: StrategyValue, value: string) {
|
||||||
|
if (strategy === '*') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lodash.isString(strategy) && strategy === value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lodash.isArray(strategy) && strategy.includes(value)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ACLAvailableStrategy {
|
||||||
|
options: AvailableStrategyOptions;
|
||||||
|
|
||||||
|
constructor(options: AvailableStrategyOptions) {
|
||||||
|
this.options = options;
|
||||||
|
}
|
||||||
|
|
||||||
|
matchAction(actionName: string) {
|
||||||
|
return strategyValueMatched(this.options.actions, actionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
allow(resourceName: string, actionName: string) {
|
||||||
|
return this.matchAction(actionName);
|
||||||
|
}
|
||||||
|
}
|
62
packages/acl/src/acl-resource.ts
Normal file
62
packages/acl/src/acl-resource.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { ACLRole, RoleActionParams } from './acl-role';
|
||||||
|
import { ACL, ListenerContext } from './acl';
|
||||||
|
|
||||||
|
export type ResourceActions = { [key: string]: RoleActionParams };
|
||||||
|
|
||||||
|
interface AclResourceOptions {
|
||||||
|
name: string;
|
||||||
|
role: ACLRole;
|
||||||
|
actions?: ResourceActions;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ACLResource {
|
||||||
|
actions = new Map<string, RoleActionParams>();
|
||||||
|
acl: ACL;
|
||||||
|
role: ACLRole;
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
constructor(options: AclResourceOptions) {
|
||||||
|
this.acl = options.role.acl;
|
||||||
|
|
||||||
|
this.role = options.role;
|
||||||
|
this.name = options.name;
|
||||||
|
|
||||||
|
const actionsOption: ResourceActions = options.actions || {};
|
||||||
|
for (const actionName of Object.keys(actionsOption)) {
|
||||||
|
this.actions.set(actionName, actionsOption[actionName]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getActions() {
|
||||||
|
return Array.from(this.actions.keys()).reduce((carry, key) => {
|
||||||
|
carry[key] = this.actions.get(key);
|
||||||
|
return carry;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
getAction(name: string) {
|
||||||
|
return this.actions.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
setAction(name: string, params: RoleActionParams) {
|
||||||
|
const context: ListenerContext = {
|
||||||
|
role: this.role,
|
||||||
|
acl: this.role.acl,
|
||||||
|
params: params || {},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.acl.emit(`${this.name}:${name}.beforeGrantAction`, context);
|
||||||
|
|
||||||
|
this.actions.set(name, context.params);
|
||||||
|
}
|
||||||
|
|
||||||
|
setActions(actions: { [key: string]: RoleActionParams }) {
|
||||||
|
for (const actionName of Object.keys(actions)) {
|
||||||
|
this.setAction(actionName, actions[actionName]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAction(name: string) {
|
||||||
|
this.actions.delete(name);
|
||||||
|
}
|
||||||
|
}
|
116
packages/acl/src/acl-role.ts
Normal file
116
packages/acl/src/acl-role.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import { ACLResource } from './acl-resource';
|
||||||
|
import { AvailableStrategyOptions } from './acl-available-strategy';
|
||||||
|
import { ACL, DefineOptions } from './acl';
|
||||||
|
|
||||||
|
export interface RoleActionParams {
|
||||||
|
fields?: string[];
|
||||||
|
filter?: any;
|
||||||
|
own?: boolean;
|
||||||
|
whitelist?: string[];
|
||||||
|
blacklist?: string[];
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResourceActionsOptions {
|
||||||
|
[actionName: string]: RoleActionParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ACLRole {
|
||||||
|
strategy: string | AvailableStrategyOptions;
|
||||||
|
resources = new Map<string, ACLResource>();
|
||||||
|
|
||||||
|
constructor(public acl: ACL, public name: string) {}
|
||||||
|
|
||||||
|
getResource(name: string): ACLResource | undefined {
|
||||||
|
return this.resources.get(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
setResource(name: string, resource: ACLResource) {
|
||||||
|
this.resources.set(name, resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setStrategy(value: string | AvailableStrategyOptions) {
|
||||||
|
this.strategy = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public grantResource(resourceName: string, options: ResourceActionsOptions) {
|
||||||
|
const resource = new ACLResource({
|
||||||
|
role: this,
|
||||||
|
name: resourceName,
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const [actionName, actionParams] of Object.entries(options)) {
|
||||||
|
resource.setAction(actionName, actionParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.resources.set(resourceName, resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getResourceActionsParams(resourceName: string) {
|
||||||
|
const resource = this.getResource(resourceName);
|
||||||
|
return resource.getActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
public revokeResource(resourceName) {
|
||||||
|
this.resources.delete(resourceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public grantAction(path: string, options?: RoleActionParams) {
|
||||||
|
let { resource, resourceName, actionName } = this.getResourceActionFromPath(path);
|
||||||
|
|
||||||
|
if (!resource) {
|
||||||
|
resource = new ACLResource({
|
||||||
|
role: this,
|
||||||
|
name: resourceName,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.resources.set(resourceName, resource);
|
||||||
|
}
|
||||||
|
|
||||||
|
resource.setAction(actionName, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getActionParams(path: string): RoleActionParams {
|
||||||
|
const { action } = this.getResourceActionFromPath(path);
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
public revokeAction(path: string) {
|
||||||
|
const { resource, actionName } = this.getResourceActionFromPath(path);
|
||||||
|
resource.removeAction(actionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public toJSON(): DefineOptions {
|
||||||
|
const actions = {};
|
||||||
|
|
||||||
|
for (const resourceName of this.resources.keys()) {
|
||||||
|
const resourceActions = this.getResourceActionsParams(resourceName);
|
||||||
|
for (const actionName of Object.keys(resourceActions)) {
|
||||||
|
actions[`${resourceName}:${actionName}`] = resourceActions[actionName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
role: this.name,
|
||||||
|
strategy: this.strategy,
|
||||||
|
actions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getResourceActionFromPath(path: string) {
|
||||||
|
const [resourceName, actionName] = path.split(':');
|
||||||
|
|
||||||
|
const resource = this.resources.get(resourceName);
|
||||||
|
let action = null;
|
||||||
|
if (resource) {
|
||||||
|
action = resource.getAction(actionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
resourceName,
|
||||||
|
actionName,
|
||||||
|
resource,
|
||||||
|
action,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
132
packages/acl/src/acl.ts
Normal file
132
packages/acl/src/acl.ts
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
import lodash from 'lodash';
|
||||||
|
import { ACLAvailableStrategy, AvailableStrategyOptions } from './acl-available-strategy';
|
||||||
|
import { ACLRole, RoleActionParams } from './acl-role';
|
||||||
|
import { AclAvailableAction, AvailableActionOptions } from './acl-available-action';
|
||||||
|
import EventEmitter from 'events';
|
||||||
|
|
||||||
|
interface StrategyOptions {
|
||||||
|
role: string;
|
||||||
|
strategy: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CanResult {
|
||||||
|
role: string;
|
||||||
|
resource: string;
|
||||||
|
action: string;
|
||||||
|
params?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DefineOptions {
|
||||||
|
role: string;
|
||||||
|
strategy?: string | AvailableStrategyOptions;
|
||||||
|
actions?: {
|
||||||
|
[key: string]: RoleActionParams;
|
||||||
|
};
|
||||||
|
routes?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ListenerContext {
|
||||||
|
acl: ACL;
|
||||||
|
role: ACLRole;
|
||||||
|
params: RoleActionParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Listener = (ctx: ListenerContext) => void;
|
||||||
|
|
||||||
|
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>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setAvailableStrategy(name: string, options: AvailableStrategyOptions) {
|
||||||
|
this.availableStrategy.set(name, new ACLAvailableStrategy(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeGrantAction(path: string, listener?: Listener) {
|
||||||
|
this.addListener(`${path}.beforeGrantAction`, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
can({ role, resource, action }: { role: string; resource: string; action: string }): CanResult | null {
|
||||||
|
action = this.resolveActionAlias(action);
|
||||||
|
|
||||||
|
if (!this.isAvailableAction(action)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const aclRole = this.roles.get(role);
|
||||||
|
const aclResource = aclRole.getResource(resource);
|
||||||
|
|
||||||
|
if (aclResource) {
|
||||||
|
const aclActionConfig = aclResource.actions.get(this.resolveActionAlias(action));
|
||||||
|
|
||||||
|
if (aclActionConfig) {
|
||||||
|
// handle single action config
|
||||||
|
return {
|
||||||
|
role,
|
||||||
|
resource,
|
||||||
|
action,
|
||||||
|
params: aclActionConfig,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const roleStrategy = lodash.isString(aclRole.strategy)
|
||||||
|
? this.availableStrategy.get(aclRole.strategy)
|
||||||
|
: new ACLAvailableStrategy(aclRole.strategy);
|
||||||
|
|
||||||
|
if (!roleStrategy) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (roleStrategy.allow(resource, action)) {
|
||||||
|
return { role, resource, action };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected isAvailableAction(actionName: string) {
|
||||||
|
return this.availableActions.has(actionName);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected resolveActionAlias(action: string) {
|
||||||
|
return this.actionAlias.get(action) ? this.actionAlias.get(action) : action;
|
||||||
|
}
|
||||||
|
}
|
5
packages/acl/src/index.ts
Normal file
5
packages/acl/src/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export * from './acl-resource';
|
||||||
|
export * from './acl-role';
|
||||||
|
export * from './acl-available-strategy';
|
||||||
|
export * from './acl-available-action';
|
||||||
|
export * from './acl';
|
9
packages/acl/tsconfig.build.json
Normal file
9
packages/acl/tsconfig.build.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.build.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "./lib",
|
||||||
|
"declaration": true
|
||||||
|
},
|
||||||
|
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
|
||||||
|
"exclude": ["./src/__tests__/*", "./esm/*", "./lib/*"]
|
||||||
|
}
|
5
packages/acl/tsconfig.json
Normal file
5
packages/acl/tsconfig.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
|
||||||
|
"exclude": ["./esm/*", "./lib/*"]
|
||||||
|
}
|
@ -19,4 +19,4 @@ export function registerActions(api: any) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default {};
|
export default actions;
|
||||||
|
@ -3,6 +3,23 @@ import path from 'path';
|
|||||||
import { Model } from '..';
|
import { Model } from '..';
|
||||||
|
|
||||||
describe('database', () => {
|
describe('database', () => {
|
||||||
|
test('get repository', async () => {
|
||||||
|
const db = mockDatabase();
|
||||||
|
db.collection({
|
||||||
|
name: 'tests',
|
||||||
|
fields: [{ type: 'hasMany', name: 'relations' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
db.collection({
|
||||||
|
name: 'relations',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(db.getRepository('tests')).toEqual(db.getCollection('tests').repository);
|
||||||
|
expect(db.getRepository('tests.relations', '1')).toEqual(
|
||||||
|
db.getCollection('tests').repository.relation('relations').of('1'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('import', async () => {
|
test('import', async () => {
|
||||||
const db = mockDatabase();
|
const db = mockDatabase();
|
||||||
await db.import({
|
await db.import({
|
||||||
@ -191,5 +208,4 @@ describe('database', () => {
|
|||||||
test.customMethod();
|
test.customMethod();
|
||||||
expect(test.get('abc')).toBe('abc');
|
expect(test.get('abc')).toBe('abc');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@ -11,6 +11,8 @@ import { ModelHook } from './model-hook';
|
|||||||
import { ImporterReader, ImportFileExtension } from './collection-importer';
|
import { ImporterReader, ImportFileExtension } from './collection-importer';
|
||||||
|
|
||||||
import extendOperators from './operators';
|
import extendOperators from './operators';
|
||||||
|
import { Repository } from './repository';
|
||||||
|
import { RelationRepository } from './relation-repository/relation-repository';
|
||||||
|
|
||||||
export interface MergeOptions extends merge.Options {}
|
export interface MergeOptions extends merge.Options {}
|
||||||
|
|
||||||
@ -126,6 +128,18 @@ export class Database extends EventEmitter implements AsyncEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getRepository(name: string): Repository;
|
||||||
|
getRepository<R extends RelationRepository>(name: string, relationId: string | number): R;
|
||||||
|
|
||||||
|
getRepository<R extends RelationRepository>(name: string, relationId?: string | number): Repository | R {
|
||||||
|
if (relationId) {
|
||||||
|
const [collection, relation] = name.split('.');
|
||||||
|
return this.getRepository(collection).relation(relation).of(relationId) as R;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.getCollection(name).repository;
|
||||||
|
}
|
||||||
|
|
||||||
addPendingField(field: RelationField) {
|
addPendingField(field: RelationField) {
|
||||||
const associating = this.pendingFields;
|
const associating = this.pendingFields;
|
||||||
const items = this.pendingFields.get(field.target) || [];
|
const items = this.pendingFields.get(field.target) || [];
|
||||||
|
Loading…
Reference in New Issue
Block a user