UNPKG

happn-3

Version:

pub/sub api as a service using primus and mongo & redis or nedb, can work as cluster, single process or embedded using nedb

296 lines (262 loc) 9.4 kB
const CONSTANTS = require('../..').constants; var ALLOWED_PERMISSIONS = [ 'set', 'get', 'remove', 'on', '*', 'delete', 'put', 'post', 'head', 'options', ]; module.exports = class Permissions { constructor(config, type, happn, securityService) { this.__config = this.defaults(config); this.securityService = securityService; this.cacheService = happn.services.cache; this.dataService = happn.services.data; this.type = type; this.cache = this.cacheService.create(`cache_security_${this.type}_permissions`, { type: 'LRU', cache: this.__config.__cache_permissions, }); this.__userPrefix = this.__config.__userPermissionsPrefix; } static create(config, type, happn, securityService) { return new Permissions(config, type, happn, securityService); } defaults(config) { let defaultConfig = !config ? {} : { ...config }; if (!defaultConfig.__cache_permissions) defaultConfig.__cache_permissions = { max: 10e3, maxAge: 0, }; defaultConfig.__userPermissionsPrefix = defaultConfig.__userPermissionsPrefix || '_USER/'; return defaultConfig; } attachPermissions(entity) { entity.permissions = {}; return this.listPermissions(entity.name || entity.username).then((permissions) => { permissions.forEach((permission) => { if (typeof permission.authorized !== 'boolean') return; if (permission.authorized === false) { // explicitly deny if (!entity.permissions[permission.path]) entity.permissions[permission.path] = { prohibit: [] }; entity.permissions[permission.path].prohibit = entity.permissions[permission.path].prohibit || []; entity.permissions[permission.path].prohibit.push(permission.action); return; } // allow if (!entity.permissions[permission.path]) entity.permissions[permission.path] = { actions: [] }; entity.permissions[permission.path].actions = entity.permissions[permission.path].actions || []; entity.permissions[permission.path].actions.push(permission.action); }); return entity; }); } async listPermissions(name) { if (!name) throw new Error(`please supply a ${this.type}Name`); if (this.cache.has(name)) { return this.cache.get(name); } const rawPermissions = await this.dataService.get( '/_SYSTEM/_SECURITY/_PERMISSIONS/' + this.__prepareName(name) + '/*', { sort: { path: 1, }, } ); const deserialized = this.dataService.extractData(rawPermissions); this.cache.set(name, deserialized); return deserialized; } async removeAllUserPermissions(name) { if (!name) throw new Error(`please supply a username`); return this.dataService.remove(`/_SYSTEM/_SECURITY/_PERMISSIONS/${this.__prepareName(name)}/*`); } removePermission(name, path, action) { return new Promise((resolve, reject) => { if (!name) return reject(new Error(`please supply a ${this.type}Name`)); if (!action) action = '*'; if (!path) path = '*'; return this.__removePermission(name, path, action) .then((result) => { if (!result || !result.data) return resolve(); if (result.data.removed) return this.securityService.dataChanged( CONSTANTS.SECURITY_DIRECTORY_EVENTS.PERMISSION_REMOVED, { ...this.__getNameObj(name), path, action, }, null, () => { resolve(result); } ); resolve(result); }) .catch(reject); }); } async __removePermission(name, path, action, removeProhibition = false) { let permissionPath = [ '/_SYSTEM/_SECURITY/_PERMISSIONS', this.__prepareName(name), action, this.__escapePermissionsPath(path), ].join('/'); let storedData = await this.dataService.get(permissionPath); if (!storedData) return; storedData = Array.isArray(storedData) ? storedData : [storedData]; let promises = storedData.map((stored) => { if (!stored || !stored.data) return; if (removeProhibition && stored.data.authorized === true) return; if (!removeProhibition && stored.data.authorized === false) return; return this.dataService.remove(permissionPath); }); let results = await Promise.all(promises); return results.filter((result) => result !== undefined)[0]; } upsertPermission(name, path, action, authorized) { return new Promise((resolve, reject) => { if (authorized == null) authorized = true; authorized = !!authorized; //must always be stored true or false return this.__upsertPermission(name, path, action, authorized) .then((result) => { this.securityService.dataChanged( CONSTANTS.SECURITY_DIRECTORY_EVENTS.PERMISSION_UPSERTED, { ...this.__getNameObj(name), path: path, action: action, authorized: authorized, }, () => { resolve(result); } ); }) .catch(reject); }); } validatePermissions(permissions) { var errors = []; Object.keys(permissions).forEach(function (permissionPath) { var permission = permissions[permissionPath]; if (!permission.actions && !permission.prohibit) return errors.push('missing allowed actions or prohibit rules: ' + permissionPath); if (permission.actions) permission.actions.forEach(function (action) { if (ALLOWED_PERMISSIONS.indexOf(action) === -1) return errors.push('unknown action: ' + action + ' for path: ' + permissionPath); }); if (permission.prohibit) permission.prohibit.forEach(function (action) { if (ALLOWED_PERMISSIONS.indexOf(action) === -1) return errors.push( 'unknown prohibit action: ' + action + ' for path: ' + permissionPath ); }); }); if (errors.length === 0) return true; else return errors; } upsertMultiplePermissions(name, permissions) { return new Promise((resolve, reject) => { var promises = []; if (!name) return reject(new Error(`please supply a ${this.type}Name`)); var permissionsValidation = this.validatePermissions(permissions); if (permissionsValidation !== true) return reject( new Error(`${this.type} permissions invalid: ` + permissionsValidation.join(',')) ); Object.keys(permissions).forEach((permissionPath) => { var permission = permissions[permissionPath]; if (permission.remove) { if (permission.actions) permission.actions.forEach((action) => { promises.push(this.__removePermission(name, permissionPath, action)); }); if (permission.prohibit) permission.prohibit.forEach((action) => { promises.push(this.__removePermission(name, permissionPath, action, true)); }); } else { if (permission.actions) permission.actions.forEach((action) => { promises.push(this.__upsertPermission(name, permissionPath, action)); }); if (permission.prohibit) permission.prohibit.forEach((action) => { promises.push(this.__upsertPermission(name, permissionPath, action, false)); }); } }); Promise.all(promises) .then((responses) => { this.cache.remove(name); resolve(responses); }) .catch(reject); }); } async __upsertPermissions(name, permissionPath, actions, authorized) { if (actions) { for (let action of actions) { await this.__upsertPermission(name, permissionPath, action, authorized); } } } async __upsertPermission(name, path, action, authorized) { if (!name) throw new Error(`please supply a ${this.type}Name`); const validPath = this.__validatePermissionsPath(path); if (validPath !== true) throw new Error(validPath); if (!action) action = '*'; if (authorized == null) authorized = true; authorized = !!authorized; //must always be stored true or false return await this.dataService.upsert( [ '/_SYSTEM/_SECURITY/_PERMISSIONS', this.__prepareName(name), action, this.__escapePermissionsPath(path), ].join('/'), { action: action, authorized: authorized, path: path, } ); } __escapePermissionsPath(path) { return path.replace(/\*/g, '{{w}}'); } __unescapePermissionsPath(path) { return path.replace(/\{\{w}}/g, '*'); } __validatePermissionsPath(path) { if (!path) return 'permission path is null'; if (path.indexOf('{{w}}') > -1) return 'invalid permission path, cannot contain special string {{w}}'; return true; } __getNameObj(name) { let nameObj = {}; let suffix = this.type === 'user' ? 'name' : 'Name'; nameObj[this.type + suffix] = name; return nameObj; } __prepareName(name) { return this.type === 'user' ? this.__userPrefix + name : name; } };