@live-change/access-control-service 0.2.37 → 0.2.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/access.js CHANGED
@@ -1,38 +1,297 @@
1
+ const { parents, parentsSourcesMap } = require('./accessControlParents.js')
2
+ const App = require('@live-change/framework')
3
+ const app = App.app()
1
4
 
2
5
  module.exports = (definition) => {
3
6
 
4
- const Access = definition.foreignModel('access-control', 'Access')
5
- const PublicAccess = definition.foreignModel('access-control', 'PublicAccess')
7
+ const Access = definition.foreignModel('accessControl', 'Access')
8
+ const PublicAccess = definition.foreignModel('accessControl', 'PublicAccess')
6
9
 
7
- function clientHasAnyAccess(client, { objectType, object }) {
8
- /// TODO: access control
9
- return true
10
+ const config = definition?.config?.access ?? {}
11
+
12
+ const {
13
+ hasAny = (roles, client, { objectType, object }) => roles.length > 0,
14
+ isAdmin = (roles, client, { objectType, object }) => roles.includes('administrator'),
15
+ canInvite = (roles, client, { objectType, object }) => roles.length > 0,
16
+ canRequest = (roles, client, { objectType, object }) => false
17
+ } = config
18
+
19
+ async function clientHasAnyAccess(client, { objectType, object, objects }) {
20
+ return checkRoles(client, { objectType, object, objects }, hasAny)
10
21
  }
11
22
 
12
- function clientHasAdminAccess(client, { objectType, object }) {
13
- /// TODO: access control
14
- return true
23
+ function clientHasAdminAccess(client, { objectType, object, objects }) {
24
+ return checkRoles(client, { objectType, object, objects }, isAdmin)
15
25
  }
16
26
 
17
- function clientCanInvite(client, { roles, objectType, object }) {
18
- /// TODO: access control
19
- return true
27
+ function clientCanInvite(client, { objectType, object, objects }) {
28
+ return checkRoles(client, { objectType, object, objects }, canInvite, true)
20
29
  }
21
30
 
22
- function clientCanRequest(client, { roles, objectType, object }) {
23
- /// TODO: access control
24
- return true
31
+ function clientCanRequest(client, { objectType, object, objects }) {
32
+ return checkRoles(client, { objectType, object, objects }, canRequest)
33
+ }
34
+
35
+ function clientHasAccessRole(client, { objectType, object, objects }, role) {
36
+ return checkRoles(client, { objectType, object, objects }, (roles) => roles.includes(role) )
37
+ }
38
+
39
+ function clientHasAccessRoles(client, { objectType, object, objects }, roles) {
40
+ return checkRoles(client, { objectType, object, objects },
41
+ (clientRoles) => roles.every(role => clientRoles.includes(role))
42
+ )
43
+ }
44
+
45
+ async function getUnitClientRoles(client, { objectType, object }, ignorePublic) {
46
+ const [ sessionOrUserType, sessionOrUser ] = client.user ? ['user_User', client.user] : ['session_Session']
47
+ const [
48
+ publicAccessData,
49
+ sessionAccess,
50
+ userAccess,
51
+ ] = await Promise.all([
52
+ ignorePublic ? null : PublicAccess.get(App.encodeIdentifier([ objectType, object ])),
53
+ Access.get(App.encodeIdentifier([ 'session_Session', client.session, objectType, object ])),
54
+ client.user
55
+ ? Access.get(App.encodeIdentifier([ 'user_User', client.user, objectType, object ]))
56
+ : Promise.resolve(null)
57
+ ])
58
+ let roles = []
59
+ if(publicAccessData) {
60
+ roles.push(...publicAccessData.sessionRoles)
61
+ if(client.user) roles.push(...publicAccessData.userRoles)
62
+ }
63
+ if(sessionAccess) roles.push(...sessionAccess.roles)
64
+ if(userAccess) roles.push(...userAccess.roles)
65
+ return Array.from(new Set(roles))
66
+ }
67
+
68
+ async function getClientObjectRoles(client, { objectType, object }, ignorePublic) {
69
+ const unitRolesPromise = getUnitClientRoles(client, { objectType, object}, ignorePublic)
70
+ const accessParentsPromise = parents[objectType]
71
+ ? parents[objectType]({ objectType, object })
72
+ : Promise.resolve([])
73
+ const parentRolesPromise = accessParentsPromise.then(accessParents => Promise.all(
74
+ accessParents.map(
75
+ ({ objectType, object }) =>
76
+ getClientObjectRoles(client, { objectType, object }, ignorePublic)
77
+ )
78
+ ).then(rolesArrays => rolesArrays.flat()))
79
+ const [ unitRoles, parentRoles ] = await Promise.all([ unitRolesPromise, parentRolesPromise ])
80
+ return Array.from(new Set([ ...client.roles, ...unitRoles, ...parentRoles]))
81
+ }
82
+
83
+ async function getClientObjectsRoles(client, objects, ignorePublic) {
84
+ const objectsRoles = await Promise.all(objects.map(obj => getClientObjectRoles(client, obj, ignorePublic)))
85
+ const firstObjectRoles = objectsRoles.shift()
86
+ let roles = firstObjectRoles
87
+ for(const objectRoles of objectsRoles) {
88
+ roles = roles.filter(role => objectRoles.includes(role))
89
+ }
90
+ return roles
91
+ }
92
+
93
+ async function checkRoles(client, { objectType, object, objects }, callback, ignorePublic) {
94
+ const allObjects = ((objectType && object) ? [{ objectType, object }] : []).concat(objects || [])
95
+ const roles = await getClientObjectsRoles(client, allObjects, ignorePublic)
96
+ return await callback(roles, client, { objectType, object })
97
+ }
98
+
99
+ /// QUERIES:
100
+
101
+ function dbAccessFunctions({ input, publicAccessTable, accessTable, updateRoles, isLoaded }) {
102
+ async function treeNode(objectType, object) {
103
+ const node = {
104
+ objectType, object,
105
+ data: null,
106
+ parents: [],
107
+ publicSessionRoles: [],
108
+ publicUserRoles: [],
109
+ sessionRoles: [],
110
+ userRoles: []
111
+ }
112
+ let objectObserver, publicAccessObserver, sessionAccessObserver, userAccessObserver
113
+
114
+ const publicAccessObject = publicAccessTable.object(`${JSON.stringify(objectType)}:${JSON.stringify(object)}`)
115
+ publicAccessObserver = publicAccessObject.onChange((accessData, oldAccessData) => {
116
+ node.publicSessionRoles = accessData?.sessionRoles ?? []
117
+ node.publicUserRoles = (client.user && accessData?.userRoles) ?? []
118
+ if(isLoaded()) updateRoles()
119
+ })
120
+
121
+ const sessionAccessObject = accessTable.object(
122
+ `session_Session:${JSON.stringify(client.session)}:${JSON.stringify(objectType)}:${JSON.stringify(object)}`
123
+ )
124
+ sessionAccessObserver = sessionAccessObject && sessionAccessObject.onChange((accessData, oldAccessData) => {
125
+ node.sessionRoles = accessData?.roles ?? []
126
+ if(isLoaded()) updateRoles()
127
+ })
128
+
129
+ const userAccessObject = client.user && accessTable.object(
130
+ `user_User:${JSON.stringify(client.user)}:${JSON.stringify(objectType)}:${JSON.stringify(object)}`
131
+ )
132
+ userAccessObserver = userAccessObject && userAccessObject.onChange((accessData, oldAccessData) => {
133
+ node.sessionRoles = accessData?.roles ?? []
134
+ if(isLoaded()) updateRoles()
135
+ })
136
+
137
+ async function disposeParents() {
138
+ const oldParents = node.parents
139
+ return Promise.all(oldParents.map(parent => parent.dispose()))
140
+ }
141
+ const parentsSources = parentsSourcesMap[objectType]
142
+ if(parentsSources) {
143
+ const objectTable = input.table(objectType)
144
+ const objectTableObject = objectTable.object(object)
145
+ objectObserver = objectTableObject.onChange(async (objectData, oldObjectData) => {
146
+ await disposeParents()
147
+ node.parents = objectData ? await Promise.all(parentsSources.map(parentSource => {
148
+ const parentType = parentSource.type || objectData[parentSource.property + 'Type']
149
+ const parent = objectData[parentSource.property]
150
+ return treeNode(parentType, parent)
151
+ })) : []
152
+ })
153
+ }
154
+ node.dispose = async function() {
155
+ const disposePromises = []
156
+ if(objectObserver) disposePromises.push(objectObserver.then(obs => obs.dispose()))
157
+ if(publicAccessObserver) disposePromises.push(publicAccessObserver.then(obs => obs.dispose()))
158
+ if(sessionAccessObserver) disposePromises.push(sessionAccessObserver.then(obs => obs.dispose()))
159
+ if(userAccessObserver) disposePromises.push(userAccessObserver.then(obs => obs.dispose()))
160
+ disposePromises.push(disposeParents())
161
+ return Promise.all(disposePromises)
162
+ }
163
+ await Promise.all([ objectObserver, publicAccessObserver, sessionAccessObserver, userAccessObserver ])
164
+ return node
165
+ }
166
+ function computeNodeRoles(node) {
167
+ const parentsRoles = node.parents.map(parent => computeNodeRoles(parent)).flat()
168
+ return Array.from(new Set([
169
+ ...parentsRoles,
170
+ ...node.publicUserRoles,
171
+ ...node.publicSessionRoles,
172
+ ...node.userRoles,
173
+ ...node.sessionRoles
174
+ ]))
175
+ }
176
+ return { treeNode, computeNodeRoles }
177
+ }
178
+
179
+ function accessPath(client, objects) {
180
+ return ['database', 'queryObject', app.databaseName, `(${
181
+ async (input, output, {
182
+ objects, parentsSourcesMap, client,
183
+ accessTableName, publicAccessTableName, dbAccessFunctions
184
+ }) => {
185
+ const accessTable = input.table(accessTableName)
186
+ const publicAccessTable = input.table(publicAccessTableName)
187
+ let loaded = false
188
+
189
+ const { treeNode, computeNodeRoles } =
190
+ eval(dbAccessFunctions)({ input, publicAccessTable, accessTable, updateRoles, isLoaded: () => loaded })
191
+
192
+ let rolesTreesRoots = objects.map(({ object, objectType }) => treeNode(objectType, object))
193
+
194
+ const outputObjectId = `${JSON.stringify(client.session)}:${JSON.stringify(client.user)}:` +
195
+ objects.map( obj => `${JSON.stringify(objectType)}:${JSON.stringify(object)}`)
196
+ .join(':')
197
+ let oldOutputObject = null
198
+ async function updateRoles() {
199
+ const roots = await Promise.all(rolesTreesRoots)
200
+ const accesses = roots.map(root => computeNodeRoles(root))
201
+ const firstAccess = accesses.shift()
202
+ let roles = firstAccess.roles
203
+ for(const access of accesses) {
204
+ roles = roles.filter(role => access.roles.includes(role))
205
+ }
206
+ const accessControlRoles = computeNodeRoles()
207
+ const outputObject = {
208
+ id: outputObjectId,
209
+ roles: Array.from(new Set([...accessControlRoles, ...client.roles]))
210
+ }
211
+ output.change(outputObject, oldOutputObject)
212
+ oldOutputObject = outputObject
213
+ }
214
+ await Promise.all(rolesTreesRoots)
215
+ loaded = true
216
+ await updateRoles()
217
+ }
218
+ })`, {
219
+ objectType, object, parentsSourcesMap, client,
220
+ accessTableName: Access.tableName, publicAccessTableName: PublicAccess.tableName,
221
+ dbAccessFunctions: `(${dbAccessFunctions})`
222
+ }]
223
+ }
224
+
225
+ function accessesPath(client, objects) {
226
+ return ['database', 'query', app.databaseName, `(${
227
+ async (input, output, {
228
+ objects, parentsSourcesMap, client,
229
+ accessTableName, publicAccessTableName, dbAccessFunctions
230
+ }) => {
231
+ const accessTable = input.table(accessTableName)
232
+ const publicAccessTable = input.table(publicAccessTableName)
233
+ let loaded = false
234
+
235
+ const { treeNode, computeNodeRoles } =
236
+ eval(dbAccessFunctions)({ input, publicAccessTable, accessTable, updateRoles, isLoaded: () => loaded })
237
+
238
+ let rolesTreesRoots = objects.map(({ object, objectType }) => treeNode(objectType, object))
239
+ const accesses = []
240
+ async function updateRoles() {
241
+ const roots = await Promise.all(rolesTreesRoots)
242
+ for(let root of roots) {
243
+ const outputObjectId = `${JSON.stringify(client.session)}:${JSON.stringify(client.user)}` +
244
+ `:${JSON.stringify(root.objectType)}:${JSON.stringify(root.object)}`
245
+ const nodeRoles = computeNodeRoles(root)
246
+ const outputObject = {
247
+ id: outputObjectId,
248
+ roles: Array.from(new Set([...nodeRoles, ...client.roles]))
249
+ }
250
+ const existingAccessIndex = accesses.findIndex(acc => acc.id == outputObjectId)
251
+ if(existingAccessIndex != -1) {
252
+ if(JSON.stringify(outputObject) != JSON.stringify(accesses[existingAccessIndex])) {
253
+ output.change(outputObject, accesses[existingAccessIndex])
254
+ accesses[existingAccessIndex] = outputObject
255
+ } /// else ignore
256
+ } else {
257
+ output.change(outputObject, null)
258
+ accesses.push(outputObject)
259
+ }
260
+ }
261
+ }
262
+ await Promise.all(rolesTreesRoots)
263
+ loaded = true
264
+ await updateRoles()
265
+ }
266
+ })`, {
267
+ objects, parentsSourcesMap, client,
268
+ accessTableName: Access.tableName, publicAccessTableName: PublicAccess.tableName,
269
+ }]
270
+ }
271
+
272
+ function accessLimitedGet(client, objects, requiredRoles, path) {
273
+ const roles = getClientObjectsRoles(client, objects)
274
+ for(const requiredRole of requiredRoles) {
275
+
276
+ }
25
277
  }
26
278
 
27
- function clientHasAccessRole(client, { objectType, object }, role) {
28
- return true
279
+ function accessLimitedObservable(client, objects, path) {
280
+ if(path[0] != 'database') throw new Error("non database path "+ JSON.stringify(path))
281
+ const isObject = path[1] == 'queryObject' || path[1] == ''
29
282
  }
30
283
 
31
284
  return {
32
285
  clientHasAnyAccess, clientHasAdminAccess,
33
286
  clientCanInvite,
34
287
  clientCanRequest,
35
- clientHasAccessRole
288
+ clientHasAccessRole,
289
+ clientHasAccessRoles,
290
+ getClientObjectRoles,
291
+ getClientObjectsRoles,
292
+ checkRoles,
293
+ accessPath,
294
+ accessesPath
36
295
  }
37
296
 
38
297
  }
@@ -0,0 +1,22 @@
1
+ const definition = require("./definition.js")
2
+
3
+ const parents = { }
4
+ const parentsSources = { }
5
+
6
+ definition.processor(function(service, app) {
7
+
8
+ for(let modelName in service.models) {
9
+ const model = service.models[modelName]
10
+ if(!model.accessControlParents) continue
11
+ parents[service.name + '_' + modelName] = model.accessControlParents
12
+ }
13
+
14
+ for(let modelName in service.models) {
15
+ const model = service.models[modelName]
16
+ if(!model.accessControlParentsSource) continue
17
+ parentsSources[service.name + '_' + modelName] = model.accessControlParentsSource
18
+ }
19
+
20
+ })
21
+
22
+ module.exports = { parents, parentsSources }
package/index.js CHANGED
@@ -5,5 +5,6 @@ const definition = require('./definition.js')
5
5
  require('./model.js')
6
6
  require('./invite.js')
7
7
  require('./request.js')
8
+ require('./view.js')
8
9
 
9
10
  module.exports = definition
package/invite.js CHANGED
@@ -221,6 +221,14 @@ for(const contactType of config.contactTypes) {
221
221
  access: (params, { client, context, visibilityTest }) =>
222
222
  visibilityTest || access.clientCanInvite(client, params),
223
223
  async execute(params, { client, service }, emit) {
224
+ const { roles } = params
225
+ const myRoles = await access.getClientObjectRoles(client, { objectType, object }, true)
226
+ if(!myRoles.includes('administrator')) {
227
+ for(const requestedRole of roles) {
228
+ if(!myRoles.includes(requestedRole)) throw 'notAuthorized'
229
+ }
230
+ }
231
+
224
232
  const [ fromType, from ] = client.user ? ['user_User', client.user] : ['session_Session', client.session]
225
233
  const { [contactTypeName]: contact } = params
226
234
  const { objectType, object } = params
@@ -0,0 +1,107 @@
1
+ const definition = require('./definition.js')
2
+ const App = require("@live-change/framework")
3
+ const { ObservableValue, ObservableList, ObservableProxy } = require("@live-change/dao")
4
+ const app = App.app()
5
+ const access = require('./access.js')(definition)
6
+
7
+ definition.processor(function(service, app) {
8
+
9
+ for(const actionName in service.actions) {
10
+ const action = service.actions[actionName]
11
+ if(!action.limitedAccess) continue
12
+ const config = action.limitedAccess
13
+
14
+ console.log("LIMITED ACCESS", service.name, "ACTION", action.name)
15
+
16
+ const oldExec = action.execute
17
+ action.execute = async (...args) => {
18
+ const [ properties, context, emit ] = args
19
+ const { client } = context
20
+
21
+ const objects = [].concat(
22
+ config.objects ? config.objects(properties) : [],
23
+ (objectType && object) ? [{ objectType, object }] : []
24
+ )
25
+ if(objects.length == 0) {
26
+ throw new Error('no objects for access control to work')
27
+ }
28
+ const accessible = access.clientHasAccessRoles(client, { objects }, config.roles)
29
+ if(!accessible) throw 'notAuthorized'
30
+
31
+ return oldExec.apply(action, args)
32
+ }
33
+ }
34
+
35
+ for(const viewName in service.views) {
36
+ const view = service.view[viewName]
37
+ if(!view.limitedAccess) continue
38
+ const config = view.limitedAccess
39
+
40
+ console.log("LIMITED ACCESS", service.name, "VIEW", view.name)
41
+
42
+ const oldGet = view.get
43
+ const oldObservable = view.observable
44
+ view.get = async (...args) => {
45
+ const [ properties, context ] = args
46
+ const { client } = context
47
+ const { objectType, object } = properties
48
+ const objects = [].concat(
49
+ config.objects ? config.objects(properties) : [],
50
+ (objectType && object) ? [{ objectType, object }] : []
51
+ )
52
+ if(objects.length == 0) {
53
+ throw new Error('no objects for access control to work')
54
+ }
55
+ const accessible = access.clientHasAccessRoles(client, { objects }, config.roles)
56
+ if(!accessible) throw 'notAuthorized'
57
+ return oldGet.apply(view, args)
58
+ }
59
+ view.observable = (...args) => {
60
+ const [ properties, context ] = args
61
+ const { client } = context
62
+ const { objectType, object } = properties
63
+ const objects = [].concat(
64
+ config.objects ? config.objects(properties) : [],
65
+ (objectType && object) ? [{ objectType, object }] : []
66
+ )
67
+ if(objects.length == 0) {
68
+ throw new Error('no objects for access control to work')
69
+ }
70
+
71
+ const rolesPath = access.accessPath(client, objects)
72
+
73
+ const errorObservable = new ObservableValue()
74
+ errorObservable.handleError('notAuthorized')
75
+
76
+ const observableProxy = new ObservableProxy(null)
77
+
78
+ let valueObservable
79
+
80
+ let accessible
81
+ const rolesObservable = app.dao.observable(rolesPath)
82
+ const rolesObserver = (signal, value) => {
83
+ const accessObject = rolesObservable.getValue()
84
+ const newAccessible = config.roles.every(role => accessObject.roles.includes(role))
85
+ if(newAccessible !== accessible) {
86
+ if(newAccessible === true /*&& !valueObservable*/) {
87
+ valueObservable = oldObservable.apply(view, args)
88
+ }
89
+ observableProxy.setTarget(accessible ? valueObservable : errorObservable)
90
+ }
91
+ }
92
+ rolesObservable.observe(rolesObserver)
93
+
94
+ const oldDispose = observableProxy.dispose
95
+ const oldRespawn = observableProxy.respawn
96
+ observableProxy.dispose = () => {
97
+ rolesObservable.unobserve(rolesObserver)
98
+ oldDispose.apply(observableProxy)
99
+ }
100
+ observableProxy.respawn = () => {
101
+ rolesObservable.observe(rolesObserver)
102
+ oldRespawn.apply(observableProxy)
103
+ }
104
+ }
105
+ }
106
+
107
+ })
package/model.js CHANGED
@@ -39,7 +39,7 @@ const PublicAccess = definition.model({
39
39
  to: 'object',
40
40
  readAccess: (params, { client, context, visibilityTest }) =>
41
41
  visibilityTest || access.clientHasAnyAccess(client, params),
42
- writeAccess: (params, { client, context, visibilityTest }) =>
42
+ writeAccess: async (params, { client, context, visibilityTest }) =>
43
43
  visibilityTest || access.clientHasAdminAccess(client, params)
44
44
  },
45
45
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@live-change/access-control-service",
3
- "version": "0.2.37",
3
+ "version": "0.2.40",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -21,7 +21,7 @@
21
21
  "url": "https://www.viamage.com/"
22
22
  },
23
23
  "dependencies": {
24
- "@live-change/framework": "0.6.5"
24
+ "@live-change/framework": "0.6.6"
25
25
  },
26
- "gitHead": "f3bc615d20a0112c7cc76d55ba1cbefb53b84f01"
26
+ "gitHead": "4a920b328b0a7f3f25c67cdba3e574687971ee22"
27
27
  }
package/request.js CHANGED
@@ -43,6 +43,12 @@ definition.action({
43
43
  access: (params, { client, context, visibilityTest }) =>
44
44
  visibilityTest || access.clientCanInvite(client, params),
45
45
  async execute({ objectType, object, sessionOrUserType, sessionOrUser, roles }, { client, service }, emit) {
46
+ const myRoles = await access.getClientObjectRoles(client, { objectType, object }, true)
47
+ if(!myRoles.includes('administrator')) {
48
+ for(const requestedRole of roles) {
49
+ if(!myRoles.includes(requestedRole)) throw 'notAuthorized'
50
+ }
51
+ }
46
52
  const request = App.encodeIdentifier([ sessionOrUserType, sessionOrUser, objectType, object ])
47
53
  const requestData = await AccessRequest.get(request)
48
54
  if(!requestData) throw 'not_found'
package/view.js ADDED
@@ -0,0 +1,72 @@
1
+ const definition = require("./definition.js")
2
+ const App = require("@live-change/framework")
3
+ const app = App.app()
4
+
5
+ const access = require('./access.js')(definition)
6
+
7
+ definition.view({
8
+ name: "myAccessTo",
9
+ properties: {
10
+ objectType: {
11
+ type: String
12
+ },
13
+ object: {
14
+ type: String
15
+ },
16
+ objects: {
17
+ type: Array,
18
+ of: {
19
+ type: Object,
20
+ properties: {
21
+ objectType: {
22
+ type: String
23
+ },
24
+ object: {
25
+ type: String
26
+ },
27
+ }
28
+ }
29
+ }
30
+ },
31
+ returns: {
32
+ type: Array,
33
+ of: {
34
+ type: String
35
+ }
36
+ },
37
+ async daoPath({ objectType, object, objects }, { client, service }, method) {
38
+ const allObjects = ((objectType && object) ? [{ objectType, object }] : []).concat(objects || [])
39
+ if(allObjects.length == 0) throw 'empty_objects_list'
40
+ return access.accessPath(client, allObjects)
41
+ }
42
+ })
43
+
44
+ definition.view({
45
+ name: "myAccessesTo",
46
+ properties: {
47
+ objects: {
48
+ type: Array,
49
+ of: {
50
+ type: Object,
51
+ properties: {
52
+ objectType: {
53
+ type: String
54
+ },
55
+ object: {
56
+ type: String
57
+ }
58
+ }
59
+ }
60
+ }
61
+ },
62
+ returns: {
63
+ type: Array,
64
+ of: {
65
+ type: String
66
+ }
67
+ },
68
+ async daoPath({ objects }, { client, service }, method) {
69
+ if(objects.length == 0) throw 'empty_objects_list'
70
+ return access.accessesPath(client, objects)
71
+ }
72
+ })