@live-change/access-control-service 0.9.28 → 0.9.30

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/index.js CHANGED
@@ -8,5 +8,6 @@ import './invite.js'
8
8
  import './request.js'
9
9
  import './view.js'
10
10
  import './accessControl.js'
11
+ import './indexes.js'
11
12
 
12
13
  export default definition
package/indexes.js ADDED
@@ -0,0 +1,480 @@
1
+ import App from '@live-change/framework'
2
+ const app = App.app()
3
+ import definition from './definition.js'
4
+ const config = definition.config
5
+
6
+ import { parentsSources } from './accessControlParents.js'
7
+
8
+ const AccessParent = definition.model({
9
+ name: "AccessParent",
10
+ properties: {
11
+ childType: {
12
+ type: String
13
+ },
14
+ property: {
15
+ type: String
16
+ },
17
+ type: {
18
+ type: String
19
+ },
20
+ possibleTypes: {
21
+ type: Array,
22
+ of: {
23
+ type: String
24
+ }
25
+ }
26
+ }
27
+ })
28
+
29
+ definition.afterStart(async () => {
30
+ const destObjects =
31
+ Object.entries(parentsSources)
32
+ .flatMap(([childType, properties]) =>
33
+ properties.map(property => ({
34
+ id: `${childType}.${property.property}`,
35
+ childType,
36
+ ...property
37
+ }))
38
+ )
39
+ let idsSet = new Set(destObjects.map(obj => obj.id))
40
+ const existingParents = await AccessParent.rangeGet({})
41
+ const promises = []
42
+ for(const obj of destObjects) {
43
+ promises.push(AccessParent.create(obj))
44
+ }
45
+ for(const parent of existingParents) {
46
+ if(!idsSet.has(parent.id)) {
47
+ promises.push(AccessParent.delete(parent.id))
48
+ }
49
+ }
50
+ await Promise.all(promises)
51
+ })
52
+
53
+ if(config.indexed) {
54
+
55
+ definition.index({
56
+ name: 'childByParent',
57
+ async function(input, output,
58
+ { accessParentTableName }) {
59
+
60
+ const reindexerBucket = 128
61
+
62
+ function indexEntry(parentType, childType, parent, child, property) {
63
+ const indexPart = [parentType, parent, childType, child]
64
+ .map(v => JSON.stringify(v)).join(':')
65
+ return {
66
+ id: indexPart + '_' + property,
67
+ parentType,
68
+ parent,
69
+ childType,
70
+ child,
71
+ property
72
+ }
73
+ }
74
+
75
+ function indexEntryByProperty(childType, item, property) {
76
+ const value = item?.[property.property]
77
+ if(!value) return
78
+ const type = property.type || item[property.property + 'Type']
79
+ return indexEntry(type, childType, value, item.id, property.property)
80
+ }
81
+
82
+ class TableIndexer {
83
+ constructor(childType, properties) {
84
+ this.childType = childType
85
+ this.properties = properties
86
+ this.removedProperties = []
87
+ this.table = input.table(childType)
88
+ this.promise = this.table.onChange(async (item, oldItem) => await this.index(item, oldItem))
89
+ }
90
+
91
+ async removeProperty(property) {
92
+ const index = this.properties.indexOf(property)
93
+ if(index >= 0) {
94
+ this.properties.splice(index, 1)
95
+ this.removedProperties.push(property)
96
+ await this.reindex()
97
+ this.removedProperties.splice(this.removedProperties.indexOf(property), 1)
98
+ }
99
+ }
100
+
101
+ async addProperty(property) {
102
+ this.properties.push(property)
103
+ await this.reindex()
104
+ }
105
+
106
+ async reindex() {
107
+ let position = ''
108
+ while(true) {
109
+ const bucket = await this.table.get({
110
+ gt: position,
111
+ limit: reindexerBucket
112
+ })
113
+ for(const item of bucket) await this.index(item)
114
+ if(bucket.length < reindexerBucket) break
115
+ }
116
+ }
117
+
118
+ async index(item, oldItem) {
119
+ output.debug("!!!", item, oldItem, this.properties)
120
+ for(const property of this.removedProperties) {
121
+ await output.remove(indexEntryByProperty(this.childType, item, property))
122
+ }
123
+ for(const property of this.properties) {
124
+ const newEntry = indexEntryByProperty(this.childType, item, property)
125
+ const oldEntry = indexEntryByProperty(this.childType, oldItem, property)
126
+ output.debug('<-->', newEntry, oldEntry)
127
+ if(newEntry || oldEntry) await output.change(newEntry, oldEntry)
128
+ }
129
+ }
130
+ }
131
+
132
+ const tableIndexers = new Map()
133
+
134
+ async function addParentProperty(property) {
135
+ let indexer = tableIndexers.get(property.childType)
136
+ if(!indexer) {
137
+ indexer = new TableIndexer(property.childType, [property.property])
138
+ tableIndexers.set(property.childType, indexer)
139
+ await indexer.promise
140
+ }
141
+ await indexer.addProperty(property)
142
+ }
143
+
144
+ function removeParentProperty(property) {
145
+ let indexer = tableIndexers.get(property.childType)
146
+ if(!indexer) return
147
+ indexer.removeProperty(property)
148
+ }
149
+
150
+ const accessParentTable = input.table(accessParentTableName)
151
+ let parentsLoaded = false
152
+ await accessParentTable.onChange((parentProperty, oldParentProperty) => {
153
+ if(!parentsLoaded) return
154
+ if(oldParentProperty && !parentProperty) removeParentProperty(oldParentProperty)
155
+ if(parentProperty) addParentProperty(parentProperty)
156
+ })
157
+ const initialParentsState = await accessParentTable.get({})
158
+ const propertiesByChildType = new Map()
159
+ for(const parentProperty of initialParentsState) {
160
+ const properties = propertiesByChildType.get(parentProperty.childType) || []
161
+ properties.push(parentProperty)
162
+ propertiesByChildType.set(parentProperty.childType, properties)
163
+ }
164
+ const indexerPromises = []
165
+ for(const [childType, properties] of propertiesByChildType) {
166
+ const indexer = new TableIndexer(childType, properties)
167
+ tableIndexers.set(childType, indexer)
168
+ indexerPromises.push(indexer.promise)
169
+ }
170
+ parentsLoaded = true
171
+ await Promise.all(indexerPromises)
172
+ },
173
+ parameters: {
174
+ accessParentTableName: definition.name + '_AccessParent'
175
+ }
176
+ })
177
+
178
+ definition.index({
179
+ name: 'parentByChild',
180
+ dependencies: ['childByParent'],
181
+ async function(input, output, { childByParentIndexName }) {
182
+ function mapper(entry) {
183
+ if(!entry) return null
184
+ const { parentType, parent, childType, child, property } = entry
185
+ const indexPart = [childType, child, parentType, parent]
186
+ .map(v => JSON.stringify(v)).join(':')
187
+ return {
188
+ ...entry,
189
+ id: indexPart + '_' + property,
190
+ }
191
+ }
192
+
193
+ const childByParentIndex = await input.index(childByParentIndexName)
194
+ await childByParentIndex.onChange(async (entry, oldEntry) =>
195
+ await output.change(mapper(entry), mapper(oldEntry))
196
+ )
197
+ },
198
+ parameters: {
199
+ childByParentIndexName: definition.name + '_childByParent'
200
+ }
201
+ })
202
+
203
+ definition.index({
204
+ name: 'pathsByAncestorDescendantRelation',
205
+ dependencies: ['childByParent', 'parentByChild'],
206
+ async function(input, output, { childByParentIndexName, parentByChildIndexName }) {
207
+ const childByParentIndex = await input.index(childByParentIndexName)
208
+ const parentByChildIndex = await input.index(parentByChildIndexName)
209
+
210
+ const bucketSize = 128
211
+
212
+ async function iterate(index, prefix, cb) {
213
+ let position = prefix + ':'
214
+ while(true) {
215
+ const bucket = await index.get({
216
+ gt: position,
217
+ lte: prefix + '_\xFF\xFF\xFF\xFF',
218
+ limit: bucketSize
219
+ })
220
+ for(const entry of bucket) await cb(entry)
221
+ if(bucket.length < bucketSize) break
222
+ position = bucket[bucket.length - 1].id
223
+ }
224
+ }
225
+
226
+ async function propagateChange(create, ancestorType, ancestor, descendantType, descendant,
227
+ intermediate = []) {
228
+ if(ancestorType === descendantType && ancestor === descendant) {// break recursion cycle
229
+ output.debug("FOUND PARENT RECURSION", ancestorType, ancestor, intermediate)
230
+ return
231
+ }
232
+ //output.debug("PROPAGATE", create, ancestorType, ancestor, descendantType, descendant, intermediate)
233
+ const hash = sha1([...intermediate].join('>'), 'base64')
234
+ const pathId = [ancestorType, ancestor, descendantType, descendant].map(v => JSON.stringify(v))
235
+ .join(':') + '_' + hash
236
+ const exists = await output.objectGet(pathId)
237
+ if(create === exists) return // no change
238
+ const entry = {
239
+ id: pathId,
240
+ ancestorType, ancestor, descendantType, descendant, intermediate, hash
241
+ }
242
+ if(create) {
243
+ await output.put(entry)
244
+ } else {
245
+ await output.delete(entry)
246
+ }
247
+ const descendantPrefix = [descendantType, descendant].map(v => JSON.stringify(v)).join(':')
248
+ const ancestorPrefix = [ancestorType, ancestor].map(v => JSON.stringify(v)).join(':')
249
+ await iterate(childByParentIndex, descendantPrefix, async descendantChild => {
250
+ await propagateChange(create,
251
+ ancestorType, ancestor, descendantChild.childType, descendantChild.child,
252
+ [...intermediate, descendantPrefix, descendantChild.property])
253
+ })
254
+ await iterate(parentByChildIndex, ancestorPrefix, async ancestorParent => {
255
+ await propagateChange(create,
256
+ ancestorParent.parentType, ancestorParent.parent, descendantType, descendant,
257
+ [ancestorParent.property, ancestorPrefix, ...intermediate])
258
+ })
259
+ }
260
+
261
+ /// reacting to index that are generated from childByParent index, because it will be updated later.
262
+ await parentByChildIndex.onChange(async (entry, oldEntry) => {
263
+ const anyEntry = entry || oldEntry
264
+ await propagateChange(!!entry, entry.parentType, entry.parent, entry.childType, entry.child, [anyEntry.property])
265
+ })
266
+ },
267
+ parameters: {
268
+ childByParentIndexName: definition.name + '_childByParent',
269
+ parentByChildIndexName: definition.name + '_parentByChild'
270
+ }
271
+ })
272
+
273
+ definition.index({
274
+ name: 'expandedRoles',
275
+ async function(input, output, { accessIndexName, publicAccessTableName, pathsIndexName }) {
276
+ const bucketSize = 128
277
+
278
+ async function iterate(source, prefix, cb) {
279
+ let position = prefix + ':'
280
+ while(true) {
281
+ const bucket = await source.get({
282
+ gt: position,
283
+ lte: prefix + '_\xFF\xFF\xFF\xFF',
284
+ limit: bucketSize
285
+ })
286
+ for(const entry of bucket) await cb(entry)
287
+ if(bucket.length < bucketSize) break
288
+ position = bucket[bucket.length - 1].id
289
+ }
290
+ }
291
+
292
+ const accessIndex = await input.index(accessIndexName)
293
+ const publicAccessTable = input.table(publicAccessTableName)
294
+ const pathsIndex = await input.index(pathsIndexName)
295
+
296
+ function updateRoles(descendantType, descendant, sessionOrUserType, sessionOrUser, pathId,
297
+ rolesAdded, rolesRemoved,
298
+ pathIdHash = sha1(pathId, 'base64')) {
299
+ const prefix = [descendantType, descendant, sessionOrUserType, sessionOrUser]
300
+ .map(v => JSON.stringify(v)).join(':')
301
+ const promises = []
302
+ for(const role of rolesAdded) {
303
+ promises.push(output.put({
304
+ id: prefix + ':' + JSON.stringify(role) + '_' + pathIdHash,
305
+ objectType: descendantType,
306
+ object: descendant,
307
+ sessionOrUserType,
308
+ sessionOrUser,
309
+ role,
310
+ path: pathId
311
+ }))
312
+ }
313
+ for(const role of rolesRemoved) {
314
+ promises.push(output.delete({
315
+ id: prefix + ':' + JSON.stringify(role) + '_' + pathIdHash
316
+ }))
317
+ }
318
+ return Promise.all(promises)
319
+ }
320
+
321
+ function rolesDiff(roles, oldRoles) {
322
+ if(!roles) roles = []
323
+ if(!oldRoles) oldRoles = []
324
+ const rolesAdded = roles.filter(r => !oldRoles.includes(r))
325
+ const rolesRemoved = oldRoles.filter(r => !roles.includes(r))
326
+ return [ rolesAdded, rolesRemoved ]
327
+ }
328
+
329
+ async function handleAccessChanged(objectType, object, sessionOrUserType, sessionOrUser,
330
+ rolesAdded, rolesRemoved) {
331
+ /// Find object descendants
332
+ const pathsPrefix = [objectType, object].map(v => JSON.stringify(v)).join(':')
333
+ await iterate(pathsIndex, pathsPrefix, async path => {
334
+ const promises = []
335
+ const { descendantType, descendant, id: pathId } = path
336
+ const pathIdHash = sha1(pathId, 'base64')
337
+ await updateRoles(descendantType, descendant, sessionOrUserType, sessionOrUser, pathId,
338
+ rolesAdded, rolesRemoved, pathIdHash)
339
+ })
340
+ }
341
+
342
+ async function handlePathChanged(path, oldPath) {
343
+ const existingPath = path || oldPath
344
+ const { ancestorType, ancestor, descendantType, descendant, id: pathId } = existingPath
345
+ const pathIdHash = sha1(pathId, 'base64')
346
+ const prefix = [ancestorType, ancestor].map(v => JSON.stringify(v)).join(':')
347
+ const publicAccess = await publicAccessTable.objectGet(prefix)
348
+ await Promise.all([
349
+ updateRoles(descendantType, descendant, 'publicSession', '', pathId,
350
+ path && publicAccess?.sessionRoles || [],
351
+ !path && publicAccess?.sessionRoles || [],
352
+ pathIdHash),
353
+ updateRoles(descendantType, descendant, 'publicUser', '', pathId,
354
+ path && publicAccess?.userRoles || [],
355
+ !path && publicAccess?.userRoles || [],
356
+ pathIdHash),
357
+ ])
358
+ await iterate(accessIndex, prefix, async access => {
359
+ const { sessionOrUserType, sessionOrUser, roles } = access
360
+ await updateRoles(descendantType, descendant, sessionOrUserType, sessionOrUser, pathId,
361
+ path ? roles : [], path ? [] : roles, pathIdHash)
362
+ })
363
+ }
364
+
365
+ await accessIndex.onChange(async (access, oldAccess) => {
366
+ const existingAccess = access || oldAccess
367
+ const { objectType, object, sessionOrUserType, sessionOrUser } = existingAccess
368
+ const [ rolesAdded, rolesRemoved ] = rolesDiff(access?.roles, oldAccess?.roles)
369
+ await updateRoles(objectType, object, sessionOrUserType, sessionOrUser, null,
370
+ rolesAdded, rolesRemoved, 'self')
371
+ await handleAccessChanged(objectType, object, sessionOrUserType, sessionOrUser, rolesAdded, rolesRemoved)
372
+ })
373
+
374
+ await publicAccessTable.onChange(async (publicAccess, oldPublicAccess) => {
375
+ const existingPublicAccess = publicAccess || oldPublicAccess
376
+ const { objectType, object } = existingPublicAccess
377
+ const [sessionRolesAdded, sessionRolesRemoved] = rolesDiff(
378
+ publicAccess?.sessionRoles, oldPublicAccess?.sessionRoles
379
+ )
380
+ const [userRolesAdded, userRolesRemoved] = rolesDiff(
381
+ publicAccess?.userRoles, oldPublicAccess?.userRoles
382
+ )
383
+ await Promise.all([
384
+ updateRoles(objectType, object, 'publicSession', '', null,
385
+ sessionRolesAdded, sessionRolesRemoved, 'self'),
386
+ updateRoles(objectType, object, 'publicUser', '', null,
387
+ userRolesAdded, userRolesRemoved, 'self')
388
+ ])
389
+ await handleAccessChanged(objectType, object, 'publicSession', '',
390
+ sessionRolesAdded, sessionRolesRemoved)
391
+ await handleAccessChanged(objectType, object, 'publicUser', '',
392
+ userRolesAdded, userRolesRemoved)
393
+ })
394
+
395
+ //await pathsIndex.onChange(handlePathChanged)
396
+ },
397
+ parameters: {
398
+ accessIndexName: definition.name + '_Access_byObjectExtended',
399
+ publicAccessTableName: definition.name + '_PublicAccess',
400
+ pathsIndexName: definition.name + '_pathsByAncestorDescendantRelation'
401
+ }
402
+ })
403
+
404
+ definition.index({
405
+ name: 'roleByOwnerAndObject',
406
+ async function(input, output, { expandedRolesIndexName }) {
407
+ const expandedRolesIndex = await input.index(expandedRolesIndexName)
408
+ await expandedRolesIndex.onChange(async (expandedRole, oldExpandedRole) => {
409
+ const existingExpandedRole = expandedRole || oldExpandedRole
410
+ const { sessionOrUserType, sessionOrUser, role, objectType, object } = existingExpandedRole
411
+ const sourcePrefix = existingExpandedRole.id.slice(0, existingExpandedRole.id.lastIndexOf('_'))
412
+ // counting needed only when delete detected!
413
+ const count = (expandedRole && 1) || (await expandedRolesIndex.count({
414
+ gte: sourcePrefix + ':',
415
+ lte: sourcePrefix + '_\xFF\xFF\xFF\xFF',
416
+ limit: 1
417
+ }))
418
+ if(count) {
419
+ await output.put({
420
+ id: [sessionOrUserType, sessionOrUser, objectType, object, role]
421
+ .map(v => JSON.stringify(v)).join(':'),
422
+ sessionOrUserType, sessionOrUser, role, objectType, object
423
+ })
424
+ } else {
425
+ await output.delete({
426
+ id: [sessionOrUserType, sessionOrUser, objectType, object, role]
427
+ .map(v => JSON.stringify(v)).join(':')
428
+ })
429
+ }
430
+ })
431
+ },
432
+ parameters: {
433
+ expandedRolesIndexName: definition.name + '_expandedRoles'
434
+ }
435
+ })
436
+ definition.index({
437
+ name: 'objectByOwnerAndRole',
438
+ async function(input, output, { rolesIndexName }) {
439
+ const rolesIndex = await input.index(rolesIndexName)
440
+ const mapper = (source) => {
441
+ if(!source) return null
442
+ const { sessionOrUserType, sessionOrUser, role, objectType, object } = source
443
+ return {
444
+ id: [sessionOrUserType, sessionOrUser, role, objectType, object]
445
+ .map(v => JSON.stringify(v)).join(':'),
446
+ sessionOrUserType, sessionOrUser, role, objectType, object
447
+ }
448
+ }
449
+ await rolesIndex.onChange(async (role, oldRole) => {
450
+ await output.change(mapper(role), mapper(oldRole))
451
+ })
452
+ },
453
+ parameters: {
454
+ rolesIndexName: definition.name + '_roleByOwnerAndObject'
455
+ }
456
+ })
457
+
458
+ definition.index({
459
+ name: 'ownerByObjectAndRole',
460
+ async function(input, output, { rolesIndexName }) {
461
+ const rolesIndex = await input.index(rolesIndexName)
462
+ const mapper = (source) => {
463
+ if(!source) return null
464
+ const { sessionOrUserType, sessionOrUser, role, objectType, object } = source
465
+ return {
466
+ id: [objectType, object, role, sessionOrUserType, sessionOrUser]
467
+ .map(v => JSON.stringify(v)).join(':'),
468
+ sessionOrUserType, sessionOrUser, role, objectType, object
469
+ }
470
+ }
471
+ await rolesIndex.onChange(async (role, oldRole) => {
472
+ await output.change(mapper(role), mapper(oldRole))
473
+ })
474
+ },
475
+ parameters: {
476
+ rolesIndexName: definition.name + '_roleByOwnerAndObject'
477
+ }
478
+ })
479
+
480
+ }
package/invite.js CHANGED
@@ -73,7 +73,7 @@ definition.event({
73
73
  })
