├── .gitignore ├── LICENSE ├── README.md ├── lib ├── rbac.js └── utils.js ├── package-lock.json ├── package.json └── test ├── async.promise.test.js ├── async.test.js ├── data └── index.js ├── sync.test.js └── utils └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git 27 | node_modules 28 | .idea 29 | .vscode -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Karl Düüna 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # easy-rbac 2 | 3 | Promise based HRBAC (Hierarchical Role Based Access Control) implementation for Node.js 4 | 5 | ## NB! Important changes with v3 6 | 7 | v3 is a rewrite of the library as such there are important changes: 8 | 9 | * Callbacks are no longer supported 10 | * Promise rejection will happen on error, otherwise boolean result will be in resolve handler 11 | * As of v3.2 Node >=v10.x is required 12 | 13 | ## Installation 14 | 15 | npm install easy-rbac 16 | 17 | ## Test 18 | 19 | npm test 20 | 21 | ## Initialization 22 | 23 | Require and create `rbac` object. 24 | 25 | const RBAC = require('easy-rbac'); 26 | const rbac = new RBAC(opts); 27 | 28 | Or use create function 29 | 30 | const rbac = require('easy-rbac').create(opts); 31 | 32 | ## Options 33 | 34 | Options for RBAC can be either an object, function returning a promise or a promise 35 | 36 | The expected configuration object example: 37 | 38 | { 39 | user: { // Role name 40 | can: [ // list of allowed operations 41 | 'account', 42 | 'post:add', 43 | { 44 | name: 'post:save', 45 | when: async (params) => params.userId === params.ownerId 46 | }, 47 | 'user:create', 48 | { 49 | name: 'user:*', 50 | when: async (params) => params.id === params.userId 51 | } 52 | ] 53 | }, 54 | manager: { 55 | can: ['post:save', 'post:delete', 'account:*'], 56 | inherits: ['user'] 57 | }, 58 | admin: { 59 | can: ['rule the server'], 60 | inherits: ['manager'] 61 | } 62 | } 63 | 64 | The `roles` property is required and must be an object. The keys of this object are counted to be the names of roles. 65 | 66 | Each role must have a `can` property, which is an array. Elements in the array can be strings or objects. 67 | 68 | If the element is a string then it is expected to be the name of the permitted operation. 69 | 70 | If the element is an object: 71 | 72 | * It must have the `name` and `when` properties 73 | * `name` property must be a string 74 | * `when` property must be a function that returns a promise 75 | 76 | ## Wildcards (v3.1+) 77 | 78 | Each name of operation can include `*` character as a wildcard match. It will match anything in its stead. So something like `account:*` will match everything starting with `account:`. 79 | 80 | Specific operations are always prioritized over wildcard operations. This means that if you have a definition like: 81 | 82 | { 83 | user: { 84 | can: [ 85 | 'user:create', 86 | { 87 | name: 'user:*', 88 | when: async (params) => params.id === params.userId 89 | } 90 | ] 91 | } 92 | } 93 | 94 | Then `user:create` will not run the provided when operation, whereas everything else starting with `user:` does 95 | 96 | ## Usage can(role, operation, params?) 97 | 98 | After initialization you can use the `can` function of the object to check if role should have access to an operation. 99 | 100 | The function will return a Promise that will resolve if the role can access the operation or reject if something goes wrong 101 | or the user is not allowed to access. 102 | 103 | rbac.can('user', 'post:add') 104 | .then(result => { 105 | if (result) { 106 | // we are allowed access 107 | } else { 108 | // we are not allowed access 109 | } 110 | }) 111 | .catch(err => { 112 | // something else went wrong - refer to err object 113 | }); 114 | 115 | The function accepts parameters as the third parameter, it will be used if there is a `when` type operation in the validation 116 | hierarchy. 117 | 118 | rbac.can('user', 'post:save', {userId: 1, ownerId: 2}) 119 | .then(result => { 120 | if (result) { 121 | // we are allowed access 122 | } else { 123 | // we are not allowed access 124 | } 125 | }) 126 | .catch(err => { 127 | // something else went wrong - refer to err object 128 | }); 129 | 130 | You can also validate multiple roles at the same time, by providing an array of roles. 131 | 132 | rbac.can(['user', 'manager'], 'post:save', {userId: 1, ownerId: 2}) 133 | .then(result => { 134 | if (result) { 135 | // we are allowed access 136 | } else { 137 | // we are not allowed access 138 | } 139 | }) 140 | .catch(err => { 141 | // something else went wrong - refer to err object 142 | }); 143 | 144 | 145 | If the options of the initialization is async then it will wait for the initialization to resolve before resolving 146 | any checks. 147 | 148 | const rbac = require('easy-rbac') 149 | .create(async () => opts); 150 | 151 | rbac.can('user', 'post:add') 152 | .then(result => { 153 | if (result) { 154 | // we are allowed access 155 | } else { 156 | // we are not allowed access 157 | } 158 | }) 159 | .catch(err => { 160 | // something else went wrong - refer to err object 161 | }); 162 | 163 | ## License 164 | 165 | The MIT License (MIT) 166 | Copyright (c) 2015 Karl Düüna 167 | 168 | Permission is hereby granted, free of charge, to any person obtaining a copy of 169 | this software and associated documentation files (the "Software"), to deal in 170 | the Software without restriction, including without limitation the rights to 171 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 172 | the Software, and to permit persons to whom the Software is furnished to do so, 173 | subject to the following conditions: 174 | 175 | The above copyright notice and this permission notice shall be included in all 176 | copies or substantial portions of the Software. 177 | 178 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 179 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 180 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 181 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 182 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 183 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 184 | SOFTWARE. 185 | -------------------------------------------------------------------------------- /lib/rbac.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('rbac'); 4 | const {any, globToRegex, isGlob} = require('./utils'); 5 | 6 | class RBAC { 7 | constructor(roles) { 8 | this._inited = false; 9 | if(typeof roles !== 'function' && typeof roles.then !== 'function') { 10 | debug('sync init'); 11 | // Add roles to class and mark as inited 12 | this.roles = this._parseRoleMap(roles); 13 | this._inited = true; 14 | } else { 15 | debug('async init'); 16 | this._init = this.asyncInit(roles); 17 | } 18 | } 19 | 20 | _parseRoleMap(roles) { 21 | debug('parsing rolemap'); 22 | // If not a function then should be object 23 | if(typeof roles !== 'object') { 24 | throw new TypeError('Expected input to be object'); 25 | } 26 | 27 | let map = new Map(); 28 | 29 | // Standardize roles 30 | Object.keys(roles).forEach(role => { 31 | let roleObj = { 32 | can: {}, 33 | canGlob: [] 34 | }; 35 | // Check can definition 36 | if(!Array.isArray(roles[role].can)) { 37 | throw new TypeError('Expected roles[' + role + '].can to be an array'); 38 | } 39 | if(roles[role].inherits) { 40 | if(!Array.isArray(roles[role].inherits)) { 41 | throw new TypeError('Expected roles[' + role + '].inherits to be an array'); 42 | } 43 | roleObj.inherits = []; 44 | roles[role].inherits.forEach(child => { 45 | if(typeof child !== 'string') { 46 | throw new TypeError('Expected roles[' + role + '].inherits element'); 47 | } 48 | if(!roles[child]) { 49 | throw new TypeError('Undefined inheritance role: ' + child); 50 | } 51 | roleObj.inherits.push(child); 52 | }); 53 | } 54 | // Iterate allowed operations 55 | roles[role].can.forEach(operation => { 56 | // If operation is string 57 | if(typeof operation === 'string') { 58 | // Add as an operation 59 | if(!isGlob(operation)) { 60 | roleObj.can[operation] = 1; 61 | } else { 62 | roleObj.canGlob.push({name: globToRegex(operation), original: operation}); 63 | } 64 | return; 65 | } 66 | // Check if operation has a .when function 67 | if(typeof operation.when === 'function' && typeof operation.name === 'string') { 68 | if(!isGlob(operation.name)) { 69 | roleObj.can[operation.name] = operation.when; 70 | } else { 71 | roleObj.canGlob.push({name: globToRegex(operation.name), original: operation.name, when: operation.when}); 72 | } 73 | return; 74 | } 75 | throw new TypeError('Unexpected operation type', operation); 76 | }); 77 | 78 | map.set(role, roleObj); 79 | }); 80 | 81 | return map; 82 | } 83 | 84 | async asyncInit(roles) { 85 | // If opts is a function execute for async loading 86 | if(typeof roles === 'function') { 87 | roles = await roles(); 88 | } 89 | if(typeof roles.then === 'function') { 90 | roles = await roles; 91 | } 92 | 93 | // Add roles to class and mark as inited 94 | this.roles = this._parseRoleMap(roles); 95 | this._inited = true; 96 | } 97 | async can(role, operation, params, cb) { 98 | 99 | if (typeof cb === 'function') { 100 | throw new Error('v3 does not support callbacks, you might try v2'); 101 | } 102 | // If not inited then wait until init finishes 103 | if (!this._inited) { 104 | debug('Not inited, wait'); 105 | await this._init; 106 | debug('Init complete, continue'); 107 | } 108 | 109 | if (Array.isArray(role)) { 110 | debug('array of roles, try all'); 111 | return any(role.map(r => this.can(r, operation, params))); 112 | } 113 | 114 | if (typeof role !== 'string') { 115 | debug('Expected first parameter to be string : role'); 116 | return false; 117 | } 118 | 119 | if (typeof operation !== 'string') { 120 | debug('Expected second parameter to be string : operation'); 121 | return false 122 | } 123 | 124 | const $role = this.roles.get(role); 125 | 126 | if (!$role) { 127 | debug('Undefined role'); 128 | return false; 129 | } 130 | 131 | // IF this operation is not defined at current level try higher 132 | if (!$role.can[operation] && !$role.canGlob.find(glob => glob.name.test(operation))) { 133 | debug('Not allowed at this level, try higher'); 134 | // If no parents reject 135 | if (!$role.inherits || $role.inherits.length < 1) { 136 | debug('No inherit, reject false'); 137 | return false; 138 | } 139 | // Return if any parent resolves true or all reject 140 | return any($role.inherits.map(parent => { 141 | debug('Try from ' + parent); 142 | return this.can(parent, operation, params); 143 | })); 144 | } 145 | 146 | // We have the operation resolve 147 | if ($role.can[operation] === 1) { 148 | debug('We have a match, resolve'); 149 | return true; 150 | } 151 | 152 | // Operation is conditional, run async function 153 | if (typeof $role.can[operation] === 'function') { 154 | debug('Operation is conditional, run fn'); 155 | try { 156 | return $role.can[operation](params); 157 | } catch (e) { 158 | debug('conditional function threw', e); 159 | return false; 160 | } 161 | } 162 | 163 | // Try globs 164 | let globMatch = $role.canGlob.find(glob => glob.name.test(operation)); 165 | if(globMatch && !globMatch.when) { 166 | debug(`We have a globmatch (${globMatch.original}), resolve`); 167 | return true; 168 | } 169 | 170 | if(globMatch && globMatch.when) { 171 | debug(`We have a conditional globmatch (${globMatch.original}), run fn`); 172 | try { 173 | return globMatch.when(params); 174 | } catch (e) { 175 | debug('conditional function threw', e); 176 | return false; 177 | } 178 | } 179 | 180 | // No operation reject as false 181 | debug('Shouldnt have reached here, something wrong, reject'); 182 | throw new Error('something went wrong'); 183 | } 184 | } 185 | 186 | RBAC.create = function create(opts) { 187 | return new RBAC(opts); 188 | }; 189 | 190 | module.exports = RBAC; -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('rbac'); 4 | module.exports.any = any; 5 | module.exports.isGlob = isGlob; 6 | module.exports.globToRegex = globToRegex; 7 | 8 | /**********************/ 9 | 10 | function any(promises) { 11 | if(promises.length < 1) { 12 | return Promise.resolve(false); 13 | } 14 | return Promise.all( 15 | promises.map($p => 16 | $p 17 | .catch(err => { 18 | debug('Underlying promise rejected', err); 19 | return false; 20 | }) 21 | .then(result => { 22 | if(result) { 23 | throw new Error('authorized'); 24 | } 25 | }) 26 | ) 27 | ) 28 | .then(() => false) 29 | .catch(err => err && err.message === 'authorized'); 30 | } 31 | 32 | function isGlob(string) { 33 | return string.includes('*'); 34 | } 35 | 36 | function globToRegex(string) { 37 | return new RegExp('^' + string.replace(/\*/g, '.*')); 38 | } -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "easy-rbac", 3 | "version": "3.2.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@ungap/promise-all-settled": { 8 | "version": "1.1.2", 9 | "resolved": "https://registry.npmjs.org/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz", 10 | "integrity": "sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==", 11 | "dev": true 12 | }, 13 | "ansi-colors": { 14 | "version": "4.1.1", 15 | "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", 16 | "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", 17 | "dev": true 18 | }, 19 | "ansi-regex": { 20 | "version": "5.0.1", 21 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 22 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 23 | "dev": true 24 | }, 25 | "ansi-styles": { 26 | "version": "4.3.0", 27 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 28 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 29 | "dev": true, 30 | "requires": { 31 | "color-convert": "^2.0.1" 32 | } 33 | }, 34 | "anymatch": { 35 | "version": "3.1.2", 36 | "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", 37 | "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", 38 | "dev": true, 39 | "requires": { 40 | "normalize-path": "^3.0.0", 41 | "picomatch": "^2.0.4" 42 | } 43 | }, 44 | "argparse": { 45 | "version": "2.0.1", 46 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 47 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 48 | "dev": true 49 | }, 50 | "balanced-match": { 51 | "version": "1.0.2", 52 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 53 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 54 | "dev": true 55 | }, 56 | "binary-extensions": { 57 | "version": "2.2.0", 58 | "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", 59 | "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", 60 | "dev": true 61 | }, 62 | "brace-expansion": { 63 | "version": "1.1.11", 64 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 65 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 66 | "dev": true, 67 | "requires": { 68 | "balanced-match": "^1.0.0", 69 | "concat-map": "0.0.1" 70 | } 71 | }, 72 | "braces": { 73 | "version": "3.0.2", 74 | "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", 75 | "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", 76 | "dev": true, 77 | "requires": { 78 | "fill-range": "^7.0.1" 79 | } 80 | }, 81 | "browser-stdout": { 82 | "version": "1.3.1", 83 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 84 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 85 | "dev": true 86 | }, 87 | "camelcase": { 88 | "version": "6.2.1", 89 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.1.tgz", 90 | "integrity": "sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA==", 91 | "dev": true 92 | }, 93 | "chalk": { 94 | "version": "4.1.2", 95 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 96 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 97 | "dev": true, 98 | "requires": { 99 | "ansi-styles": "^4.1.0", 100 | "supports-color": "^7.1.0" 101 | }, 102 | "dependencies": { 103 | "supports-color": { 104 | "version": "7.2.0", 105 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 106 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 107 | "dev": true, 108 | "requires": { 109 | "has-flag": "^4.0.0" 110 | } 111 | } 112 | } 113 | }, 114 | "chokidar": { 115 | "version": "3.5.2", 116 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.2.tgz", 117 | "integrity": "sha512-ekGhOnNVPgT77r4K/U3GDhu+FQ2S8TnK/s2KbIGXi0SZWuwkZ2QNyfWdZW+TVfn84DpEP7rLeCt2UI6bJ8GwbQ==", 118 | "dev": true, 119 | "requires": { 120 | "anymatch": "~3.1.2", 121 | "braces": "~3.0.2", 122 | "fsevents": "~2.3.2", 123 | "glob-parent": "~5.1.2", 124 | "is-binary-path": "~2.1.0", 125 | "is-glob": "~4.0.1", 126 | "normalize-path": "~3.0.0", 127 | "readdirp": "~3.6.0" 128 | } 129 | }, 130 | "cliui": { 131 | "version": "7.0.4", 132 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", 133 | "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", 134 | "dev": true, 135 | "requires": { 136 | "string-width": "^4.2.0", 137 | "strip-ansi": "^6.0.0", 138 | "wrap-ansi": "^7.0.0" 139 | } 140 | }, 141 | "color-convert": { 142 | "version": "2.0.1", 143 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 144 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 145 | "dev": true, 146 | "requires": { 147 | "color-name": "~1.1.4" 148 | } 149 | }, 150 | "color-name": { 151 | "version": "1.1.4", 152 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 153 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 154 | "dev": true 155 | }, 156 | "concat-map": { 157 | "version": "0.0.1", 158 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 159 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 160 | "dev": true 161 | }, 162 | "debug": { 163 | "version": "4.3.3", 164 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.3.tgz", 165 | "integrity": "sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==", 166 | "requires": { 167 | "ms": "2.1.2" 168 | } 169 | }, 170 | "decamelize": { 171 | "version": "4.0.0", 172 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", 173 | "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", 174 | "dev": true 175 | }, 176 | "diff": { 177 | "version": "5.0.0", 178 | "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", 179 | "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", 180 | "dev": true 181 | }, 182 | "emoji-regex": { 183 | "version": "8.0.0", 184 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 185 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 186 | "dev": true 187 | }, 188 | "escalade": { 189 | "version": "3.1.1", 190 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", 191 | "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", 192 | "dev": true 193 | }, 194 | "escape-string-regexp": { 195 | "version": "4.0.0", 196 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 197 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 198 | "dev": true 199 | }, 200 | "fill-range": { 201 | "version": "7.0.1", 202 | "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", 203 | "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", 204 | "dev": true, 205 | "requires": { 206 | "to-regex-range": "^5.0.1" 207 | } 208 | }, 209 | "find-up": { 210 | "version": "5.0.0", 211 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 212 | "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 213 | "dev": true, 214 | "requires": { 215 | "locate-path": "^6.0.0", 216 | "path-exists": "^4.0.0" 217 | } 218 | }, 219 | "flat": { 220 | "version": "5.0.2", 221 | "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", 222 | "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", 223 | "dev": true 224 | }, 225 | "fs.realpath": { 226 | "version": "1.0.0", 227 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 228 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 229 | "dev": true 230 | }, 231 | "fsevents": { 232 | "version": "2.3.2", 233 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", 234 | "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", 235 | "dev": true, 236 | "optional": true 237 | }, 238 | "get-caller-file": { 239 | "version": "2.0.5", 240 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 241 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 242 | "dev": true 243 | }, 244 | "glob": { 245 | "version": "7.1.7", 246 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", 247 | "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", 248 | "dev": true, 249 | "requires": { 250 | "fs.realpath": "^1.0.0", 251 | "inflight": "^1.0.4", 252 | "inherits": "2", 253 | "minimatch": "^3.0.4", 254 | "once": "^1.3.0", 255 | "path-is-absolute": "^1.0.0" 256 | } 257 | }, 258 | "glob-parent": { 259 | "version": "5.1.2", 260 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", 261 | "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", 262 | "dev": true, 263 | "requires": { 264 | "is-glob": "^4.0.1" 265 | } 266 | }, 267 | "growl": { 268 | "version": "1.10.5", 269 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", 270 | "integrity": "sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==", 271 | "dev": true 272 | }, 273 | "has-flag": { 274 | "version": "4.0.0", 275 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 276 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 277 | "dev": true 278 | }, 279 | "he": { 280 | "version": "1.2.0", 281 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 282 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", 283 | "dev": true 284 | }, 285 | "inflight": { 286 | "version": "1.0.6", 287 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 288 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 289 | "dev": true, 290 | "requires": { 291 | "once": "^1.3.0", 292 | "wrappy": "1" 293 | } 294 | }, 295 | "inherits": { 296 | "version": "2.0.4", 297 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 298 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 299 | "dev": true 300 | }, 301 | "is-binary-path": { 302 | "version": "2.1.0", 303 | "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", 304 | "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", 305 | "dev": true, 306 | "requires": { 307 | "binary-extensions": "^2.0.0" 308 | } 309 | }, 310 | "is-extglob": { 311 | "version": "2.1.1", 312 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 313 | "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", 314 | "dev": true 315 | }, 316 | "is-fullwidth-code-point": { 317 | "version": "3.0.0", 318 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 319 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 320 | "dev": true 321 | }, 322 | "is-glob": { 323 | "version": "4.0.3", 324 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 325 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 326 | "dev": true, 327 | "requires": { 328 | "is-extglob": "^2.1.1" 329 | } 330 | }, 331 | "is-number": { 332 | "version": "7.0.0", 333 | "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", 334 | "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", 335 | "dev": true 336 | }, 337 | "is-plain-obj": { 338 | "version": "2.1.0", 339 | "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", 340 | "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", 341 | "dev": true 342 | }, 343 | "is-unicode-supported": { 344 | "version": "0.1.0", 345 | "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", 346 | "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", 347 | "dev": true 348 | }, 349 | "isexe": { 350 | "version": "2.0.0", 351 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 352 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", 353 | "dev": true 354 | }, 355 | "js-yaml": { 356 | "version": "4.1.0", 357 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 358 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 359 | "dev": true, 360 | "requires": { 361 | "argparse": "^2.0.1" 362 | } 363 | }, 364 | "locate-path": { 365 | "version": "6.0.0", 366 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 367 | "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 368 | "dev": true, 369 | "requires": { 370 | "p-locate": "^5.0.0" 371 | } 372 | }, 373 | "log-symbols": { 374 | "version": "4.1.0", 375 | "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", 376 | "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", 377 | "dev": true, 378 | "requires": { 379 | "chalk": "^4.1.0", 380 | "is-unicode-supported": "^0.1.0" 381 | } 382 | }, 383 | "minimatch": { 384 | "version": "3.0.4", 385 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 386 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 387 | "dev": true, 388 | "requires": { 389 | "brace-expansion": "^1.1.7" 390 | } 391 | }, 392 | "mocha": { 393 | "version": "9.1.3", 394 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-9.1.3.tgz", 395 | "integrity": "sha512-Xcpl9FqXOAYqI3j79pEtHBBnQgVXIhpULjGQa7DVb0Po+VzmSIK9kanAiWLHoRR/dbZ2qpdPshuXr8l1VaHCzw==", 396 | "dev": true, 397 | "requires": { 398 | "@ungap/promise-all-settled": "1.1.2", 399 | "ansi-colors": "4.1.1", 400 | "browser-stdout": "1.3.1", 401 | "chokidar": "3.5.2", 402 | "debug": "4.3.2", 403 | "diff": "5.0.0", 404 | "escape-string-regexp": "4.0.0", 405 | "find-up": "5.0.0", 406 | "glob": "7.1.7", 407 | "growl": "1.10.5", 408 | "he": "1.2.0", 409 | "js-yaml": "4.1.0", 410 | "log-symbols": "4.1.0", 411 | "minimatch": "3.0.4", 412 | "ms": "2.1.3", 413 | "nanoid": "3.1.25", 414 | "serialize-javascript": "6.0.0", 415 | "strip-json-comments": "3.1.1", 416 | "supports-color": "8.1.1", 417 | "which": "2.0.2", 418 | "workerpool": "6.1.5", 419 | "yargs": "16.2.0", 420 | "yargs-parser": "20.2.4", 421 | "yargs-unparser": "2.0.0" 422 | }, 423 | "dependencies": { 424 | "debug": { 425 | "version": "4.3.2", 426 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", 427 | "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", 428 | "dev": true, 429 | "requires": { 430 | "ms": "2.1.2" 431 | }, 432 | "dependencies": { 433 | "ms": { 434 | "version": "2.1.2", 435 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 436 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", 437 | "dev": true 438 | } 439 | } 440 | }, 441 | "ms": { 442 | "version": "2.1.3", 443 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 444 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 445 | "dev": true 446 | } 447 | } 448 | }, 449 | "ms": { 450 | "version": "2.1.2", 451 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", 452 | "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" 453 | }, 454 | "nanoid": { 455 | "version": "3.1.25", 456 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz", 457 | "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==", 458 | "dev": true 459 | }, 460 | "normalize-path": { 461 | "version": "3.0.0", 462 | "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", 463 | "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", 464 | "dev": true 465 | }, 466 | "once": { 467 | "version": "1.4.0", 468 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 469 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 470 | "dev": true, 471 | "requires": { 472 | "wrappy": "1" 473 | } 474 | }, 475 | "p-limit": { 476 | "version": "3.1.0", 477 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 478 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 479 | "dev": true, 480 | "requires": { 481 | "yocto-queue": "^0.1.0" 482 | } 483 | }, 484 | "p-locate": { 485 | "version": "5.0.0", 486 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 487 | "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 488 | "dev": true, 489 | "requires": { 490 | "p-limit": "^3.0.2" 491 | } 492 | }, 493 | "path-exists": { 494 | "version": "4.0.0", 495 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 496 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 497 | "dev": true 498 | }, 499 | "path-is-absolute": { 500 | "version": "1.0.1", 501 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 502 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 503 | "dev": true 504 | }, 505 | "picomatch": { 506 | "version": "2.3.0", 507 | "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", 508 | "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", 509 | "dev": true 510 | }, 511 | "randombytes": { 512 | "version": "2.1.0", 513 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 514 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 515 | "dev": true, 516 | "requires": { 517 | "safe-buffer": "^5.1.0" 518 | } 519 | }, 520 | "readdirp": { 521 | "version": "3.6.0", 522 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", 523 | "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", 524 | "dev": true, 525 | "requires": { 526 | "picomatch": "^2.2.1" 527 | } 528 | }, 529 | "require-directory": { 530 | "version": "2.1.1", 531 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 532 | "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", 533 | "dev": true 534 | }, 535 | "safe-buffer": { 536 | "version": "5.2.1", 537 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 538 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 539 | "dev": true 540 | }, 541 | "serialize-javascript": { 542 | "version": "6.0.0", 543 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", 544 | "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", 545 | "dev": true, 546 | "requires": { 547 | "randombytes": "^2.1.0" 548 | } 549 | }, 550 | "string-width": { 551 | "version": "4.2.3", 552 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 553 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 554 | "dev": true, 555 | "requires": { 556 | "emoji-regex": "^8.0.0", 557 | "is-fullwidth-code-point": "^3.0.0", 558 | "strip-ansi": "^6.0.1" 559 | } 560 | }, 561 | "strip-ansi": { 562 | "version": "6.0.1", 563 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 564 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 565 | "dev": true, 566 | "requires": { 567 | "ansi-regex": "^5.0.1" 568 | } 569 | }, 570 | "strip-json-comments": { 571 | "version": "3.1.1", 572 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 573 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 574 | "dev": true 575 | }, 576 | "supports-color": { 577 | "version": "8.1.1", 578 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", 579 | "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", 580 | "dev": true, 581 | "requires": { 582 | "has-flag": "^4.0.0" 583 | } 584 | }, 585 | "to-regex-range": { 586 | "version": "5.0.1", 587 | "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", 588 | "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", 589 | "dev": true, 590 | "requires": { 591 | "is-number": "^7.0.0" 592 | } 593 | }, 594 | "which": { 595 | "version": "2.0.2", 596 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 597 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 598 | "dev": true, 599 | "requires": { 600 | "isexe": "^2.0.0" 601 | } 602 | }, 603 | "workerpool": { 604 | "version": "6.1.5", 605 | "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.1.5.tgz", 606 | "integrity": "sha512-XdKkCK0Zqc6w3iTxLckiuJ81tiD/o5rBE/m+nXpRCB+/Sq4DqkfXZ/x0jW02DG1tGsfUGXbTJyZDP+eu67haSw==", 607 | "dev": true 608 | }, 609 | "wrap-ansi": { 610 | "version": "7.0.0", 611 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 612 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 613 | "dev": true, 614 | "requires": { 615 | "ansi-styles": "^4.0.0", 616 | "string-width": "^4.1.0", 617 | "strip-ansi": "^6.0.0" 618 | } 619 | }, 620 | "wrappy": { 621 | "version": "1.0.2", 622 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 623 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 624 | "dev": true 625 | }, 626 | "y18n": { 627 | "version": "5.0.8", 628 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 629 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 630 | "dev": true 631 | }, 632 | "yargs": { 633 | "version": "16.2.0", 634 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", 635 | "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", 636 | "dev": true, 637 | "requires": { 638 | "cliui": "^7.0.2", 639 | "escalade": "^3.1.1", 640 | "get-caller-file": "^2.0.5", 641 | "require-directory": "^2.1.1", 642 | "string-width": "^4.2.0", 643 | "y18n": "^5.0.5", 644 | "yargs-parser": "^20.2.2" 645 | } 646 | }, 647 | "yargs-parser": { 648 | "version": "20.2.4", 649 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", 650 | "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", 651 | "dev": true 652 | }, 653 | "yargs-unparser": { 654 | "version": "2.0.0", 655 | "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", 656 | "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", 657 | "dev": true, 658 | "requires": { 659 | "camelcase": "^6.0.0", 660 | "decamelize": "^4.0.0", 661 | "flat": "^5.0.2", 662 | "is-plain-obj": "^2.1.0" 663 | } 664 | }, 665 | "yocto-queue": { 666 | "version": "0.1.0", 667 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 668 | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 669 | "dev": true 670 | } 671 | } 672 | } 673 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "easy-rbac", 3 | "version": "3.2.0", 4 | "description": "RBAC implementation for Node.js", 5 | "main": "lib/rbac.js", 6 | "scripts": { 7 | "test": "mocha test/*" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/DeadAlready/easy-rbac.git" 12 | }, 13 | "keywords": [ 14 | "rbac" 15 | ], 16 | "author": "Karl Düüna", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/DeadAlready/easy-rbac/issues" 20 | }, 21 | "homepage": "https://github.com/DeadAlready/easy-rbac#readme", 22 | "dependencies": { 23 | "debug": "^4.3.3" 24 | }, 25 | "devDependencies": { 26 | "mocha": "^9.1.3" 27 | }, 28 | "engines": { 29 | "node": ">=10.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/async.promise.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const RBAC = require('../lib/rbac'); 5 | let data = require('./data'); 6 | 7 | const {shouldBeAllowed, shouldNotBeAllowed, catchError} = require('./utils'); 8 | 9 | describe('RBAC async', function() { 10 | it('should reject if function throws', function (done) { 11 | (new RBAC(Promise.reject(new Error()))) 12 | ._init 13 | .then(function () { 14 | done(new Error('Should not succeed')); 15 | }) 16 | .catch(function () { 17 | done(); 18 | }); 19 | }); 20 | 21 | it('should reject if function returns non object', function (done) { 22 | (new RBAC(Promise.resolve(1))) 23 | ._init 24 | .then(function () { 25 | done(new Error('Should not succeed')); 26 | }) 27 | .catch(function () { 28 | done(); 29 | }); 30 | }); 31 | 32 | it('should reject if roles[$i].inherits is not an array', function (done) { 33 | (new RBAC(Promise.resolve({ 34 | hello: { 35 | can: ['hel'], 36 | inherits: 1 37 | } 38 | }))) 39 | ._init 40 | .then(function () { 41 | done(new Error('Should not succeed')); 42 | }) 43 | .catch(function () { 44 | done(); 45 | }); 46 | }); 47 | 48 | it('should reject if roles[$i].inherits[$i2] is not a string', function (done) { 49 | (new RBAC(Promise.resolve({ 50 | hello: { 51 | can: ['hel'], 52 | inherits: [1] 53 | } 54 | }))) 55 | ._init 56 | .then(function () { 57 | done(new Error('Should not succeed')); 58 | }) 59 | .catch(function () { 60 | done(); 61 | }); 62 | }); 63 | 64 | it('should reject if roles[$i].inherits[$i2] is not a defined role', function (done) { 65 | (new RBAC(Promise.resolve({ 66 | hello: { 67 | can: ['hel'], 68 | inherits: ['what'] 69 | } 70 | }))) 71 | ._init 72 | .then(function () { 73 | done(new Error('Should not succeed')); 74 | }) 75 | .catch(function () { 76 | done(); 77 | }); 78 | }); 79 | 80 | it('should resolve if function returns correct object', function (done) { 81 | (new RBAC(Promise.resolve(data.all))) 82 | ._init 83 | .then(function () { 84 | done(); 85 | }) 86 | .catch(function () { 87 | done(new Error('Should not reject')); 88 | }); 89 | }); 90 | 91 | describe('resolve current role operations', function () { 92 | it('should respect operations', function (done) { 93 | (new RBAC(Promise.resolve(data.all))) 94 | .can('user', 'post:add') 95 | .catch(catchError(done)) 96 | .then(shouldBeAllowed(done)); 97 | }); 98 | it('should reject undefined operations', function (done) { 99 | (new RBAC(Promise.resolve(data.all))) 100 | .can('user', 'post:what') 101 | .catch(catchError(done)) 102 | .then(shouldNotBeAllowed(done)); 103 | }); 104 | it('should reject undefined users', function (done) { 105 | (new RBAC(Promise.resolve(data.all))) 106 | .can('what', 'post:add') 107 | .catch(catchError(done)) 108 | .then(shouldNotBeAllowed(done)); 109 | }); 110 | 111 | it('should reject function operations with no operands', function (done) { 112 | (new RBAC(Promise.resolve(data.all))) 113 | .can('user', 'post:save') 114 | .then(shouldNotBeAllowed(done)) 115 | .catch(err => done()); 116 | }); 117 | 118 | it('should reject function operations with rejectable values', function (done) { 119 | (new RBAC(Promise.resolve(data.all))) 120 | .can('user', 'post:save', {ownerId: 1, postId: 2}) 121 | .catch(catchError(done)) 122 | .then(shouldNotBeAllowed(done)); 123 | }); 124 | 125 | it('should allow function operations with correct values', function (done) { 126 | (new RBAC(Promise.resolve(data.all))) 127 | .can('user', 'post:save', {ownerId: 1, postId: 1}) 128 | .catch(catchError(done)) 129 | .then(shouldBeAllowed(done)); 130 | }); 131 | 132 | it('should reject conditional glob operations with no params', done => { 133 | (new RBAC(Promise.resolve(data.all))) 134 | .can('user', 'user:save') 135 | .catch(catchError(done)) 136 | .then(shouldNotBeAllowed(done)); 137 | }); 138 | 139 | it('should reject conditional glob operations with wrong params', done => { 140 | (new RBAC(Promise.resolve(data.all))) 141 | .can('user', 'user:save', {userId: 1, id: 2}) 142 | .catch(catchError(done)) 143 | .then(shouldNotBeAllowed(done)); 144 | }); 145 | 146 | it('should allow glob operations with correct params', done => { 147 | (new RBAC(Promise.resolve(data.all))) 148 | .can('user', 'user:save', {userId: 1, id: 1}) 149 | .catch(catchError(done)) 150 | .then(shouldBeAllowed(done)); 151 | }); 152 | 153 | it('should prioritize non glob operations', done => { 154 | (new RBAC(Promise.resolve(data.all))) 155 | .can('user', 'user:create') 156 | .catch(catchError(done)) 157 | .then(shouldBeAllowed(done)); 158 | }); 159 | }); 160 | 161 | describe('parent role operations', function () { 162 | it('should respect allowed operations', function (done) { 163 | (new RBAC(Promise.resolve(data.all))) 164 | .can('manager', 'account:add') 165 | .catch(catchError(done)) 166 | .then(shouldBeAllowed(done)); 167 | }); 168 | it('should reject undefined operations', function (done) { 169 | (new RBAC(Promise.resolve(data.all))) 170 | .can('manager', 'post:what') 171 | .catch(catchError(done)) 172 | .then(shouldNotBeAllowed(done)); 173 | }); 174 | 175 | it('should respect allowed glob operations', done => { 176 | (new RBAC(Promise.resolve(data.all))) 177 | .can('manager', 'account:whatever') 178 | .catch(catchError(done)) 179 | .then(shouldBeAllowed(done)); 180 | }); 181 | it('should handle overwritten glob operations', done => { 182 | (new RBAC(Promise.resolve(data.all))) 183 | .can('manager', 'user:save', {regionId: 1, userRegionId: 1}) 184 | .catch(catchError(done)) 185 | .then(shouldBeAllowed(done)); 186 | }); 187 | }); 188 | describe('parents parent role operations', function () { 189 | it('should respect allowed operations', function (done) { 190 | (new RBAC(Promise.resolve(data.all))) 191 | .can('admin', 'account:add') 192 | .catch(catchError(done)) 193 | .then(shouldBeAllowed(done)); 194 | }); 195 | it('should reject undefined operations', function (done) { 196 | (new RBAC(Promise.resolve(data.all))) 197 | .can('admin', 'post:what') 198 | .catch(catchError(done)) 199 | .then(shouldNotBeAllowed(done)); 200 | }); 201 | 202 | it('should respect glob operations', done => { 203 | (new RBAC(Promise.resolve(data.all))) 204 | .can('admin', 'user:what') 205 | .catch(catchError(done)) 206 | .then(shouldBeAllowed(done)); 207 | }); 208 | }); 209 | 210 | describe('parent role operations with callback', function () { 211 | it('should respect allowed operations', function (done) { 212 | (new RBAC(Promise.resolve(data.all))) 213 | .can('manager', 'post:create', {postId: 1, ownerId: 1}) 214 | .catch(catchError(done)) 215 | .then(shouldBeAllowed(done)); 216 | }); 217 | it('should reject not allowed operation', function (done) { 218 | (new RBAC(Promise.resolve(data.all))) 219 | .can('manager', 'post:create', {postId: 1, ownerId: 2}) 220 | .catch(catchError(done)) 221 | .then(shouldNotBeAllowed(done)); 222 | }); 223 | }); 224 | 225 | describe('array of roles', () => { 226 | it('should not allow if empty array of roles', done => { 227 | (new RBAC(Promise.resolve(data.all))) 228 | .can([], 'post:what') 229 | .catch(catchError(done)) 230 | .then(shouldNotBeAllowed(done)); 231 | }); 232 | it('should not allow if none of the roles is allowed', done => { 233 | (new RBAC(Promise.resolve(data.all))) 234 | .can(['user', 'manager'], 'rule the world') 235 | .catch(catchError(done)) 236 | .then(shouldNotBeAllowed(done)); 237 | }); 238 | it('should allow if one of the roles is allowed', done => { 239 | (new RBAC(Promise.resolve(data.all))) 240 | .can(['user', 'admin'], 'post:delete') 241 | .catch(catchError(done)) 242 | .then(shouldBeAllowed(done)); 243 | }); 244 | it('should allow if one of the roles is allowed', done => { 245 | (new RBAC(Promise.resolve(data.all))) 246 | .can(['user', 'admin'], 'post:rename', {ownerId: 1, postId: 1}) 247 | .catch(catchError(done)) 248 | .then(shouldBeAllowed(done)); 249 | }); 250 | it('should allow if one of the roles is allowed', done => { 251 | (new RBAC(Promise.resolve(data.all))) 252 | .can(['user', 'admin'], 'post:rename', {ownerId: 1, postId: 2}) 253 | .catch(catchError(done)) 254 | .then(shouldNotBeAllowed(done)); 255 | }); 256 | 257 | it('should allow if one of the roles is allowed through glob', done => { 258 | (new RBAC(Promise.resolve(data.all))) 259 | .can(['user', 'manager'], 'user:save', {regionId: 1, userRegionId: 1}) 260 | .catch(catchError(done)) 261 | .then(shouldBeAllowed(done)); 262 | }); 263 | }); 264 | }); -------------------------------------------------------------------------------- /test/async.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | const RBAC = require('../lib/rbac'); 5 | let data = require('./data'); 6 | 7 | const {shouldBeAllowed, shouldNotBeAllowed, catchError} = require('./utils'); 8 | 9 | describe('RBAC async', function() { 10 | it('should reject if function throws', function (done) { 11 | (new RBAC(async () => { 12 | throw new Error(); 13 | }))._init 14 | .then(function () { 15 | done(new Error('Should not succeed')); 16 | }) 17 | .catch(function () { 18 | done(); 19 | }); 20 | }); 21 | 22 | it('should reject if function returns non object', function (done) { 23 | (new RBAC(async () => 1)) 24 | ._init 25 | .then(function () { 26 | done(new Error('Should not succeed')); 27 | }) 28 | .catch(function () { 29 | done(); 30 | }); 31 | }); 32 | 33 | 34 | 35 | it('should reject if roles[$i].inherits is not an array', function (done) { 36 | (new RBAC(async () => ({ 37 | hello: { 38 | can: ['hel'], 39 | inherits: 1 40 | } 41 | }))) 42 | ._init 43 | .then(function () { 44 | done(new Error('Should not succeed')); 45 | }) 46 | .catch(function () { 47 | done(); 48 | }); 49 | }); 50 | 51 | it('should reject if roles[$i].inherits[$i2] is not a string', function (done) { 52 | (new RBAC(async () => ({ 53 | hello: { 54 | can: ['hel'], 55 | inherits: [1] 56 | } 57 | }))) 58 | ._init 59 | .then(function () { 60 | done(new Error('Should not succeed')); 61 | }) 62 | .catch(function () { 63 | done(); 64 | }); 65 | }); 66 | 67 | it('should reject if roles[$i].inherits[$i2] is not a defined role', function (done) { 68 | (new RBAC(async () => ({ 69 | hello: { 70 | can: ['hel'], 71 | inherits: ['what'] 72 | } 73 | }))) 74 | ._init 75 | .then(function () { 76 | done(new Error('Should not succeed')); 77 | }) 78 | .catch(function () { 79 | done(); 80 | }); 81 | }); 82 | 83 | it('should resolve if function returns correct object', function (done) { 84 | (new RBAC(async () => data.all)) 85 | ._init 86 | .then(function () { 87 | done(); 88 | }) 89 | .catch(function () { 90 | done(new Error('Should not reject')); 91 | }); 92 | }); 93 | 94 | describe('resolve current role operations', function () { 95 | it('should respect operations', function (done) { 96 | (new RBAC(async () => data.all)) 97 | .can('user', 'post:add') 98 | .catch(catchError(done)) 99 | .then(shouldBeAllowed(done)); 100 | }); 101 | it('should reject undefined operations', function (done) { 102 | (new RBAC(async () => data.all)) 103 | .can('user', 'post:what') 104 | .catch(catchError(done)) 105 | .then(shouldNotBeAllowed(done)); 106 | }); 107 | it('should reject undefined users', function (done) { 108 | (new RBAC(async () => data.all)) 109 | .can('what', 'post:add') 110 | .catch(catchError(done)) 111 | .then(shouldNotBeAllowed(done)); 112 | }); 113 | 114 | it('should reject function operations with no operands', function (done) { 115 | (new RBAC(async () => data.all)) 116 | .can('user', 'post:save') 117 | .then(shouldNotBeAllowed(done)) 118 | .catch(err => done()); 119 | }); 120 | 121 | it('should reject function operations with rejectable values', function (done) { 122 | (new RBAC(async () => data.all)) 123 | .can('user', 'post:save', {ownerId: 1, postId: 2}) 124 | .catch(catchError(done)) 125 | .then(shouldNotBeAllowed(done)); 126 | }); 127 | 128 | it('should allow function operations with correct values', function (done) { 129 | (new RBAC(async () => data.all)) 130 | .can('user', 'post:save', {ownerId: 1, postId: 1}) 131 | .catch(catchError(done)) 132 | .then(shouldBeAllowed(done)); 133 | }); 134 | 135 | it('should reject conditional glob operations with no params', done => { 136 | (new RBAC(async () => data.all)) 137 | .can('user', 'user:save') 138 | .catch(catchError(done)) 139 | .then(shouldNotBeAllowed(done)); 140 | }); 141 | 142 | it('should reject conditional glob operations with wrong params', done => { 143 | (new RBAC(async () => data.all)) 144 | .can('user', 'user:save', {userId: 1, id: 2}) 145 | .catch(catchError(done)) 146 | .then(shouldNotBeAllowed(done)); 147 | }); 148 | 149 | it('should allow glob operations with correct params', done => { 150 | (new RBAC(async () => data.all)) 151 | .can('user', 'user:save', {userId: 1, id: 1}) 152 | .catch(catchError(done)) 153 | .then(shouldBeAllowed(done)); 154 | }); 155 | 156 | it('should prioritize non glob operations', done => { 157 | (new RBAC(async () => data.all)) 158 | .can('user', 'user:create') 159 | .catch(catchError(done)) 160 | .then(shouldBeAllowed(done)); 161 | }); 162 | }); 163 | 164 | describe('parent role operations', function () { 165 | it('should respect allowed operations', function (done) { 166 | (new RBAC(async () => data.all)) 167 | .can('manager', 'account:add') 168 | .catch(catchError(done)) 169 | .then(shouldBeAllowed(done)); 170 | }); 171 | it('should reject undefined operations', function (done) { 172 | (new RBAC(async () => data.all)) 173 | .can('manager', 'post:what') 174 | .catch(catchError(done)) 175 | .then(shouldNotBeAllowed(done)); 176 | }); 177 | 178 | it('should respect allowed glob operations', done => { 179 | (new RBAC(async () => data.all)) 180 | .can('manager', 'account:whatever') 181 | .catch(catchError(done)) 182 | .then(shouldBeAllowed(done)); 183 | }); 184 | it('should handle overwritten glob operations', done => { 185 | (new RBAC(async () => data.all)) 186 | .can('manager', 'user:save', {regionId: 1, userRegionId: 1}) 187 | .catch(catchError(done)) 188 | .then(shouldBeAllowed(done)); 189 | }); 190 | }); 191 | describe('parents parent role operations', function () { 192 | it('should respect allowed operations', function (done) { 193 | (new RBAC(async () => data.all)) 194 | .can('admin', 'account:add') 195 | .catch(catchError(done)) 196 | .then(shouldBeAllowed(done)); 197 | }); 198 | it('should reject undefined operations', function (done) { 199 | (new RBAC(async () => data.all)) 200 | .can('admin', 'post:what') 201 | .catch(catchError(done)) 202 | .then(shouldNotBeAllowed(done)); 203 | }); 204 | 205 | it('should respect glob operations', done => { 206 | (new RBAC(async () => data.all)) 207 | .can('admin', 'user:what') 208 | .catch(catchError(done)) 209 | .then(shouldBeAllowed(done)); 210 | }); 211 | }); 212 | 213 | describe('parent role operations with callback', function () { 214 | it('should respect allowed operations', function (done) { 215 | (new RBAC(async () => data.all)) 216 | .can('manager', 'post:create', {postId: 1, ownerId: 1}) 217 | .catch(catchError(done)) 218 | .then(shouldBeAllowed(done)); 219 | }); 220 | it('should reject not allowed operation', function (done) { 221 | (new RBAC(async () => data.all)) 222 | .can('manager', 'post:create', {postId: 1, ownerId: 2}) 223 | .catch(catchError(done)) 224 | .then(shouldNotBeAllowed(done)); 225 | }); 226 | }); 227 | 228 | describe('array of roles', () => { 229 | it('should not allow if empty array of roles', done => { 230 | (new RBAC(async () => data.all)) 231 | .can([], 'post:what') 232 | .catch(catchError(done)) 233 | .then(shouldNotBeAllowed(done)); 234 | }); 235 | it('should not allow if none of the roles is allowed', done => { 236 | (new RBAC(async () => data.all)) 237 | .can(['user', 'manager'], 'rule the world') 238 | .catch(catchError(done)) 239 | .then(shouldNotBeAllowed(done)); 240 | }); 241 | it('should allow if one of the roles is allowed', done => { 242 | (new RBAC(async () => data.all)) 243 | .can(['user', 'admin'], 'post:delete') 244 | .catch(catchError(done)) 245 | .then(shouldBeAllowed(done)); 246 | }); 247 | it('should allow if one of the roles is allowed', done => { 248 | (new RBAC(async () => data.all)) 249 | .can(['user', 'admin'], 'post:rename', {ownerId: 1, postId: 1}) 250 | .catch(catchError(done)) 251 | .then(shouldBeAllowed(done)); 252 | }); 253 | it('should not allow if none of the roles is allowed', done => { 254 | (new RBAC(async () => data.all)) 255 | .can(['user', 'admin'], 'post:rename', {ownerId: 1, postId: 2}) 256 | .catch(catchError(done)) 257 | .then(shouldNotBeAllowed(done)); 258 | }); 259 | 260 | it('should allow if one of the roles is allowed through glob', done => { 261 | (new RBAC(async () => data.all)) 262 | .can(['user', 'manager'], 'user:save', {regionId: 1, userRegionId: 1}) 263 | .catch(catchError(done)) 264 | .then(shouldBeAllowed(done)); 265 | }); 266 | }); 267 | 268 | }); -------------------------------------------------------------------------------- /test/data/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var roles = { 4 | user: { 5 | can: [ 6 | 'account:add', 7 | 'account:save', 8 | 'account:delete', 9 | 'post:add', 10 | { 11 | name: 'post:save', 12 | when: async (params) => params.ownerId === params.postId 13 | }, 14 | { 15 | name: 'post:create', 16 | when: async (params) => params.ownerId === params.postId 17 | }, 18 | 'user:create', 19 | { 20 | name: 'user:*', 21 | when: async (params) => params.id === params.userId 22 | } 23 | ] 24 | }, 25 | manager: { 26 | can: [ 27 | 'account:*', 28 | 'post:save', 29 | 'post:delete', 30 | { 31 | name: 'post:rename', 32 | when: async (params) => params.ownerId === params.postId 33 | }, 34 | { 35 | name: 'user:*', 36 | when: async (params) => params.regionId === params.userRegionId 37 | } 38 | ], 39 | inherits: ['user'] 40 | }, 41 | admin: { 42 | can: ['rule the world', 'user:*'], 43 | inherits: ['manager'] 44 | } 45 | }; 46 | 47 | module.exports.all = roles; -------------------------------------------------------------------------------- /test/sync.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const RBAC = require('../lib/rbac'); 4 | let data = require('./data'); 5 | 6 | const assert = require('assert'); 7 | 8 | const {shouldBeAllowed, shouldNotBeAllowed, catchError} = require('./utils'); 9 | 10 | describe('RBAC sync', () => { 11 | let rbac; 12 | it('should reject if no roles object', () => { 13 | assert.throws( 14 | () => { 15 | rbac = new RBAC(); 16 | }, 17 | TypeError 18 | ); 19 | }); 20 | it('should throw error if no roles object', () => { 21 | assert.throws( 22 | () => { 23 | rbac = new RBAC('hello'); 24 | }, 25 | TypeError 26 | ); 27 | }); 28 | it('should throw error if roles[$i].can is not an array', () => { 29 | assert.throws( 30 | () => { 31 | rbac = new RBAC({ 32 | hello: { 33 | can: 1 34 | } 35 | }); 36 | }, 37 | TypeError 38 | ); 39 | }); 40 | it('should throw error if roles[$i].can is not an array', () => { 41 | assert.throws( 42 | () => { 43 | rbac = new RBAC({ 44 | hello: 1 45 | }); 46 | }, 47 | TypeError 48 | ); 49 | }); 50 | it('should throw error if roles[$i].can[$i2] is not a string or object with .when', () => { 51 | assert.throws( 52 | () => { 53 | rbac = new RBAC({ 54 | hello: { 55 | can: [function (){}] 56 | } 57 | }); 58 | }, 59 | TypeError 60 | ); 61 | }); 62 | 63 | it('should throw error if roles[$i].inherits is not an array', () => { 64 | assert.throws( 65 | () => { 66 | rbac = new RBAC({ 67 | hello: { 68 | can: ['hel'], 69 | inherits: 1 70 | } 71 | }); 72 | }, 73 | TypeError 74 | ); 75 | }); 76 | 77 | it('should throw error if roles[$i].inherits[$i2] is not a string', () => { 78 | assert.throws( 79 | () => { 80 | rbac = new RBAC({ 81 | hello: { 82 | can: ['hel'], 83 | inherits: [1] 84 | } 85 | }); 86 | }, 87 | TypeError 88 | ); 89 | }); 90 | 91 | it('should throw error if roles[$i].inherits[$i2] is not a defined role', () => { 92 | assert.throws( 93 | () => { 94 | rbac = new RBAC({ 95 | hello: { 96 | can: ['hel'], 97 | inherits: ['what'] 98 | } 99 | }); 100 | }, 101 | TypeError 102 | ); 103 | }); 104 | 105 | it('should create model if all OK', () => { 106 | rbac = new RBAC(data.all); 107 | }); 108 | describe('current role operations', () => { 109 | it('should respect allowed operations', done => { 110 | rbac.can('user', 'post:add') 111 | .catch(catchError(done)) 112 | .then(shouldBeAllowed(done)); 113 | }); 114 | it('should not allow when undefined operations', done => { 115 | rbac.can('user', 'post:what') 116 | .catch(catchError(done)) 117 | .then(shouldNotBeAllowed(done)); 118 | }); 119 | it('should not allow undefined users', done => { 120 | rbac.can('what', 'post:add') 121 | .catch(catchError(done)) 122 | .then(shouldNotBeAllowed(done)); 123 | }); 124 | 125 | it('should reject function operations with no operands', done => { 126 | rbac.can('user', 'post:save') 127 | .then(() => done(new Error('should not be here'))) 128 | .catch(err => done()); 129 | }); 130 | 131 | it('should not allow function operations based on params', done => { 132 | rbac.can('user', 'post:save', {ownerId: 1, postId: 2}) 133 | .catch(catchError(done)) 134 | .then(shouldNotBeAllowed(done)); 135 | }); 136 | 137 | it('should allow function operations with correct values', done => { 138 | rbac.can('user', 'post:save', {ownerId: 1, postId: 1}) 139 | .catch(catchError(done)) 140 | .then(shouldBeAllowed(done)); 141 | }); 142 | 143 | 144 | it('should reject conditional glob operations with no params', done => { 145 | rbac.can('user', 'user:save') 146 | .catch(catchError(done)) 147 | .then(shouldNotBeAllowed(done)); 148 | }); 149 | 150 | it('should reject conditional glob operations with wrong params', done => { 151 | rbac.can('user', 'user:save', {userId: 1, id: 2}) 152 | .catch(catchError(done)) 153 | .then(shouldNotBeAllowed(done)); 154 | }); 155 | 156 | it('should allow glob operations with correct params', done => { 157 | rbac.can('user', 'user:save', {userId: 1, id: 1}) 158 | .catch(catchError(done)) 159 | .then(shouldBeAllowed(done)); 160 | }); 161 | 162 | it('should prioritize non glob operations', done => { 163 | rbac.can('user', 'user:create') 164 | .catch(catchError(done)) 165 | .then(shouldBeAllowed(done)); 166 | }); 167 | }); 168 | 169 | describe('parent role operations', () => { 170 | it('should respect allowed operations', done => { 171 | rbac.can('manager', 'account:add') 172 | .catch(catchError(done)) 173 | .then(shouldBeAllowed(done)); 174 | }); 175 | it('should reject undefined operations', done => { 176 | rbac.can('manager', 'post:what') 177 | .catch(catchError(done)) 178 | .then(shouldNotBeAllowed(done)); 179 | }); 180 | 181 | it('should respect allowed glob operations', done => { 182 | rbac.can('manager', 'account:whatever') 183 | .catch(catchError(done)) 184 | .then(shouldBeAllowed(done)); 185 | }); 186 | it('should handle overwritten glob operations', done => { 187 | rbac.can('manager', 'user:save', {regionId: 1, userRegionId: 1}) 188 | .catch(catchError(done)) 189 | .then(shouldBeAllowed(done)); 190 | }); 191 | }); 192 | describe('parents parent role operations', () => { 193 | it('should respect allowed operations', done => { 194 | rbac.can('admin', 'account:add') 195 | .catch(catchError(done)) 196 | .then(shouldBeAllowed(done)); 197 | }); 198 | it('should reject undefined operations', done => { 199 | rbac.can('admin', 'post:what') 200 | .catch(catchError(done)) 201 | .then(shouldNotBeAllowed(done)); 202 | }); 203 | 204 | it('should respect glob operations', done => { 205 | rbac.can('admin', 'user:what') 206 | .catch(catchError(done)) 207 | .then(shouldBeAllowed(done)); 208 | }); 209 | }); 210 | 211 | describe('array of roles', () => { 212 | it('should not allow if empty array of roles', done => { 213 | rbac.can([], 'post:what') 214 | .catch(catchError(done)) 215 | .then(shouldNotBeAllowed(done)); 216 | }); 217 | it('should not allow if none of the roles is allowed', done => { 218 | rbac.can(['user', 'manager'], 'rule the world') 219 | .catch(catchError(done)) 220 | .then(shouldNotBeAllowed(done)); 221 | }); 222 | it('should allow if one of the roles is allowed', done => { 223 | rbac.can(['user', 'admin'], 'post:delete') 224 | .catch(catchError(done)) 225 | .then(shouldBeAllowed(done)); 226 | }); 227 | it('should allow if one of the roles is allowed', done => { 228 | rbac.can(['user', 'admin'], 'post:rename', {ownerId: 1, postId: 1}) 229 | .catch(catchError(done)) 230 | .then(shouldBeAllowed(done)); 231 | }); 232 | it('should not allow if none of the roles is allowed', done => { 233 | rbac.can(['user', 'admin'], 'post:rename', {ownerId: 1, postId: 2}) 234 | .catch(catchError(done)) 235 | .then(shouldNotBeAllowed(done)); 236 | }); 237 | 238 | 239 | it('should allow if one of the roles is allowed through glob', done => { 240 | rbac.can(['user', 'manager'], 'user:save', {regionId: 1, userRegionId: 1}) 241 | .catch(catchError(done)) 242 | .then(shouldBeAllowed(done)); 243 | }); 244 | }); 245 | 246 | describe('complex setup', () => { 247 | const rbac = new RBAC({ 248 | signup: { 249 | can: [], 250 | inherits: [] 251 | }, 252 | investor: { 253 | can: [ 254 | 'deal:read' 255 | ], 256 | inherits: ['signup'] 257 | }, 258 | manager: { 259 | can: [ 260 | 'deal:readAdmin' 261 | ], 262 | inherits: ['investor'] 263 | }, 264 | admin: { 265 | can: [], 266 | inherits: ['manager'] 267 | } 268 | }); 269 | 270 | it('should reject on deal:readAdmin', done => { 271 | rbac.can('investor', 'deal:readAdmin') 272 | .catch(catchError(done)) 273 | .then(shouldNotBeAllowed(done)); 274 | }); 275 | }); 276 | }); -------------------------------------------------------------------------------- /test/utils/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports.catchError = catchError; 4 | module.exports.shouldBeAllowed = shouldBeAllowed; 5 | module.exports.shouldNotBeAllowed = shouldNotBeAllowed; 6 | 7 | /**********************/ 8 | 9 | function shouldBeAllowed(done) { 10 | return function (result) { 11 | if(result) { 12 | done(); 13 | } else { 14 | done(new Error('should not be denied')); 15 | } 16 | } 17 | } 18 | 19 | function shouldNotBeAllowed(done) { 20 | return function (result) { 21 | if(result) { 22 | done(new Error('should not be allowed')); 23 | } else { 24 | done(); 25 | } 26 | } 27 | } 28 | 29 | function catchError(done) { 30 | return function (err) { 31 | return false; 32 | } 33 | } --------------------------------------------------------------------------------