1 | const httpError = require('http-errors')
|
2 | const isEmpty = obj => !Object.keys(obj || {}).length
|
3 | const pick = require('lodash.pick')
|
4 | const omit = require('lodash.omit')
|
5 |
|
6 | module.exports = (acl, library = 'role-acl', opts) => {
|
7 | if (!acl) throw new Error('acl is a required parameter!')
|
8 | if (typeof library === 'object') {
|
9 | throw new Error(
|
10 | 'objection-authorize@3 now has the signature (acl, library, opts)'
|
11 | )
|
12 | }
|
13 |
|
14 | const defaultOpts = {
|
15 | defaultRole: 'anonymous',
|
16 | unauthenticatedErrorCode: 401,
|
17 | unauthorizedErrorCode: 403,
|
18 | userFromResult: false,
|
19 |
|
20 | contextKey: 'req',
|
21 | roleFromUser: user => user.role,
|
22 | resourceAugments: { true: true, false: false, undefined: undefined }
|
23 | }
|
24 | opts = Object.assign(defaultOpts, opts)
|
25 |
|
26 | const lib = require(`./lib/${library}`)
|
27 |
|
28 | return Model => {
|
29 | class AuthQueryBuilder extends Model.QueryBuilder {
|
30 | get _shouldCheckAccess() {
|
31 | return this.context()._authorize
|
32 | }
|
33 |
|
34 | get _resource() {
|
35 | return this.context()._resource
|
36 | }
|
37 |
|
38 |
|
39 |
|
40 | set _resource(_resource) {
|
41 |
|
42 |
|
43 |
|
44 | if (!_resource || !_resource.$query)
|
45 | _resource = this.modelClass().fromJson(_resource, {
|
46 | skipValidation: true
|
47 | })
|
48 | this.mergeContext({ _resource })
|
49 | }
|
50 |
|
51 |
|
52 | _checkAccess(action, body) {
|
53 | if (!this._shouldCheckAccess) return body
|
54 |
|
55 | const {
|
56 | _user: user,
|
57 | _resource: resource,
|
58 | _opts: opts,
|
59 | _action
|
60 | } = this.context()
|
61 |
|
62 | action = _action || action
|
63 |
|
64 | const access = lib.getAccess(acl, user, resource, action, body, opts)
|
65 |
|
66 |
|
67 | if (!lib.isAuthorized(access, action, resource))
|
68 | throw httpError(
|
69 | user.role === opts.defaultRole
|
70 | ? opts.unauthenticatedErrorCode
|
71 | : opts.unauthorizedErrorCode
|
72 | )
|
73 |
|
74 | return access
|
75 | }
|
76 |
|
77 |
|
78 | _filterBody(action, body) {
|
79 | if (!this._shouldCheckAccess) return body
|
80 |
|
81 | const access = this._checkAccess(action, body)
|
82 | const resource = this._resource
|
83 |
|
84 |
|
85 | const pickFields = lib.pickFields(access, action, resource)
|
86 | const omitFields = lib.omitFields(access, action, resource)
|
87 |
|
88 | if (pickFields.length) body = pick(body, pickFields)
|
89 | if (omitFields.length) body = omit(body, omitFields)
|
90 |
|
91 | return body
|
92 | }
|
93 |
|
94 |
|
95 |
|
96 |
|
97 |
|
98 |
|
99 | insert(body) {
|
100 | return super.insert(this._filterBody('create', body))
|
101 | }
|
102 |
|
103 | insertAndFetch(body) {
|
104 | return super.insertAndFetch(this._filterBody('create', body))
|
105 | }
|
106 |
|
107 | patch(body) {
|
108 | return super.patch(this._filterBody('update', body))
|
109 | }
|
110 |
|
111 | patchAndFetch(body) {
|
112 | return super.patchAndFetch(this._filterBody('update', body))
|
113 | }
|
114 |
|
115 |
|
116 | patchAndFetchById(id, body) {
|
117 | return super.patchAndFetchById(id, this._filterBody('update', body))
|
118 | }
|
119 |
|
120 |
|
121 | update(body) {
|
122 | return super.update(this._filterBody('update', body))
|
123 | }
|
124 |
|
125 |
|
126 | updateAndFetch(body) {
|
127 | return super.updateAndFetch(this._filterBody('update', body))
|
128 | }
|
129 |
|
130 |
|
131 | updateAndFetchById(id, body) {
|
132 | return super.updateAndFetchById(id, this._filterBody('update', body))
|
133 | }
|
134 |
|
135 | delete(body) {
|
136 | this._checkAccess('delete', body)
|
137 | return super.delete()
|
138 | }
|
139 |
|
140 |
|
141 | deleteById(id, body) {
|
142 | this._checkAccess('delete', body)
|
143 | return super.deleteById(id)
|
144 | }
|
145 |
|
146 |
|
147 | action(_action) {
|
148 | this.mergeContext({ _action })
|
149 | return this
|
150 | }
|
151 |
|
152 |
|
153 |
|
154 | first() {
|
155 | this.mergeContext({ _first: true })
|
156 | return super.first()
|
157 | }
|
158 |
|
159 |
|
160 |
|
161 | authorize(user, resource, optOverride) {
|
162 | resource = resource || this.context()._instance || {}
|
163 | this._resource = resource
|
164 | this.mergeContext({
|
165 | _user: Object.assign({ role: opts.defaultRole }, user),
|
166 | _opts: Object.assign({}, opts, optOverride),
|
167 | _authorize: true
|
168 | })
|
169 |
|
170 |
|
171 |
|
172 |
|
173 |
|
174 |
|
175 | .runBefore(async (result, query) => {
|
176 | if (query.isFind() && !isEmpty(resource)) {
|
177 | const readAccess = query._checkAccess('read')
|
178 |
|
179 |
|
180 | query.mergeContext({ readAccess })
|
181 | }
|
182 |
|
183 | return result
|
184 | })
|
185 | .runAfter(async (result, query) => {
|
186 |
|
187 | if (typeof result !== 'object' || !query._shouldCheckAccess)
|
188 | return result
|
189 |
|
190 | const isArray = Array.isArray(result)
|
191 |
|
192 | let {
|
193 | _resource: resource,
|
194 | _first: first,
|
195 | _opts: opts,
|
196 | _user: user,
|
197 | _readAccess: readAccess
|
198 | } = query.context()
|
199 |
|
200 |
|
201 |
|
202 |
|
203 |
|
204 |
|
205 |
|
206 |
|
207 | if (isEmpty(resource) && (!isArray || first)) {
|
208 | resource = isArray ? result[0] : result
|
209 | resource = query.modelClass().fromJson(resource, {
|
210 | skipValidation: true
|
211 | })
|
212 | query.mergeContext({ _resource: resource })
|
213 | }
|
214 |
|
215 |
|
216 | if (
|
217 | (query.isInsert() || query.isUpdate()) &&
|
218 | !isArray &&
|
219 | opts.userFromResult
|
220 | ) {
|
221 |
|
222 | const resultIsUser =
|
223 | typeof opts.userFromResult === 'function'
|
224 | ? opts.userFromResult(user, result)
|
225 | : true
|
226 |
|
227 |
|
228 | if (resultIsUser) {
|
229 |
|
230 | query.mergeContext({ _user: result })
|
231 |
|
232 | readAccess = query._checkAccess('read')
|
233 | }
|
234 | }
|
235 |
|
236 | readAccess = readAccess || query._checkAccess('read')
|
237 |
|
238 |
|
239 |
|
240 |
|
241 |
|
242 |
|
243 | return isArray
|
244 | ? result.map(model => model._filterModel(readAccess))
|
245 | : result._filterModel(readAccess)
|
246 | })
|
247 |
|
248 |
|
249 | return this
|
250 | }
|
251 | }
|
252 |
|
253 | return class extends Model {
|
254 |
|
255 | _filterModel(readAccess) {
|
256 | const pickFields = lib.pickFields(readAccess, 'read', this)
|
257 | const omitFields = lib.omitFields(readAccess, 'read', this)
|
258 |
|
259 | if (pickFields.length) this.$pick(pickFields)
|
260 | if (omitFields.length) this.$omit(omitFields)
|
261 |
|
262 | return this
|
263 | }
|
264 |
|
265 |
|
266 | $query(trx) {
|
267 | return super.$query(trx).mergeContext({ _instance: this })
|
268 | }
|
269 |
|
270 | $relatedQuery(relation, trx) {
|
271 | return super
|
272 | .$relatedQuery(relation, trx)
|
273 | .mergeContext({ _instance: this })
|
274 | }
|
275 |
|
276 | static get QueryBuilder() {
|
277 | return AuthQueryBuilder
|
278 | }
|
279 | }
|
280 | }
|
281 | }
|