UNPKG

7.95 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 (!id) return Promise.reject(new TypeError('Requestor Id must be set'))
80
81 if (!Array.isArray(permissions)) {
82 return Promise.reject(new TypeError('Invalid permissions value: must be an array'))
83 }
84
85 if ((permissions.length > 1 && !checkType) || (permissions.length < 2 && checkType)) {
86 return Promise.reject(
87 new TypeError(`Invalid permissions:checkType combination. [${permissions}]:${checkType}`))
88 }
89
90 if (!this._opts.getPermission) {
91 return Promise.reject(new Error('Local authorization not configured.'))
92 }
93
94 return this._opts
95 .getPermission(id)
96 .then((principalPermissions) => {
97 const granted = ((type) => {
98 switch (type) {
99 case null:
100 return Hoek.intersect(permissions, principalPermissions).length === permissions.length
101 case 'OR':
102 return Hoek.contain(permissions, principalPermissions)
103 case 'AND':
104 return Hoek.deepEqual(permissions, principalPermissions, { prototype: false })
105 default:
106 return false
107 }
108 })(checkType)
109
110 return granted || Promise.reject(new Error('Permission denied.'))
111 })
112 }
113
114 /**
115 * Checks if a given principal is authorized for any of the given permissions. Returns a Promise resolving to the
116 * principal being allowed the permission. The remote server can also return some claims about the principal, which
117 * will be returned in the Promise. This function can authorize the principal remotely, for which you need to define
118 * the `remoteAuth` object in the instance options.
119 * @param {string} permission - The permission to be checked against the principal.
120 * @param {string} auth - Authorization.
121 * @returns {Promise.<*>} - A promise resolving to the principal being authorized for the given permissions.
122 */
123 authorizeRemote (permission, auth) {
124 if (typeof permission !== 'string') {
125 return Promise.reject(new TypeError('Invalid permissions value: must be a string'))
126 }
127 return this._authorizeRemote([ permission ], auth)
128 }
129
130 /**
131 * Checks if a given principal is authorized for any of the given permissions.
132 * @param {array} permissions - An array of permissions to check agains
133 * @param {string} auth - Authorization.
134 * @returns {Promise.<*>}
135 */
136 authorizeRemoteOr (permissions, auth) {
137 if (!Array.isArray(permissions)) {
138 return Promise.reject(new TypeError('Invalid permissions value: must be an array'))
139 }
140 return this._authorizeRemote(permissions, auth, 'or')
141 }
142
143 /**
144 * Checks if a given principal is authorized for all of the given permissions.
145 * @param {array} permissions - An array of permissions to check agains
146 * @param {string} auth - Authorization.
147 * @returns {Promise.<*>}
148 */
149 authorizeRemoteAnd (permissions, auth) {
150 if (!Array.isArray(permissions)) {
151 return Promise.reject(new TypeError('Invalid permissions value: must be an array'))
152 }
153 return this._authorizeRemote(permissions, auth, 'and')
154 }
155
156 /**
157 * Checks for permissions over HTTP request.
158 * @param {array} permissions - The permissions to be checked against the principal.
159 * @param {string} auth - Auth header as string. Ex: Bearer ....
160 * @param {string|null} checkType - The check type to compary the permissions against. Either null, "or", "and"
161 * @param {object} headers - Additional request headers
162 * @returns {Promise.<*>} - A promise resolving to the principal being authorized for the given permissions.
163 * @private
164 */
165 _authorizeRemote (permissions, auth, checkType, headers = {}) {
166 if (!this._opts.remoteAuth) {
167 return Promise.reject(new Error('Remote authorization not configured.'))
168 }
169
170 headers = (auth) ? Object.assign(headers, { authorization: auth }) : headers
171
172 const opts = {
173 uri: this._opts.remoteAuth.url,
174 method: 'POST',
175 headers: Object.assign(this._opts.remoteAuth.headers, headers),
176 json: true,
177 body: {
178 permissions: permissions,
179 checkType: checkType || null
180 },
181 simple: true // status codes other than 2xx should also reject the promise
182 }
183 return request(opts)
184 }
185}
186
187module.exports = Rbac