hapi-rbac
Version:
Rule Based Access Control for Hapijs
611 lines (532 loc) • 19.9 kB
JavaScript
'use strict';
const Joi = require('joi');
const Code = require('code');
const Lab = require('lab');
const {createServer} = require('./helpers/server');
const lab = exports.lab = Lab.script();
const experiment = lab.experiment;
const test = lab.test;
const before = lab.before;
const expect = Code.expect;
/**
* Rule based access control policy tests, based on username
**/
experiment('Global RBAC policy, based on username', () => {
let server;
before(async () => {
const users = {};
users.sg1001 = {
'scope': 'admin',
'firstName': 'Some',
'lastName': 'Guy',
'username': 'sg1001',
'password': 'pwtest',
'group': ['reader']
};
users.sg1002 = {
'scope': 'admin',
'firstName': 'Some',
'lastName': 'Other Guy',
'username': 'sg1002',
'password': 'pwtest',
'group': ['reader']
};
// Set up the hapi server route
server = await createServer(users, {
policy: {
target: {'credentials:username': 'sg1001'},
apply: 'permit-overrides',
rules: [
{
'effect': 'permit'
}
]
}
});
// No policy configured -> use global configuration
server.route({
method: 'GET',
path: '/endpoint',
handler: (request, h) => h.response({ok: true})
});
// Policy configured -> use route configuration (ignore global configuration)
server.route({
method: 'GET',
path: '/overriden-policy',
handler: (request, h) => h.response({ok: true}),
config: {
plugins: {
rbac: {
target: {'credentials:username': 'sg1002'},
apply: 'permit-overrides',
rules: [
{
'effect': 'permit'
}
]
}
}
}
});
// Policy disabled -> do not use access control (ignore global configuration)
server.route({
method: 'GET',
path: '/disabled-ac',
handler: (request, h) => h.response({ok: true}),
config: {
plugins: {
rbac: 'none'
}
}
});
// If param1 is 'forbiddenParam', access should be denied. Always allowed otherwise.
server.route({
method: 'GET',
path: '/route-params/{param1}',
handler: (request, h) => h.response({ok: true}),
config: {
plugins: {
rbac: {
apply: 'deny-overrides',
rules: [
{
target: {'params:param1': 'forbiddenParam'},
'effect': 'deny'
},
{
'effect': 'permit'
}
]
}
}
}
});
// If query param1 is 'forbiddenParam', access should be denied. Always allowed otherwise.
server.route({
method: 'GET',
path: '/route-query-params',
handler: (request, h) => h.response({ok: true}),
config: {
validate: {
query: {
param1: Joi.string().required()
}
},
plugins: {
rbac: {
apply: 'deny-overrides',
rules: [
{
target: {'query:param1': 'forbiddenParam'},
'effect': 'deny'
},
{
'effect': 'permit'
}
]
}
}
}
});
// If request method is 'get', access should be denied. This is a stupid example, this rule would only make sense in a global configuration.
server.route({
method: 'GET',
path: '/route-request-get-deny',
handler: (request, h) => h.response({ok: true}),
config: {
plugins: {
rbac: {
apply: 'deny-overrides',
rules: [
{
target: {'request:method': 'get'},
'effect': 'deny'
},
{
'effect': 'permit'
}
]
}
}
}
});
// If request method is 'get', access should be denied. This is a stupid example, this rule would only make sense in a global configuration.
server.route({
method: 'GET',
path: '/route-request-get-allow',
handler: (request, h) => h.response({ok: true}),
config: {
plugins: {
rbac: {
apply: 'deny-overrides',
rules: [
{
target: {'request:method': 'post'},
'effect': 'deny'
},
{
'effect': 'permit'
}
]
}
}
}
});
// Target matching on the first two rules will be always false, since the fields do not exist
server.route({
method: 'GET',
path: '/route-non-existing-field',
handler: (request, h) => h.response({ok: true}),
config: {
plugins: {
rbac: {
apply: 'permit-overrides',
rules: [
{ // Invalid field
target: {'request:somefield': 'test'},
'effect': 'permit'
},
{ // Invalid data source
target: {'somesource:somefield': 'test'},
'effect': 'permit'
},
{
'effect': 'deny'
}
]
}
}
}
});
// Target matching between two fields
server.route({
method: 'GET',
path: '/match-fields/{id}',
handler: (request, h) => h.response({ok: true}),
config: {
plugins: {
rbac: {
apply: 'permit-overrides',
rules: [
{
target: {
// Valid match: /match-fields/123?test=123
// Invalid match: /match-fields/123?test=456
'params:id': {field: 'query:test'}
},
'effect': 'permit'
},
{
'effect': 'deny'
}
]
}
}
}
});
// Target matching using RegExp
server.route({
method: 'GET',
path: '/regex-match',
handler: (request, h) => h.response({ok: true}),
config: {
plugins: {
rbac: {
apply: 'permit-overrides',
rules: [
{
// Allow all users which last name starts with Other
target: {
'credentials:lastName': /^Other.*$/
},
'effect': 'permit'
},
{
'effect': 'deny'
}
]
}
}
}
});
});
test('Should have access to the route, with policy targeting the username', async () => {
const response = await server.inject({
method: 'GET',
url: '/endpoint',
headers: {
authorization: 'Basic ' + (new Buffer('sg1001:pwtest', 'utf8')).toString('base64')
}
});
expect(response.statusCode).to.equal(200);
});
test('Should not have access to the route, with policy targeting the username', async () => {
const response = await server.inject({
method: 'GET',
url: '/endpoint',
headers: {
authorization: 'Basic ' + (new Buffer('sg1002:pwtest', 'utf8')).toString('base64')
}
});
expect(response.statusCode).to.equal(401);
});
test('Should not have access to the route, with overriden policy targeting the username', async () => {
const response = await server.inject({
method: 'GET',
url: '/overriden-policy',
headers: {
authorization: 'Basic ' + (new Buffer('sg1001:pwtest', 'utf8')).toString('base64')
}
});
expect(response.statusCode).to.equal(401);
});
test('Should have access to the route, with overriden policy targeting the username', async () => {
const response = await server.inject({
method: 'GET',
url: '/overriden-policy',
headers: {
authorization: 'Basic ' + (new Buffer('sg1002:pwtest', 'utf8')).toString('base64')
}
});
expect(response.statusCode).to.equal(200);
});
test('Should allow access to the route with params', async () => {
const response = await server.inject({
method: 'GET',
url: '/route-params/validParam',
headers: {
authorization: 'Basic ' + (new Buffer('sg1002:pwtest', 'utf8')).toString('base64')
}
});
expect(response.statusCode).to.equal(200);
});
test('Should deny access to the route with denied param', async () => {
const response = await server.inject({
method: 'GET',
url: '/route-params/forbiddenParam',
headers: {
authorization: 'Basic ' + (new Buffer('sg1002:pwtest', 'utf8')).toString('base64')
}
});
expect(response.statusCode).to.equal(401);
});
test('Should allow access to the route with query param', async () => {
const response = await server.inject({
method: 'GET',
url: '/route-query-params?param1=validParam',
headers: {
authorization: 'Basic ' + (new Buffer('sg1002:pwtest', 'utf8')).toString('base64')
}
});
expect(response.statusCode).to.equal(200);
});
test('Should deny access to the route with denied query param', async () => {
const response = await server.inject({
method: 'GET',
url: '/route-query-params?param1=forbiddenParam',
headers: {
authorization: 'Basic ' + (new Buffer('sg1002:pwtest', 'utf8')).toString('base64')
}
});
expect(response.statusCode).to.equal(401);
});
test('Should deny access to the route with get request method', async () => {
const response = await server.inject({
method: 'GET',
url: '/route-request-get-deny',
headers: {
authorization: 'Basic ' + (new Buffer('sg1002:pwtest', 'utf8')).toString('base64')
}
});
expect(response.statusCode).to.equal(401);
});
test('Should allow access to the route with get request method', async () => {
const response = await server.inject({
method: 'GET',
url: '/route-request-get-allow',
headers: {
authorization: 'Basic ' + (new Buffer('sg1002:pwtest', 'utf8')).toString('base64')
}
});
expect(response.statusCode).to.equal(200);
});
test('Should deny access to the route with get request method', async () => {
const response = await server.inject({
method: 'GET',
url: '/route-non-existing-field',
headers: {
authorization: 'Basic ' + (new Buffer('sg1002:pwtest', 'utf8')).toString('base64')
}
});
expect(response.statusCode).to.equal(401);
});
test('Should allow access to the route with matching param and query fields', async () => {
const response = await server.inject({
method: 'GET',
url: '/match-fields/123?test=123',
headers: {
authorization: 'Basic ' + (new Buffer('sg1002:pwtest', 'utf8')).toString('base64')
}
});
expect(response.statusCode).to.equal(200);
});
test('Should deny access to the route with non-matching param and query fields', async () => {
const response = await server.inject({
method: 'GET',
url: '/match-fields/123?test=456',
headers: {
authorization: 'Basic ' + (new Buffer('sg1002:pwtest', 'utf8')).toString('base64')
}
});
expect(response.statusCode).to.equal(401);
});
test('Should have access to the route, with policy targeting a regex of last name', async () => {
const response = await server.inject({
method: 'GET',
url: '/regex-match',
headers: {
authorization: 'Basic ' + (new Buffer('sg1002:pwtest', 'utf8')).toString('base64')
}
});
expect(response.statusCode).to.equal(200);
});
test('Should not have access to the route, with policy targeting a regex of last name', async () => {
const response = await server.inject({
method: 'GET',
url: '/regex-match',
headers: {
authorization: 'Basic ' + (new Buffer('sg1001:pwtest', 'utf8')).toString('base64')
}
});
expect(response.statusCode).to.equal(401);
});
});
/**
* Rule based access control policy tests, with callback function configuration
**/
experiment('Global dynamic RBAC policy with callback function', () => {
let server;
before(async () => {
const users = {};
users.sg1001 = {
'scope': 'admin',
'firstName': 'Some',
'lastName': 'Guy',
'username': 'sg1001',
'password': 'pwtest',
'group': ['reader']
};
users.sg1002 = {
'scope': 'admin',
'firstName': 'Some',
'lastName': 'Other Guy',
'username': 'sg1002',
'password': 'pwtest',
'group': ['reader']
};
// Set up the hapi server route
server = await createServer(users, {
policy: () => {
/* Usually retrieved from a DB... */
return {
target: {'credentials:username': 'sg1001'},
apply: 'permit-overrides',
rules: [
{
'effect': 'permit'
}
]
};
}
});
// No policy configured -> use global configuration
server.route({
method: 'GET',
path: '/endpoint',
handler: (request, h) => h.response({ok: true})
});
// Policy configured -> use route configuration (ignore global configuration)
server.route({
method: 'GET',
path: '/overriden-policy',
handler: (request, h) => h.response({ok: true}),
config: {
plugins: {
rbac: () => {
/* Usually retrieved from a DB... */
return {
target: {'credentials:username': 'sg1002'},
apply: 'permit-overrides',
rules: [
{
'effect': 'permit'
}
]
};
}
}
}
});
// Policy disabled -> do not use access control (ignore global configuration)
server.route({
method: 'GET',
path: '/disabled-ac',
handler: (request, h) => h.response({ok: true}),
config: {
plugins: {
rbac: 'none'
}
}
});
});
test('Should have access to the route, with policy targeting the username', async () => {
const response = await server.inject({
method: 'GET',
url: '/endpoint',
headers: {
authorization: 'Basic ' + (new Buffer('sg1001:pwtest', 'utf8')).toString('base64')
}
});
expect(response.statusCode).to.equal(200);
});
test('Should not have access to the route, with policy targeting the username', async () => {
const response = await server.inject({
method: 'GET',
url: '/endpoint',
headers: {
authorization: 'Basic ' + (new Buffer('sg1002:pwtest', 'utf8')).toString('base64')
}
});
expect(response.statusCode).to.equal(401);
});
test('Should not have access to the route, with overriden policy targeting the username', async () => {
const response = await server.inject({
method: 'GET',
url: '/overriden-policy',
headers: {
authorization: 'Basic ' + (new Buffer('sg1001:pwtest', 'utf8')).toString('base64')
}
});
expect(response.statusCode).to.equal(401);
});
test('Should have access to the route, with overriden policy targeting the username', async () => {
const response = await server.inject({
method: 'GET',
url: '/overriden-policy',
headers: {
authorization: 'Basic ' + (new Buffer('sg1002:pwtest', 'utf8')).toString('base64')
}
});
expect(response.statusCode).to.equal(200);
});
test('Should have access to the route, with disabled ac', async () => {
const response = await server.inject({
method: 'GET',
url: '/disabled-ac',
headers: {
authorization: 'Basic ' + (new Buffer('sg1002:pwtest', 'utf8')).toString('base64')
}
});
expect(response.statusCode).to.equal(200);
});
});