@live-change/access-control-service 0.9.229 → 0.9.231

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/accessControl.js CHANGED
@@ -1,11 +1,12 @@
1
1
  import definition from './definition.js'
2
2
  import App from '@live-change/framework'
3
- import { ObservableValue, ObservableList, ObservableProxy, ObservablePromiseProxy } from '@live-change/dao'
4
3
  const app = App.app()
5
4
 
6
5
  import accessModule from './access.js'
7
6
  const access = accessModule(definition)
8
7
 
8
+ import { wrapViewObservable } from './wrapViewObservable.js'
9
+
9
10
  definition.processor({
10
11
  priority: -1,
11
12
  process(service, app) {
@@ -25,7 +26,7 @@ definition.processor({
25
26
 
26
27
  const { objectType, object } = properties
27
28
  const objects = [].concat(
28
- config.objects ? config.objects(properties) :
29
+ config.objects ? await config.objects(properties) :
29
30
  ((objectType && object) ? [{ objectType, object }] : [])
30
31
  )
31
32
 
@@ -57,7 +58,7 @@ definition.processor({
57
58
  const { client } = context
58
59
  const { objectType, object } = properties
59
60
  const objects = [].concat(
60
- config.objects ? config.objects(properties) :
61
+ config.objects ? await config.objects(properties) :
61
62
  ((objectType && object) ? [{ objectType, object }] : [])
62
63
  )
63
64
  if(objects.length === 0) {
@@ -72,57 +73,14 @@ definition.processor({
72
73
  if(!accessible) throw app.logicError("notAuthorized")
73
74
  return oldGet.apply(view, args)
74
75
  }
75
- view.observable = (...args) => {
76
- const [ properties, context ] = args
77
- const { client } = context
78
- const { objectType, object } = properties
79
- const objects = [].concat(
80
- config.objects ? config.objects(properties) :
81
- ((objectType && object) ? [{ objectType, object }] : [])
82
- )
83
- if(objects.length === 0) {
84
- console.warn('no objects for ' + service.name + ' view ' + viewName + ' access control to work - only global roles will be checked')
85
- }
86
-
87
- const rolesPath = access.accessPath(client, objects)
88
-
89
- const errorObservable = new ObservableValue()
90
- errorObservable.handleError('notAuthorized')
91
-
92
- const observableProxy = new ObservableProxy(null)
93
-
94
- let valueObservable
95
-
96
- let accessible
97
- const rolesObservable = app.dao.observable(rolesPath)
98
- const rolesObserver = (signal, value) => {
99
- const accessObject = rolesObservable.getValue()
100
- const newAccessible = accessObject ? access.testRoles(config.roles, accessObject.roles) : false
101
- if(newAccessible !== accessible) {
102
- if(newAccessible === true /*&& !valueObservable*/) {
103
- valueObservable = oldObservable.apply(view, args)
104
- if(valueObservable.then) {
105
- valueObservable = new ObservablePromiseProxy(valueObservable)
106
- }
107
- }
108
- observableProxy.setTarget(newAccessible ? valueObservable : errorObservable)
109
- accessible = newAccessible
110
- }
111
- }
112
- rolesObservable.observe(rolesObserver)
113
-
114
- const oldDispose = observableProxy.dispose
115
- const oldRespawn = observableProxy.respawn
116
- observableProxy.dispose = () => {
117
- rolesObservable.unobserve(rolesObserver)
118
- oldDispose.apply(observableProxy)
119
- }
120
- observableProxy.respawn = () => {
121
- rolesObservable.observe(rolesObserver)
122
- oldRespawn.apply(observableProxy)
123
- }
124
- return observableProxy
125
- }
76
+ view.observable = wrapViewObservable({
77
+ access,
78
+ app,
79
+ oldObservable,
80
+ view,
81
+ config,
82
+ viewName: service.name + ' view ' + viewName
83
+ })
126
84
  }
127
85
 
128
86
  }
@@ -0,0 +1,166 @@
1
+ import test from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { ObservableValue } from '@live-change/dao'
4
+ import { wrapViewObservable } from './wrapViewObservable.js'
5
+
6
+ function testRoles(requiredRoles, clientRoles) {
7
+ if (!requiredRoles || requiredRoles.length === 0) return true
8
+ for (const requiredRolesOption of requiredRoles) {
9
+ const option = Array.isArray(requiredRolesOption) ? requiredRolesOption : [requiredRolesOption]
10
+ if (option.every(role => clientRoles.includes(role))) return true
11
+ }
12
+ return false
13
+ }
14
+
15
+ function createRolesObservable() {
16
+ const observers = []
17
+ return {
18
+ value: undefined,
19
+ observeCount: 0,
20
+ unobserveCount: 0,
21
+ observers,
22
+ observe(fn) {
23
+ this.observeCount++
24
+ observers.push(fn)
25
+ if (this.value !== undefined) {
26
+ if (typeof fn === 'function') fn('set', this.value)
27
+ else if (fn.set) fn.set(this.value)
28
+ }
29
+ },
30
+ unobserve(fn) {
31
+ this.unobserveCount++
32
+ const i = observers.indexOf(fn)
33
+ if (i >= 0) observers.splice(i, 1)
34
+ },
35
+ getValue() {
36
+ return this.value
37
+ },
38
+ set(value) {
39
+ this.value = value
40
+ for (const fn of [...observers]) {
41
+ if (typeof fn === 'function') fn('set', value)
42
+ else if (fn.set) fn.set(value)
43
+ }
44
+ }
45
+ }
46
+ }
47
+
48
+ function createValueObservable(initial) {
49
+ const inner = new ObservableValue(initial)
50
+ inner.disposeCount = 0
51
+ const originalDispose = inner.dispose.bind(inner)
52
+ inner.dispose = () => {
53
+ inner.disposeCount++
54
+ originalDispose()
55
+ }
56
+ return inner
57
+ }
58
+
59
+ function wrapWith(rolesObservable, oldObservable) {
60
+ return wrapViewObservable({
61
+ access: {
62
+ accessPath: () => ['roles', 'path'],
63
+ testRoles
64
+ },
65
+ app: {
66
+ dao: {
67
+ observable() {
68
+ return rolesObservable
69
+ }
70
+ }
71
+ },
72
+ oldObservable,
73
+ view: {},
74
+ config: {
75
+ roles: ['owner'],
76
+ objects: () => [{ objectType: 'x', object: '1' }]
77
+ },
78
+ viewName: 'test view'
79
+ })
80
+ }
81
+
82
+ const client = { session: 's1', user: 'u1', roles: [] }
83
+ const args = [{ objectType: 'x', object: '1' }, { client }]
84
+
85
+ test('T3.1 accessible-true-forwards-set', async () => {
86
+ const roles = createRolesObservable()
87
+ const value = createValueObservable({ ok: true })
88
+ const observable = wrapWith(roles, () => value)
89
+ const proxy = observable(...args)
90
+ const got = []
91
+ proxy.observe({
92
+ set(v) { got.push(v) }
93
+ })
94
+ roles.set({ roles: ['owner'] })
95
+ assert.deepEqual(got[got.length - 1], { ok: true })
96
+ })
97
+
98
+ test('T3.2 accessible-false-emits-error', async () => {
99
+ const roles = createRolesObservable()
100
+ const value = createValueObservable({ ok: true })
101
+ const observable = wrapWith(roles, () => value)
102
+ const proxy = observable(...args)
103
+ let err
104
+ proxy.observe({
105
+ set() {},
106
+ error(e) { err = e }
107
+ })
108
+ roles.set({ roles: [] })
109
+ assert.equal(err, 'notAuthorized')
110
+ })
111
+
112
+ test('T3.3 accessible-flip-disposes-old-value', async () => {
113
+ const roles = createRolesObservable()
114
+ const value = createValueObservable({ ok: true })
115
+ const observable = wrapWith(roles, () => value)
116
+ const proxy = observable(...args)
117
+ proxy.observe({ set() {}, error() {} })
118
+ roles.set({ roles: ['owner'] })
119
+ assert.equal(value.disposeCount, 0)
120
+ roles.set({ roles: [] })
121
+ assert.ok(value.disposeCount >= 1)
122
+ assert.ok(value.isDisposed())
123
+ })
124
+
125
+ test('T3.4 proxy-dispose-disposes-both', async () => {
126
+ const roles = createRolesObservable()
127
+ const value = createValueObservable({ ok: true })
128
+ const observable = wrapWith(roles, () => value)
129
+ const proxy = observable(...args)
130
+ proxy.observe({ set() {} })
131
+ roles.set({ roles: ['owner'] })
132
+ proxy.dispose()
133
+ assert.equal(roles.unobserveCount, 1)
134
+ assert.ok(value.disposeCount >= 1)
135
+ assert.ok(value.isDisposed())
136
+ })
137
+
138
+ test('T3.5 proxy-dispose-before-value-resolve', async () => {
139
+ const roles = createRolesObservable()
140
+ const inner = createValueObservable({ delayed: true })
141
+ let resolve
142
+ const pending = new Promise(res => { resolve = res })
143
+ const observable = wrapWith(roles, () => pending)
144
+ const proxy = observable(...args)
145
+ proxy.observe({ set() {}, error() {} })
146
+ roles.set({ roles: ['owner'] })
147
+ proxy.dispose()
148
+ resolve(inner)
149
+ await new Promise(r => setImmediate(r))
150
+ await new Promise(r => setImmediate(r))
151
+ assert.equal(inner.disposeCount, 1)
152
+ assert.ok(inner.isDisposed())
153
+ })
154
+
155
+ test('T3.6 proxy-respawn-reobserves', async () => {
156
+ const roles = createRolesObservable()
157
+ const value = createValueObservable({ ok: true })
158
+ const observable = wrapWith(roles, () => value)
159
+ const proxy = observable(...args)
160
+ proxy.observe({ set() {} })
161
+ roles.set({ roles: ['owner'] })
162
+ assert.equal(roles.observeCount, 1)
163
+ proxy.dispose()
164
+ proxy.respawn()
165
+ assert.equal(roles.observeCount, 2)
166
+ })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@live-change/access-control-service",
3
- "version": "0.9.229",
3
+ "version": "0.9.231",
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.229"
24
+ "@live-change/framework": "^0.9.231"
25
25
  },
26
- "gitHead": "10de6af051ea567a0ae917d34ac08378da5465ea",
26
+ "gitHead": "d49b1369964dafe82610664dd56255b6458d0cae",
27
27
  "type": "module"
28
28
  }
@@ -0,0 +1,96 @@
1
+ import { ObservableValue, ObservableProxy, ObservablePromiseProxy } from '@live-change/dao'
2
+
3
+ function safeDispose(observable) {
4
+ if (!observable) return
5
+ if (observable.isDisposed && observable.isDisposed()) return
6
+ try {
7
+ if (typeof observable.dispose === 'function') observable.dispose()
8
+ } catch {
9
+ // already torn down
10
+ }
11
+ }
12
+
13
+ /**
14
+ * Wrap a view.observable with access-control rolesPath on app.dao.
15
+ * Extracted so contract tests can mock app.dao / access without booting App.
16
+ */
17
+ export function wrapViewObservable({
18
+ access,
19
+ app,
20
+ oldObservable,
21
+ view,
22
+ config,
23
+ viewName
24
+ }) {
25
+ return (...args) => {
26
+ const [ properties, context ] = args
27
+ const { client } = context
28
+ const { objectType, object } = properties
29
+ const objects = [].concat(
30
+ config.objects ? config.objects(properties) :
31
+ ((objectType && object) ? [{ objectType, object }] : [])
32
+ )
33
+ if (objects.length === 0) {
34
+ console.warn(
35
+ 'no objects for ' + (viewName || 'view') +
36
+ ' access control to work - only global roles will be checked'
37
+ )
38
+ }
39
+
40
+ const rolesPath = access.accessPath(client, objects)
41
+
42
+ const errorObservable = new ObservableValue()
43
+ errorObservable.handleError('notAuthorized')
44
+
45
+ const observableProxy = new ObservableProxy(null)
46
+
47
+ let valueObservable
48
+ let accessible
49
+ let rolesObserved = false
50
+
51
+ const rolesObservable = app.dao.observable(rolesPath)
52
+ const rolesObserver = () => {
53
+ const accessObject = rolesObservable.getValue()
54
+ const newAccessible = accessObject ? access.testRoles(config.roles, accessObject.roles) : false
55
+ if (newAccessible !== accessible) {
56
+ const previousValue = valueObservable
57
+ if (newAccessible === true) {
58
+ valueObservable = oldObservable.apply(view, args)
59
+ if (valueObservable && valueObservable.then) {
60
+ valueObservable = new ObservablePromiseProxy(valueObservable)
61
+ }
62
+ } else {
63
+ valueObservable = undefined
64
+ }
65
+ observableProxy.setTarget(newAccessible ? valueObservable : errorObservable)
66
+ if (previousValue && previousValue !== valueObservable) safeDispose(previousValue)
67
+ accessible = newAccessible
68
+ }
69
+ }
70
+ rolesObservable.observe(rolesObserver)
71
+ rolesObserved = true
72
+
73
+ const oldDispose = observableProxy.dispose.bind(observableProxy)
74
+ const oldRespawn = observableProxy.respawn.bind(observableProxy)
75
+ observableProxy.dispose = () => {
76
+ if (rolesObserved) {
77
+ try {
78
+ rolesObservable.unobserve(rolesObserver)
79
+ } catch {
80
+ // already torn down
81
+ }
82
+ rolesObserved = false
83
+ }
84
+ oldDispose()
85
+ safeDispose(valueObservable)
86
+ }
87
+ observableProxy.respawn = () => {
88
+ if (!rolesObserved) {
89
+ rolesObservable.observe(rolesObserver)
90
+ rolesObserved = true
91
+ }
92
+ oldRespawn()
93
+ }
94
+ return observableProxy
95
+ }
96
+ }