Skip to content
This repository was archived by the owner on Jul 5, 2022. It is now read-only.

Commit 4de714c

Browse files
committed
feat: add basic ACL implementation
1 parent 5349a9e commit 4de714c

File tree

1 file changed

+309
-0
lines changed

1 file changed

+309
-0
lines changed

src/index.js

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,310 @@
1+
import _ from 'lodash'
2+
import roles from './roles'
3+
4+
export default function acl(spec) {
5+
const WILDCARD = '*'
6+
const ALLOW = 'ALLOW'
7+
const DENY = 'DENY'
8+
const {settings} = spec
9+
let o = {}
10+
11+
if (!settings) {
12+
throw new Error('You must provide an ACL "settings" option')
13+
}
14+
15+
/**
16+
* Get all keys from resource and settings
17+
* @returns {[String]}
18+
*/
19+
function keys(resource) {
20+
let fields = []
21+
22+
if (settings.fields && _.isObject(settings.fields)) {
23+
fields = fields.concat(Object.keys(settings.fields))
24+
}
25+
26+
if (_.isFunction(resource.toObject)) {
27+
resource = resource.toObject()
28+
}
29+
30+
return Object
31+
.keys(resource)
32+
.concat(fields)
33+
.sort()
34+
}
35+
36+
/**
37+
* Get all keys which a possible population
38+
* @returns {Object}
39+
*/
40+
function refs() {
41+
const paths = {}
42+
43+
if (settings.fields) {
44+
_.forEach(settings.fields, function(field, key) {
45+
if (field.ref) {
46+
paths[key] = field.ref
47+
}
48+
})
49+
}
50+
51+
return paths
52+
}
53+
54+
/**
55+
* Check path for allowed keys, can also handle wildcard entries
56+
* @param {[String]} allKeys
57+
* @param {String} modelPath
58+
* @param {String} type
59+
* @param {Object} role
60+
* @param {String} role.name
61+
* @param {String} privilege
62+
* @returns {[String]}
63+
*/
64+
function path(allKeys, modelPath, type, role, privilege) {
65+
if (settings.fields.hasOwnProperty(modelPath)) {
66+
if (settings.fields[modelPath].hasOwnProperty(type)) {
67+
if (settings.fields[modelPath][type][role.name] &&
68+
settings.fields[modelPath][type][role.name].indexOf(privilege) !== -1
69+
) {
70+
if (modelPath === WILDCARD) {
71+
return allKeys
72+
}
73+
return [modelPath]
74+
}
75+
}
76+
}
77+
78+
return []
79+
}
80+
81+
/**
82+
* Get hierarchy for current role
83+
* @param {Object} role
84+
* @returns {[Object]}
85+
*/
86+
function hierarchy(role) {
87+
if (!role) {
88+
return []
89+
}
90+
91+
if (!role.parent) {
92+
return [roles.get(role)]
93+
}
94+
95+
return [roles.get(role)].concat(hierarchy(role.parent))
96+
}
97+
98+
/**
99+
* Get allowed keys for a specific role
100+
* @param {Document} user
101+
* @param {Object} resource
102+
* @param {Object} role
103+
* @param {Boolean} role.superuser
104+
* @param {String} [privilege='R']
105+
* @param {Array} [asserts=[]]
106+
* @returns {[String]} A list of allowed keys for given collection
107+
*/
108+
function allowedForRole(user, resource, role, privilege, asserts) {
109+
role = roles.get(role)
110+
const allKeys = keys(resource)
111+
112+
let allowedKeys = []
113+
let deniedKeys = []
114+
115+
// return all keys for superuser
116+
if (role.superuser === true) {
117+
return allKeys
118+
}
119+
120+
// ALLOW wildcard
121+
allowedKeys = allowedKeys.concat(
122+
path(allKeys, WILDCARD, ALLOW, role, privilege)
123+
)
124+
125+
// ALLOW statements
126+
allKeys.forEach(function(key) {
127+
allowedKeys = allowedKeys.concat(
128+
path(allKeys, key, ALLOW, role, privilege)
129+
)
130+
})
131+
132+
// asserts
133+
asserts.forEach(function(assert) {
134+
allowedKeys = allowedKeys.concat(
135+
assert(user, o, resource, role, privilege)
136+
)
137+
})
138+
139+
// ALLOW wildcard
140+
deniedKeys = deniedKeys.concat(
141+
path(allKeys, WILDCARD, DENY, role, privilege)
142+
)
143+
144+
// DENY statements
145+
allKeys.forEach(function(key) {
146+
deniedKeys = deniedKeys.concat(
147+
path(allKeys, key, DENY, role, privilege)
148+
)
149+
})
150+
151+
return _.uniq(
152+
_.difference(allowedKeys, deniedKeys)
153+
)
154+
}
155+
156+
/**
157+
* Get allowed keys for given resource
158+
* @param {Document} user
159+
* @param {Document} resource
160+
* @param {String|Object} role
161+
* @param {String} [privilege='R']
162+
* @param {Array} [asserts=[]]
163+
* @returns {[String]} A list of allowed keys for given collection
164+
*/
165+
function allowed(user, resource, role, privilege = 'R', asserts = []) {
166+
role = roles.get(role)
167+
const roleHierarchy = hierarchy(role).reverse()
168+
let allowedKeys = []
169+
170+
if (!_.isArray(asserts)) {
171+
asserts = [asserts]
172+
}
173+
174+
asserts.forEach(function(assert) {
175+
allowedKeys = allowedKeys.concat(
176+
assert(user, o, resource, role, privilege))
177+
})
178+
179+
roleHierarchy.forEach(function(r) {
180+
allowedKeys = allowedKeys.concat(
181+
allowedForRole(user, resource, r, privilege, asserts))
182+
})
183+
184+
return allowedKeys
185+
}
186+
187+
/**
188+
* Test wheter a given parameter is of type object and has a constructor
189+
* of ObjectID
190+
* @param {*} obj
191+
* @returns {Boolean}
192+
*/
193+
function isObjectID(obj) {
194+
return _.isObject(obj) && obj.constructor.name === 'ObjectID'
195+
}
196+
197+
/**
198+
* Filters a resource object by ACL
199+
* @param {Document} user
200+
* @param {Document} resource
201+
* @param {Document} role
202+
* @param {String} [privilege='R']
203+
* @param {Array} [asserts=[]]
204+
* @returns {Object}
205+
*/
206+
function filter(user, resource, role, privilege = 'R', asserts = []) {
207+
const data = _.pick(
208+
resource,
209+
allowed(
210+
user,
211+
resource,
212+
role,
213+
privilege,
214+
asserts
215+
)
216+
)
217+
218+
// filter in populated paths
219+
const allRefs = refs()
220+
_.forEach(allRefs, function(ref, path) {
221+
const subacl = acl({settings: ref})
222+
223+
if (_.isArray(data[path]) && data[path].length > 0) {
224+
data[path] = data[path].map(function(nestedResource) {
225+
// HACK Check for ObjectID objects -> lean does not convert them to String
226+
if (isObjectID(nestedResource)) {
227+
return nestedResource.toHexString()
228+
}
229+
230+
if (_.isObject(nestedResource)) {
231+
return subacl.filter(user, nestedResource, role, privilege, asserts)
232+
}
233+
234+
return nestedResource
235+
})
236+
return
237+
}
238+
239+
// HACK Check for ObjectID objects -> lean does not convert them to String
240+
if (isObjectID(data[path])) {
241+
data[path] = data[path].toHexString()
242+
return
243+
}
244+
245+
if (_.isObject(data[path])) {
246+
data[path] = subacl.filter(user, data[path], role, privilege, asserts)
247+
}
248+
})
249+
250+
return data
251+
}
252+
253+
/**
254+
* Is role with privilege allowed to access resource
255+
* @param {User} user
256+
* @param {Object} role
257+
* @param {String} [privilege='R']
258+
* @returns {Boolean} True if allowed, otherwise false
259+
*/
260+
function resource(user, role, privilege = 'R') {
261+
if (!settings.resource) {
262+
return false
263+
}
264+
265+
if (!settings.resource[ALLOW]) {
266+
return false
267+
}
268+
269+
let isAllowed = false
270+
271+
role = roles.get(role)
272+
273+
// wildcard allow
274+
if (settings.resource[ALLOW] && settings.resource[ALLOW][WILDCARD]) {
275+
if (settings.resource[ALLOW][WILDCARD].indexOf(privilege) !== -1) {
276+
isAllowed = true
277+
}
278+
}
279+
280+
// allow
281+
if (settings.resource[ALLOW] && settings.resource[ALLOW][role.name]) {
282+
if (settings.resource[ALLOW][role.name].indexOf(privilege) !== -1) {
283+
isAllowed = true
284+
}
285+
}
286+
287+
// wildcard deny
288+
if (settings.resource[DENY] && settings.resource[DENY][WILDCARD]) {
289+
if (settings.resource[DENY][WILDCARD].indexOf(privilege) !== -1) {
290+
isAllowed = false
291+
}
292+
}
293+
294+
// deny
295+
if (settings.resource[DENY] && settings.resource[DENY][role.name]) {
296+
if (settings.resource[DENY][role.name].indexOf(privilege) !== -1) {
297+
isAllowed = false
298+
}
299+
}
300+
301+
return isAllowed
302+
}
303+
304+
o.settings = settings
305+
o.allowed = allowed
306+
o.filter = filter
307+
o.resource = resource
308+
309+
return Object.freeze(o)
1310
}

0 commit comments

Comments
 (0)