fix: append acl resource params (#1923)
* test: append acl resource params * fix: test
This commit is contained in:
		
							parent
							
								
									7080db72eb
								
							
						
					
					
						commit
						710bd2dfd2
					
				@ -1,5 +1,6 @@
 | 
				
			|||||||
import { ACLRole, RoleActionParams } from './acl-role';
 | 
					import { ACLRole, RoleActionParams } from './acl-role';
 | 
				
			||||||
import { ACL, ListenerContext } from './acl';
 | 
					import { ACL, ListenerContext } from './acl';
 | 
				
			||||||
 | 
					import lodash from 'lodash';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type ResourceActions = { [key: string]: RoleActionParams };
 | 
					export type ResourceActions = { [key: string]: RoleActionParams };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -35,7 +36,15 @@ export class ACLResource {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getAction(name: string) {
 | 
					  getAction(name: string) {
 | 
				
			||||||
    return this.actions.get(name) || this.actions.get(this.acl.resolveActionAlias(name));
 | 
					    const result = this.actions.get(name) || this.actions.get(this.acl.resolveActionAlias(name));
 | 
				
			||||||
 | 
					    if (!result) {
 | 
				
			||||||
 | 
					      return null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (Array.isArray(result.fields) && result.fields.length > 0) {
 | 
				
			||||||
 | 
					      result.fields = lodash.uniq(result.fields);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return lodash.cloneDeep(result);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  setAction(name: string, params: RoleActionParams) {
 | 
					  setAction(name: string, params: RoleActionParams) {
 | 
				
			||||||
 | 
				
			|||||||
@ -45,20 +45,15 @@ interface CanArgs {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class ACL extends EventEmitter {
 | 
					export class ACL extends EventEmitter {
 | 
				
			||||||
  protected availableActions = new Map<string, ACLAvailableAction>();
 | 
					 | 
				
			||||||
  public availableStrategy = new Map<string, ACLAvailableStrategy>();
 | 
					  public availableStrategy = new Map<string, ACLAvailableStrategy>();
 | 
				
			||||||
  protected fixedParamsManager = new FixedParamsManager();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  protected middlewares: Toposort<any>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public allowManager = new AllowManager(this);
 | 
					  public allowManager = new AllowManager(this);
 | 
				
			||||||
  public snippetManager = new SnippetManager();
 | 
					  public snippetManager = new SnippetManager();
 | 
				
			||||||
 | 
					 | 
				
			||||||
  roles = new Map<string, ACLRole>();
 | 
					  roles = new Map<string, ACLRole>();
 | 
				
			||||||
 | 
					 | 
				
			||||||
  actionAlias = new Map<string, string>();
 | 
					  actionAlias = new Map<string, string>();
 | 
				
			||||||
 | 
					 | 
				
			||||||
  configResources: string[] = [];
 | 
					  configResources: string[] = [];
 | 
				
			||||||
 | 
					  protected availableActions = new Map<string, ACLAvailableAction>();
 | 
				
			||||||
 | 
					  protected fixedParamsManager = new FixedParamsManager();
 | 
				
			||||||
 | 
					  protected middlewares: Toposort<any>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor() {
 | 
					  constructor() {
 | 
				
			||||||
    super();
 | 
					    super();
 | 
				
			||||||
@ -100,68 +95,6 @@ export class ACL extends EventEmitter {
 | 
				
			|||||||
    this.addCoreMiddleware();
 | 
					    this.addCoreMiddleware();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected addCoreMiddleware() {
 | 
					 | 
				
			||||||
    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;
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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) {
 | 
					 | 
				
			||||||
          const filteredParams = filterParams(ctx, resourceName, params);
 | 
					 | 
				
			||||||
          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',
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  define(options: DefineOptions): ACLRole {
 | 
					  define(options: DefineOptions): ACLRole {
 | 
				
			||||||
    const roleName = options.role;
 | 
					    const roleName = options.role;
 | 
				
			||||||
    const role = new ACLRole(this, roleName);
 | 
					    const role = new ACLRole(this, roleName);
 | 
				
			||||||
@ -302,10 +235,6 @@ export class ACL extends EventEmitter {
 | 
				
			|||||||
    return null;
 | 
					    return null;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected isAvailableAction(actionName: string) {
 | 
					 | 
				
			||||||
    return this.availableActions.has(this.resolveActionAlias(actionName));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public resolveActionAlias(action: string) {
 | 
					  public resolveActionAlias(action: string) {
 | 
				
			||||||
    return this.actionAlias.get(action) ? this.actionAlias.get(action) : action;
 | 
					    return this.actionAlias.get(action) ? this.actionAlias.get(action) : action;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@ -360,7 +289,9 @@ export class ACL extends EventEmitter {
 | 
				
			|||||||
      const { resourceName, actionName } = ctx.action;
 | 
					      const { resourceName, actionName } = ctx.action;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      ctx.can = (options: Omit<CanArgs, 'role'>) => {
 | 
					      ctx.can = (options: Omit<CanArgs, 'role'>) => {
 | 
				
			||||||
        return acl.can({ role: roleName, ...options });
 | 
					        const canResult = acl.can({ role: roleName, ...options });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return canResult;
 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      ctx.permission = {
 | 
					      ctx.permission = {
 | 
				
			||||||
@ -397,4 +328,70 @@ export class ACL extends EventEmitter {
 | 
				
			|||||||
  registerSnippet(snippet: SnippetOptions) {
 | 
					  registerSnippet(snippet: SnippetOptions) {
 | 
				
			||||||
    this.snippetManager.register(snippet);
 | 
					    this.snippetManager.register(snippet);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  protected addCoreMiddleware() {
 | 
				
			||||||
 | 
					    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;
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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) {
 | 
				
			||||||
 | 
					          const filteredParams = filterParams(ctx, resourceName, params);
 | 
				
			||||||
 | 
					          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));
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -12,6 +12,8 @@ describe('acl', () => {
 | 
				
			|||||||
  let admin;
 | 
					  let admin;
 | 
				
			||||||
  let adminAgent;
 | 
					  let adminAgent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let userPlugin;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let uiSchemaRepository: UiSchemaRepository;
 | 
					  let uiSchemaRepository: UiSchemaRepository;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  afterEach(async () => {
 | 
					  afterEach(async () => {
 | 
				
			||||||
@ -30,7 +32,7 @@ describe('acl', () => {
 | 
				
			|||||||
      },
 | 
					      },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const userPlugin = app.getPlugin('users') as UsersPlugin;
 | 
					    userPlugin = app.getPlugin('users') as UsersPlugin;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    adminAgent = app.agent().auth(
 | 
					    adminAgent = app.agent().auth(
 | 
				
			||||||
      userPlugin.jwtService.sign({
 | 
					      userPlugin.jwtService.sign({
 | 
				
			||||||
@ -42,6 +44,131 @@ describe('acl', () => {
 | 
				
			|||||||
    uiSchemaRepository = db.getRepository('uiSchemas');
 | 
					    uiSchemaRepository = db.getRepository('uiSchemas');
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  test('append createById', async () => {
 | 
				
			||||||
 | 
					    const Company = await db.getRepository('collections').create({
 | 
				
			||||||
 | 
					      context: {},
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        name: 'companies',
 | 
				
			||||||
 | 
					        fields: [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            type: 'string',
 | 
				
			||||||
 | 
					            name: 'name',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            type: 'hasMany',
 | 
				
			||||||
 | 
					            name: 'users',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const Repair = await db.getRepository('collections').create({
 | 
				
			||||||
 | 
					      context: {},
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        name: 'repairs',
 | 
				
			||||||
 | 
					        createdBy: true,
 | 
				
			||||||
 | 
					        fields: [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            type: 'belongsTo',
 | 
				
			||||||
 | 
					            name: 'company',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            type: 'string',
 | 
				
			||||||
 | 
					            name: 'name',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const c1 = await db.getRepository('companies').create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        name: 'c1',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await db.getRepository('roles').create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        name: 'test-role',
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    await adminAgent.resource('roles.resources', 'test-role').create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        name: 'repairs',
 | 
				
			||||||
 | 
					        usingActionsConfig: true,
 | 
				
			||||||
 | 
					        actions: [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            name: 'list',
 | 
				
			||||||
 | 
					            fields: ['id'],
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const u1 = await db.getRepository('users').create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        name: 'u1',
 | 
				
			||||||
 | 
					        company: { id: c1.get('id') },
 | 
				
			||||||
 | 
					        roles: ['test-role'],
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const r1 = await db.getRepository('repairs').create({
 | 
				
			||||||
 | 
					      values: {
 | 
				
			||||||
 | 
					        name: 'r1',
 | 
				
			||||||
 | 
					        company: { id: c1.get('id') },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    userPlugin = app.getPlugin('users') as UsersPlugin;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const testAgent = app.agent().auth(
 | 
				
			||||||
 | 
					      userPlugin.jwtService.sign({
 | 
				
			||||||
 | 
					        userId: u1.get('id'),
 | 
				
			||||||
 | 
					      }),
 | 
				
			||||||
 | 
					      { type: 'bearer' },
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    const response1 = await testAgent.resource('repairs').list({
 | 
				
			||||||
 | 
					      filter: {
 | 
				
			||||||
 | 
					        company: {
 | 
				
			||||||
 | 
					          id: {
 | 
				
			||||||
 | 
					            $isVar: 'currentUser.company.id',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    const response2 = await testAgent.resource('repairs').list({
 | 
				
			||||||
 | 
					      filter: {
 | 
				
			||||||
 | 
					        company: {
 | 
				
			||||||
 | 
					          id: {
 | 
				
			||||||
 | 
					            $isVar: 'currentUser.company.id',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // @ts-ignore
 | 
				
			||||||
 | 
					    const response3 = await testAgent.resource('repairs').list({
 | 
				
			||||||
 | 
					      filter: {
 | 
				
			||||||
 | 
					        company: {
 | 
				
			||||||
 | 
					          id: {
 | 
				
			||||||
 | 
					            $isVar: 'currentUser.company.id',
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const acl = app.acl;
 | 
				
			||||||
 | 
					    const canResult = acl.can({ role: 'test-role', resource: 'repairs', action: 'list' });
 | 
				
			||||||
 | 
					    const params = canResult['params'];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    expect(params['fields']).toHaveLength(3);
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('should not have permission to list comments', async () => {
 | 
					  it('should not have permission to list comments', async () => {
 | 
				
			||||||
    await db.getCollection('collections').repository.create({
 | 
					    await db.getCollection('collections').repository.create({
 | 
				
			||||||
      values: {
 | 
					      values: {
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
		Reference in New Issue
	
	Block a user