UNPKG

@batolye/bdk-core

Version:

Module to provide core utilities for BulusAtolyesi applications and services

292 lines (225 loc) 11.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.populateSubjects = populateSubjects; exports.unpopulateSubjects = unpopulateSubjects; exports.populateResource = populateResource; exports.unpopulateResource = unpopulateResource; exports.preventEscalation = preventEscalation; exports.authorise = authorise; exports.core_updateAbilities = core_updateAbilities; var _lodash = _interopRequireDefault(require("lodash")); var _feathersHooksCommon = require("feathers-hooks-common"); var _errors = require("@feathersjs/errors"); var _hooks = require("./hooks.query"); var _mongoDb = require("../utils/mongoDb"); var _permissions = require("../common/permissions"); var _debug = _interopRequireDefault(require("debug")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const debug = (0, _debug.default)("batolye:bdk-core:authorisations:hooks"); function populateSubjects(hook) { if (hook.type !== "before") { throw new Error(`The 'populateSubjects' hook should only be used as a 'before' hook.`); } return (0, _hooks.populateObjects)({ serviceField: "subjectsService", idField: "subjects", throwOnNotFound: true })(hook); } function unpopulateSubjects(hook) { if (hook.type !== "after") { throw new Error(`The 'unpopulateSubjects' hook should only be used as a 'after' hook.`); } return (0, _hooks.unpopulateObjects)({ serviceField: "subjectsService", idField: "subjects" })(hook); } function populateResource(hook) { if (hook.type !== "before") { throw new Error(`The 'populateResource' hook should only be used as a 'before' hook.`); } return (0, _hooks.populateObject)({ serviceField: "resourcesService", idField: "resource", throwOnNotFound: true })(hook); } function unpopulateResource(hook) { if (hook.type !== "after") { throw new Error(`The 'unpopulateResource' hook should only be used as a 'after' hook.`); } return (0, _hooks.unpopulateObject)({ serviceField: "resourcesService", idField: "resource" })(hook); } function preventEscalation(hook) { if (hook.type !== "before") { throw new Error(`The 'preventEscalation' hook should only be used as a 'before' hook.`); } let params = hook.params; // If called internally we skip authorisation // https://docs.feathersjs.com/api/services.html#params let checkEscalation = params.hasOwnProperty("provider"); debug("Escalation check " + (checkEscalation ? "enabled" : "disabled") + " for provider"); // If explicitely asked to perform/skip, override defaults if (params.hasOwnProperty("checkEscalation")) { checkEscalation = params.checkEscalation; debug("Escalation check " + (checkEscalation ? "forced" : "unforced")); } if (checkEscalation) { const user = params.user; // Make hook usable on remove as well let data = hook.data || {}; // Make hook usable with query params as well let query = params.query || {}; let scopeName = data.scope || query.scope; // Get scope name first // Retrieve the right scope on the user let scope = _lodash.default.get(user, scopeName, []); // Then the target resource let resource = _lodash.default.find(scope, resource => resource._id && resource._id.toString() === params.resource._id.toString()); // Then user permission level const permissions = resource ? resource.permissions : undefined; const role = permissions ? _permissions.Roles[permissions] : undefined; if (_lodash.default.isUndefined(role)) { debug("Role for authorisation not found on user for scope " + scopeName); throw new _errors.Forbidden(`You are not allowed to change authorisation on resource`); } // Check if privilege escalation might occur, if so clamp to user permission level // Input subjects need to be checked: // - on create you should not be able to change permissions on others having higher permissions than yourself // (e.g. cannot change a owner into a manager when you are a manager) // - on remove you should not be able to remove permissions on others having higher permissions than yourself // (e.g. cannot remove a owner when you are a manager) const subjects = params.subjects.filter(subject => { const subjectScope = _lodash.default.get(subject, scopeName, []); const subjectResource = _lodash.default.find(subjectScope, resource => resource._id && resource._id.toString() === params.resource._id.toString()); const subjectPermissions = subjectResource ? subjectResource.permissions : undefined; const subjectRole = subjectPermissions ? _permissions.Roles[subjectPermissions] : undefined; const hasRole = !_lodash.default.isUndefined(subjectRole); if (hook.method === "create") { return !hasRole || subjectRole <= role; // The first time no authorisation can be found } else { return hasRole && subjectRole <= role; // Authorisation must be found on remove } }); if (subjects.length < params.subjects.length) { debug(`${params.subjects.length - subjects.length} subjects with higher permissions level found for scope ${scopeName}`); throw new _errors.Forbidden(`You are not allowed to change authorisation on subject(s)`); } // Input permissions needs to be checked since: // - you should not be able to give higher permissions than your own ones to others // (e.g. cannot create a owner when you are a manager) let authorisationRole; if (data.permissions) { authorisationRole = _permissions.Roles[data.permissions]; } else if (query.permissions) { authorisationRole = _permissions.Roles[query.permissions]; } if (!_lodash.default.isUndefined(authorisationRole)) { if (authorisationRole > role) { debug("Cannot escalate with higher permissions level for scope " + scopeName); throw new _errors.Forbidden(`You are not allowed to change authorisation on resource`); } } } return hook; } function authorise(hook) { if (hook.type !== "before") { throw new Error(`The 'authorise' hook should only be used as a 'before' hook.`); } const operation = hook.method; debug("Operation is", operation); debug("Provider is", hook.params.provider); if (hook.params.user) debug("User is", hook.params.user); const resourceType = hook.service.core_name; if (resourceType) debug("Resource type is", resourceType); // If called internally we skip authorisation let checkAuthorisation = hook.params.hasOwnProperty("provider"); debug("Access check " + (checkAuthorisation ? "enabled" : "disabled") + " for provider"); // If already checked we skip authorisation if (hook.params.authorised) { debug("Access already granted"); checkAuthorisation = false; } // We also skip check authorisation for built-in Feathers services like authentication if (typeof hook.service.core_getPath !== "function") { debug("Access disabled on built-in services"); checkAuthorisation = false; } // If explicitely asked to perform/skip, override defaults if (hook.params.hasOwnProperty("checkAuthorisation")) { checkAuthorisation = hook.params.checkAuthorisation; // Bypass authorisation for next hooks otherwise we will loop infinitely delete hook.params.checkAuthorisation; debug("Access check " + (checkAuthorisation ? "forced" : "unforced")); } if (checkAuthorisation) { // Build ability for user let authorisationService = hook.app.core_getService("authorisations"); const abilities = authorisationService.core_getAbilities(hook.params.user); hook.params.abilities = abilities; debug("User abilities are", abilities.rules); // Check for access to service fisrt if (!(0, _permissions.hasServiceAbilities)(abilities, hook.service)) { debug("Service access not granted"); throw new _errors.Forbidden(`You are not allowed to access service ${hook.service.core_getPath()}`); } const zone = hook.service.zone; if (!hook.id) { // In this specific case there is no query to be run, // simply check against the object we'd like to create if (operation === "create") { let resource = hook.data; debug("Target resource is ", resource); if (!(0, _permissions.hasResourceAbilities)(abilities, operation, resourceType, zone, resource)) { debug("Resource access not granted"); throw new _errors.Forbidden(`You are not allowed to perform ${operation} operation on ${resourceType}`); } } else { // When we find/update/patch/remove multiple items this ensures that // only the ones authorised by constraints on the resources will be fetched // This avoid fetching all first then check it one by one const dbQuery = (0, _mongoDb.objectifyIDs)((0, _permissions.getQueryForAbilities)(abilities, operation, resourceType)); if (dbQuery) { debug("Target resource conditions are ", dbQuery); _lodash.default.merge(hook.params.query, dbQuery); } else { hook.result = { total: 0, skip: 0, data: [] }; } } debug("Resource access granted"); // Some specific services might not expose a get function, in this case we cannot check for authorisation // this has to be implemented by the service itself } else if (typeof hook.service.get === "function") { // In this case (single get/update/patch/remove) we need to fetch the item first return hook.service.get(hook.id, Object.assign({ checkAuthorisation: false }, hook.params)).then(resource => { debug("Target resource is", resource); // Then check against the object we'd like to manage if (!(0, _permissions.hasResourceAbilities)(abilities, operation, resourceType, zone, resource)) { debug("Resource access not granted"); throw new _errors.Forbidden(`You are not allowed to perform ${operation} operation on ${resourceType}`); } // Avoid fetching again the object in this case if (operation === "get") { hook.result = resource; } hook.params.authorised = true; debug("Resource access granted"); return hook; }); } } else { debug("Authorisation check skipped, access granted"); } hook.params.authorised = true; return Promise.resolve(hook); } function core_updateAbilities(options = {}) { return async function (hook) { let app = hook.app; let params = hook.params; let authorisationService = app.core_getService("authorisations"); let subject = options.subjectAsItem ? (0, _feathersHooksCommon.getItems)(hook) : params.user; // We might not have all information required eg on patch to compute new abilities, // in this case we have to fetch the whole subject if (options.fetchSubject) { subject = await hook.service.get(subject._id.toString()); } const abilities = authorisationService.core_updateAbilities(subject); debug("Abilities updated on subject", subject, abilities.rules); return hook; }; }