UNPKG

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