hapi-rbac
Version:
Rule Based Access Control for Hapijs
548 lines (439 loc) • 14.2 kB
Markdown
# 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