diff --git a/lib/AccessControl.d.ts b/lib/AccessControl.d.ts index 3fd8115..de05119 100644 --- a/lib/AccessControl.d.ts +++ b/lib/AccessControl.d.ts @@ -103,7 +103,7 @@ declare class AccessControl { /** * @private */ - private _locked; + private _isLocked; /** * Initializes a new instance of `AccessControl` with the given grants. * @ignore diff --git a/lib/AccessControl.js b/lib/AccessControl.js index b7e027f..61450a7 100644 --- a/lib/AccessControl.js +++ b/lib/AccessControl.js @@ -111,7 +111,7 @@ var AccessControl = /** @class */ (function () { /** * @private */ - this._locked = false; + this._isLocked = false; // explicit undefined is not allowed if (arguments.length === 0) grants = {}; @@ -128,7 +128,7 @@ var AccessControl = /** @class */ (function () { * @type {Boolean} */ get: function () { - return this._locked && Object.isFrozen(this._grants); + return this._isLocked && Object.isFrozen(this._grants); }, enumerable: true, configurable: true @@ -288,8 +288,14 @@ var AccessControl = /** @class */ (function () { if (this.isLocked) throw new core_1.AccessControlError(utils_1.ERR_LOCK); var rolesToRemove = utils_1.utils.toStringArray(roles); - rolesToRemove.forEach(function (role) { - delete _this._grants[role]; + if (rolesToRemove.length === 0 || !utils_1.utils.isFilledStringArray(rolesToRemove)) { + throw new core_1.AccessControlError("Invalid role(s): " + JSON.stringify(roles)); + } + rolesToRemove.forEach(function (roleName) { + if (!_this._grants[roleName]) { + throw new core_1.AccessControlError("Cannot remove a non-existing role: \"" + roleName + "\""); + } + delete _this._grants[roleName]; }); // also remove these roles from $extend list of each remaining role. utils_1.utils.eachRole(this._grants, function (roleItem, roleName) { @@ -626,8 +632,17 @@ var AccessControl = /** @class */ (function () { AccessControl.prototype._removePermission = function (resources, roles, actionPossession) { var _this = this; resources = utils_1.utils.toStringArray(resources); - if (roles) + // resources is set but returns empty array. + if (resources.length === 0 || !utils_1.utils.isFilledStringArray(resources)) { + throw new core_1.AccessControlError("Invalid resource(s): " + JSON.stringify(resources)); + } + if (roles !== undefined) { roles = utils_1.utils.toStringArray(roles); + // roles is set but returns empty array. + if (roles.length === 0 || !utils_1.utils.isFilledStringArray(roles)) { + throw new core_1.AccessControlError("Invalid role(s): " + JSON.stringify(roles)); + } + } utils_1.utils.eachRoleResource(this._grants, function (role, resource, permissions) { if (resources.indexOf(resource) >= 0 // roles is optional. so remove if role is not defined. diff --git a/lib/core/Access.js b/lib/core/Access.js index d4941aa..73bda6b 100644 --- a/lib/core/Access.js +++ b/lib/core/Access.js @@ -448,14 +448,12 @@ var Access = /** @class */ (function () { this._.possession = possession; if (resource) this._.resource = resource; - if (attributes) - this._.attributes = attributes; if (this._.denied) { this._.attributes = []; } else { // if omitted and not denied, all attributes are allowed - this._.attributes = this._.attributes ? utils_1.utils.toStringArray(this._.attributes) : ['*']; + this._.attributes = attributes ? utils_1.utils.toStringArray(attributes) : ['*']; } utils_1.utils.commitToGrants(this._grants, this._, false); // important: reset attributes for chained methods diff --git a/lib/core/AccessControlError.js b/lib/core/AccessControlError.js index 511fb2b..52868f4 100644 --- a/lib/core/AccessControlError.js +++ b/lib/core/AccessControlError.js @@ -21,9 +21,10 @@ var AccessControlError = /** @class */ (function (_super) { __extends(AccessControlError, _super); function AccessControlError(message) { if (message === void 0) { message = ''; } - var _this = _super.call(this, message) || this; + var _this = _super.call(this, message) /* istanbul ignore next */ || this; _this.message = message; _this.name = 'AccessControlError'; + // https://github.com/gotwarlost/istanbul/issues/690 // http://stackoverflow.com/a/41429145/112731 Object.setPrototypeOf(_this, AccessControlError.prototype); return _this; diff --git a/lib/utils.d.ts b/lib/utils.d.ts index fc1f23f..c3caada 100644 --- a/lib/utils.d.ts +++ b/lib/utils.d.ts @@ -37,7 +37,7 @@ declare const utils: { getRoleHierarchyOf(grants: any, roleName: string, rootRole?: string): string[]; getFlatRoles(grants: any, roles: string | string[]): string[]; getNonExistentRoles(grants: any, roles: string[]): string[]; - getCrossInheritedRole(grants: any, roleName: string, extenderRoles: string | string[]): string | boolean; + getCrossExtendingRole(grants: any, roleName: string, extenderRoles: string | string[]): string; extendRole(grants: any, roles: string | string[], extenderRoles: string | string[]): void; preCreateRoles(grants: any, roles: string | string[]): void; commitToGrants(grants: any, access: IAccessInfo, normalizeAll?: boolean): void; diff --git a/lib/utils.js b/lib/utils.js index 7fc2716..2a1b05b 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -50,19 +50,20 @@ var utils = { return o.hasOwnProperty(propName) && o[propName] !== undefined; }, /** - * Converts the given (string) value into an array of string. - * Note that this does not throw if the value is not a string or array. - * It will silently return `null`. + * Converts the given (string) value into an array of string. Note that + * this does not throw if the value is not a string or array. It will + * silently return `[]` (empty array). So where ever it's used, the host + * function should consider throwing. * @param {Any} value - * @returns {Boolean|null} + * @returns {string[]} */ toStringArray: function (value) { if (Array.isArray(value)) return value; if (typeof value === 'string') return value.trim().split(/\s*[;,]\s*/); - // throw new Error('Cannot convert value to array!'); - return null; + // throw new Error('Expected a string or array of strings, got ' + utils.type(value)); + return []; }, /** * Checks whether the given array consists of non-empty string items. @@ -298,14 +299,11 @@ var utils = { * is invalid. */ validRoleObject: function (grants, roleName) { - console.log('enter!'); var role = grants[roleName]; if (!role || utils.type(role) !== 'object') { - console.log('Invalid role:', roleName); throw new core_1.AccessControlError("Invalid role definition."); } utils.eachKey(role, function (resourceName) { - console.log(resourceName, utils.validName(resourceName, false)); if (!utils.validName(resourceName, false)) { if (resourceName === '$extend') { var extRoles = role[resourceName]; // semantics @@ -322,8 +320,8 @@ var utils = { throw new core_1.AccessControlError("Cannot use reserved name \"" + resourceName + "\" for a resource."); } } - else if (!utils.validResourceObject(role[resourceName])) { - // throw new AccessControlError(`Invalid resource definition ("${resourceName}") for role "${roleName}".`); + else { + utils.validResourceObject(role[resourceName]); // throws on failure } }); return true; @@ -347,9 +345,12 @@ var utils = { if (type === 'object') { utils.eachKey(o, function (roleName) { if (utils.validName(roleName)) { - return utils.validRoleObject(o, roleName); // will throw on failure + return utils.validRoleObject(o, roleName); // throws on failure } + /* istanbul ignore next */ return false; + // above is redundant, previous checks will already throw on + // failure so we'll never need to break early from this. }); grants = o; } @@ -393,7 +394,8 @@ var utils = { if (asString === void 0) { asString = false; } // validate and normalize action if (typeof info.action !== 'string') { - throw new core_1.AccessControlError("Invalid action: " + info.action); + // throw new AccessControlError(`Invalid action: ${info.action}`); + throw new core_1.AccessControlError("Invalid action: " + JSON.stringify(info)); } var s = info.action.split(':'); if (enums_1.actions.indexOf(s[0].trim().toLowerCase()) < 0) { @@ -466,12 +468,12 @@ var utils = { access = Object.assign({}, access); // validate and normalize role(s) access.role = utils.toStringArray(access.role); - if (!utils.isFilledStringArray(access.role)) { + if (access.role.length === 0 || !utils.isFilledStringArray(access.role)) { throw new core_1.AccessControlError("Invalid role(s): " + JSON.stringify(access.role)); } // validate and normalize resource access.resource = utils.toStringArray(access.resource); - if (!utils.isFilledStringArray(access.resource)) { + if (access.resource.length === 0 || !utils.isFilledStringArray(access.resource)) { throw new core_1.AccessControlError("Invalid resource(s): " + JSON.stringify(access.resource)); } // normalize attributes @@ -515,7 +517,7 @@ var utils = { getRoleHierarchyOf: function (grants, roleName, rootRole) { // `rootRole` is for memory storage. Do NOT set it when using; // and do NOT document this paramter. - rootRole = rootRole || roleName; + // rootRole = rootRole || roleName; var role = grants[roleName]; if (!role) throw new core_1.AccessControlError("Role not found: \"" + roleName + "\""); @@ -531,10 +533,10 @@ var utils = { } // throw if cross-inheritance and also avoid memory leak with // maximum call stack error - if (rootRole === exRoleName) { + if (rootRole && (rootRole === exRoleName)) { throw new core_1.AccessControlError("Cross inheritance is not allowed. Role \"" + exRoleName + "\" already extends \"" + rootRole + "\"."); } - var ext = utils.getRoleHierarchyOf(grants, exRoleName, rootRole); + var ext = utils.getRoleHierarchyOf(grants, exRoleName, rootRole || roleName); arr = utils.uniqConcat(arr, ext); }); return arr; @@ -543,11 +545,12 @@ var utils = { * Gets roles and extended roles in a flat array. */ getFlatRoles: function (grants, roles) { - roles = utils.toStringArray(roles); - if (!roles) + var arrRoles = utils.toStringArray(roles); + if (arrRoles.length === 0) { throw new core_1.AccessControlError("Invalid role(s): " + JSON.stringify(roles)); - var arr = utils.uniqConcat([], roles); // roles.concat(); - roles.forEach(function (roleName) { + } + var arr = utils.uniqConcat([], arrRoles); // roles.concat(); + arrRoles.forEach(function (roleName) { arr = utils.uniqConcat(arr, utils.getRoleHierarchyOf(grants, roleName)); }); // console.log(`flat roles for ${roles}`, arr); @@ -581,13 +584,14 @@ var utils = { * * @param {Any} grants - Grants model to be checked. * @param {string} roles - Target role to be checked. - * @param {string|string[]} extenderRoles - Extender role(s) to be - * checked. - * @returns {Boolean} + * @param {string|string[]} extenderRoles - Extender role(s) to be checked. + * + * @returns {string|null} - Returns the first cross extending role. `null` + * if none. */ - getCrossInheritedRole: function (grants, roleName, extenderRoles) { - var extenders = utils.toStringArray(extenderRoles); // [roleName].concat(); - var crossInherited = false; + getCrossExtendingRole: function (grants, roleName, extenderRoles) { + var extenders = utils.toStringArray(extenderRoles); + var crossInherited = null; utils.each(extenders, function (e) { if (crossInherited || roleName === e) { return false; // break out of loop @@ -623,43 +627,41 @@ var utils = { * a cross-inherited role. */ extendRole: function (grants, roles, extenderRoles) { + // roles cannot be omitted or an empty array + roles = utils.toStringArray(roles); + if (roles.length === 0) { + throw new core_1.AccessControlError("Invalid role(s): " + JSON.stringify(roles)); + } + // extenderRoles cannot be omitted or but can be an empty array + if (utils.isEmptyArray(extenderRoles)) + return; var arrExtRoles = utils.toStringArray(extenderRoles).concat(); - if (!arrExtRoles) + if (arrExtRoles.length === 0) { throw new core_1.AccessControlError("Cannot inherit invalid role(s): " + JSON.stringify(extenderRoles)); + } var nonExistentExtRoles = utils.getNonExistentRoles(grants, arrExtRoles); if (nonExistentExtRoles.length > 0) { throw new core_1.AccessControlError("Cannot inherit non-existent role(s): \"" + nonExistentExtRoles.join(', ') + "\""); } - roles = utils.toStringArray(roles); - if (!roles) - throw new core_1.AccessControlError("Invalid role(s): " + JSON.stringify(roles)); roles.forEach(function (roleName) { if (!grants[roleName]) throw new core_1.AccessControlError("Role not found: \"" + roleName + "\""); if (arrExtRoles.indexOf(roleName) >= 0) { throw new core_1.AccessControlError("Cannot extend role \"" + roleName + "\" by itself."); } - // getCrossInheritedRole() returns false or the first + // getCrossExtendingRole() returns false or the first // cross-inherited role, if found. - var crossInherited = utils.getCrossInheritedRole(grants, roleName, arrExtRoles); + var crossInherited = utils.getCrossExtendingRole(grants, roleName, arrExtRoles); if (crossInherited) { throw new core_1.AccessControlError("Cross inheritance is not allowed. Role \"" + crossInherited + "\" already extends \"" + roleName + "\"."); } - if (utils.validName(roleName)) { - if (!grants.hasOwnProperty(roleName)) { - grants[roleName] = { - $extend: arrExtRoles - }; - } - else { - var r = grants[roleName]; - if (Array.isArray(r.$extend)) { - r.$extend = utils.uniqConcat(r.$extend, arrExtRoles); - } - else { - r.$extend = arrExtRoles; - } - } + utils.validName(roleName); // throws if false + var r = grants[roleName]; + if (Array.isArray(r.$extend)) { + r.$extend = utils.uniqConcat(r.$extend, arrExtRoles); + } + else { + r.$extend = arrExtRoles; } }); }, @@ -728,31 +730,27 @@ var utils = { * @returns {string[]} - Array of union'ed attributes. */ getUnionAttrsOfRoles: function (grants, query) { - if (!grants) { - throw new core_1.AccessControlError('Grants are not set.'); - } // throws if has any invalid property value query = utils.normalizeQueryInfo(query); - var grantItem; + var role; var resource; var attrsList = []; // get roles and extended roles in a flat array var roles = utils.getFlatRoles(grants, query.role); // iterate through roles and add permission attributes (array) of // each role to attrsList (array). - roles.forEach(function (role, index) { - grantItem = grants[role]; - if (grantItem) { - resource = grantItem[query.resource]; - if (resource) { - // e.g. resource['create:own'] - // If action has possession "any", it will also return - // `granted=true` for "own", if "own" is not defined. - attrsList.push((resource[query.action + ':' + query.possession] - || resource[query.action + ':any'] - || []).concat()); - // console.log(resource, 'for:', action + '.' + possession); - } + roles.forEach(function (roleName, index) { + role = grants[roleName]; + // no need to check role existence #getFlatRoles() does that. + resource = role[query.resource]; + if (resource) { + // e.g. resource['create:own'] + // If action has possession "any", it will also return + // `granted=true` for "own", if "own" is not defined. + attrsList.push((resource[query.action + ':' + query.possession] + || resource[query.action + ':any'] + || []).concat()); + // console.log(resource, 'for:', action + '.' + possession); } }); // union all arrays of (permitted resource) attributes (for each role) @@ -776,16 +774,17 @@ var utils = { */ lockAC: function (ac) { var _ac = ac; // ts - if (!_ac._grants) { - throw new core_1.AccessControlError('Cannot lock due to invalid grants model.'); + if (!_ac._grants || Object.keys(_ac._grants).length === 0) { + throw new core_1.AccessControlError('Cannot lock empty or invalid grants model.'); } var locked = ac.isLocked && Object.isFrozen(_ac._grants); if (!locked) locked = Boolean(utils.deepFreeze(_ac._grants)); + /* istanbul ignore next */ if (!locked) { throw new core_1.AccessControlError("Could not lock grants: " + typeof _ac._grants); } - _ac._locked = locked; + _ac._isLocked = locked; }, // ---------------------- // NOTATION/GLOB UTILS