UNPKG

4.88 kBJavaScriptView Raw
1'use strict'
2
3// get required modules
4const fastifyPlugin = require('fastify-plugin')
5const get = require('lodash.get')
6const createError = require('http-errors')
7const pkg = require('../package.json')
8
9// plugin defaults
10const defaults = {
11 decorator: 'guard',
12 requestProperty: 'user',
13 roleProperty: 'role',
14 scopeProperty: 'scope',
15 errorHandler: undefined
16}
17
18// definitions of role and scope checker functions
19const 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 // loop roles/scopes array list (may contain sub arrays)
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
63const hasScopeAndRole = (value, req, options, property) => {
64 // validations
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 // data type validation of permissions
92 if (!Array.isArray(permissions)) {
93 throw new Error(`"${property}" expected to be an aray but got: ${typeof permissions}`)
94 }
95
96 // check if a user has the permission
97 return permissions.indexOf(value) >= 0
98}
99
100// definition of guard function
101const Guard = function (options) {
102 this._options = options
103}
104
105Guard.prototype = {
106 hasRole: function (request, role) {
107 return hasScopeAndRole(role, request, this._options, 'roleProperty')
108 },
109 role: function (...roles) {
110 // thanks javascript :)
111 const self = this
112
113 // middleware to check authenticated user role(s)
114 return (req, reply, done) => {
115 const result = checkScopeAndRole(roles, req, self._options, 'roleProperty')
116
117 // use cutom handler if possible
118 if (result && self._options.errorHandler) {
119 return self._options.errorHandler(result, req, reply)
120 }
121
122 // use predefined handler as fallback
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 // thanks javascript :)
131 const self = this
132
133 // middleware to check authenticated user scıoe(s)
134 return (req, reply, done) => {
135 const result = checkScopeAndRole(scopes, req, self._options, 'scopeProperty')
136
137 // use cutom handler if possible
138 if (result && self._options.errorHandler) {
139 return self._options.errorHandler(result, req, reply)
140 }
141
142 // use predefined handler as fallback
143 return done(result)
144 }
145 }
146}
147
148// declaration of guard plugin for fastify
149function guardPlugin (fastify, opts, next) {
150 // combine defaults with provided options
151 const options = Object.assign({}, defaults, opts)
152
153 // validation
154 if (options.errorHandler && typeof options.errorHandler !== 'function') {
155 throw new Error('custom error handler must be a function')
156 }
157
158 // register the guard as a decorator
159 fastify.decorate(options.decorator, new Guard(options))
160
161 // done
162 next()
163}
164
165// export the plugin
166module.exports = fastifyPlugin(
167 guardPlugin,
168 {
169 fastify: '3.x',
170 name: pkg.name
171 }
172)