UNPKG

@superawesome/permissions

Version:

Fine grained permissions / access control with ownerships & attribute picking, done right.

160 lines (138 loc) 5.37 kB
import * as _ from 'lodash'; import { AccessControl, IAccessInfo } from 'accesscontrol'; import { AccessControlRe } from 'accesscontrol-re'; import { EPossession, TGrants, Tid } from './types'; import { PermissionDefinitionInternal, PermissionDefinition, PermissionDefinitionDefaults, } from './PermissionDefinitions'; import { getLogger } from './logger'; export const stringify = (obj) => JSON.stringify(obj, null, 2); export const hasSomeOwnGrant = (ipd: PermissionDefinitionInternal) => _.some(ipd.grant, (attrs, action): any => _.endsWith(action, `:${EPossession.own}`)); export const projectPDWithDefaultsToInternal = _.curry( <TUserId extends Tid = number, TResourceId extends Tid = number>( defaults: PermissionDefinitionDefaults, pd: PermissionDefinition<TUserId, TResourceId> ): PermissionDefinitionInternal<TUserId, TResourceId> => { const { isOwner, listOwned, limitOwned, descr } = pd; const roles = pd.roles || defaults.roles; const resource = pd.resource || defaults.resource; // prettier-ignore if (!roles) throw new Error(`SA-Permissions: InvalidPermissionDefinitionError: missing "roles" in ${stringify(pd)}.`); // prettier-ignore if (!resource) throw new Error(`SA-Permissions: InvalidPermissionDefinitionError: missing "resource" in ${stringify(pd)}.`); // prettier-ignore if (_.isEmpty(pd.grant)) throw new Error(`SA-Permissions: InvalidPermissionDefinitionError: missing or empty "grant" in ${stringify(pd)}`); // convert ['list', 'ofActions'] to {list: null, ofActions: null} const grantTempObj = !_.isArray(pd.grant) ? pd.grant : _.reduce( pd.grant, (acc, cur) => { acc[cur] = null; return acc; }, {} ); // apply defaults const grant: TGrants = _.reduce( grantTempObj, (acc, possibleAttributes, actionPerhapsWithPossession: string) => { const [action, possiblePossession] = actionPerhapsWithPossession.split(':'); const possession: string = possiblePossession || pd.possession || defaults.possession || EPossession.any; const actionWithPossession = `${action}:${possession}`; acc[actionWithPossession] = possibleAttributes || pd.attributes || defaults.attributes || ['*']; return acc; }, {} ); const ipd: PermissionDefinitionInternal<TUserId, TResourceId> = { roles: _.isArray(roles) ? roles : [roles], resource, descr, // will be updated below if empty isOwner, listOwned, limitOwned, grant, }; return ipd; } ); export const buildAccessControl = ( permissionDefinitions: PermissionDefinitionInternal[] ): [AccessControl, AccessControlRe] => { if (_.isEmpty(permissionDefinitions)) throw new Error('SA-Permissions: cant build with empty permissionDefinitions!'); const acre = new AccessControlRe(); // first pass: execute only the grant's // so all roles are defined _.each(permissionDefinitions, ( pd // {grant, resource, roles} ) => _.each(pd.grant, (attributes, actionPossession) => { const [action, possession] = actionPossession.split(':'); const accessInfo: IAccessInfo = { action, possession, attributes, role: pd.roles, resource: pd.resource, }; // @todo: check it's a valid IAccessInfo - use class-validator // - has at least one Role, one Resource // - has possession, added either as the default or the grant it self getLogger().debug(`addAccessInfo(${stringify(accessInfo)})`); acre.addAccessInfo(accessInfo); }) ); // @todo: second pass: extend roles (already defined on 1st pass) _.each(permissionDefinitions, (pd: any) => { if (pd.extend) { throw new Error( 'extend is not supported yet, and will not until this is fixed https://github.com/onury/accesscontrol/issues/34#issuecomment-466387586 and the whole extend idea redesigned and tested properly.' ); // this._accessControl.grant(pd.role).extend(pd.extend); } }); acre.build(); return [acre.accessControl, acre]; }; export const isArraySetEqual = ( ar1: any[], ar2: any[], comparator1?: (a: any, b: any) => boolean, comparator2?: (a: any, b: any) => boolean ) => { if (!comparator1) comparator1 = (a, b) => a === b; if (!comparator2) comparator2 = _.flip(comparator1); return ( _.isArray(ar1) && _.isArray(ar2) && ar1.length === ar2.length && _.isEmpty(_.differenceWith(ar1, ar2, comparator1 as any)) && _.isEmpty(_.differenceWith(ar2, ar1, comparator2 as any)) ); }; export const isHash = (o: any): o is object => _.isObjectLike(o) && !_.isArray(o) && !_.isFunction(o); // Returns true if the keys+values of `o1` are all _.isEqual to `o2`'s corresponding ones export const isLike = (o1: {} = {}, o2: {} = {}) => _.isEqual(o1, o2) || (isHash(o1) && isHash(o2) && _.every(_.keys(o1), (o1Key) => isLike(o1[o1Key], o2[o1Key]))); // Deletes all object keys with a value of `[]` (empty array) // note: mutates the object export const deleteEmptyArrayKeys = (o: object) => { if (isHash(o)) { _.each(o, (val, key) => { if (_.isEqual(val, [])) { delete o[key]; } else deleteEmptyArrayKeys(val); }); } return o; };