UNPKG

am-i-allowed

Version:

A generic, very powerful yet very friendly, 0 dependencies, agnostic, permissions/access-control/authorization library

236 lines 9.42 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Role = exports.NoPrivilegeException = exports.PrivilegeManager = void 0; const types_1 = require("./types"); const permission_checker_1 = require("./permission-checker"); const operations_taxonomy_1 = require("./operations-taxonomy"); /** * This is the main class. Normally you'd need just one PrivilegeManager for the whole application. * Use it to check permissions. * * * Special terms: * * The relation between ac actor to a given entity may be one of the following: * - Visitor: a not logged-in user * - User: a logged-in user, with an account * - Group Member: a user that shares the group of the entity * - a role owner: there's a role explicitly assigned to the use on the entity * */ class PrivilegeManager { /** * Builds a privilege manager instance. * @param store the persistence backend for the permission storage * @param operationsTransformer an optional operation tree transformer, in case you wish to alter the default one, add more operations, etc */ constructor(store, operationsTransformer = (operationTree) => operationTree) { this.store = store; this.operationTree = new OperationTree(operationsTransformer(operations_taxonomy_1.DefaultOperationsTaxonomy)); this.entityMetaDataLookup = new EntityMetaDataLookup(this); } /** * Check for if the actor is allowed to do something and throws an exception if he isn't * @param actor * @param operation * @param entity * @param specialContext for custom logic * @throws NoPrivilegeException if actor is not allowed */ async test(actor, operation, entity, specialContext) { // @ts-ignore const isAllowed = await this.isAllowed(...arguments); if (!isAllowed) { // @ts-ignore throw new NoPrivilegeException(...arguments); } } /** * Check if the actor is allowed to do * @param actor * @param operation * @param entity * @param specialContext * @return <promise> of true or false */ isAllowed(actor, operation, entity, specialContext) { var _a; if (!this.operationTree.find(operation)) throw new Error(`Operation ${operation.toString()} is not defined. Consider adding it to the operations tree.`); // @ts-ignore const customPermissionChecker = entity.customPermissionChecker || ((_a = entity.constructor) === null || _a === void 0 ? void 0 : _a.customPermissionChecker); if (customPermissionChecker) return customPermissionChecker(this, ...arguments); // @ts-ignore return permission_checker_1.standardPermissionChecker(this, ...arguments); } /** * @return all the actors that have explicit roles assigned on that entity * @param entity the entity */ getRoleOwners(entity) { return this.store.getRoleOwners(entity); } /** * @return all the roles explicitly assigned to the actor on any entity * @param actorId * @param skip pagination support * @param limit pagination support */ getActorRoles(actorId, skip = 0, limit = 1000) { return this.store.getActorRoles(actorId, skip, limit); } /** * assign a role to use in entity * @param entity the entity * @param actor either IActor or an id * @param role the role */ assignRole(entity, actor, role) { return this.store.assignRole(entity, actor, role.roleName); } /** * @Return the roles the actor have on an entity * @param actor * @param entity */ async getRolesForActor(actor, entity) { return this.store.getRolesForUser(actor, entity, await this.findMetaData(entity)); } // noinspection JSIgnoredPromiseFromCall /** * Define a new role. Also add itself to the corresponding entityType. Y * @param roleName name of role * @param entityType The entity types this role is applicable to * @param operations the operation the role holder may do on the entities of the aforementioned types * @return the new role object */ addRole(roleName, operations, entityType) { const role = new Role(this, roleName, operations, entityType); const metaData = this.getOrAddMetaData(entityType); metaData.roles[roleName] = role; this.store.saveRole(metaData.name, role).then(() => { }); return role; } /** * Define a new role. Also add itself to the corresponding entityType. You can add multiple roles, each for * different entity type if you provide more than one entity type. * @param roleName name of role * @param entityTypes The entity types this role is applicable to * @param operations the operation the role holder may do on the entities of the aforementioned types * @return the new roles objects */ addRoles(roleName, operations, ...entityTypes) { return entityTypes.map(entityType => this.addRole(roleName, operations, entityType)); } deleteRole(roleName, entityTypeName) { return this.store.deleteRole(roleName, entityTypeName); } async saveRole(entityTypeName, role) { await this.store.saveRole(entityTypeName, role); } getOrAddMetaData(type) { return this.entityMetaDataLookup.getOrAddMetaData(type); } async findMetaData(entity) { return this.entityMetaDataLookup.findMetaData(entity); } } exports.PrivilegeManager = PrivilegeManager; class NoPrivilegeException extends Error { constructor(actor, operation, entity, specialContext) { super(`${actor.id} attempted unprivileged operation ${operation.toString()} on ${entity.id} with ${JSON.stringify(specialContext || '')}`); } } exports.NoPrivilegeException = NoPrivilegeException; //////////////////////////////////////////// /** * This class's purpose is to holds the operation definition tree and provide the expandOperation method */ class OperationTree { constructor(tree) { this.tree = tree; this.parentsMap = new Map(); this.processTree(tree); } processTree(tree) { const self = this; populate(tree); function populate(node, parents = []) { for (let [name, children] of Object.entries(node)) { const entryParents = self.parentsMap.get(name) || []; self.parentsMap.set(name, entryParents.concat(parents)); children && Object.keys(children).length && populate(children, [name, ...parents]); } } } /** * expand to include the super-operations * @param operation */ expandOperation(operation) { if (!operation) return []; const parents = this.parentsMap.get(operation); if (parents.length) return [operation, ...parents]; return [operation, ...parents, ...parents.reduce((a, c) => { a.push(...this.expandOperation(c)); return a; }, [])]; } find(operation) { return this.parentsMap.has(operation); } } class EntityMetaDataLookup { constructor(privilegeManager) { this.privilegeManager = privilegeManager; this.metaDataMap = new Map(); } getOrAddMetaData(entityType) { const name = typeof entityType == 'string' ? entityType : entityType.name; const clazz = typeof entityType == 'string' ? null : entityType; let metadata = this.metaDataMap.get(name); if (!metadata) { // @ts-ignore metadata = (clazz === null || clazz === void 0 ? void 0 : clazz.permissionsMetaData) || new types_1.PermissionsMetaData(name, {}); this.metaDataMap.set(name, metadata); } return metadata; } async findMetaData(entity) { var _a; // first, we check if there's meta data on the entity itself // @ts-ignore let metaData = entity.permissionsMetaData || ((_a = entity.constructor) === null || _a === void 0 ? void 0 : _a.permissionsMetaData); if (!metaData) { const entityName = entity.constructor === Object ? entity.___name : entity.constructor.name; return this.getOrAddMetaData(entityName); } // if it is defined as function - execute the function metaData = typeof metaData === 'function' ? await metaData() : metaData; // validate the metadata if it wasn't validated before if (!metaData._validated) metaData._validated = this.validateMetaData(metaData); return metaData; } validateMetaData(md) { [...md.defaultGroupMemberPermissions, ...md.defaultUserPermissions, ...md.defaultVisitorPermissions].forEach(o => { if (!this.privilegeManager.operationTree.find(o)) throw new Error(`Operation "${o}" is not in the taxonomy`); }); return true; } } /** * Role defines the set of permitted operations. Each role is applicable to a provided entity types */ class Role { constructor(pm, roleName, operations, entityType) { this.roleName = roleName; this.entityType = entityType; this.operations = new Set(operations); } } exports.Role = Role; //# sourceMappingURL=am-i-allowed.js.map