UNPKG

hapi-rbac

Version:
548 lines (439 loc) 14.2 kB
# 2.0.0 API Reference * [`Terms`](#terms) * [`Setting up a policy`](#setting-up-a-policy) * [`Target`](#target-matching) * [`Rule`](#rule) * [`Policy`](#policy) * [`Policy Set`](#policy-set) * [`Configuration`](#configuration) * [`Global policy`](#global-policy) * [`Route policy`](#route-policy) * [`Dynamic policy`](#dynamic-policy) * [`Response code`](#defining-the-response-code) * [`Data retrievers`](#data-retrievers) # hapi-rbac This module is a **Rule** Based Access Control plugin for hapi. It decides, based on a set of `rules` (a `policy`), if the access should be `allowed` or `denied` to a certain route in a request. If there are rules configured, but no rules can applied to a certain case, the access decision is `undetermined`. When it happens, the access is also `denied`. ## Terms * `Target` - A set of key-value pairs which are matched with the available information on a request. It is used to decide if a *Rule*, *Policy* or *Policy Set* apply to the request's case. * `Rule` - A *Rule* specifies if a matched *Target* should have or not access to the route. * `Policy` - A *Policy* is composed by a set of *Rules*. It specifies how the combination of the *Rules'* results should be considered. * `Policy Set` - A *Policy Set* is composed by a set of *Policies*. It also specifies how the combination of the *Policies'* results should be considered. ## Setting up a policy ### Target matching Targets are the conditions which will define if a policy set, policy or rule apply in a request. If the policy set, policy or rule should always apply, you can simply omit the `target`. When present, it can either be a target element or an array of target elements. When the array has more than one target element, they are combined with an `OR` condition. All the keys inside a target element are combined with an `AND` condition. Check the following examples: #### AND ```js { 'credentials:group': 'writer', 'credentials:premium': true } ``` With this target, only users in group `writer` **and** with `premium` account will match. So, if the logged in user has the following `request.auth.credentials` document: ```js { username: 'user00001', group: ['writer'], // match premium: true, // match ... } ``` Then, the *rule* or *policy* with the configured *target* will be evaluated, because the target applies. But, if the logged in user has one of the following `request.auth.credentials` documents: ```js { username: 'user00002', group: ['writer'], // match premium: false, // do not match :-( ... } ``` ```js { username: 'user00003', group: ['reader'], // do not match :-( premium: true, // match ... } ``` Then, the rule or policy with the configured target will not be evaluated. Since the match used is `AND`, the user doesn't match the target. #### OR ``` [ { 'credentials:group': 'writer' }, { 'credentials:premium': true }, { 'credentials:username': 'user00002' } ] ``` With this target, any user in the group `writer` **or** with `premium` account **or** with username `user00002` will be matched. So, users with the following `request.auth.credentials` documents will be matched: ```js { username: 'user00001', group: ['writer'], // match premium: false, ... } ``` ```js { username: 'user00002', // match group: ['reader'], premium: false, ... } ``` ```js { username: 'user00003', group: ['reader'], premium: true, // match ... } ``` ```js { username: 'user00004', group: ['writer'], // match premium: true, // match ... } ``` But, not the one with the following document: ```js { username: 'user00005', group: ['reader'], premium: false, ... } ``` The following words are prefixes that can be used for matching information: * `credentials` - Information from `request.auth.credentials` object. Information in this object depends on your authentication implementation. * `connection` - Connection information, from `request.info`, as documented in [hapi](http://hapijs.com/api#request-object): * `connection:host` - Content of the HTTP 'Host' header (e.g. 'example.com:8080'). * `connection:hostname` - The hostname part of the 'Host' header (e.g. 'example.com'). * `connection:received` - Request reception timestamp. * `connection:referrer` - Content of the HTTP 'Referrer' (or 'Referer') header. * `connection:remoteAddress` - Remote client IP address. * `connection:remotePort` - Remote client port. * `query` - Query parameters, as in `request.query`. * `param` - URL parameters, as in `request.params`. * `request` - Other request information: * `request:path` - Requested path. * `request.method` - Requested method (e.g. `post`). ### Policy and Rules combinatory algorithms When there is more than one policy inside a policy set or more than one rule inside a policy, the combinatory algorithm will decide the final result from the multiple results. There are, at the moment, two possibilities: * `permit-overrides` - If at least one policy/rule permits, then the final decision for that policy set/policy should be `PERMIT` (deny, unless one permits) * `deny-overrides` - If at least one policy/rule denies, then the final decision for that policy set/policy should be `DENY` (permit, unless one denies) ### Rule effects If a rule applies (target match), the `effect` is the access decision for that rule. It can be: * `permit` - If rule apply, decision is to allow access * `deny` - If rule apply, decision is to deny access When a policy set, policy or rule do not apply (the target don't match), then the decision is `undetermined`. If all the policy sets, policies and rules have the `undetermined` result, then the access is denied, since it is not clear if the user can access or not the route. ### Rule A __Rule__ defines a decision to _allow_ or _deny_ access. It contains: * `target` (optional) - The target (default: matches with any) * `effect` - The decision if the target matches. Can be `permit` or `deny` Example ``` { target: {'credentials:blocked': true}, // if the user is blocked effect: 'deny' // then deny } ``` ### Policy A __Policy__ is a _set of rules_. It contains: * `target` (*optional*) - The target (*default*: matches with any) * `apply` - The combinatory algorithm for the rules * `rules` - An array of rules Example ```js { // if writer AND premium account target: { 'credentials:group': 'writer', 'credentials:premium': true }, apply: 'deny-overrides', // permit, unless one denies rules: [ { target: { 'credentials:username': 'bad_user' }, // if the username is bad_user effect: 'deny' // then deny }, { target: { 'credentials:blocked': true }, // if the user is blocked effect: 'deny' // then deny }, { effect: 'permit' // else permit } ] } ``` ### Policy Set A __Policy Set__ is a set of __Policies__. It contains: * `target` (_optional_) - The target (_default_: matches with any) * `apply` - The combinatory algorithm for the policies * `policies` - An array of policies Example ```js { target: [{ 'credentials:group': 'writer' }, { 'credentials:group': 'publisher'}], // writer OR publisher apply: 'permit-overrides', // deny, unless one permits policies: [ { target: { 'credentials:group': 'writer', 'credentials:premium': true }, // if writer AND premium account apply: 'deny-overrides', // permit, unless one denies rules: [ { target: { 'credentials:username': 'bad_user'}, // if the username is bad_user effect: 'deny' // then deny }, { target: { 'credentials:blocked': true }, // if the user is blocked effect: 'deny' // then deny }, { effect: 'permit' // else permit } ] }, { target: { 'credentials:premium': false }, // if (writer OR publisher) AND no premium account apply: 'permit-overrides', // deny, unless one permits rules: [ { target: { 'credentials:username': 'special_user' }, // if the username is special_user effect: 'permit' // then permit }, { effect: 'deny' // else deny } ] } ] } ``` ## Configuration ### Global Policy If you wish to define a default access control policy for the routes, you can do it with `policy` key inside the `options`, when you register the `hapi-rbac` in hapi. ```js server.register({ plugin: require('hapi-rbac'), options: { policy: { target: { 'credentials:group': 'readers' }, apply: 'deny-overrides', // Combinatory algorithm rules: [ { target: { 'credentials:username': 'bad_guy' }, effect: 'deny' }, { effect: 'permit' } ] } } }); ``` This configuration will allow access to all the routes to all the users in the `readers` group, except to the user `bad_guy`. ### Route Policy If you wish to define access control policies for a single route, you can do it at the route level configuration: ```js server.route({ method: 'GET', path: '/example', handler: function(request, reply) { reply({ ok: true }); }, config: { plugins: { rbac: { target: { 'credentials:group': 'readers' }, apply: 'deny-overrides', // Combinatory algorithm rules: [ { target: { 'credentials:username': 'bad_guy' }, effect: 'deny' }, { effect: 'permit' } ] } } } }); ``` If you have access control policies configured globally, this configuration overrides them. You can disable a global access control policy at the route level, by using the string `none`: ```js server.route({ method: 'GET', path: '/example', handler: function(request, reply) { reply({ ok: true }); }, options: { plugins: { rbac: 'none' } } }); ``` ### Dynamic Policy It is also possible to retrieve the policies dynamically (e.g.: from a database). Instead of defining them directly, use a callback function instead. ```js server.register({ plugin: require('hapi-rbac'), options: { async policy(request) { /* Retrieve your policies from a database */ const query = { resource: { // Use the path and method as a resource identifier path: request.route.path, method: request.route.method } }; // if policy is null, then hapi-rbac assumes that there is no policy configured for the route const policy = await db.collection('policies').findOne(query) return policy } } }); ``` In this example, it is assumed that your policies have a `resource` key with `path` and `method` sub-keys. ```js const policy ={ resource: { // resource identifies what is being requested path: '/example', method: 'get' }, target: { 'credentials:group': 'readers' }, apply: 'deny-overrides', // Combinatory algorithm rules: [ { target: { 'credentials:username': 'bad_guy' }, effect: 'deny' }, { effect: 'permit' } ] } ``` You can also have dynamic access control policy retrieval at the route level: ```js server.route({ method: 'GET', path: '/example', handler() { return { ok: true }; }, options: { plugins: { async rbac(request) { /* Retrieve your policies from a database */ const query = { resource: { // Use the path and method as a resource identifier path: request.route.path, method: request.route.method } }; const policy = await db.collection('policies').findOne(query) return policy } } } }); ``` ### Defining the response code When importing the `hapi-rbac` plugin, it is possible to define what are the response codes for `deny` and `undetermined` cases: ```js server.register({ plugin: require('hapi-rbac'), options: { responseCode: { onDeny: 403, onUndetermined: 403 } } }); ``` This configuration is applied to all the cases. ### Data retrievers You can define your own data sources for target matching. To do so, you can define in the module options an array of dataRetrievers. ```js server.register({ plugin: require('hapi-rbac'), options: { dataRetrievers: [ { handles: ['document'], // Name the source this data retriever handles handler: (source, key, context, callback) => { // You can use the key as you wish // e.g. key: 12345.name const splitKey = key.split('.'); const id = splitKey[0]; const field = splitKey[1]; const query = { _id: id, // In hapi-rbac, the context is the Request object user: Hoek.reach(context, 'auth.credentials._id') }; db.collection('documents').findOne(query, (err, result) => { if (err) { return callback(err); } // Pass the value to the callback callback(null, Hoek.reach(result, field)); }); } } ] } }); ``` The, you can use it in your targets: ```js { target: { 'document:12345.title': 'The Swallow\'s Tale' }, ... } ``` [npm-badge]: https://img.shields.io/npm/v/hapi-rbac.svg [npm-url]: https://npmjs.com/package/hapi-rbac [travis-badge]: https://travis-ci.org/franciscogouveia/hapi-rbac.svg?branch=master [travis-url]: https://travis-ci.org/franciscogouveia/hapi-rbac [coveralls-badge]:https://coveralls.io/repos/franciscogouveia/hapi-rbac/badge.svg?branch=master&service=github [coveralls-url]: https://coveralls.io/github/franciscogouveia/hapi-rbac?branch=master [david-badge]: https://david-dm.org/franciscogouveia/hapi-rbac.svg [david-url]: https://david-dm.org/franciscogouveia/hapi-rbac