mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-13 02:20:45 +08:00
fix(uds-auth): emergency login workspaces, Desktop key prompts, persistent sessions
Emergency (fallback / sealed-box) admins lost the workspace sidebar and saw "sign-in failed / add API key" on DSH Desktop because the remote.mux opens anonymously at boot and every early call was rejected terminally. - gateway: anonymous workspace/follow reaches dsh-acl's empty baseline; anonymous streams are parked until the carrier aborts instead of failing; anonymous workspace/initializeDefault answers "nothing created" - request-auth: a dead UDS_SESSION cookie no longer shadows a valid WS ticket or bridge (sync + async variants used by all callers) - dsh-acl: /api/* waits (bounded 15s) for the gateway ACL during Host startup instead of 403, so the Desktop welcome read no longer fails - desktop-bootstrap: key/account projection is best-effort per reference - client: emit connection/reset after login so boot-time caches (settings describe mirror -> Settings > Models) re-read under the real principal - sessions: absolute lifetime fixed at 7 days (was min(ttl, 7d) = 30 min); bridge expiry slides with its session; MemoryStore persisted to sessions.json (bearer hash only, userData AES-256-GCM sealed) - README: session lifetime/persistence and DSH compatibility notes Also includes previously uncommitted uds-auth 0.3.x work in this tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
fa4d5d6d97
commit
df97c5e042
72 changed files with 8436 additions and 1192 deletions
111
uds-auth/test/desktop-bootstrap.test.js
Normal file
111
uds-auth/test/desktop-bootstrap.test.js
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { createDesktopBootstrap, DESKTOP_BOOTSTRAP_REF } from '../lib/desktop-bootstrap.js'
|
||||
import { installGatewayPolicy } from '../lib/gateway-policy.js'
|
||||
|
||||
function fixture() {
|
||||
let configured = true, invoked = 0
|
||||
const services = {
|
||||
settings: { describe: () => [
|
||||
{ ns: 'locale', value: { preference: 'zh', hidden: 'LOCALE-SECRET' } },
|
||||
{ ns: 'llm-deepseek', value: { apiKeyEnv: 'REAL_KEY_NAME', endpoint: 'PRIVATE-ENDPOINT' } },
|
||||
{ ns: 'uds-auth', value: { initialAdminEmpNo: 'PRIVATE-ADMIN' } },
|
||||
] },
|
||||
llm: { listConfigurableProviders: () => [] },
|
||||
credentials: { describe: async ref => { assert.equal(ref, 'REAL_KEY_NAME'); return { configured, writable: true, value: 'RAW-KEY' } } },
|
||||
deepseekAccount: { getState: async () => ({ status: 'credential-stored', attempt: { authorizeUrl: 'SECRET-LOGIN-LINK' }, token: 'ACCOUNT-TOKEN' }) },
|
||||
}
|
||||
const deny = code => { throw new Error(code) }
|
||||
const project = createDesktopBootstrap({ getService: name => services[name], deny })
|
||||
let principal = null, admin = false, live = true
|
||||
const gateway = {
|
||||
async *openRemoteEvents() {}, receiveRemoteEventResult() {},
|
||||
resolveDescriptor: (namespace, method) => ({ namespace, method, parameters: [] }),
|
||||
prepareInvocation: async () => ({ method: () => { invoked++; return 'full-admin-result' } }),
|
||||
}
|
||||
installGatewayPolicy(gateway, { identity: () => principal, assertPrincipal: p => { if (!p || !live) deny('login_required') },
|
||||
assertSession: () => {}, permissions: () => ({ canAccessSettings: admin }), deny, desktopBootstrap: project })
|
||||
const invoke = async (namespace, method, args = {}) => (await gateway.prepareInvocation({ namespace, method, args })).method()
|
||||
return { invoke, services, setConfigured: value => { configured = value },
|
||||
setPrincipal: value => { principal = value }, setAdmin: value => { admin = value },
|
||||
revoke: () => { live = false }, invocations: () => invoked }
|
||||
}
|
||||
|
||||
test('unmodified Desktop welcome reads a bounded projection before UDS login', async () => {
|
||||
const f = fixture()
|
||||
const settings = await f.invoke('settings', 'describe')
|
||||
assert.equal(settings.writable, false); assert.equal(settings.hasDocument, false)
|
||||
assert.equal(settings.namespaces.find(row => row.ns === 'locale').value.preference, 'zh')
|
||||
const providers = await f.invoke('llm', 'listConfigurableProviders')
|
||||
const refs = providers.map(provider => settings.namespaces.find(row => row.ns === provider.settingsNs).value.apiKeyEnv)
|
||||
const credentials = await f.invoke('credentials', 'describe', { refs })
|
||||
assert.deepEqual(credentials, { [DESKTOP_BOOTSTRAP_REF]: { configured: true, writable: false } })
|
||||
const account = await f.invoke('account', 'getState')
|
||||
assert.equal(account.status, 'credential-stored'); assert.equal(account.attempt, null)
|
||||
assert.equal(f.invocations(), 0, 'anonymous calls never invoke full Remote controllers')
|
||||
const data = JSON.stringify([settings, providers, credentials, account])
|
||||
for (const secret of ['REAL_KEY_NAME', 'PRIVATE-ENDPOINT', 'PRIVATE-ADMIN', 'RAW-KEY', 'ACCOUNT-TOKEN', 'SECRET-LOGIN-LINK', 'LOCALE-SECRET']) assert.equal(data.includes(secret), false)
|
||||
f.setConfigured(false)
|
||||
assert.equal((await f.invoke('credentials', 'describe', { refs }))[DESKTOP_BOOTSTRAP_REF].configured, false)
|
||||
})
|
||||
|
||||
test('bootstrap cannot enumerate credential refs or access settings writes and sessions', async () => {
|
||||
const f = fixture()
|
||||
for (const args of [{ refs: ['REAL_KEY_NAME'] }, { refs: [DESKTOP_BOOTSTRAP_REF, 'OTHER'] }, {}, { refs: 'REAL_KEY_NAME' }]) {
|
||||
await assert.rejects(f.invoke('credentials', 'describe', args), /forbidden_settings/)
|
||||
}
|
||||
for (const [ns, method] of [['settings', 'update'], ['credentials', 'set'], ['account', 'startSignIn'], ['session', 'list'], ['newPlugin', 'describe']]) {
|
||||
await assert.rejects(f.invoke(ns, method), /login_required/)
|
||||
}
|
||||
assert.equal(f.invocations(), 0)
|
||||
})
|
||||
|
||||
test('admin and trusted Host retain full reads; ordinary or revoked principals never receive them', async () => {
|
||||
const f = fixture()
|
||||
f.setPrincipal({ empNo: 'u' })
|
||||
assert.equal((await f.invoke('settings', 'describe')).writable, false)
|
||||
f.setAdmin(true)
|
||||
assert.equal(await f.invoke('settings', 'describe'), 'full-admin-result')
|
||||
f.setAdmin(false)
|
||||
assert.equal((await f.invoke('settings', 'describe')).writable, false)
|
||||
f.revoke()
|
||||
await assert.rejects(f.invoke('settings', 'describe'), /login_required/)
|
||||
f.setPrincipal(undefined)
|
||||
assert.equal(await f.invoke('settings', 'describe'), 'full-admin-result')
|
||||
})
|
||||
|
||||
test('custom providers are detected without exposing their namespace or credential names', async () => {
|
||||
const f = fixture()
|
||||
f.services.settings.describe = () => [{ ns: 'private-provider', value: { profiles: { one: { apiKeyEnv: 'REAL_KEY_NAME' } } } }]
|
||||
f.services.llm.listConfigurableProviders = () => [{ settingsNs: 'private-provider', settingsPath: ['profiles', 'one'] }]
|
||||
const value = await f.invoke('credentials', 'describe', { refs: [DESKTOP_BOOTSTRAP_REF] })
|
||||
assert.equal(value[DESKTOP_BOOTSTRAP_REF].configured, true)
|
||||
assert.equal(JSON.stringify(await f.invoke('settings', 'describe')).includes('private-provider'), false)
|
||||
})
|
||||
|
||||
test('projection still reports a stored key when another provider or reference throws', async () => {
|
||||
const deny = code => { throw new Error(code) }
|
||||
const services = {
|
||||
settings: { describe: () => [
|
||||
{ ns: 'locale', value: {} },
|
||||
{ ns: 'llm-deepseek', value: { apiKeyEnv: 'BROKEN_REF' } },
|
||||
{ ns: 'llm-pi-ai', value: { providers: { suanlidao: { apiKeyEnv: 'SUANLIDAO_API_KEY' } } } },
|
||||
] },
|
||||
llm: { listConfigurableProviders: () => [
|
||||
{ provider: 'suanlidao', settingsNs: 'llm-pi-ai', settingsPath: ['providers', 'suanlidao'] },
|
||||
] },
|
||||
credentials: { describe: async ref => {
|
||||
if (ref === 'BROKEN_REF') throw new Error('provider exploded')
|
||||
return { configured: ref === 'SUANLIDAO_API_KEY', writable: true }
|
||||
} },
|
||||
deepseekAccount: { getState: async () => { throw new Error('account backend down') } },
|
||||
}
|
||||
const project = createDesktopBootstrap({ getService: name => services[name], deny })
|
||||
const described = await project({ namespace: 'credentials', method: 'describe', args: { refs: [DESKTOP_BOOTSTRAP_REF] } })
|
||||
assert.equal(described[DESKTOP_BOOTSTRAP_REF].configured, true)
|
||||
const account = await project({ namespace: 'account', method: 'getState', args: {} })
|
||||
assert.equal(account.status, 'signed-out')
|
||||
services.settings.describe = () => { throw new Error('settings not mounted') }
|
||||
const none = await project({ namespace: 'credentials', method: 'describe', args: { refs: [DESKTOP_BOOTSTRAP_REF] } })
|
||||
assert.equal(none[DESKTOP_BOOTSTRAP_REF].configured, false)
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue