Permalink
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
584 lines (458 sloc) 14.3 KB

2.0.0 API Reference

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

{
  '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.

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:

{
  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 from request.auth.credentials object. Information in this object depends on your authentication implementation.
  • connection - Connection information, from request.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 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

{
  // 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

{
  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.

server.register({
  register: require('hapi-rbac'),
  options: {
    policy: {
      target: { 'credentials:group': 'readers' },
      apply: 'deny-overrides', // Combinatory algorithm
      rules: [
        {
          target: { 'credentials:username': 'bad_guy' },
          effect: 'deny'
        },
        {
          effect: 'permit'
        }
      ]
    }
  }
}, function(err) {
  ...
});

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:

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
    });
  },
  config: {
    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.

server.register({
  register: require('hapi-rbac'),
  options: {
    policy: function(request, callback) {

      /* 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
        }
      };

      db.collection('policies').findOne(query, function(err, policy) {

        if(err) {
          return callback(err);
        }

        // callback with the found policy
        // if policy is null, then hapi-rbac assumes that there is no policy configured for the route
        callback(null, policy);
      });
    }
  }
}, function(err) {
  ...
});

In this example, it is assumed that your policies have a resource key with path and method sub-keys.

{
  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: function(request, reply) {
    reply({
      ok: true
    });
  },
  config: {
    plugins: {
      rbac: function(request, callback) {

        /* 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
          }
        };

        db.collection('policies').findOne(query, function(err, policy) {

          if(err) {
            return callback(err);
          }

          // callback with the found policy
          // if policy is null, then hapi-rbac assumes that there is no policy configured for the route
          callback(null, 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:

server.register({
  register: require('hapi-rbac'),
  options: {
    responseCode: {
        onDeny: 403,
        onUndetermined: 403
    }
  }
}, function(err) {
  ...
});

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.

server.register({
  register: 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));
                });
            }
        }
    ]
  }
}, function(err) {
  ...
});

The, you can use it in your targets:

{
    target: { 'document:12345.title': 'The Swallow\'s Tale' },
    ...
}