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
.
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.
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:
{
'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:
{
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:
{
username: 'user00002',
group: ['writer'], // match
premium: false, // do not match :-(
...
}
{
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.
[
{
'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:
{
username: 'user00001',
group: ['writer'], // match
premium: false,
...
}
{
username: 'user00002', // match
group: ['reader'],
premium: false,
...
}
{
username: 'user00003',
group: ['reader'],
premium: true, // match
...
}
{
username: 'user00004',
group: ['writer'], // match
premium: true, // match
...
}
But, not the one with the following document:
{
username: 'user00005',
group: ['reader'],
premium: false,
...
}
The following words are prefixes that can be used for matching information:
credentials
- Information fromrequest.auth.credentials
object. Information in this object depends on your authentication implementation.connection
- Connection information, fromrequest.info
, as documented in hapi: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 inrequest.query
.param
- URL parameters, as inrequest.params
.request
- Other request information:request:path
- Requested path.request.method
- Requested method (e.g.post
).
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 bePERMIT
(deny, unless one permits)deny-overrides
- If at least one policy/rule denies, then the final decision for that policy set/policy should beDENY
(permit, unless one denies)
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 accessdeny
- 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.
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 bepermit
ordeny
Example
{
target: {'credentials:blocked': true}, // if the user is blocked
effect: 'deny' // then deny
}
A Policy is a set of rules. It contains:
target
(optional) - The target (default: matches with any)apply
- The combinatory algorithm for the rulesrules
- An array of rules
Example
{
// 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
}
]
}
A Policy Set is a set of Policies. It contains:
target
(optional) - The target (default: matches with any)apply
- The combinatory algorithm for the policiespolicies
- An array of policies
Example
{
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
}
]
}
]
}
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.
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
.
If you wish to define access control policies for a single route, you can do it at the route level configuration:
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
:
server.route({
method: 'GET',
path: '/example',
handler: function(request, reply) {
reply({
ok: true
});
},
options: {
plugins: {
rbac: 'none'
}
}
});
It is also possible to retrieve the policies dynamically (e.g.: from a database). Instead of defining them directly, use a callback function instead.
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.
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:
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
}
}
}
});
When importing the hapi-rbac
plugin, it is possible to define what are the response codes for deny
and undetermined
cases:
server.register({
plugin: require('hapi-rbac'),
options: {
responseCode: {
onDeny: 403,
onUndetermined: 403
}
}
});
This configuration is applied to all the cases.
You can define your own data sources for target matching. To do so, you can define in the module options an array of dataRetrievers.
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:
{
target: { 'document:12345.title': 'The Swallow\'s Tale' },
...
}