@live-change/access-control-service 0.9.228 → 0.9.230
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 +12 -54
- package/accessControl.test.mjs +166 -0
- package/package.json +3 -3
- package/wrapViewObservable.js +96 -0
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 = (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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.
|
|
3
|
+
"version": "0.9.230",
|
|
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.
|
|
24
|
+
"@live-change/framework": "^0.9.230"
|
|
25
25
|
},
|
|
26
|
-
"gitHead": "
|
|
26
|
+
"gitHead": "dce3ca9bbe8c025874a4927dc1b273566d0bee67",
|
|
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
|
+
}
|