1 | 'use strict';
|
2 |
|
3 | const debug = require('debug')('grown:access');
|
4 |
|
5 | const RE_DOUBLE_STAR = /\/\*\*/g;
|
6 | const RE_SINGLE_STAR = /\/\*/g;
|
7 |
|
8 | module.exports = (Grown, util) => {
|
9 | function _reduceHandler(handler, permissions) {
|
10 | const parts = handler.split('.');
|
11 |
|
12 | let _handler;
|
13 |
|
14 |
|
15 | while (parts.length) {
|
16 | _handler = permissions[parts.join('.')];
|
17 |
|
18 |
|
19 | if (_handler) {
|
20 | break;
|
21 | }
|
22 |
|
23 | parts.pop();
|
24 | }
|
25 |
|
26 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
104 | let test = this._reduceHandler(handler, this.permissions);
|
105 |
|
106 |
|
107 | if (test !== null && typeof test === 'object' && typeof test[role] !== 'undefined') {
|
108 | test = test[role];
|
109 | }
|
110 |
|
111 |
|
112 | if (typeof test === 'boolean' || typeof test === 'string' || typeof test === 'function') {
|
113 | ok.push([handler, test]);
|
114 | test = {};
|
115 | }
|
116 |
|
117 |
|
118 | if (test !== null) {
|
119 | const y = parents.length;
|
120 |
|
121 | for (let k = 0; k < y; k += 1) {
|
122 |
|
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 |
|
133 | if (test && typeof test[children[l]] !== 'undefined') {
|
134 | ok.push([children[l], test[children[l]]]);
|
135 | break;
|
136 | }
|
137 | }
|
138 | }
|
139 | }
|
140 |
|
141 |
|
142 | return ok
|
143 | .map(check => {
|
144 |
|
145 | if (typeof check[1] === 'function') {
|
146 | check[1] = check[1](ctx);
|
147 | }
|
148 |
|
149 |
|
150 | if (check[1] === 'inherit') {
|
151 | check[1] = null;
|
152 | }
|
153 |
|
154 |
|
155 | if (check[1] === 'allow') {
|
156 | check[1] = true;
|
157 | }
|
158 |
|
159 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
276 | if (!prev) {
|
277 | return cur;
|
278 | }
|
279 |
|
280 |
|
281 | if (!this._groups[prev]) {
|
282 | this._groups[prev] = {
|
283 | parents: [],
|
284 | children: [],
|
285 | };
|
286 | }
|
287 |
|
288 |
|
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 |
|
303 | if (!Object.keys(this._groups).length) {
|
304 | throw new Error('No role-groups were defined');
|
305 | }
|
306 |
|
307 |
|
308 | if (!this._ruleset.length) {
|
309 | throw new Error('Ruleset cannot be empty');
|
310 | }
|
311 |
|
312 | return this;
|
313 | },
|
314 | });
|
315 | };
|