UNPKG

@kurbar/access-control

Version:

Role, Attribute and Condition based Access Control for Node.js

559 lines (557 loc) 23.1 kB
"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; Object.defineProperty(exports, "__esModule", { value: true }); exports.AccessControl = void 0; var utils_1 = require("./utils/"); var core_1 = require("./core"); var conditions_1 = require("./conditions"); /** * @classdesc * AccessControl class that implements RBAC (Role-Based Access Control) basics * and ABAC (Attribute-Based Access Control) <i>resource</i> and <i>action</i> * attributes. * * Construct an `AccessControl` instance by either passing a grants object (or * array fetched from database) or simple omit `grants` parameter if you are * willing to build it programmatically. * * <p><pre><code> var grants = { * role1: { * grants: [ * { * resource: 'resource1', * action: 'create' * attributes: ['*'] * }, * { * resource: 'resource1', * action: 'read' * attributes: ['*'] * }, * { * resource: 'resource2', * action: 'create' * attributes: ['*'] * } * ] * }, * role2: { ... } * }; * var ac = new AccessControl(grants);</code></pre></p> * * The `grants` object can also be an array, such as a flat list * fetched from a database. * * <p><pre><code> var flatList = [ * { role: "role1", resource: "resource1", action: "create", attributes: [ attrs ] }, * { role: "role1", resource: "resource1", action: "read", attributes: [ attrs ] }, * { role: "role2", ... }, * ... * ];</code></pre></p> * * We turn this list into a hashtable for better performance. We aggregate * the list by roles first, resources second. * * Below are equivalent: * <p><pre><code> var grants = { role: "role1", resource: "resource1", action: "create", attributes: [ attrs ] } * var same = { role: "role1", resource: "resource1", action: "create", attributes: [ attrs ] }</code></pre></p> * * So we can also initialize with this flat list of grants: * <p><pre><code> var ac = new AccessControl(flatList); * console.log(ac.getGrants());</code></pre></p> * * @author Onur Yıldırım <onur@cutepilot.com> * @license MIT * * @class * @global * */ var AccessControl = /** @class */ (function () { /** * Initializes a new instance of `AccessControl` with the given grants. * * @param {Object|Array} grants - A list containing the access grant * definitions. See the structure of this object in the examples. * * @param {Object} customConditionFns - custom condition functions */ function AccessControl(grants, customConditionFns) { if (grants === void 0) { grants = {}; } if (customConditionFns === void 0) { customConditionFns = {}; } conditions_1.ConditionUtil.resetCustomConditionFunctions(); conditions_1.ConditionUtil.setCustomConditionFunctions(customConditionFns); this.setGrants(grants); } // ------------------------------- // PUBLIC METHODS // ------------------------------- /** * Gets the internal grants object that stores all current grants. * * @return {Object} - Hash-map of grants. */ AccessControl.prototype.getGrants = function () { return this._grants; }; /** * Sets all access grants at once, from an object or array. * Note that this will reset the object and remove all previous grants. * @chainable * * @param {Object|Array} grantsObject - A list containing the access grant * definitions. * @param {boolean} merge - Whether to merge with the existing grants. * * @returns {AccessControl} - `AccessControl` instance for chaining. */ AccessControl.prototype.setGrants = function (grantsObject, merge) { var _this = this; if (merge === void 0) { merge = false; } this._grants = merge ? this._grants : {}; var type = utils_1.CommonUtil.type(grantsObject); if (type === "object") { this._grants = utils_1.CommonUtil.normalizeGrantsObject(grantsObject); } else if (type === "array") { grantsObject .filter(function (grant) { return !grant.extend || !grant.extend.length; }) .forEach(function (item) { return utils_1.CommonUtil.commitToGrants(_this._grants, item); }); grantsObject .filter(function (item) { return item.extend && item.extend.length; }) .forEach(function (item) { return utils_1.CommonUtil.extendRole(_this._grants, item.role, item.extend); }); } return this; }; /** * Resets the internal grants object and removes all previous grants. * @chainable * * @returns {AccessControl} - `AccessControl` instance for chaining. */ AccessControl.prototype.reset = function () { this._grants = {}; conditions_1.ConditionUtil.resetCustomConditionFunctions(); return this; }; /** * Extends the given role(s) with privileges of one or more other roles. * @chainable * * @param {String|Array<String>} roles * Role(s) to be extended. * Single role as a `String` or multiple roles as an `Array`. * Note that if a role does not exist, it will be automatically * created. * * @param {String|Array<String>} extenderRoles * Role(s) to inherit from. * Single role as a `String` or multiple roles as an `Array`. * Note that if a extender role does not exist, it will throw. * * @returns {AccessControl} - `AccessControl` instance for chaining. * * @throws {Error} * If a role is extended by itself or a non-existent role. */ AccessControl.prototype.extendRole = function (roles, extenderRoles, condition) { // When extending role we are not checking for conditions so we can use sync method return this.extendRoleSync(roles, extenderRoles, condition); }; AccessControl.prototype.extendRoleSync = function (roles, extenderRoles, condition) { utils_1.CommonUtil.extendRoleSync(this._grants, roles, extenderRoles, condition); return this; }; /** * Removes all the given role(s) and their granted permissions, at once. * @chainable * * @param {String|Array<String>} roles - An array of roles to be removed. * Also accepts a string that can be used to remove a single role. * * @returns {AccessControl} - `AccessControl` instance for chaining. */ AccessControl.prototype.removeRoles = function (roles) { var _this = this; var rolesToRemove = utils_1.ArrayUtil.toStringArray(roles).sort(); // Remove these roles from $extend list of each remaining role. this._each(function (role, roleItem) { if (roleItem.$extend) { // Adjust scores and remove rolesToRemove.forEach(function (role) { if (roleItem.$extend[role]) { roleItem.score -= _this._grants[role].score; delete roleItem.$extend[role]; } }); } }); rolesToRemove.forEach(function (role) { delete _this._grants[role]; }); return this; }; /** * Gets all the unique roles that have at least one access information. * * @returns {Array<String>} * * @example * ac.grant('admin, user').createAny('video').grant('user').readOwn('profile'); * console.log(ac.getRoles()); // ["admin", "user"] */ AccessControl.prototype.getRoles = function () { return Object.keys(this._grants); }; /** * Checks whether any permissions are granted to the given role. * * @param {String} role - Role to be checked. * * @returns {Boolean} */ AccessControl.prototype.hasRole = function (role) { return this._grants.hasOwnProperty(role); }; /** * Get allowed grants when conditions are skipped return CommonUtil.getUnionGrantsOfRoles(this._grants, query); * @returns {IAccessInfo[]} - grants */ AccessControl.prototype.allowedGrants = function (query) { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/, utils_1.CommonUtil.getUnionGrantsOfRoles(this._grants, query)]; }); }); }; AccessControl.prototype.allowedGrantsSync = function (query) { return utils_1.CommonUtil.getUnionGrantsOfRolesSync(this._grants, query); }; /** * Get roles which allow this permission * @param {IQueryInfo} query - permission query object we want to check * * @returns {String[]} - roles */ AccessControl.prototype.allowingRoles = function (query) { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/, utils_1.CommonUtil.getAllowingRoles(this._grants, query)]; }); }); }; AccessControl.prototype.allowingRolesSync = function (query) { return utils_1.CommonUtil.getAllowingRolesSync(this._grants, query); }; /** * Get allowed actions of resource when conditions are skipped * @param {IQueryInfo} query - permission query object we want to check * * @returns {String[]} - actions */ AccessControl.prototype.allowedActions = function (query) { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/, utils_1.CommonUtil.getUnionActionsOfRoles(this._grants, query)]; }); }); }; AccessControl.prototype.allowedActionsSync = function (query) { return utils_1.CommonUtil.getUnionActionsOfRolesSync(this._grants, query); }; /** * Get allowed resources when conditions are skipped * @param {IQueryInfo} query - permission query object we want to check * * @returns {String[]} - resources */ AccessControl.prototype.allowedResources = function (query) { return __awaiter(this, void 0, void 0, function () { return __generator(this, function (_a) { return [2 /*return*/, utils_1.CommonUtil.getUnionResourcesOfRoles(this._grants, query)]; }); }); }; AccessControl.prototype.allowedResourcesSync = function (query) { return utils_1.CommonUtil.getUnionResourcesOfRolesSync(this._grants, query); }; AccessControl.prototype.can = function (roleOrUser) { var _a; if ((0, core_1.isIUser)(roleOrUser)) { // check if user has any custom grants if (roleOrUser.roles) { // extend current grants with user grants this.setGrants(((_a = roleOrUser.roles) === null || _a === void 0 ? void 0 : _a.flatMap(function (role) { return role.grants; })) || [], true); } return new core_1.Query(this._grants, roleOrUser.roles.map(function (r) { return r.id; }) || []).context({ user: roleOrUser, }); } return new core_1.Query(this._grants, roleOrUser); }; AccessControl.prototype.user = function (user) { var _a; return this.setGrants(((_a = user.roles) === null || _a === void 0 ? void 0 : _a.flatMap(function (role) { return role.grants; })) || []); }; /** * Alias of `can()`. * @private */ AccessControl.prototype.access = function (role) { return this.can(role); }; /** * Gets an instance of `Permission` object that checks and defines * the granted access permissions for the target resource and role. * Normally you would use `AccessControl#can()` method to check for * permissions but this is useful if you need to check at once by passing * a `IQueryInfo` object; instead of chaining methods * (as in `.can(<role>).<action>(<resource>)`). * * @param {IQueryInfo} queryInfo * A fulfilled {@link ?api=ac#AccessControl~IQueryInfo|`IQueryInfo` object}. * * @returns {Permission} - An object that provides properties * and methods that defines the granted access permissions. See * {@link ?api=ac#AccessControl~Permission|`Permission` inner class}. * * @example * var ac = new AccessControl(grants); * var permission = ac.permission({ * role: "user", * action: "update", * resource: "profile" * }); * permission.granted; // Boolean * permission.attributes; // Array e.g. [ 'username', 'password', 'company.*'] * permission.filter(object); // { username, password, company: { name, address, ... } } */ AccessControl.prototype.permission = function (queryInfo) { return __awaiter(this, void 0, void 0, function () { var _a, _b; return __generator(this, function (_c) { switch (_c.label) { case 0: _a = core_1.Permission.bind; _b = [void 0, queryInfo]; return [4 /*yield*/, utils_1.CommonUtil.getUnionAttrsOfRoles(this._grants, queryInfo)]; case 1: return [2 /*return*/, new (_a.apply(core_1.Permission, _b.concat([_c.sent()])))()]; } }); }); }; AccessControl.prototype.permissionSync = function (queryInfo) { return new core_1.Permission(queryInfo, utils_1.CommonUtil.getUnionAttrsOfRolesSync(this._grants, queryInfo)); }; /** * Gets an instance of `Grant` (inner) object. This is used to grant access * to specified resource(s) for the given role(s). * @name AccessControl#grant * @alias AccessControl#allow * @function * @chainable * * @param {String|Array<String>|IAccessInfo} role * A single role (as a string), a list of roles (as an array) or an * {@link ?api=ac#AccessControl~IAccessInfo|`IAccessInfo` object} * that fully or partially defines the access to be granted. * * @return {Access} * The returned object provides chainable properties to build and * define the access to be granted. See the examples for details. * See {@link ?api=ac#AccessControl~Access|`Access` inner class}. * * @example * var ac = new AccessControl(), * attributes = ['*']; * * ac.grant('admin').createAny('profile', attributes); * // equivalent to: * ac.grant().role('admin').createAny('profile', attributes); * // equivalent to: * ac.grant().role('admin').resource('profile').createAny(null, attributes); * // equivalent to: * ac.grant({ * role: 'admin', * resource: 'profile', * }).createAny(null, attributes); * // equivalent to: * ac.grant({ * role: 'admin', * resource: 'profile', * action: 'create:any', * attributes: attributes * }); * // equivalent to: * ac.grant({ * role: 'admin', * resource: 'profile', * action: 'create', * attributes: attributes * }); * * // To grant same resource and attributes for multiple roles: * ac.grant(['admin', 'user']).createOwn('profile', attributes); * * // Note: when attributes is omitted, it will default to `['*']` * // which means all attributes (of the resource) are allowed. */ AccessControl.prototype.grant = function (role) { return new core_1.Access(this._grants, role); }; /** * Alias of `grant()`. */ AccessControl.prototype.allow = function (role) { return this.grant(role); }; /** * Converts grants object to JSON format */ AccessControl.prototype.toJSON = function () { return utils_1.CommonUtil.toExtendedJSON({ grants: this._grants, customConditionFunctions: conditions_1.ConditionUtil.getCustomConditionFunctions(), }); }; AccessControl.prototype.registerConditionFunction = function (funtionName, fn) { conditions_1.ConditionUtil.registerCustomConditionFunction(funtionName, fn); return this; }; // ------------------------------- // PRIVATE METHODS // ------------------------------- /** * @private */ AccessControl.prototype._each = function (callback) { var _this = this; utils_1.CommonUtil.eachKey(this._grants, function (role) { return callback(role, _this._grants[role]); }); }; Object.defineProperty(AccessControl, "Error", { /** * Documented separately in AccessControlError * @private */ get: function () { return core_1.AccessControlError; }, enumerable: false, configurable: true }); // ------------------------------- // PUBLIC STATIC METHODS // ------------------------------- /** * A utility method for deep cloning the given data object(s) while * filtering its properties by the given attribute (glob) notations. * Includes all matched properties and removes the rest. * * Note that this should be used to manipulate data / arbitrary objects * with enumerable properties. It will not deal with preserving the * prototype-chain of the given object. * * @param {Object|Array} data - A single or array of data objects * to be filtered. * @param {Array|String} attributes - The attribute glob notation(s) * to be processed. You can use wildcard stars (*) and negate * the notation by prepending a bang (!). A negated notation * will be excluded. Order of the globs do not matter, they will * be logically sorted. Loose globs will be processed first and * verbose globs or normal notations will be processed last. * e.g. `[ "car.model", "*", "!car.*" ]` * will be sorted as: * `[ "*", "!car.*", "car.model" ]`. * Passing no parameters or passing an empty string (`""` or `[""]`) * will empty the source object. * * @returns {Object|Array} - Returns the filtered data object or array * of data objects. * * @example * var assets = { notebook: "Mac", car: { brand: "Ford", model: "Mustang", year: 1970, color: "red" } }; * * var filtered = AccessControl.filter(assets, [ "*", "!car.*", "car.model" ]); * console.log(assets); // { notebook: "Mac", car: { model: "Mustang" } } * * filtered = AccessControl.filter(assets, "*"); // or AccessControl.filter(assets, ["*"]); * console.log(assets); // { notebook: "Mac", car: { model: "Mustang" } } * * filtered = AccessControl.filter(assets); // or AccessControl.filter(assets, ""); * console.log(assets); // {} */ AccessControl.filter = function (data, attributes) { return utils_1.CommonUtil.filterAll(data, attributes); }; /** * Checks whether the given object is an instance of `AccessControl.Error`. * @name AccessControl.isACError * @alias AccessControl.isAccessControlError * @function * * @param {Any} object * Object to be checked. * * @returns {Boolean} */ AccessControl.isACError = function (object) { return object instanceof core_1.AccessControlError; }; /** * Alias of `isACError` * @private */ AccessControl.isAccessControlError = function (object) { return AccessControl.isACError(object); }; /** * Prepare AccessControl from JSON * @param aclJSON JSON generated from toJSON method. */ AccessControl.fromJSON = function (aclJSON) { var aclObj = utils_1.CommonUtil.fromExtendedJSON(aclJSON); return new AccessControl(aclObj.grants, aclObj.customConditionFunctions); }; return AccessControl; }()); exports.AccessControl = AccessControl;