74
74
 
75
75
  definition.trigger({
76
- name: 'contactOrUserOwnedAccessInvitationMoved',
76
+ name: 'AccessInvitationMoved',
77
77
  properties: {
78
78
  ...contactProperties,
79
79
  from: {
package/model.js CHANGED
@@ -30,6 +30,29 @@ const Access = definition.model({
30
30
  byOwnerRoleAndObject: {
31
31
  property: ['sessionOrUserType', 'sessionOrUser', 'roles', 'objectType', 'object'],
32
32
  multi: true
33
+ },
34
+ byObjectExtended: {
35
+ function: async function(input, output, { tableName }) {
36
+ function mapper(access) {
37
+ return access && {
38
+ id: JSON.stringify(access.objectType) + ':' + JSON.stringify(access.object)
39
+ + '_' + sha1(access.id, 'base64'),
40
+ objectType: access.objectType,
41
+ object: access.object,
42
+ sessionOrUserType: access.sessionOrUserType,
43
+ sessionOrUser: access.sessionOrUser,
44
+ roles: access.roles,
45
+ lastUpdate: access.lastUpdate
46
+ }
47
+ }
48
+ const table = await input.table(tableName)
49
+ await table.onChange(
50
+ async (access, oldAccess) => output.change(mapper(access), mapper(oldAccess))
51
+ )
52
+ },
53
+ parameters: {
54
+ tableName: definition.name + '_Access'
55
+ }
33
56
  }
34
57
  },
35
58
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@live-change/access-control-service",
3
- "version": "0.9.28",
3
+ "version": "0.9.30",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -21,8 +21,8 @@
21
21
  "url": "https://www.viamage.com/"
22
22
  },
23
23
  "dependencies": {
24
- "@live-change/framework": "^0.9.28"
24
+ "@live-change/framework": "^0.9.30"
25
25
  },
26
- "gitHead": "f308e368e678fa38ddef6a6d4999ad730b18e8ce",
26
+ "gitHead": "dc063de0998d29acb7c7d5e2032ed63be8895a8e",
27
27
  "type": "module"
28
28
  }