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 | 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 |
|
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 |
|
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 |
|
101 | let test = this._reduceHandler(handler, this.permissions);
|
102 |
|
103 |
|
104 | if (test !== null && typeof test === 'object' && typeof test[role] !== 'undefined') {
|
105 | test = test[role];
|
106 | }
|
107 |
|
108 |
|
109 | if (typeof test === 'boolean') {
|
110 | ok.push([handler, test]);
|
111 | test = {};
|
112 | }
|
113 |
|
114 |
|
115 | if (typeof test === 'string') {
|
116 | ok.push([handler, test]);
|
117 | test = {};
|
118 | }
|
119 |
|
120 |
|
121 | if (typeof test === 'function') {
|
122 | ok.push([handler, test]);
|
123 | test = {};
|
124 | }
|
125 |
|
126 |
|
127 | if (test !== null) {
|
128 | const y = parents.length;
|
129 |
|
130 | for (let k = 0; k < y; k += 1) {
|
131 |
|
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 |
|
142 | if (test && typeof test[children[l]] !== 'undefined') {
|
143 | ok.push([children[l], test[children[l]]]);
|
144 | break;
|
145 | }
|
146 | }
|
147 | }
|
148 | }
|
149 |
|
150 |
|
151 | return ok
|
152 | .map(check => {
|
153 |
|
154 | if (typeof check[1] === 'function') {
|
155 | check[1] = check[1](conn);
|
156 | }
|
157 |
|
158 |
|
159 | if (check[1] === 'inherit') {
|
160 | check[1] = null;
|
161 | }
|
162 |
|
163 |
|
164 | if (check[1] === 'allow') {
|
165 | check[1] = true;
|
166 | }
|
167 |
|
168 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
289 | if (!prev) {
|
290 | return cur;
|
291 | }
|
292 |
|
293 |
|
294 | if (!this._groups[prev]) {
|
295 | this._groups[prev] = {
|
296 | parents: [],
|
297 | children: [],
|
298 | };
|
299 | }
|
300 |
|
301 |
|
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 | };
|