UNPKG

7.91 kBJavaScriptView Raw
1'use strict'
2
3const Express = require('./middleware/express')
4const Hoek = require('hoek')
5const request = require('request-promise')
6
7class Rbac {
8 /**
9 * Creates a new Rbac instance with the given options for local or remote authorization. It also provides an express
10 * middleware that can use information in the request (i.e. the authentication token or principal) in the authorization
11 * process.
12 *
13 * ```js
14 * const rbac = new Rbac({
15 * remoteAuth: {
16 * url: 'http://www.example.com/authorize'
17 * }
18 * })
19 *
20 * app.get('/',
21 * rbac.express.authorizeRemote(['users:read']),
22 * (req, res, next) => {
23 * res.json({ message: 'You have acces to this awesome content!' })
24 * })
25 * ```
26 *
27 * @param {object} opts - Options object.
28 * @param {object} [opts.remoteAuth] - Optional configuration object for allowing remote HTTP permission evaluation.
29 * @param {object} [opts.remoteAuth.headers] - Optional headers to pass in the HTTP request.
30 * @param {string} [opts.remoteAuth.url] - Url for the HTTP request, required if `opts.remoteAuth` is set. The endpoint
31 * is expected to accept a JSON object with `permissions {array}` property and return 200 in case of
32 * success or different 200 in case of unauthorized. It can also return some claims about the principal (i.e. the user
33 * id) which will be merged with `req.user`, when called by the express middleware.
34 * @param {function} [opts.getPermission] - Callback function for local permission evaluation with the signature
35 * `function (id)` and returning a Promise resolving to the principal permissions array. **If `opts.remoteAuth` is not
36 * set, then this property is required.**
37 * @param {function} [getReqId] - A callback with the signature `(req) => {}` that returns the principal ID from the
38 * HTTP request object. Defaults to `(req) => req.user.id`
39 */
40 constructor (opts) {
41 Hoek.assert(typeof opts !== 'undefined', new TypeError('Invalid opts value: must be an object'))
42 this._checkOptions(opts)
43 this._opts = opts
44 this.express = new Express(this)
45 }
46
47 /**
48 * Checks option constraints and set defaults.
49 * @param opts
50 * @private
51 */
52 _checkOptions (opts) {
53 if (typeof opts.remoteAuth === 'object') {
54 opts.remoteAuth.headers = opts.remoteAuth.headers || {}
55 Hoek.assert(typeof opts.remoteAuth.url === 'string', new TypeError('Invalid opts.remoteAuth.url value: must be an string'))
56 } else {
57 // If permission validation is not remote, then must define getPermission function
58 Hoek.assert(typeof opts.getPermission === 'function', new TypeError('Invalid opts.getPermission value: must be an function'))
59 }
60
61 // Set default getReqId
62 opts.getReqId = opts.getReqId || ((req) => req.user.id)
63 }
64
65 /**
66 * Checks if a given principal is authorized for any of the given permissions. Returns a Promise resolving to the
67 * principal being allowed the permission. This function can authorize the principal locally, for which you need to
68 * define the `getPermission` callback in the instance options.
69 * @param {number} id - The principal id to be checked against the permissions.
70 * @param {object} body - The permission object.
71 * @param {array} body.permissions - The permissions to be checked against the principal.
72 * @param {string|null} body.checkType - The permissions check type to be applied
73 * @returns {Promise.<*>} - A promise resolving to the principal being authorized for a specific permission.
74 */
75 authorize (id, body) {
76 let permissions = body.permissions || Promise.reject(new TypeError('Missing permissions'))
77 let checkType = body.checkType || null
78
79 if (isNaN(id)) {
80 return Promise.reject(new TypeError('Invalid userId value: must be a number.'))
81 }
82
83 if (!Array.isArray(permissions)) {
84 return Promise.reject(new TypeError('Invalid permissions value: must be an array'))
85 }
86
87 if ((permissions.length > 1 && !checkType) || (permissions.length < 2 && checkType)) {
88 return Promise.reject(
89 new TypeError(`Invalid permissions:checkType combination. [${permissions}]:${checkType}`))
90 }
91
92 if (!this._opts.getPermission) {
93 return Promise.reject(new Error('Local authorization not configured.'))
94 }
95
96 return this._opts
97 .getPermission(id)
98 .then((principalPermissions) => {
99 const granted = ((type) => {
100 switch (type) {
101 case null: return Hoek.intersect(permissions, principalPermissions).length === permissions.length
102 case 'OR': return Hoek.contain(permissions, principalPermissions)
103 case 'AND': return Hoek.deepEqual(permissions, principalPermissions, { prototype: false })
104 default: return false
105 }
106 })(checkType)
107
108 return granted || Promise.reject(new Error('Permission denied.'))
109 })
110 }
111
112 /**
113 * Checks if a given principal is authorized for any of the given permissions. Returns a Promise resolving to the
114 * principal being allowed the permission. The remote server can also return some claims about the principal, which
115 * will be returned in the Promise. This function can authorize the principal remotely, for which you need to define
116 * the `remoteAuth` object in the instance options.
117 * @param {string} permission - The permission to be checked against the principal.
118 * @param {object} [headers] - Optional headers to pass in the HTTP request.
119 * @returns {Promise.<*>} - A promise resolving to the principal being authorized for the given permissions.
120 */
121 authorizeRemote (permission, headers) {
122 if (typeof permission !== 'string') {
123 return Promise.reject(new TypeError('Invalid permissions value: must be a string'))
124 }
125 return this._authorizeRemote([ permission ], headers)
126 }
127
128 /**
129 * Checks if a given principal is authorized for any of the given permissions.
130 * @param {array} permissions - An array of permissions to check agains
131 * @param {object} headers - Optional headers to pass in the HTTP request.
132 * @returns {Promise.<*>}
133 */
134 authorizeRemoteOr (permissions, headers) {
135 if (!Array.isArray(permissions)) {
136 return Promise.reject(new TypeError('Invalid permissions value: must be an array'))
137 }
138 return this._authorizeRemote(permissions, headers, 'or')
139 }
140
141 /**
142 * Checks if a given principal is authorized for all of the given permissions.
143 * @param {array} permissions - An array of permissions to check agains
144 * @param {object} headers - Optional headers to pass in the HTTP request.
145 * @returns {Promise.<*>}
146 */
147 authorizeRemoteAnd (permissions, headers) {
148 if (!Array.isArray(permissions)) {
149 return Promise.reject(new TypeError('Invalid permissions value: must be an array'))
150 }
151 return this._authorizeRemote(permissions, headers, 'and')
152 }
153
154 /**
155 * Checks for permissions over HTTP request.
156 * @param {array} permissions - The permissions to be checked against the principal.
157 * @param {object} [headers] - Extra headers to be passed along in the request.
158 * @param {string|null} checkType - The check type to compary the permissions against. Either null, "or", "and"
159 * @returns {Promise.<*>} - A promise resolving to the principal being authorized for the given permissions.
160 * @private
161 */
162 _authorizeRemote (permissions, headers, checkType = null) {
163 if (!this._opts.remoteAuth) {
164 return Promise.reject(new Error('Remote authorization not configured.'))
165 }
166
167 const opts = {
168 uri: this._opts.remoteAuth.url,
169 method: 'POST',
170 headers: Object.assign(this._opts.remoteAuth.headers, headers),
171 json: true,
172 body: {
173 permissions: permissions,
174 checkType: checkType
175 },
176 simple: true // status codes other than 2xx should also reject the promise
177 }
178 return request(opts)
179 }
180}
181
182module.exports = Rbac