├── .npmrc
├── .travis.yml
├── .gitignore
├── example
├── files
│ ├── static403.html
│ └── template403.marko
├── 4 - caching.js
├── 1 - simple.js
├── 2 - hierarchy.js
├── 3 - withHTTPBasicAuth.js
├── 6 - withHapiFormAuthentication.js
└── 5 - customForbiddenPage.js
├── test
├── test.cache.js
├── test.options.js
├── test.authorization.js
└── integration.js
├── lib
├── cache.js
├── options.js
└── authorization.js
├── package.json
├── index.js
└── README.md
/.npmrc:
--------------------------------------------------------------------------------
1 | registry=https://registry.npmjs.org/
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "8"
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .idea
3 | npm-debug.log
4 | coverage
5 | .DS_Store
--------------------------------------------------------------------------------
/example/files/static403.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Go away!
5 |
6 |
7 |
8 |
9 | Oops :/
10 |
11 |
12 | Looks like you're not allowed to be here.
13 |
14 |
15 |
--------------------------------------------------------------------------------
/example/files/template403.marko:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Go away!
5 |
6 |
7 |
8 |
9 | Oops :/
10 |
11 |
12 | Sorry ${input.firstName}, you can't handle this page!
13 |
14 |
15 |
--------------------------------------------------------------------------------
/test/test.cache.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const path = require('path')
4 | const chai = require('chai')
5 | chai.should()
6 |
7 | let cache
8 |
9 | describe('cache.js', function () {
10 | beforeEach(function () {
11 | cache = require(path.join(__dirname, '..', 'lib', 'cache.js'))()
12 | })
13 | afterEach(function () {
14 | cache.reset()
15 | cache = undefined
16 | })
17 | it('cache should initialize', function () {
18 | cache.should.not.be.null
19 | })
20 | it('cache should store value', function () {
21 | cache.set('foo', 'bar')
22 | cache.get('foo').should.equal('bar')
23 | })
24 | })
25 |
--------------------------------------------------------------------------------
/lib/cache.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const LRU = require('lru-cache')
4 | const deepExtend = require('deep-extend')
5 | const debug = require('debug')('hapi-acl-auth:cache')
6 |
7 | let _cache
8 |
9 | const defaultOptions = {
10 | max: 500,
11 | length: function (n, key) {
12 | return n * 2 + key.length
13 | },
14 | maxAge: 1000 * 60 * 60
15 | }
16 |
17 | module.exports = function (options) {
18 | if (!_cache) {
19 | const cacheOptions = deepExtend({}, defaultOptions, options)
20 | debug('cacheOptions: ')
21 | debug(cacheOptions)
22 | _cache = LRU(cacheOptions)
23 | }
24 | return _cache
25 | }
26 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hapi-acl-auth",
3 | "version": "1.0.5",
4 | "description": "Authentication agnostic authorization plugin for HapiJS",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "mocha"
8 | },
9 | "author": "",
10 | "license": "ISC",
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/charlesread/hapi-acl-auth"
14 | },
15 | "bugs": {
16 | "url": "https://github.com/charlesread/hapi-acl-auth/issues",
17 | "email": "charles@charlesread.com"
18 | },
19 | "keywords": [
20 | "hapi",
21 | "authorization",
22 | "acl",
23 | "auth"
24 | ],
25 | "dependencies": {
26 | "boom": "^7.1.1",
27 | "debug": "^3.1.0",
28 | "deep-extend": "^0.5.0",
29 | "joi": "^13.1.1",
30 | "lodash": "^4.17.4",
31 | "lru-cache": "^4.1.1",
32 | "type-detect": "^4.0.7"
33 | },
34 | "devDependencies": {
35 | "chai": "^4.1.2",
36 | "hapi": "^17.2.0",
37 | "mocha": "^5.0.0",
38 | "pre-commit": "^1.2.2",
39 | "request": "^2.83.0"
40 | },
41 | "standard": {
42 | "env": [
43 | "mocha"
44 | ],
45 | "ignore": [
46 | "test/**"
47 | ]
48 | },
49 | "pre-commit": [
50 | "test"
51 | ],
52 | "optionalDependencies": {
53 | "hapi-auth-basic": "^5.0.0",
54 | "marko": "^4.7.5"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/lib/options.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const util = require('util')
4 | const deepExtend = require('deep-extend')
5 | const joi = require('joi')
6 | const debug = require('debug')('hapi-acl-auth:options')
7 |
8 | const pluginDefaults = {
9 | any: true,
10 | all: false,
11 | policy: 'deny',
12 | secure: true
13 | }
14 |
15 | const optionsSchema = joi.object().keys({
16 | handler: joi.func().required(),
17 | roles: joi.alternatives().try(joi.array(), joi.func()),
18 | any: joi.boolean().required(),
19 | all: joi.boolean().required(),
20 | forbiddenPageFunction: joi.func().optional(),
21 | hierarchy: joi.array().items(joi.string()).optional(),
22 | secure: joi.boolean().required(),
23 | policy: joi.string().valid('allow', 'deny').required(),
24 | cache: joi.boolean().optional(),
25 | allowUnauthenticated: joi.boolean().optional(),
26 | exempt: joi.array().optional()
27 | })
28 |
29 | module.exports = function (req, options) {
30 | const routeOptions = req.route.settings.plugins['hapi-acl-auth'] || req.route.settings.plugins['hapiAclAuth'] || {}
31 | const pluginOptions = deepExtend({}, pluginDefaults, options)
32 | const combinedOptions = deepExtend({}, pluginOptions, routeOptions)
33 | debug('routeOptions:')
34 | debug(routeOptions)
35 | debug('pluginOptions:')
36 | debug(pluginOptions)
37 | debug('combinedOptions:')
38 | debug(combinedOptions)
39 | const _options = joi.validate(combinedOptions, optionsSchema)
40 | if (_options.error) {
41 | throw new Error(util.format('options could not be validated: %s', _options.error.message))
42 | }
43 | return {routeOptions, pluginOptions, combinedOptions}
44 | }
45 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const pjson = require('./package.json')
4 |
5 | const path = require('path')
6 | const Boom = require('boom')
7 | const debug = require('debug')('hapi-acl-auth:plugin')
8 |
9 | const options = require(path.join(__dirname, 'lib', 'options.js'))
10 | const authorization = require(path.join(__dirname, 'lib', 'authorization.js'))
11 |
12 | const plugin = {}
13 |
14 | plugin.register = async function (server, opts) {
15 | let result
16 | debug('opts:')
17 | debug(opts)
18 | server.ext('onPostAuth', async function (req, h) {
19 | try {
20 | debug('request for %s caught', req.path)
21 | if (opts.exempt && opts.exempt.includes(req.path)) {
22 | debug('%s is exempt', req.path)
23 | return h.continue
24 | } else {
25 | debug('%s is not exempt', req.path)
26 | }
27 | if (opts.allowUnauthenticated && !req.auth.isAuthenticated) {
28 | return h.continue
29 | }
30 | const {routeOptions, pluginOptions, combinedOptions} = options(req, opts)
31 | if ((pluginOptions.policy === 'deny' && combinedOptions.secure) || (pluginOptions.policy === 'allow' && routeOptions.secure)) {
32 | debug('policy if statement has evaluated to true')
33 | const handlerResult = combinedOptions.handler(req)
34 | const handlerObject = handlerResult.then ? await handlerResult : handlerResult
35 | debug('handlerObject:')
36 | debug(handlerObject)
37 | const isAuthorized = await authorization.determineAuthorization(combinedOptions, handlerObject, req)
38 | debug('isAuthorized: %s', isAuthorized)
39 | if (!isAuthorized) {
40 | const forbiddenResult = combinedOptions.forbiddenPageFunction ? combinedOptions.forbiddenPageFunction(handlerObject, req, h) : Boom.forbidden()
41 | result = forbiddenResult && forbiddenResult.then ? await forbiddenResult : forbiddenResult
42 | } else {
43 | result = h.continue
44 | }
45 | } else {
46 | debug('policy if statement has evaluated to false')
47 | result = h.continue
48 | }
49 | } catch (e) {
50 | throw e
51 | }
52 | debug('result:')
53 | debug(result)
54 | return result
55 | })
56 | }
57 |
58 | plugin.pkg = pjson
59 |
60 | module.exports = plugin
61 |
--------------------------------------------------------------------------------
/example/4 - caching.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /*
4 |
5 | Simple example with caching, also displays the ability for `roles` to be a function.
6 |
7 | The `roles` attributes in both the plugin and route options can be either arrays of strings, functions that return
8 | arrays, or functions that return Promises that resolve arrays. If the functions are do resource intensive operations,
9 | or operations that can take some time, it might make sense to cache the results so that each request does not have
10 | to wait on the function to return. The `cache` plugin attribute will enable caching of the roles, increasing
11 | response time and generally just being more responsible with resources.
12 |
13 | Policy is `deny`, by default.
14 | User has one role ('admin').
15 | User has role required by /admin.
16 |
17 | */
18 |
19 | const Hapi = require('hapi')
20 |
21 | const server = new Hapi.Server()
22 | server.connection({
23 | host: 'localhost',
24 | port: 8000
25 | })
26 |
27 | const plugins = [
28 | {
29 | register: require('../index'),
30 | options: {
31 | handler: function (request, callback) {
32 | callback(null, {username: 'cread', roles: ['admin']})
33 | },
34 | cache: true
35 | }
36 | }
37 | ]
38 |
39 | server.register(
40 | plugins,
41 | function (err) {
42 | if (err) {
43 | throw err
44 | }
45 |
46 | // notice that the first request to this route will take 5000 milliseconds to respond, subsequent
47 | // request will not, because the result of the function is cached
48 | server.route({
49 | method: 'GET',
50 | path: '/admin',
51 | handler: function (request, reply) {
52 | return reply('admin')
53 | },
54 | config: {
55 | plugins: {
56 | hapiAclAuth: {
57 | roles: function () {
58 | return new Promise((resolve) => {
59 | // simulating some task that takes some time, like hitting a DB for roles for this route
60 | setTimeout(function () {
61 | resolve(['admin1'])
62 | }, 5000)
63 | })
64 | }
65 | }
66 | }
67 | }
68 | })
69 | })
70 |
71 | server.start((err) => {
72 | if (err) {
73 | throw err
74 | }
75 | console.log('Server running at:', server.info.uri)
76 | })
77 |
--------------------------------------------------------------------------------
/example/1 - simple.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /*
4 |
5 | Simple example.
6 |
7 | Policy is `deny`, by default.
8 | User has one role ('admin').
9 | User has role required by /admin.
10 | User does not have role required by /superuser.
11 | Endpoint /notsecure has secure: false, thus overriding the default deny policy.
12 |
13 | */
14 |
15 | const Hapi = require('hapi')
16 |
17 | const server = Hapi.server({
18 | host: 'localhost',
19 | port: 8000
20 | })
21 |
22 | ;(async function () {
23 | await server.register({
24 | plugin: require('hapi-acl-auth'),
25 | options: {
26 | handler: async function () {
27 | return {user: 'cread', roles: ['admin']}
28 | },
29 | // optional, dy default a simple 403 will be returned when not authorized
30 | forbiddenPageFunction: async function (credentials, request, h) {
31 | // some fancy "logging"
32 | console.log('%s (roles: %s) wanted %s (requires %s) but was not allowed', credentials.user, credentials.roles, request.path, request.route.settings.plugins['hapiAclAuth'].roles)
33 | // some fancy error page
34 | const response = h.response('Not Authorized!
')
35 | response.code(200)
36 | return response.takeover()
37 | }
38 | }
39 | })
40 | server.route({
41 | method: 'get',
42 | path: '/admin',
43 | handler: async function (request, h) {
44 | return 'Welcome to /admin!
'
45 | },
46 | config: {
47 | plugins: {
48 | hapiAclAuth: {
49 | roles: ['admin']
50 | }
51 | }
52 | }
53 | })
54 | server.route({
55 | method: 'get',
56 | path: '/superuser',
57 | handler: async function (request, h) {
58 | return 'Welcome to /superuser!
'
59 | },
60 | config: {
61 | plugins: {
62 | hapiAclAuth: {
63 | roles: ['superuser']
64 | }
65 | }
66 | }
67 | })
68 | server.route({
69 | method: 'get',
70 | path: '/notsecure',
71 | handler: async function (request, h) {
72 | return 'Welcome to /notsecure!
'
73 | },
74 | config: {
75 | plugins: {
76 | hapiAclAuth: {
77 | secure: false
78 | }
79 | }
80 | }
81 | })
82 | await server.start()
83 | })()
84 | .then(function () {
85 | console.log('server started: %s', server.info.uri)
86 | })
87 | .catch(function (err) {
88 | console.error(err.message)
89 | })
90 |
--------------------------------------------------------------------------------
/lib/authorization.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const path = require('path')
4 | const type = require('type-detect')
5 | const _ = require('lodash')
6 | const debug = require('debug')('hapi-acl-auth:authorization')
7 |
8 | const cache = require(path.join(__dirname, 'cache.js'))()
9 |
10 | module.exports = {
11 | determineAuthorization: async function (combinedOptions, handlerObject, req) {
12 | debug('combinedOptions:')
13 | debug(combinedOptions)
14 | debug('handlerObject:')
15 | debug(handlerObject)
16 | let allowed = []
17 | let actual = []
18 | const cachedData = cache.get(req.path)
19 | if (cachedData && combinedOptions.cache) {
20 | allowed = cachedData.allowed
21 | actual = cachedData.actual
22 | } else {
23 | if (type(combinedOptions.roles) === 'Array') {
24 | allowed = combinedOptions.roles
25 | } else if (type(combinedOptions.roles) === 'string') {
26 | allowed.push(combinedOptions.roles)
27 | } else if (type(combinedOptions.roles) === 'function') {
28 | const res = combinedOptions.roles(handlerObject, req)
29 | allowed = res.then ? await res : res
30 | }
31 | if (type(handlerObject.roles) === 'Array') {
32 | actual = handlerObject.roles
33 | } else if (type(handlerObject.roles) === 'string') {
34 | actual.push(handlerObject.roles)
35 | } else if (type(handlerObject.roles) === 'function') {
36 | const res = handlerObject.roles(handlerObject, req)
37 | actual = res.then ? await res : res
38 | }
39 | if (combinedOptions.cache) {
40 | cache.set(req.path, {allowed, actual})
41 | }
42 | }
43 | debug('allowed:')
44 | debug(allowed)
45 | debug('actual:')
46 | debug(actual)
47 | if (combinedOptions.hierarchy) {
48 | let lowestAllowedIndex = 50
49 | let highestActualIndex = -1
50 | for (let actualRole of actual) {
51 | let i = combinedOptions.hierarchy.indexOf(actualRole)
52 | if (i >= 0 && i > highestActualIndex) {
53 | highestActualIndex = i
54 | }
55 | }
56 | for (let allowedRole of allowed) {
57 | let i = combinedOptions.hierarchy.indexOf(allowedRole)
58 | if (i >= 0 && i < lowestAllowedIndex) {
59 | lowestAllowedIndex = i
60 | }
61 | }
62 | debug('highestActualIndex: %s, lowestAllowedIndex: %s', highestActualIndex, lowestAllowedIndex)
63 | return (highestActualIndex >= lowestAllowedIndex)
64 | }
65 | const intersection = _.intersection(allowed, actual)
66 | debug('intersection: %j', intersection)
67 | if (combinedOptions.all) {
68 | return (intersection.length === allowed.length)
69 | }
70 | return (combinedOptions.any && intersection.length > 0)
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/example/2 - hierarchy.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /*
4 |
5 | Example using a hierarchy of roles.
6 |
7 | Policy is `deny`, by default.
8 | User has one role ('admin').
9 | User does not explicitly have the role 'user', but because the hierarchy places 'admin' as higher privilege
10 | than 'user' the user will be authorized for /user
11 | User has role required by /admin.
12 | User does not have role required by /superuser.
13 | Endpoint /notsecure has secure: false, thus overriding the default deny policy.
14 |
15 | */
16 |
17 | const Hapi = require('hapi')
18 |
19 | const server = new Hapi.Server()
20 | server.connection({
21 | host: 'localhost',
22 | port: 8000
23 | })
24 |
25 | const plugins = [
26 | {
27 | register: require('../index'),
28 | options: {
29 | handler: function (request, callback) {
30 | callback(null, {username: 'cread', roles: ['admin']})
31 | },
32 | hierarchy: ['user', 'admin', 'superuser']
33 | }
34 | }
35 | ]
36 |
37 | server.register(
38 | plugins,
39 | function (err) {
40 | if (err) {
41 | throw err
42 | }
43 |
44 | // should return 200
45 | server.route({
46 | method: 'GET',
47 | path: '/user',
48 | handler: function (request, reply) {
49 | return reply('user')
50 | },
51 | config: {
52 | plugins: {
53 | hapiAclAuth: {
54 | roles: ['user']
55 | }
56 | }
57 | }
58 | })
59 |
60 | // should return 200
61 | server.route({
62 | method: 'GET',
63 | path: '/admin',
64 | handler: function (request, reply) {
65 | return reply('admin')
66 | },
67 | config: {
68 | plugins: {
69 | hapiAclAuth: {
70 | roles: ['admin']
71 | }
72 | }
73 | }
74 | })
75 |
76 | // should return 403
77 | server.route({
78 | method: 'GET',
79 | path: '/superuser',
80 | handler: function (request, reply) {
81 | return reply('superuser')
82 | },
83 | config: {
84 | plugins: {
85 | hapiAclAuth: {
86 | roles: ['superuser']
87 | }
88 | }
89 | }
90 | })
91 |
92 | // should return 200
93 | server.route({
94 | method: 'GET',
95 | path: '/notsecure',
96 | config: {
97 | plugins: {
98 | hapiAclAuth: {
99 | secure: false
100 | }
101 | }
102 | },
103 | handler: function (request, reply) {
104 | return reply('notsecure')
105 | }
106 | })
107 | })
108 |
109 | server.start((err) => {
110 | if (err) {
111 | throw err
112 | }
113 | console.log('Server running at:', server.info.uri)
114 | })
115 |
--------------------------------------------------------------------------------
/example/3 - withHTTPBasicAuth.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /*
4 |
5 | Simple example using HTTP Basic for authentication.
6 | In this example we are "getting" the roles for the user in validateFunc, really just specifying them, they will
7 | go into the request.auth.credentials object, which will later be used in the hapi-acl-auth handler. In practice
8 | you would probably get roles from some other service, like a DB, during the authentication process.
9 |
10 | Policy is `deny`, by default.
11 | User has one role ('admin').
12 | User has role required by /admin.
13 | User does not have role required by /superuser.
14 | Endpoint /notsecure has secure: false, thus overriding the default deny policy.
15 |
16 | */
17 |
18 | const Hapi = require('hapi')
19 |
20 | const server = new Hapi.Server()
21 | server.connection({
22 | host: 'localhost',
23 | port: 8000
24 | })
25 |
26 | const plugins = [
27 | {
28 | register: require('../index'),
29 | options: {
30 | handler: function (request, callback) {
31 | // request.auth.credentials just happens to be the object passed to callback() in validateFunc
32 | callback(null, request.auth.credentials)
33 | }
34 | }
35 | },
36 | {
37 | register: require('hapi-auth-basic')
38 | }
39 | ]
40 |
41 | const validateFunc = function (request, username, password, callback) {
42 | callback(null, true, {id: username, name: username, roles: ['admin']})
43 | }
44 |
45 | server.register(
46 | plugins,
47 | function (err) {
48 | if (err) {
49 | throw err
50 | }
51 |
52 | server.auth.strategy('simple', 'basic', {validateFunc: validateFunc})
53 |
54 | server.route({
55 | method: 'GET',
56 | path: '/admin',
57 | handler: function (request, reply) {
58 | return reply('admin')
59 | },
60 | config: {
61 | auth: 'simple',
62 | plugins: {
63 | hapiAclAuth: {
64 | roles: ['admin']
65 | }
66 | }
67 | }
68 | })
69 |
70 | server.route({
71 | method: 'GET',
72 | path: '/superuser',
73 | handler: function (request, reply) {
74 | return reply('superuser')
75 | },
76 | config: {
77 | auth: 'simple',
78 | plugins: {
79 | hapiAclAuth: {
80 | roles: ['superuser']
81 | }
82 | }
83 | }
84 | })
85 |
86 | server.route({
87 | method: 'GET',
88 | path: '/notsecure',
89 | config: {
90 | plugins: {
91 | hapiAclAuth: {
92 | secure: false
93 | }
94 | }
95 | },
96 | handler: function (request, reply) {
97 | return reply('notsecure')
98 | }
99 | })
100 | })
101 |
102 | server.start((err) => {
103 | if (err) {
104 | throw err
105 | }
106 | console.log('Server running at:', server.info.uri)
107 | })
108 |
--------------------------------------------------------------------------------
/example/6 - withHapiFormAuthentication.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /*
4 |
5 | Simple example using hapi-form-authentication for authentication.
6 |
7 | Policy is `deny`, by default.
8 | User has one role ('admin').
9 | User has role required by /admin.
10 | User does not have role required by /superuser.
11 | Endpoint /notsecure has secure: false, thus overriding the default deny policy.
12 |
13 | */
14 |
15 | const Hapi = require('hapi')
16 |
17 | const server = new Hapi.Server()
18 | server.connection({
19 | host: 'localhost',
20 | port: 8000
21 | })
22 |
23 | const plugins = [
24 | {
25 | register: require('hapi-form-authentication'),
26 | options: {
27 | handler: function (username, password, callback) {
28 | // if the password is "password" let them in
29 | const isValid = password === 'password'
30 | // the callback takes two parameters; the first is a simple Boolean
31 | // that indicates whether or not the user is valid, the second is an
32 | // object that must contain, at a minimum, a `username` attribute,
33 | // this object will accessible as `request.auth.credentials` in routes
34 | callback(isValid, {username: username})
35 | }
36 | }
37 | },
38 | {
39 | register: require('../index'),
40 | options: {
41 | handler: function (request, callback) {
42 | callback(null, {username: request.auth.credentials.username, roles: ['admin']})
43 | },
44 | allowUnauthenticated: true
45 | }
46 | }
47 | ]
48 |
49 | server.register(
50 | plugins,
51 | function (err) {
52 | if (err) {
53 | throw err
54 | }
55 |
56 | server.auth.strategy('form', 'form')
57 |
58 | server.route({
59 | method: 'GET',
60 | path: '/admin',
61 | handler: function (request, reply) {
62 | return reply('admin')
63 | },
64 | config: {
65 | auth: 'form',
66 | plugins: {
67 | hapiAclAuth: {
68 | roles: ['admin']
69 | }
70 | }
71 | }
72 | })
73 |
74 | server.route({
75 | method: 'GET',
76 | path: '/superuser',
77 | handler: function (request, reply) {
78 | return reply('superuser')
79 | },
80 | config: {
81 | auth: 'form',
82 | plugins: {
83 | hapiAclAuth: {
84 | roles: ['superuser']
85 | }
86 | }
87 | }
88 | })
89 |
90 | server.route({
91 | method: 'GET',
92 | path: '/notsecure',
93 | config: {
94 | plugins: {
95 | hapiAclAuth: {
96 | secure: false
97 | }
98 | }
99 | },
100 | handler: function (request, reply) {
101 | return reply('notsecure')
102 | }
103 | })
104 | })
105 |
106 | server.start((err) => {
107 | if (err) {
108 | throw err
109 | }
110 | console.log('Server running at:', server.info.uri)
111 | })
112 |
--------------------------------------------------------------------------------
/example/5 - customForbiddenPage.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /*
4 |
5 | Simple example that shows how you can render a custom page when not authorized.
6 |
7 | Policy is `deny`, by default.
8 | User has one role ('admin').
9 | User has role required by /admin.
10 | User does not have role required by /superuser.
11 | Endpoint /notsecure has secure: false, thus overriding the default deny policy.
12 |
13 | */
14 |
15 | // allowing Marko template requires
16 | require('marko/node-require')
17 | require('marko/compiler').defaultOptions.writeToDisk = false
18 |
19 | const fs = require('fs')
20 | const path = require('path')
21 | const Hapi = require('hapi')
22 |
23 | const server = new Hapi.Server()
24 | server.connection({
25 | host: 'localhost',
26 | port: 8000
27 | })
28 |
29 | const plugins = [
30 | {
31 | register: require('../index'),
32 | options: {
33 | handler: function (request, callback) {
34 | callback(null, {username: 'cread', firstName: 'Charles', roles: ['user']})
35 | },
36 | // used at the plugin options level will make this the default, it can be overridden in the routes, see below
37 | forbiddenPageFunction: function () {
38 | return fs.createReadStream(path.join(__dirname, 'files', 'static403.html'))
39 | }
40 | }
41 | }
42 | ]
43 |
44 | server.register(
45 | plugins,
46 | function (err) {
47 | if (err) {
48 | throw err
49 | }
50 |
51 | server.route({
52 | method: 'GET',
53 | path: '/admin',
54 | handler: function (request, reply) {
55 | return reply('admin')
56 | },
57 | config: {
58 | plugins: {
59 | hapiAclAuth: {
60 | roles: ['admin']
61 | }
62 | }
63 | }
64 | })
65 |
66 | // here we're just using Marko to render dynamic content, notice that the function assigned to forbiddenPageFunction
67 | // actually accepts a parameter; the object passed to callback(err, object) in the plugin handler also gets passed
68 | // to the forbiddenPageFunction function, {username: 'cread', firstName: 'Charles', roles: ['user']} in this case
69 | server.route({
70 | method: 'GET',
71 | path: '/superuser',
72 | handler: function (request, reply) {
73 | return reply('superuser')
74 | },
75 | config: {
76 | plugins: {
77 | hapiAclAuth: {
78 | roles: ['superuser'],
79 | forbiddenPageFunction: function (handlerObject) {
80 | // grab the Marko template
81 | const page = require(path.join(__dirname, 'files', 'template403.marko'))
82 | // pass the object from callback(err, object) to the template so that it can be used in the template
83 | return page.stream(handlerObject)
84 | }
85 | }
86 | }
87 | }
88 | })
89 |
90 | server.route({
91 | method: 'GET',
92 | path: '/notsecure',
93 | config: {
94 | plugins: {
95 | hapiAclAuth: {
96 | secure: false
97 | }
98 | }
99 | },
100 | handler: function (request, reply) {
101 | return reply('notsecure')
102 | }
103 | })
104 | })
105 |
106 | server.start((err) => {
107 | if (err) {
108 | throw err
109 | }
110 | console.log('Server running at:', server.info.uri)
111 | })
112 |
--------------------------------------------------------------------------------
/test/test.options.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const chai = require('chai')
4 |
5 | chai.should()
6 |
7 | const path = require('path')
8 |
9 | const options = require(path.join(__dirname, '..', 'lib', 'options.js'))
10 |
11 | describe('options.js', function () {
12 | describe('Joi validation', function () {
13 | it('should throw when nothing is passed', function () {
14 | const req = {}
15 | req.route = {
16 | settings: {
17 | plugins: {
18 | 'hapi-acl-auth': undefined
19 | }
20 | }
21 | }
22 | const pluginOptions = {}
23 | try {
24 | options(req, pluginOptions)
25 | } catch (err) {
26 | err.should.not.be.null
27 | }
28 | })
29 | it('should not throw when handler and roles are passed in route options', function () {
30 | const req = {}
31 | req.route = {
32 | settings: {
33 | plugins: {
34 | 'hapi-acl-auth': {
35 | handler: function () {
36 | },
37 | roles: []
38 | }
39 | }
40 | }
41 | }
42 | const pluginOptions = {}
43 | try {
44 | options(req, pluginOptions)
45 | } catch (err) {
46 | err.should.be.null
47 | }
48 | })
49 | it('should not throw when handler and roles are passed in plugin options', function () {
50 | const req = {}
51 | req.route = {
52 | settings: {
53 | plugins: {}
54 | }
55 | }
56 | const pluginOptions = {
57 | handler: function () {
58 | },
59 | roles: []
60 | }
61 | try {
62 | options(req, pluginOptions)
63 | } catch (err) {
64 | err.should.be.null
65 | }
66 | })
67 | it('policy should be deny when not specified in plugin options', function () {
68 | const req = {}
69 | req.route = {
70 | settings: {
71 | plugins: {}
72 | }
73 | }
74 | const _pluginOptions = {
75 | handler: function () {
76 | },
77 | roles: []
78 | }
79 | const {combinedOptions} = options(req, _pluginOptions)
80 | combinedOptions.policy.should.equal('deny')
81 | })
82 | it('secure should be false when overridden in route options', function () {
83 | const req = {}
84 | req.route = {
85 | settings: {
86 | plugins: {
87 | 'hapiAclAuth': {
88 | secure: false
89 | }
90 | }
91 | }
92 | }
93 | const _pluginOptions = {
94 | handler: function () {
95 | },
96 | roles: []
97 | }
98 | const {combinedOptions} = options(req, _pluginOptions)
99 | combinedOptions.secure.should.equal(false)
100 | })
101 | })
102 | describe('general functionality', function () {
103 | it('roles in combinedOptions should match route roles when overridden in route', function () {
104 | const req = {}
105 | req.route = {
106 | settings: {
107 | plugins: {
108 | 'hapiAclAuth': {
109 | roles: ['a', 'b']
110 | }
111 | }
112 | }
113 | }
114 | const _pluginOptions = {
115 | handler: function () {
116 | },
117 | roles: ['a']
118 | }
119 | const {combinedOptions} = options(req, _pluginOptions)
120 | req.route.settings.plugins['hapiAclAuth'].roles.should.eql(combinedOptions.roles)
121 | })
122 | it('roles in combinedOptions should match plugin roles not when overridden in route', function () {
123 | const req = {}
124 | req.route = {
125 | settings: {
126 | plugins: {}
127 | }
128 | }
129 | const _pluginOptions = {
130 | handler: function () {
131 | },
132 | roles: ['a']
133 | }
134 | const {combinedOptions} = options(req, _pluginOptions)
135 | _pluginOptions.roles.should.eql(combinedOptions.roles)
136 | })
137 | it('roles in plugin options should be able to be a function', function () {
138 | const req = {}
139 | req.route = {
140 | settings: {
141 | plugins: {}
142 | }
143 | }
144 | const _pluginOptions = {
145 | handler: function () {
146 | },
147 | roles: function () {
148 | return ['a']
149 | }
150 | }
151 | options(req, _pluginOptions)
152 | })
153 | it('roles in plugin options and route options should be able to be a function', function () {
154 | const req = {}
155 | req.route = {
156 | settings: {
157 | plugins: {
158 | hapiAclAuth: {
159 | roles: function () {
160 | return ['a']
161 | }
162 | }
163 | }
164 | }
165 | }
166 | const _pluginOptions = {
167 | handler: function () {
168 | },
169 | roles: function () {
170 | return ['a']
171 | }
172 | }
173 | options(req, _pluginOptions)
174 | })
175 | })
176 | })
177 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | [](https://travis-ci.org/charlesread/hapi-acl-auth)
3 |
4 |
5 | ----------
6 |
7 |
8 | **IMPORTANT: Please see [this issue](https://github.com/charlesread/hapi-acl-auth/issues/6).**
9 |
10 |
11 | ----------
12 |
13 |
14 |
15 | Note: most examples in the examples directory are for previous versions, although the logic will still be the same, they will not work with versions 1.x of the plugin.
16 |
17 |
18 | # hapi-acl-auth
19 |
20 | I didn't like how tightly coupled other `hapi` ACL authorization plugins were to authentication mechanisms, so I wrote my own that doesn't care what authentication mechanism that you use, or even if you use an authentication mechanism at all (although that would be a bit dumb).
21 |
22 | Basically you just tell the plugin what roles a user has, what roles an endpoint allows, and you're set.
23 |
24 | Cool stuff that `hapi-acl-auth` gives you:
25 |
26 | * The ability to lock down the entire application, or just a few routes
27 | * Typical any/all functionality (allow if user has _any_ of these roles, allow if users has _all_ of these roles, for example)
28 | * Specifying a hierarchy of roles ("admins" are clearly "users" too, so let them through without explicitly letting "admins" through, for example)
29 | * The ability to have custom forbidden pages
30 | * Caching of roles for performance
31 | * [And so much more!](https://www.google.com/search?q=but+wait+there%27s+more&tbm=isch&tbo=u&source=univ&sa=X&ved=0ahUKEwiBjtj4tJvVAhXEPD4KHbPyCN4QsAQIJw&biw=1440&bih=776)
32 |
33 | Check out the [example](https://github.com/charlesread/hapi-acl-auth/tree/master/example) directory for examples!
34 |
35 |
36 |
37 | - [Installation](#installation)
38 | - [Utilization](#utilization)
39 | - [Change Log](#change-log)
40 | * [1.0.x](#10x)
41 | - [Plugin/Route Configuration Options](#pluginroute-configuration-options)
42 | * [Plugin _and_ route options](#plugin-_and_-route-options)
43 | * [Plugin _only_ options](#plugin-_only_-options)
44 | * [Route _only_ options](#route-_only_-options)
45 |
46 |
47 |
48 | ## Installation
49 |
50 | ```bash
51 | npm i -S hapi-acl-auth
52 | ```
53 |
54 | ## Utilization
55 |
56 | ```js
57 | 'use strict'
58 |
59 | /*
60 |
61 | Simple example.
62 |
63 | Policy is `deny`, by default.
64 | User has one role ('admin').
65 | User has role required by /admin.
66 | User does not have role required by /superuser.
67 | Endpoint /notsecure has secure: false, thus overriding the default deny policy.
68 |
69 | */
70 |
71 | const Hapi = require('hapi')
72 |
73 | const server = Hapi.server({
74 | host: 'localhost',
75 | port: 8000
76 | })
77 |
78 | !async function () {
79 | await server.register({
80 | plugin: require('hapi-acl-auth'),
81 | options: {
82 | handler: async function () {
83 | return {user: 'cread', roles: ['admin']}
84 | },
85 | // optional, dy default a simple 403 will be returned when not authorized
86 | forbiddenPageFunction: async function (credentials, request, h) {
87 | // some fancy "logging"
88 | console.log('%s (roles: %s) wanted %s (requires %s) but was not allowed', credentials.user, credentials.roles, request.path, request.route.settings.plugins['hapiAclAuth'].roles)
89 | // some fancy error page
90 | const response = h.response('Not Authorized!
')
91 | response.code(200)
92 | return response.takeover()
93 | }
94 | }
95 | })
96 | server.route({
97 | method: 'get',
98 | path: '/admin',
99 | handler: async function (request, h) {
100 | return 'Welcome to /admin!
'
101 | },
102 | config: {
103 | plugins: {
104 | hapiAclAuth: {
105 | roles: ['admin']
106 | }
107 | }
108 | }
109 | })
110 | server.route({
111 | method: 'get',
112 | path: '/superuser',
113 | handler: async function (request, h) {
114 | return 'Welcome to /superuser!
'
115 | },
116 | config: {
117 | plugins: {
118 | hapiAclAuth: {
119 | roles: ['superuser']
120 | }
121 | }
122 | }
123 | })
124 | server.route({
125 | method: 'get',
126 | path: '/notsecure',
127 | handler: async function (request, h) {
128 | return 'Welcome to /notsecure!
'
129 | },
130 | config: {
131 | plugins: {
132 | hapiAclAuth: {
133 | secure: false
134 | }
135 | }
136 | }
137 | })
138 | await server.start()
139 | }()
140 | .then(function () {
141 | console.log('server started: %s', server.info.uri)
142 | })
143 | .catch(function (err) {
144 | console.error(err.message)
145 | })
146 | ```
147 |
148 | ## Change Log
149 |
150 | ### 1.0.x
151 | * `hapi-acl-auth` is now fully compatible with Hapi version 17 and above
152 | * All `function`s have been replaced with `async function`s
153 | * If you need Hapi version 16 support see the 0.x releases
154 |
155 | ## Plugin/Route Configuration Options
156 |
157 | Most options can be specified at the plugin level, or for each individual route. In other words, you can "lock down" every route all at once, or at each route, or both (with route config overriding plugin config).
158 |
159 | ### Plugin _and_ route options
160 |
161 | | Name | Type | Default | Allowed Values | Description |
162 | | --- | --- | --- | --- | --- |
163 | | handler (required) | `[async] function(request)` | | | This function must return an object (referred to as `handlerObject` henceforth), the object can be arbitrary, but it must contain a `roles` attribute that is an Array (or a function that returns an array) of roles that are allowed for the route (or routes, if configured in the plugin options).|
164 | | roles (required) | `Array`|`[async] function(handlerObject, request)` | | | An `Array` of roles (or an `[async] function` that returns an array of roles) that are allowed for the route or routes. *NOTE*: this attribute can be set at the plugin or route level, if it is set at the plugin level it will apply to _all_ routes, if set on an individual route it only applies to that route, but you can set a "policy" at the plugin level and then override it in individual routes should you so desire. |
165 | | any | `Boolean` | `true` | `true`|`false` | Apecifies whether a user may possess _any_ of the allowed roles in order to be authorized. |
166 | | all | `Boolean` | `false` | `true`|`false` | Apecifies whether a user _must_ possess _all_ of the allowed routes in order to be authorized. |
167 | | hierarchy | `Array` | | | An `Array` that specifies the privilege hierarchy of roles in order of ascending privilege. For instance, suppose we have `hierarchy: ['user', 'admin', 'superuser]` configured for a route and `roles: ['admin']` configured for that same route. A user with the `superuser` role will be able to access that route because the `superuser` role is of higher privilege than the `admin` role, as specified in the hierarchy. |
168 | | forbiddenPageFunction | `[async] function(handlerObject, request, h)` | | | By default the plugin will respond with a plain `Boom.forbidden()`, so you can use this function to override that behavior and do whatever you want. It is worth noting that if you use this function it is your responsibility to respond to the request. Thus you must return an error (preferably a Boom), a takeover response, or a continue signal.|
169 | | cache | `Boolean` | `false` | `true`|`false` | If caching is enabled the `roles` arrays will be cached, this is helpful if you use resource intensive functions to return roles in the `handler` function or the `roles` attribute |
170 | | allowUnauthenticated | `Boolean` | `false` | `true`|`false` | `hapi-acl-auth` makes use of the `onPostAuth` extension point, basically it does its processing to determine whether or not a user should have access before Hapi responds to a request. If you're using an authentication plugin for Hapi, or anything else really, that performs a redirect in order to authenticate, `hapi-acl-auth` will, depending on the value of `policy`, respond with a 403 before a user _has even been authenticated_. The `allowUnauthenticated` option, when set to `true`, will allow requests where `request.auth.isAuthenticated` is `false` to proceed so that any authentication redirects can occur. |
171 |
172 | ### Plugin _only_ options
173 |
174 | | Name | Type | Default | Allowed Values | Description |
175 | | --- | --- | --- | --- | --- |
176 | | policy | `string` | "deny" | "deny"|"allow" | The policy that the plugin should follow. If "deny" all routes will be secure, if "allow" all routes will be insecure. This can be overridden with the `secure` option in a route. |
177 |
178 | ### Route _only_ options
179 |
180 |
181 | | Name | Type | Default | Allowed Values | Description |
182 | | --- | --- | --- | --- | --- |
183 | | secure | `Boolean` | `true` | `true`|`false` | Indicates whether or not a route should be secure, i.e. if the plugin should be used on a particular route. |
184 |
--------------------------------------------------------------------------------
/test/test.authorization.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const assert = require('assert')
4 | const path = require('path')
5 | const type = require('type-detect')
6 |
7 | const cache = require(path.join(__dirname, '..', 'lib', 'cache.js'))()
8 | const authorization = require(path.join(__dirname, '..', 'lib', 'authorization.js'))
9 |
10 | const routeOptions = {}
11 | const callbackObject = {}
12 | const req = {path: '/'}
13 |
14 | describe('authorization.js', function () {
15 | afterEach(function () {
16 | cache.reset()
17 | })
18 | describe('#determineAuthorization()', function () {
19 | describe('any: true', function () {
20 | it('should return true if several roles are allowed and user has one', function () {
21 | routeOptions.any = true
22 | routeOptions.roles = ['foo', 'bar', 'baz']
23 | callbackObject.roles = ['foo']
24 | authorization.determineAuthorization(routeOptions, callbackObject, req)
25 | .then((v) => {
26 | assert.equal(true, v)
27 | })
28 | })
29 | it('should return false if several roles are allowed and user has none', function () {
30 | routeOptions.any = true
31 | routeOptions.roles = ['foo', 'bar', 'baz']
32 | callbackObject.roles = ['bat']
33 | authorization.determineAuthorization(routeOptions, callbackObject, req)
34 | .then((v) => {
35 | assert.equal(false, v)
36 | })
37 | })
38 | })
39 | describe('all: false', function () {
40 | beforeEach(function () {
41 | cache.reset()
42 | })
43 | afterEach(function () {
44 | cache.reset()
45 | })
46 | it('should return true if several roles are allowed and user has one', function () {
47 | routeOptions.all = false
48 | routeOptions.roles = ['foo', 'bar', 'baz']
49 | callbackObject.roles = ['foo']
50 | authorization.determineAuthorization(routeOptions, callbackObject, req)
51 | .then((v) => {
52 | assert.equal(true, v)
53 | })
54 | })
55 | it('should return false if several roles are allowed and user has none', function () {
56 | routeOptions.all = false
57 | routeOptions.roles = ['foo', 'bar', 'baz']
58 | callbackObject.roles = ['bat']
59 | authorization.determineAuthorization(routeOptions, callbackObject, req)
60 | .then((v) => {
61 | assert.equal(false, v)
62 | })
63 | })
64 | })
65 | describe('all: true', function () {
66 | it('should return true if several roles are allowed and user all', function () {
67 | routeOptions.all = true
68 | routeOptions.roles = ['foo', 'bar', 'baz']
69 | callbackObject.roles = ['foo', 'bar', 'baz']
70 | authorization.determineAuthorization(routeOptions, callbackObject, req)
71 | .then((v) => {
72 | assert.equal(true, v)
73 | })
74 | })
75 | it('should return false if several roles are allowed and user has none', function () {
76 | routeOptions.all = true
77 | routeOptions.roles = ['foo', 'bar', 'baz']
78 | callbackObject.roles = []
79 | authorization.determineAuthorization(routeOptions, callbackObject, req)
80 | .then((v) => {
81 | assert.equal(false, v)
82 | })
83 | })
84 | it('should return false if several roles are allowed and user has only one', function () {
85 | routeOptions.all = true
86 | routeOptions.roles = ['foo', 'bar', 'baz']
87 | callbackObject.roles = ['foo']
88 | authorization.determineAuthorization(routeOptions, callbackObject, req)
89 | .then((v) => {
90 | assert.equal(false, v)
91 | })
92 | })
93 | })
94 | describe('all: true, any: true, all should take precedence', function () {
95 | it('should return true if several roles are allowed and user all', function () {
96 | routeOptions.any = true
97 | routeOptions.all = true
98 | routeOptions.roles = ['foo', 'bar', 'baz']
99 | callbackObject.roles = ['foo', 'bar', 'baz']
100 | authorization.determineAuthorization(routeOptions, callbackObject, req)
101 | .then((v) => {
102 | assert.equal(true, v)
103 | })
104 | })
105 | it('should return false if several roles are allowed and user has none', function () {
106 | routeOptions.any = true
107 | routeOptions.all = true
108 | routeOptions.roles = ['foo', 'bar', 'baz']
109 | callbackObject.roles = []
110 | authorization.determineAuthorization(routeOptions, callbackObject, req)
111 | .then((v) => {
112 | assert.equal(false, v)
113 | })
114 | })
115 | it('should return false if several roles are allowed and user has only one', function () {
116 | routeOptions.any = true
117 | routeOptions.all = true
118 | routeOptions.roles = ['foo', 'bar', 'baz']
119 | callbackObject.roles = ['foo']
120 | authorization.determineAuthorization(routeOptions, callbackObject, req)
121 | .then((v) => {
122 | assert.equal(false, v)
123 | })
124 | })
125 | })
126 | describe('hierarchy', function () {
127 | it('should return true when actual role is more privileged than least privileged allowed role', function () {
128 | routeOptions.hierarchy = ['reporter', 'user', 'admin', 'superuser']
129 | routeOptions.roles = ['superuser', 'user']
130 | callbackObject.roles = ['admin']
131 | authorization.determineAuthorization(routeOptions, callbackObject, req)
132 | .then((v) => {
133 | assert.equal(true, v)
134 | })
135 | })
136 | it('should return true when actual role is equal to an allowed role', function () {
137 | routeOptions.hierarchy = ['reporter', 'user', 'admin', 'superuser']
138 | routeOptions.roles = ['superuser', 'admin']
139 | callbackObject.roles = ['admin']
140 | authorization.determineAuthorization(routeOptions, callbackObject, req)
141 | .then((v) => {
142 | assert.equal(true, v)
143 | })
144 | })
145 | it('should return true when actual roles are more privileged than least privileged allowed role', function () {
146 | routeOptions.hierarchy = ['reporter', 'user', 'admin', 'superuser']
147 | routeOptions.roles = ['reporter', 'user']
148 | callbackObject.roles = ['admin', 'superuser']
149 | authorization.determineAuthorization(routeOptions, callbackObject, req)
150 | .then((v) => {
151 | assert.equal(true, v)
152 | })
153 | })
154 | it('should return false when actual role is less privileged than least privileged allowed role', function () {
155 | routeOptions.hierarchy = ['reporter', 'user', 'admin', 'superuser']
156 | routeOptions.roles = ['superuser', 'admin']
157 | callbackObject.roles = ['user']
158 | authorization.determineAuthorization(routeOptions, callbackObject, req)
159 | .then((v) => {
160 | assert.equal(false, v)
161 | })
162 | })
163 | it('should return false when actual roles are less privileged than least privileged allowed role', function () {
164 | routeOptions.hierarchy = ['reporter', 'user', 'admin', 'superuser']
165 | routeOptions.roles = ['superuser', 'admin']
166 | callbackObject.roles = ['user', 'reporter']
167 | authorization.determineAuthorization(routeOptions, callbackObject, req)
168 | .then((v) => {
169 | assert.equal(false, v)
170 | })
171 | })
172 | })
173 | describe('roles as function', function () {
174 | it('plugin/route roles is function, callback roles is array, user has access', function () {
175 | routeOptions.roles = function (cbo) {
176 | assert(cbo)
177 | return ['reporter', 'user', 'admin', 'superuser']
178 | }
179 | callbackObject.roles = ['user']
180 | authorization.determineAuthorization(routeOptions, callbackObject, req)
181 | .then((v) => {
182 | assert.equal(true, v)
183 | })
184 | })
185 | it('plugin/route roles is array, callback roles is function, user has access', function () {
186 | routeOptions.roles = ['reporter', 'user', 'admin', 'superuser']
187 | callbackObject.roles = function (cbo) {
188 | assert(cbo)
189 | return ['user']
190 | }
191 | authorization.determineAuthorization(routeOptions, callbackObject, req)
192 | .then((v) => {
193 | assert.equal(true, v)
194 | })
195 | })
196 | it('plugin/route roles is function, callback roles is array, user does not have access', function () {
197 | routeOptions.roles = function (cbo) {
198 | assert(cbo)
199 | return ['admin', 'superuser']
200 | }
201 | callbackObject.roles = ['user']
202 | authorization.determineAuthorization(routeOptions, callbackObject, req)
203 | .then((v) => {
204 | assert.equal(false, v)
205 | })
206 | })
207 | it('plugin/route roles is array, callback roles is function, user does not have access', function () {
208 | routeOptions.roles = ['admin', 'superuser']
209 | callbackObject.roles = function (cbo) {
210 | assert(cbo)
211 | return ['user']
212 | }
213 | authorization.determineAuthorization(routeOptions, callbackObject, req)
214 | .then((v) => {
215 | assert.equal(false, v)
216 | })
217 | })
218 | describe('roles functions return a promise', function () {
219 | it('should work when combinedOptions roles function returns promise', function (done) {
220 | routeOptions.roles = function (cbo) {
221 | assert(cbo)
222 | return new Promise((resolve) => {
223 | setTimeout(function () {
224 | resolve(['user'])
225 | }, 1000)
226 | })
227 | }
228 | callbackObject.roles = function (cbo) {
229 | assert(cbo)
230 | return ['user']
231 | }
232 | authorization.determineAuthorization(routeOptions, callbackObject, req)
233 | .then((v) => {
234 | assert.equal(true, v)
235 | done()
236 | })
237 | })
238 | it('should work when callbackObject roles function returns promise', function (done) {
239 | routeOptions.roles = function (cbo) {
240 | assert(cbo)
241 | return ['user']
242 | }
243 | callbackObject.roles = function (cbo) {
244 | assert(cbo)
245 | return new Promise((resolve) => {
246 | setTimeout(function () {
247 | resolve(['user'])
248 | }, 500)
249 | })
250 | }
251 | authorization.determineAuthorization(routeOptions, callbackObject, req)
252 | .then((v) => {
253 | assert.equal(true, v)
254 | done()
255 | })
256 | })
257 | it('should work when both roles functions return promises', function (done) {
258 | routeOptions.roles = function (cbo) {
259 | assert(cbo)
260 | return new Promise((resolve) => {
261 | setTimeout(function () {
262 | resolve(['user'])
263 | }, 500)
264 | })
265 | }
266 | callbackObject.roles = function (cbo) {
267 | assert(cbo)
268 | return new Promise((resolve) => {
269 | setTimeout(function () {
270 | resolve(['user'])
271 | }, 500)
272 | })
273 | }
274 | authorization.determineAuthorization(routeOptions, callbackObject, req)
275 | .then((v) => {
276 | assert.equal(true, v)
277 | done()
278 | })
279 | })
280 | })
281 | })
282 | })
283 | })
284 |
--------------------------------------------------------------------------------
/test/integration.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const chai = require('chai')
4 |
5 | chai.should()
6 |
7 | const path = require('path')
8 | const Hapi = require('hapi')
9 | const boom = require('boom')
10 | const request = require('request')
11 |
12 | let server
13 |
14 | const plugin = require(path.join(__dirname, '..', 'index.js'))
15 | const cache = require(path.join(__dirname, '..', 'lib', 'cache.js'))()
16 |
17 | const url = 'http://localhost:9999/protected'
18 | const method = 'get'
19 |
20 | describe('integration testing', function () {
21 |
22 | beforeEach(function (done) {
23 | server = Hapi.server({
24 | host: 'localhost',
25 | port: 9999
26 | })
27 | done()
28 | })
29 | afterEach(function (done) {
30 | cache.reset()
31 | server.stop({timeout: 5000})
32 | .then(function () {
33 | done()
34 | })
35 | .catch(function (err) {
36 | console.error(err.message)
37 | done()
38 | })
39 | })
40 |
41 | it('secure endpoint should return 403 when required route role does not match user role', function (done) {
42 | server.register({
43 | plugin: plugin,
44 | options: {
45 | handler: async function (req) {
46 | return {username: 'cread', roles: ['USER']}
47 | }
48 | }
49 | })
50 | .then(function () {
51 | server.route({
52 | method: 'get',
53 | path: '/protected',
54 | config: {
55 | plugins: {
56 | hapiAclAuth: {
57 | roles: ['ADMIN'],
58 | secure: true
59 | }
60 | }
61 | },
62 | handler: function (req, h) {
63 | return 'protected'
64 | }
65 | })
66 | return Promise.resolve()
67 | })
68 | .then(function () {
69 | return server.start()
70 | })
71 | .then(function () {
72 | request({url, method},
73 | function (err, httpResponse, body) {
74 | if (err) throw err
75 | httpResponse.statusCode.should.equal(403)
76 | done()
77 | return true
78 | }
79 | )
80 | })
81 | .catch(function (err) {
82 | console.error(err.message)
83 | console.error(err.stack)
84 | if (err) throw err
85 | })
86 | })
87 |
88 | it('secure endpoint should return 403 when no required route roles match any user roles', function (done) {
89 | server.register({
90 | plugin: plugin,
91 | options: {
92 | handler: async function (req) {
93 | return {username: 'cread', roles: ['USER', 'REPORTER']}
94 | }
95 | }
96 | })
97 | .then(function () {
98 | server.route({
99 | method: 'get',
100 | path: '/protected',
101 | config: {
102 | plugins: {
103 | hapiAclAuth: {
104 | roles: ['ADMIN'],
105 | secure: true
106 | }
107 | }
108 | },
109 | handler: function (req, h) {
110 | return 'protected'
111 | }
112 | })
113 | return Promise.resolve()
114 | })
115 | .then(function () {
116 | return server.start()
117 | })
118 | .then(function () {
119 | request({url, method},
120 | function (err, httpResponse, body) {
121 | if (err) throw err
122 | httpResponse.statusCode.should.equal(403)
123 | done()
124 | return true
125 | }
126 | )
127 | })
128 | .catch(function (err) {
129 | console.error(err.message)
130 | console.error(err.stack)
131 | if (err) throw err
132 | })
133 | })
134 |
135 | it('secure endpoint should return 200 when A required route role DOES match user role', function (done) {
136 | server.register({
137 | plugin: plugin,
138 | options: {
139 | handler: async function (req) {
140 | return {username: 'cread', roles: ['USER']}
141 | }
142 | }
143 | })
144 | .then(function () {
145 | server.route({
146 | method: 'get',
147 | path: '/protected',
148 | config: {
149 | plugins: {
150 | hapiAclAuth: {
151 | roles: ['USER'],
152 | secure: true
153 | }
154 | }
155 | },
156 | handler: function (req, h) {
157 | return 'protected'
158 | }
159 | })
160 | return Promise.resolve()
161 | })
162 | .then(function () {
163 | return server.start()
164 | })
165 | .then(function () {
166 | request({url, method},
167 | function (err, httpResponse, body) {
168 | if (err) throw err
169 | httpResponse.statusCode.should.equal(200)
170 | done()
171 | return true
172 | }
173 | )
174 | })
175 | .catch(function (err) {
176 | console.error(err.message)
177 | console.error(err.stack)
178 | if (err) throw err
179 | })
180 | })
181 |
182 | it('secure endpoint should return 200 when ANY required route role DOES match a user role', function (done) {
183 | server.register({
184 | plugin: plugin,
185 | options: {
186 | handler: async function (req) {
187 | return {username: 'cread', roles: ['USER']}
188 | }
189 | }
190 | })
191 | .then(function () {
192 | server.route({
193 | method: 'get',
194 | path: '/protected',
195 | config: {
196 | plugins: {
197 | hapiAclAuth: {
198 | roles: ['USER', 'ADMIN'],
199 | secure: true,
200 | any: true //default
201 | }
202 | }
203 | },
204 | handler: function (req, h) {
205 | return 'protected'
206 | }
207 | })
208 | return Promise.resolve()
209 | })
210 | .then(function () {
211 | return server.start()
212 | })
213 | .then(function () {
214 | request({url, method},
215 | function (err, httpResponse, body) {
216 | if (err) throw err
217 | httpResponse.statusCode.should.equal(200)
218 | done()
219 | return true
220 | }
221 | )
222 | })
223 | .catch(function (err) {
224 | console.error(err.message)
225 | console.error(err.stack)
226 | if (err) throw err
227 | })
228 | })
229 |
230 | it('secure endpoint should return 200 when ALL required route roles DO match ALL user roles', function (done) {
231 | server.register({
232 | plugin: plugin,
233 | options: {
234 | handler: async function (req) {
235 | return {username: 'cread', roles: ['USER', 'pizza']}
236 | }
237 | }
238 | })
239 | .then(function () {
240 | server.route({
241 | method: 'get',
242 | path: '/protected',
243 | config: {
244 | plugins: {
245 | hapiAclAuth: {
246 | roles: ['USER', 'pizza'],
247 | secure: true,
248 | all: true
249 | }
250 | }
251 | },
252 | handler: function (req, h) {
253 | return 'protected'
254 | }
255 | })
256 | return Promise.resolve()
257 | })
258 | .then(function () {
259 | return server.start()
260 | })
261 | .then(function () {
262 | request({url, method},
263 | function (err, httpResponse, body) {
264 | if (err) throw err
265 | httpResponse.statusCode.should.equal(200)
266 | done()
267 | return true
268 | }
269 | )
270 | })
271 | .catch(function (err) {
272 | console.error(err.message)
273 | console.error(err.stack)
274 | if (err) throw err
275 | })
276 | })
277 |
278 | it('secure endpoint should return 403 when ALL required route roles DO NOT match ALL user roles', function (done) {
279 | server.register({
280 | plugin: plugin,
281 | options: {
282 | handler: async function (req) {
283 | return {username: 'cread', roles: ['USER', 'pizza']}
284 | }
285 | }
286 | })
287 | .then(function () {
288 | server.route({
289 | method: 'get',
290 | path: '/protected',
291 | config: {
292 | plugins: {
293 | hapiAclAuth: {
294 | roles: ['USER', 'pizza', 'cheese'],
295 | secure: true,
296 | all: true
297 | }
298 | }
299 | },
300 | handler: function (req, h) {
301 | return 'protected'
302 | }
303 | })
304 | return Promise.resolve()
305 | })
306 | .then(function () {
307 | return server.start()
308 | })
309 | .then(function () {
310 | request({url, method},
311 | function (err, httpResponse, body) {
312 | if (err) throw err
313 | httpResponse.statusCode.should.equal(403)
314 | done()
315 | return true
316 | }
317 | )
318 | })
319 | .catch(function (err) {
320 | console.error(err.message)
321 | console.error(err.stack)
322 | if (err) throw err
323 | })
324 | })
325 |
326 | it('insecure endpoint should return 200 when policy is allow', function (done) {
327 | server.register({
328 | plugin: plugin,
329 | options: {
330 | handler: async function (req) {
331 | return {username: 'cread', roles: ['USER', 'pizza']}
332 | },
333 | policy: 'allow'
334 | }
335 | })
336 | .then(function () {
337 | server.route({
338 | method: 'get',
339 | path: '/insecure',
340 | handler: async function (req, h) {
341 | return 'insecure'
342 | }
343 | })
344 | return Promise.resolve()
345 | })
346 | .then(function () {
347 | return server.start()
348 | })
349 | .then(function () {
350 | request({url: 'http://localhost:9999/insecure', method},
351 | function (err, httpResponse, body) {
352 | if (err) throw err
353 | httpResponse.statusCode.should.equal(200)
354 | done()
355 | return true
356 | }
357 | )
358 | })
359 | .catch(function (err) {
360 | console.error(err.message)
361 | console.error(err.stack)
362 | if (err) throw err
363 | })
364 | })
365 |
366 | it('insecure endpoint should return 403 when policy is deny', function (done) {
367 | server.register({
368 | plugin: plugin,
369 | options: {
370 | handler: async function (req) {
371 | return {username: 'cread', roles: ['USER', 'pizza']}
372 | },
373 | policy: 'deny'
374 | }
375 | })
376 | .then(function () {
377 | server.route({
378 | method: 'get',
379 | path: '/insecure',
380 | handler: async function (req, h) {
381 | return 'insecure'
382 | }
383 | })
384 | return Promise.resolve()
385 | })
386 | .then(function () {
387 | return server.start()
388 | })
389 | .then(function () {
390 | request({url: 'http://localhost:9999/insecure', method},
391 | function (err, httpResponse, body) {
392 | if (err) throw err
393 | httpResponse.statusCode.should.equal(403)
394 | done()
395 | return true
396 | }
397 | )
398 | })
399 | .catch(function (err) {
400 | console.error(err.message)
401 | console.error(err.stack)
402 | if (err) throw err
403 | })
404 | })
405 |
406 | it('insecure endpoint should return 200 when policy is deny but route has secure as false', function (done) {
407 | server.register({
408 | plugin: plugin,
409 | options: {
410 | handler: async function (req) {
411 | return {username: 'cread', roles: ['USER', 'pizza']}
412 | },
413 | policy: 'deny'
414 | }
415 | })
416 | .then(function () {
417 | server.route({
418 | method: 'get',
419 | path: '/insecure',
420 | config: {
421 | plugins: {
422 | hapiAclAuth: {
423 | secure: false
424 | }
425 | }
426 | },
427 | handler: async function (req, h) {
428 | return 'insecure'
429 | }
430 | })
431 | return Promise.resolve()
432 | })
433 | .then(function () {
434 | return server.start()
435 | })
436 | .then(function () {
437 | request({url: 'http://localhost:9999/insecure', method},
438 | function (err, httpResponse, body) {
439 | if (err) throw err
440 | httpResponse.statusCode.should.equal(200)
441 | done()
442 | return true
443 | }
444 | )
445 | })
446 | .catch(function (err) {
447 | console.error(err.message)
448 | console.error(err.stack)
449 | if (err) throw err
450 | })
451 | })
452 |
453 | it('when a hierarchy is used a higher privileged role should be able to access a route with a lower privileged role', function (done) {
454 | server.register({
455 | plugin: plugin,
456 | options: {
457 | handler: async function (req) {
458 | return {username: 'cread', roles: ['ADMIN']}
459 | },
460 | hierarchy: ['USER', 'ADMIN', 'SUPERUSER']
461 | }
462 | })
463 | .then(function () {
464 | server.route({
465 | method: 'get',
466 | path: '/protected',
467 | config: {
468 | plugins: {
469 | hapiAclAuth: {
470 | roles: ['USER']
471 | }
472 | }
473 | },
474 | handler: async function (req, h) {
475 | return 'insecure'
476 | }
477 | })
478 | return Promise.resolve()
479 | })
480 | .then(function () {
481 | return server.start()
482 | })
483 | .then(function () {
484 | request({url, method},
485 | function (err, httpResponse, body) {
486 | if (err) throw err
487 | httpResponse.statusCode.should.equal(200)
488 | done()
489 | return true
490 | }
491 | )
492 | })
493 | .catch(function (err) {
494 | console.error(err.message)
495 | console.error(err.stack)
496 | if (err) throw err
497 | })
498 | })
499 |
500 | it('if policy is set to allow then a route with no config should not be secure, even if other options should deny (if not overridden in route)', function (done) {
501 | server.register({
502 | plugin: plugin,
503 | options: {
504 | handler: async function (req) {
505 | return {username: 'cread', roles: ['USER']}
506 | },
507 | hierarchy: ['USER', 'ADMIN', 'SUPERUSER'],
508 | policy: 'allow'
509 | }
510 | })
511 | .then(function () {
512 | server.route({
513 | method: 'get',
514 | path: '/protected',
515 | config: {
516 | plugins: {
517 | hapiAclAuth: {
518 | roles: ['ADMIN'],
519 | // if secure: true the test should fail
520 | }
521 | }
522 | },
523 | handler: async function (req, h) {
524 | return 'insecure'
525 | }
526 | })
527 | return Promise.resolve()
528 | })
529 | .then(function () {
530 | return server.start()
531 | })
532 | .then(function () {
533 | request({url, method},
534 | function (err, httpResponse, body) {
535 | if (err) throw err
536 | httpResponse.statusCode.should.equal(200)
537 | done()
538 | return true
539 | }
540 | )
541 | })
542 | .catch(function (err) {
543 | console.error(err.message)
544 | console.error(err.stack)
545 | if (err) throw err
546 | })
547 | })
548 |
549 |
550 | it('when a hierarchy is used a lower privileged role should NOT be able to access a route with a lower privileged role', function (done) {
551 | server.register({
552 | plugin: plugin,
553 | options: {
554 | handler: async function (req) {
555 | return {username: 'cread', roles: ['USER']}
556 | },
557 | hierarchy: ['USER', 'ADMIN', 'SUPERUSER']
558 | }
559 | })
560 | .then(function () {
561 | server.route({
562 | method: 'get',
563 | path: '/protected',
564 | config: {
565 | plugins: {
566 | hapiAclAuth: {
567 | roles: ['ADMIN']
568 | }
569 | }
570 | },
571 | handler: async function (req, h) {
572 | return 'insecure'
573 | }
574 | })
575 | return Promise.resolve()
576 | })
577 | .then(function () {
578 | return server.start()
579 | })
580 | .then(function () {
581 | request({url, method},
582 | function (err, httpResponse, body) {
583 | if (err) throw err
584 | httpResponse.statusCode.should.equal(403)
585 | done()
586 | return true
587 | }
588 | )
589 | })
590 | .catch(function (err) {
591 | console.error(err.message)
592 | console.error(err.stack)
593 | if (err) throw err
594 | })
595 | })
596 |
597 | it('if policy is set to allow then a route with no config should be secure if secure: true is in route config: user does NOT have appropriate role', function (done) {
598 | server.register({
599 | plugin: plugin,
600 | options: {
601 | handler: async function (req) {
602 | return {username: 'cread', roles: ['USER']}
603 | },
604 | hierarchy: ['USER', 'ADMIN', 'SUPERUSER'],
605 | policy: 'allow'
606 | }
607 | })
608 | .then(function () {
609 | server.route({
610 | method: 'get',
611 | path: '/protected',
612 | config: {
613 | plugins: {
614 | hapiAclAuth: {
615 | roles: ['ADMIN'],
616 | secure: true
617 | }
618 | }
619 | },
620 | handler: async function (req, h) {
621 | return 'insecure'
622 | }
623 | })
624 | return Promise.resolve()
625 | })
626 | .then(function () {
627 | return server.start()
628 | })
629 | .then(function () {
630 | request({url, method},
631 | function (err, httpResponse, body) {
632 | if (err) throw err
633 | httpResponse.statusCode.should.equal(403)
634 | done()
635 | return true
636 | }
637 | )
638 | })
639 | .catch(function (err) {
640 | console.error(err.message)
641 | console.error(err.stack)
642 | if (err) throw err
643 | })
644 | })
645 |
646 | it('if policy is set to allow then a route with no config should be secure if secure: true is in route config: user DOES have appropriate role', function (done) {
647 | server.register({
648 | plugin: plugin,
649 | options: {
650 | handler: async function (req) {
651 | return {username: 'cread', roles: ['USER']}
652 | },
653 | hierarchy: ['USER', 'ADMIN', 'SUPERUSER'],
654 | policy: 'allow'
655 | }
656 | })
657 | .then(function () {
658 | server.route({
659 | method: 'get',
660 | path: '/protected',
661 | config: {
662 | plugins: {
663 | hapiAclAuth: {
664 | roles: ['SUPERUSER'],
665 | secure: true
666 | }
667 | }
668 | },
669 | handler: async function (req, h) {
670 | return 'insecure'
671 | }
672 | })
673 | return Promise.resolve()
674 | })
675 | .then(function () {
676 | return server.start()
677 | })
678 | .then(function () {
679 | request({url, method},
680 | function (err, httpResponse, body) {
681 | if (err) throw err
682 | httpResponse.statusCode.should.equal(403)
683 | done()
684 | return true
685 | }
686 | )
687 | })
688 | .catch(function (err) {
689 | console.error(err.message)
690 | console.error(err.stack)
691 | if (err) throw err
692 | })
693 | })
694 |
695 |
696 | it('enabling the cache shouldn\'t kill everything', function (done) {
697 | server.register({
698 | plugin: plugin,
699 | options: {
700 | handler: async function (req) {
701 | return {username: 'cread', roles: ['USER']}
702 | },
703 | hierarchy: ['USER', 'ADMIN', 'SUPERUSER'],
704 | policy: 'allow',
705 | cache: true
706 | }
707 | })
708 | .then(function () {
709 | server.route({
710 | method: 'get',
711 | path: '/protected',
712 | config: {
713 | plugins: {
714 | hapiAclAuth: {
715 | roles: ['SUPERUSER'],
716 | secure: true
717 | }
718 | }
719 | },
720 | handler: async function (req, h) {
721 | return 'insecure'
722 | }
723 | })
724 | return Promise.resolve()
725 | })
726 | .then(function () {
727 | return server.start()
728 | })
729 | .then(function () {
730 | request({url, method},
731 | function (err, httpResponse, body) {
732 | if (err) throw err
733 | httpResponse.statusCode.should.equal(403)
734 | done()
735 | return true
736 | }
737 | )
738 | })
739 | .catch(function (err) {
740 | console.error(err.message)
741 | console.error(err.stack)
742 | if (err) throw err
743 | })
744 | })
745 |
746 | it('if cache is enabled it should contain a cached path object', function (done) {
747 | server.register({
748 | plugin: plugin,
749 | options: {
750 | handler: async function (req) {
751 | return {username: 'cread', roles: ['USER']}
752 | },
753 | hierarchy: ['USER', 'ADMIN', 'SUPERUSER'],
754 | policy: 'allow',
755 | cache: true
756 | }
757 | })
758 | .then(function () {
759 | server.route({
760 | method: 'get',
761 | path: '/protected',
762 | config: {
763 | plugins: {
764 | hapiAclAuth: {
765 | roles: ['SUPERUSER'],
766 | secure: true
767 | }
768 | }
769 | },
770 | handler: async function (req, h) {
771 | return 'insecure'
772 | }
773 | })
774 | return Promise.resolve()
775 | })
776 | .then(function () {
777 | return server.start()
778 | })
779 | .then(function () {
780 | request({url, method},
781 | function (err, httpResponse, body) {
782 | if (err) throw err
783 | httpResponse.statusCode.should.equal(403)
784 | cache.get('/protected').should.have.own.property('allowed')
785 | cache.get('/protected').should.have.own.property('actual')
786 | done()
787 | return true
788 | }
789 | )
790 | })
791 | .catch(function (err) {
792 | console.error(err.message)
793 | console.error(err.stack)
794 | if (err) throw err
795 | })
796 | })
797 |
798 | it('secure endpoint should return 403 when required route role does not match user role and forbiddenPageFunction returns a 403 Boom', function (done) {
799 | server.register({
800 | plugin: plugin,
801 | options: {
802 | handler: async function (req) {
803 | return {username: 'cread', roles: ['USER']}
804 | },
805 | forbiddenPageFunction: async function (credentials, req, h) {
806 | return boom.forbidden()
807 | }
808 | }
809 | })
810 | .then(function () {
811 | server.route({
812 | method: 'get',
813 | path: '/protected',
814 | config: {
815 | plugins: {
816 | hapiAclAuth: {
817 | roles: ['ADMIN'],
818 | secure: true
819 | }
820 | }
821 | },
822 | handler: function (req, h) {
823 | return 'protected'
824 | }
825 | })
826 | return Promise.resolve()
827 | })
828 | .then(function () {
829 | return server.start()
830 | })
831 | .then(function () {
832 | request({url, method},
833 | function (err, httpResponse, body) {
834 | if (err) throw err
835 | httpResponse.statusCode.should.equal(403)
836 | done()
837 | return true
838 | }
839 | )
840 | })
841 | .catch(function (err) {
842 | console.error(err.message)
843 | console.error(err.stack)
844 | if (err) throw err
845 | })
846 | })
847 |
848 | it('secure endpoint should return 407 when required route role does not match user role and forbiddenPageFunction returns a 407 response', function (done) {
849 | server.register({
850 | plugin: plugin,
851 | options: {
852 | handler: async function (req) {
853 | return {username: 'cread', roles: ['USER']}
854 | },
855 | forbiddenPageFunction: async function (credentials, req, h) {
856 | const response = h.response()
857 | response.code(407)
858 | return response.takeover()
859 | }
860 | }
861 | })
862 | .then(function () {
863 | server.route({
864 | method: 'get',
865 | path: '/protected',
866 | config: {
867 | plugins: {
868 | hapiAclAuth: {
869 | roles: ['ADMIN'],
870 | secure: true
871 | }
872 | }
873 | },
874 | handler: function (req, h) {
875 | return 'protected'
876 | }
877 | })
878 | return Promise.resolve()
879 | })
880 | .then(function () {
881 | return server.start()
882 | })
883 | .then(function () {
884 | request({url, method},
885 | function (err, httpResponse, body) {
886 | if (err) throw err
887 | httpResponse.statusCode.should.equal(407)
888 | done()
889 | return true
890 | }
891 | )
892 | })
893 | .catch(function (err) {
894 | console.error(err.message)
895 | console.error(err.stack)
896 | if (err) throw err
897 | })
898 | })
899 |
900 | })
--------------------------------------------------------------------------------