UNPKG

8.19 kBJavaScriptView Raw
1'use strict';
2
3const debug = require('debug')('grown:access');
4
5const RE_DOUBLE_STAR = /\/\*\*/g;
6const RE_SINGLE_STAR = /\/\*/g;
7
8module.exports = (Grown, util) => {
9 function _reduceHandler(handler, permissions) {
10 const parts = handler.split('.');
11
12 let _handler;
13
14 // iterate until one handler matches
15 while (parts.length) {
16 _handler = permissions[parts.join('.')];
17
18 /* istanbul ignore else */
19 if (_handler) {
20 break;
21 }
22
23 parts.pop();
24 }
25
26 /* istanbul ignore else */
27 if (!_handler) {
28 return null;
29 }
30
31 return _handler;
32 }
33
34 function _compileMatch(rule) {
35 let regex;
36
37 try {
38 regex = !(rule.path instanceof RegExp)
39 ? new RegExp(`^${
40 rule.path
41 .replace(RE_DOUBLE_STAR, '/.*?')
42 .replace(RE_SINGLE_STAR, '/[^\\/]+?')
43 }$`)
44 : rule.path;
45 } catch (e) {
46 throw new Error(`Cannot compile '${rule.path}' as route handler`);
47 }
48
49 return conn => {
50 /* istanbul ignore else */
51 if (rule.method
52 && rule.method !== conn.req.method) {
53 return;
54 }
55
56 return regex.test(conn.req.url) && rule.handler;
57 };
58 }
59
60 function _makeMatcher(ruleset) {
61 const matches = ruleset.map(rule => this._compileMatch(rule));
62
63 /* istanbul ignore next */
64 debug('%s handler%s %s compiled',
65 ruleset.length,
66 ruleset.length === 1 ? '' : 's',
67 ruleset.length === 1 ? 'was' : 'were');
68
69 return conn => matches
70 .map(match => match(conn))
71 .filter(x => x);
72 }
73
74 function _makeTree(role, groups, property) {
75 const out = [];
76
77 /* istanbul ignore else */
78 if (groups[role]) {
79 groups[role][property].forEach(c => {
80 out.push(c);
81 Array.prototype.push.apply(out, this._makeTree(c, groups, property));
82 });
83 }
84
85 return out;
86 }
87
88 function _runACL(ctx, role, handlers) {
89 const children = this._makeTree(role, this._groups, 'children');
90 const parents = this._makeTree(role, this._groups, 'parents');
91
92 /* istanbul ignore next */
93 debug('#%s Checking access for %s <%s>', ctx.pid, role, handlers.join(', ') || '...');
94
95 return Promise.resolve()
96 .then(() => {
97 const c = handlers.length;
98 const ok = [];
99
100 for (let i = 0; i < c; i += 1) {
101 const handler = handlers[i];
102
103 // FIXME: normalize possible values
104 let test = this._reduceHandler(handler, this.permissions);
105
106 /* istanbul ignore else */
107 if (test !== null && typeof test === 'object' && typeof test[role] !== 'undefined') {
108 test = test[role];
109 }
110
111 /* istanbul ignore else */
112 if (typeof test === 'boolean' || typeof test === 'string' || typeof test === 'function') {
113 ok.push([handler, test]);
114 test = {};
115 }
116
117 /* istanbul ignore else */
118 if (test !== null) {
119 const y = parents.length;
120
121 for (let k = 0; k < y; k += 1) {
122 /* istanbul ignore else */
123 if (typeof test[parents[k]] !== 'undefined') {
124 ok.push([parents[k], test[parents[k]]]);
125 break;
126 }
127 }
128
129 const z = children.length;
130
131 for (let l = 0; l < z; l += 1) {
132 /* istanbul ignore else */
133 if (test && typeof test[children[l]] !== 'undefined') {
134 ok.push([children[l], test[children[l]]]);
135 break;
136 }
137 }
138 }
139 }
140
141 // FIXME: allow promises and serial callbacks?
142 return ok
143 .map(check => {
144 /* istanbul ignore else */
145 if (typeof check[1] === 'function') {
146 check[1] = check[1](ctx);
147 }
148
149 /* istanbul ignore else */
150 if (check[1] === 'inherit') {
151 check[1] = null;
152 }
153
154 /* istanbul ignore else */
155 if (check[1] === 'allow') {
156 check[1] = true;
157 }
158
159 /* istanbul ignore else */
160 if (check[1] === 'deny') {
161 check[1] = false;
162 }
163
164 debug('#%s Test access <%s> %s', ctx.pid, check[0], check[1]);
165
166 return check;
167 })
168 .reduce((prev, cur) => {
169 /* istanbul ignore else */
170 if (parents.indexOf(cur[0]) > -1) {
171 debug('#%s Parent access found <%s>', ctx.pid, cur[0]);
172 cur[1] = false;
173 }
174
175 /* istanbul ignore else */
176 if (children.indexOf(cur[0]) > -1) {
177 debug('#%s Children access found <%s>', ctx.pid, cur[0]);
178 cur[1] = true;
179 }
180
181 /* istanbul ignore else */
182 if (prev && cur[1] === null) {
183 cur[0] = prev[0];
184 cur[1] = prev[1];
185
186 debug('#%s Inherited access <%s> %s', ctx.pid, cur[0], prev[1]);
187 }
188
189 return cur;
190 }, false);
191 })
192 .then(result => {
193 /* istanbul ignore else */
194 if (result === false || result[1] === false) {
195 debug('#%s Skip. No rules were defined', ctx.pid);
196
197 return ctx.raise(403, `Access denied for role: ${role}`);
198 }
199
200 debug('#%s Got access <%s> %s', ctx.pid, result[0], result[1]);
201 });
202 }
203
204 return Grown('Access', {
205 _reduceHandler,
206 _compileMatch,
207 _makeMatcher,
208 _makeTree,
209 _runACL,
210
211 _groups: {},
212 _ruleset: [],
213
214 resources: {},
215 permissions: {},
216
217 $install(ctx) {
218 /* istanbul ignore else */
219 if (typeof this.access_rules === 'object') {
220 this.rules(this.access_rules);
221 }
222
223 const matchHandlers = this._makeMatcher(this._ruleset);
224
225 ctx.mount('Access#pipe', conn => {
226 const _handlers = matchHandlers(conn).filter(x => x);
227
228 return Promise.resolve()
229 .then(() => (this.access_filter && this.access_filter(conn)) || 'Unknown')
230 .then(x => this._runACL(conn, x, _handlers));
231 });
232 },
233
234 $mixins() {
235 const self = this;
236
237 return {
238 methods: {
239 check(role, resource, action) {
240 const _handlers = !Array.isArray(resource)
241 ? [action ? `${resource}.${action}` : resource]
242 : resource;
243
244 return self._runACL(this, role, _handlers);
245 },
246 },
247 };
248 },
249
250 rules(config = {}) {
251 util.extendValues(this.permissions, config.permissions);
252
253 Object.keys(config.resources || {}).forEach(key => {
254 this.resources[key] = util.flattenArgs(config.resources[key]);
255 this.resources[key].forEach(_path => {
256 let _method = 'GET';
257
258 /* istanbul ignore else */
259 if (typeof _path === 'string' && _path.indexOf(' ') > -1) {
260 _path = _path.split(' ');
261 _method = _path[0].toUpperCase();
262 _path = _path[1];
263 }
264
265 this._ruleset.push({
266 path: _path,
267 method: _method,
268 handler: key,
269 });
270 });
271 });
272
273 (!Array.isArray(config.roles) && config.roles ? [config.roles] : config.roles || [])
274 .forEach(roles => roles.split('.').reduce((prev, cur) => {
275 /* istanbul ignore else */
276 if (!prev) {
277 return cur;
278 }
279
280 /* istanbul ignore else */
281 if (!this._groups[prev]) {
282 this._groups[prev] = {
283 parents: [],
284 children: [],
285 };
286 }
287
288 /* istanbul ignore else */
289 if (!this._groups[cur]) {
290 this._groups[cur] = {
291 parents: [],
292 children: [],
293 };
294 }
295
296 this._groups[prev].parents.push(cur);
297 this._groups[cur].children.push(prev);
298
299 return cur;
300 }, null));
301
302 /* istanbul ignore else */
303 if (!Object.keys(this._groups).length) {
304 throw new Error('No role-groups were defined');
305 }
306
307 /* istanbul ignore else */
308 if (!this._ruleset.length) {
309 throw new Error('Ruleset cannot be empty');
310 }
311
312 return this;
313 },
314 });
315};