koa2-rbac-router v0.1.5
koa2-rbac-router
Koa2 router middleware with integrated role-based access control.
- Classic routes definition using
router.get,router.post,router.put,router.delete, etc. - Named URL parameters (with configurable marker).
- Named routes (actions).
- Multiple routers.
- Nestable routers / middlewares.
- ES7 async request handlers.
- Out-of-box simple but yet flexible role-based access control (RBAC).
- MIT License.
Intro
Main goal of this lib is to provide flexible Koa2 router with integrated role-based access
control (RBAC). Basic idea is simple: to perform access control route should be defined with name
(named routes are called 'actions'). Router automatically matches access to the route with RBAC
definitions using route name as RBAC permission (ability) identifier. RBAC role, in general, is a
simple list of accepted actions (route names). When request arrives router fetches context role(s)
(through special callback function ctxRolesFetcher) and resolves them into list of allowed
actions (route names), then verifies if current route name exists in resolved actions list. If
action is allowed router passes request to downstream handlers or returns 403 Forbidden HTTP
error otherwise. Additionally, RBAC supports roles inheritance and actions/roles exclusion.
For example:
RBAC.setup({
// define `guest` role
guest: 'index, signup, signin',
// define common `user` role
user: '@guest, ownAction, !signup, !signin'
});Example above defines:
guestrole with allowed actions:index,signupandsignin;userrole which 'inherits' roleguest(with@guestconstruct), defines own permission:ownActionand excludes permissionssignupandsignininherited fromguestrole. Resultinguserrole permissions are:indexandownAction.
Roles might be excluded as well with construction
!@<role>, what means exclusion of all<role>permissions recursively: permissions inherited by<role>get excluded as well.
Futhermore, any dependend role will be automatically adjusted if inherited role gets updated, for example above:
RBAC.apply('guest', 'index, signup, signin, welcome');adjusts user role as well, and its resulting permissions set becomes: index, welcome, and
ownAction.
Since roles definitions are simple strings (or arrays) it's easy to store them in files, DBs or any other storages: it's up to a programmer how to handle this. Same is about storing request context roles which are expected to be strings of space and/or comma separated list of role names (or array of role names). Obvious solution here is to use a session storage.
Installation
- npm:
npm install koa2-rbac-router- yarn:
yarn add koa2-rbac-routerAPI Reference
Router
new Router ([opts])Instance members
map(descriptor) => Routermap([name], mapping, handler) => Routerget|post|put|delete|...|all([name], path, handler) => Routeruse(prefix, mw) => Router|Function
Static members
ErrorCTX_ACTIONCTX_PARAMSHTTP_VERBSPARAM_MARKPATH_DELIM
Role-based access control (RBAC)
build([force=false]) => RBACimply(name, spec) => RBACmatch(perm, roles) => Booleanresolve(name) => Set<String>setup(specs, [prebuild=true]) => RBACunset(name) => RBACErrorEXCLUDE_MARKROLE_REF_MARKRX_DELIMITER
Example
Router
Exported class.
new Router([opts])
Create a new router instance.
optsObjectoptionalRouter instance configuration properties:
ctxRolesFetcher[Async]FunctionoptionalContext roles fetcher. Automatic RBAC checks are disabled if this routine not set.
Call signature:
[async] function (ctx) {
// example:
return ctx.session.roles;
}prohibitHandler[Async]FunctionoptionalRequest prohibit handler (see RBAC). By default (when this option is omitted)
ctx.throw(403)is used.Call signature:
[async] function (ctx) {
// example:
ctx.body = 'Access denied';
ctx.throw(403);
}preambleHandler[Async]Function | Array<[Async]Function>optionalFunction (or array of functions) to be invoked before any request handlers in call chain.
Call signature:
[async] function (ctx, next) { ... } `next` is _asynchronous_ downstream invokation routine:
async function (ctx, next) {
// preprocess request
...
// await for downstream handlers
await next();
// postprocess request
...
}notFoundHandler[Async]FunctionoptionalRequest route not found handler.
Call signature:
[async] function (ctx) { // default behaviour: ctx.throw(404); }noMethodHandler[Async]FunctionoptionalRequest method not found handler. By default
opts.notFoundHandlerroutine is used (see above).Call signature:
[async] function (ctx) { // example: ctx.throw(501); // return `Not Implemented` HTTP error }
NOTE:
[async]notation means optionally asynchronous.
Instance members
map(descriptor) => Router map([name], mapping, handler) => Router
Define new route.
descriptorObjectRoute descriptor object of following options:
nameStringoptionalUnique (in scope of all
Routerinstances) route name, which is used for matching access permissions (see RBAC below). Unnamed routes are handled unrestricted (with no RBAC checks).mappingStringrequiredRoute mapping of form:
'[<METHOD>] <PATH>', where:<METHOD>is HTTP method name:GET,POST,PUT,DELETE, etc; omitted or specified as'*'value means wildcard or default (fallback) HTTP method handler.<PATH>is route path of form{ /<chunk>|:<param> }, where:<chunk>is path chunk and:<param>is path named parameter (additionally, seePARAM_MARK).Important: route params MUST be same-named in same route paths. For example, following mappings cause parameters collision error, due to attempt of use different names for same parameter:
'GET /items/:serNum''PUT /items/:serNum''DELETE /items/:itemId'- ERROR: parameter should be:serNumas in other routes of path/items.
handler[Async]Function | Array<[Aync]Function>requiredAsynchronous (optionally) request handling routine (or array of routines).
Call signature:
[async] function (ctx, next) { ... }nextis asynchronous downstream invocation routine:async function (ctx, next) { // preprocess request ... // await for downstream handlers await next(); // postprocess request ... }Important: Before calling request specific handler(s) router invokes its
opts.preambleHandlerif it was configured (seenew Router([opts])).
router.get|post|put|delete|...|all([name], path, handler) => Router
Route classic definition helpers. Functions determine corresponding route HTTP methods. The list of
exposed functions is specified by Router.HTTP_VERBS parameter.
name- seemap(...)namepath- seemap(...)mapping <PATH>handler- seemap(...)handler
router.use(prefix, target) => Router | Function
Mount sub-router or middleware function on specified prefix.
prefixStringrequiredTarget mount point. This
prefixwill be cut off fromctx.pathbefore passing control to downstream handlers.NOTE: at the moment parametrized prefixes are not supported.
targetRouter | [Async]FunctionrequiredTarget
Routerinstance or Koa2 common middleware function to be used on specified prefix.
Important: Sub-routers are not enforced to define own config options with new Router([opts]).
Each subrouter recursively 'inherits' unspecified config options from its parent router(s).
Static members
Router.Error class
Router-specific error class.
Router.CTX_ACTION String
Koa context property containing request action (route name). Default: 'action'.
Router.CTX_PARAMS String
Koa context property containing request path parameters. Default: 'params'.
Router.HTTP_VERBS Array<String>
List of HTTP verbs to expose as old-style Router methods. Default: ['get', 'post', 'put', 'delete'].
Router.PARAM_MARK String
Route parameter marker char. Default: ':'.
Router.PATH_DELIM String|RegExp
Delimiter used for splitting route path into chunks. Default: /\/+/.
Role-based access control (RBAC)
Following methods, classes and options are members of RBAC namespace.
RBAC.build([force=false]) => RBAC
Preprocess and compile roles (see setup(...)).
forceBooleanoptionalAll roles re-compilation forcing flag.
RBAC.apply(name, spec) => RBAC
Apply role (new) spec, initiate dependent roles recompilation.
nameStringrequiredRole name.
specString|Array<String>requiredRole spec in one of following formats:
String: space and/or comma separated list of spec tokens;Array<String>: array of spec tokens.
Spec tokens could be:
route action (named route) inclusion, meaning that specified action is permitted by the role;
another role inclusion, marked by role reference mark (see
ROLE_REF_MARK, default:@), meaning recursive inclusion of all actions from specified role;exclusion of action or role, marked by exclude mark (see
EXCLUDE_MARK, default:!), meaning removal of an action or recursive removal of all actions from specified role (marked as:!@excludedRoleName).
Important: appearance order of tokens in role spec DOES matter: precedence grows from left to right, what means, for example, inclusion of role that permits (contains) action
someActionafter exclusion of this action (!someAction) leads to presense ofsomeActionin subject role.
RBAC.match(action, roles) => Boolean
Check if action is permitted by specified role(s).
actionStringrequiredAction name to match.
rolesString|Array<String>requiredSingle role name or space and/or comma separated list or array of roles to match.
This method is used internally by Router to match access permissions (see ctxRolesFetcher).
RBAC.resolve(name) => Set<String>
Resolve role to a set of permitted actions.
nameStringrequiredName of role to resolve.
RBAC.setup(specs, [prebuild=true]) => RBAC
Reset RBAC controller with provided roles specs batch.
specsObjectrequiredRole spec object, where property name is treated as role name and value as role spec (see
RBAC.apply).prebuildBooleanoptionalRoles prebuild flag (default:
true). If set tofalsenone role spec is preprocessed untilRBAC.resolve(...)orRBAC.build()invoked.
RBAC.unset(name) => RBAC
Undefine role and initiate dependent roles recompilation.
RBAC.Error
RBAC-specifix error class.
RBAC.EXCLUDE_MARK
Action/role exclude mark. Default: !.
RBAC.ROLE_REF_MARK
Role reference mark. Default: @.
RBAC.RX_DELIMITER
Spec delimiter regexp. Default: /[,\s]+/.
Tests
npm run testor
yarn testTests with coverage:
npm run test+coveror
yarn test+coverExample
Please visit koa2-rbac-router-example
License and Copyright
This software is a subject of MIT License:
Copyright 2019 Igor V. Dyukov
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.