1 |
|
2 |
|
3 | const Express = require('./middleware/express')
|
4 | const Hoek = require('hoek')
|
5 | const request = require('request-promise')
|
6 |
|
7 | class 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 {string} auth - Auth header as string. Ex: Bearer ....
|
158 | * @param {string|null} checkType - The check type to compary the permissions against. Either null, "or", "and"
|
159 | * @param {object} headers - Additional request headers
|
160 | * @returns {Promise.<*>} - A promise resolving to the principal being authorized for the given permissions.
|
161 | * @private
|
162 | */
|
163 | _authorizeRemote (permissions, auth, checkType = null, headers = {}) {
|
164 | if (!this._opts.remoteAuth) {
|
165 | return Promise.reject(new Error('Remote authorization not configured.'))
|
166 | }
|
167 |
|
168 | headers = (auth) ? Object.assign(headers, { authorization: auth }) : headers
|
169 |
|
170 | const opts = {
|
171 | uri: this._opts.remoteAuth.url,
|
172 | method: 'POST',
|
173 | headers: Object.assign(this._opts.remoteAuth.headers, headers),
|
174 | json: true,
|
175 | body: {
|
176 | permissions: permissions,
|
177 | checkType: checkType
|
178 | },
|
179 | simple: true // status codes other than 2xx should also reject the promise
|
180 | }
|
181 | return request(opts)
|
182 | }
|
183 | }
|
184 |
|
185 | module.exports = Rbac
|