1 | 'use strict'
|
2 |
|
3 |
|
4 | const fastifyPlugin = require('fastify-plugin')
|
5 | const get = require('lodash.get')
|
6 | const createError = require('http-errors')
|
7 | const pkg = require('../package.json')
|
8 |
|
9 | // plugin defaults
|
10 | const defaults = {
|
11 | decorator: 'guard',
|
12 | requestProperty: 'user',
|
13 | roleProperty: 'role',
|
14 | scopeProperty: 'scope',
|
15 | errorHandler: undefined
|
16 | }
|
17 |
|
18 |
|
19 | const checkScopeAndRole = (arr, req, options, property) => {
|
20 | for (let i = 0; i < arr.length; i++) {
|
21 | const item = arr[i]
|
22 |
|
23 | if (typeof item !== 'string' && !Array.isArray(item)) {
|
24 | return createError(500, `roles/scopes parameter excpected to be an array or string but got: ${typeof item}`)
|
25 | }
|
26 | }
|
27 |
|
28 | const user = get(req, options.requestProperty, undefined)
|
29 | if (typeof user === 'undefined') {
|
30 | return createError(500, `user object (${options.requestProperty}) was not found in request object`)
|
31 | }
|
32 |
|
33 | const permissions = get(user, options[property], undefined)
|
34 | if (typeof permissions === 'undefined') {
|
35 | return createError(500, `${property} was not found in user object`)
|
36 | }
|
37 |
|
38 | if (!Array.isArray(permissions)) {
|
39 | return createError(500, `${property} expected to be an aray but got: ${typeof permissions}`)
|
40 | }
|
41 |
|
42 | let sufficient = false
|
43 |
|
44 |
|
45 | arr.forEach(x => {
|
46 | sufficient =
|
47 | sufficient || (
|
48 | Array.isArray(x)
|
49 | ? x.every(
|
50 | scope => {
|
51 | return permissions.indexOf(scope) >= 0
|
52 | }
|
53 | )
|
54 | : permissions.indexOf(x) >= 0
|
55 | )
|
56 | })
|
57 |
|
58 | return sufficient
|
59 | ? null
|
60 | : createError(403, 'insufficient permission')
|
61 | }
|
62 |
|
63 | const hasScopeAndRole = (value, req, options, property) => {
|
64 |
|
65 | if (typeof req !== 'object') {
|
66 | throw new Error(`"request" is expected to be an object but got: ${typeof req}`)
|
67 | }
|
68 |
|
69 | if (typeof value !== 'string') {
|
70 | throw new Error(`"value" is expected to be a string but got: ${typeof value}`)
|
71 | }
|
72 |
|
73 | if (!value) {
|
74 | throw new Error('"value" cannot be empty.')
|
75 | }
|
76 |
|
77 | const user = get(req, options.requestProperty, undefined)
|
78 |
|
79 | // validate user existence in the request object
|
80 | if (!user) {
|
81 | throw new Error('"user" was not found in the request')
|
82 | }
|
83 |
|
84 | const permissions = get(user, options[property], undefined)
|
85 |
|
86 | // validate the property existence in the user object
|
87 | if (typeof permissions === 'undefined') {
|
88 | throw new Error(`"${property}" was not found in user object`)
|
89 | }
|
90 |
|
91 |
|
92 | if (!Array.isArray(permissions)) {
|
93 | throw new Error(`"${property}" expected to be an aray but got: ${typeof permissions}`)
|
94 | }
|
95 |
|
96 |
|
97 | return permissions.indexOf(value) >= 0
|
98 | }
|
99 |
|
100 |
|
101 | const Guard = function (options) {
|
102 | this._options = options
|
103 | }
|
104 |
|
105 | Guard.prototype = {
|
106 | hasRole: function (request, role) {
|
107 | return hasScopeAndRole(role, request, this._options, 'roleProperty')
|
108 | },
|
109 | role: function (...roles) {
|
110 |
|
111 | const self = this
|
112 |
|
113 |
|
114 | return (req, reply, done) => {
|
115 | const result = checkScopeAndRole(roles, req, self._options, 'roleProperty')
|
116 |
|
117 |
|
118 | if (result && self._options.errorHandler) {
|
119 | return self._options.errorHandler(result, req, reply)
|
120 | }
|
121 |
|
122 |
|
123 | return done(result)
|
124 | }
|
125 | },
|
126 | hasScope: function (request, scope) {
|
127 | return hasScopeAndRole(scope, request, this._options, 'scopeProperty')
|
128 | },
|
129 | scope: function (...scopes) {
|
130 |
|
131 | const self = this
|
132 |
|
133 |
|
134 | return (req, reply, done) => {
|
135 | const result = checkScopeAndRole(scopes, req, self._options, 'scopeProperty')
|
136 |
|
137 |
|
138 | if (result && self._options.errorHandler) {
|
139 | return self._options.errorHandler(result, req, reply)
|
140 | }
|
141 |
|
142 |
|
143 | return done(result)
|
144 | }
|
145 | }
|
146 | }
|
147 |
|
148 |
|
149 | function guardPlugin (fastify, opts, next) {
|
150 |
|
151 | const options = Object.assign({}, defaults, opts)
|
152 |
|
153 |
|
154 | if (options.errorHandler && typeof options.errorHandler !== 'function') {
|
155 | throw new Error('custom error handler must be a function')
|
156 | }
|
157 |
|
158 |
|
159 | fastify.decorate(options.decorator, new Guard(options))
|
160 |
|
161 |
|
162 | next()
|
163 | }
|
164 |
|
165 |
|
166 | module.exports = fastifyPlugin(
|
167 | guardPlugin,
|
168 | {
|
169 | fastify: '3.x',
|
170 | name: pkg.name
|
171 | }
|
172 | )
|