| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697 |
- // @vitest-environment node
- import { afterEach, expect, it } from 'vitest'
- import { mkdtempSync, readFileSync, readdirSync, rmSync } from 'node:fs'
- import { join } from 'node:path'
- import { tmpdir } from 'node:os'
- import { randomUUID } from 'node:crypto'
- import { createCredentialVaultClient, LocalCredentialVaultClient } from '../../src/server/credentialVault/localClient'
- import { sshFixture } from '../fixtures/credentialProtocols'
- const homes: string[] = [], clients: LocalCredentialVaultClient[] = []
- const session = 'a'.repeat(64), password = 'DUMMY-local-master-only', secret = 'DUMMY-local-website-secret'
- function client(home = mkdtempSync(join(tmpdir(), 'yaoyao-local-vault-'))) {
- if (!homes.includes(home)) homes.push(home)
- const value = new LocalCredentialVaultClient(home); clients.push(value)
- return { home, value, call: (command: string, body?: unknown, owner = 'dummy-owner', binding = session) => value.call(owner, binding, command, body) }
- }
- afterEach(async () => { clients.splice(0).forEach(c => c.close()); await Promise.resolve(); homes.splice(0).forEach(h => rmSync(h, { recursive: true, force: true })) })
- it('provides encrypted local CRUD and bounded task execution by default without an external service', async () => {
- const f = client()
- expect(await f.call('status')).toMatchObject({ online: true, initialized: false, unlocked: false, execution: 'local-adapters', reason: 'local_controlled', storageMode: 'local' })
- await f.call('initialize', { password })
- await expect(f.call('add', {})).rejects.toMatchObject({ code: 'vault_locked' })
- await f.call('unlock', { password })
- const entry = await f.call('add', { name: 'Dummy website', username: 'dummy', target: { kind: 'website', origin: 'https://example.test' }, secret })
- expect(entry).not.toHaveProperty('secret')
- const stored = readdirSync(join(f.home, 'credential-vault')).filter(name => name.endsWith('.vault')).map(name => readFileSync(join(f.home, 'credential-vault', name), 'utf8')).join('')
- expect(stored).not.toContain(secret); expect(stored).not.toContain(password); expect(stored).not.toContain('Dummy website')
- expect((await f.call('status')).entries).toHaveLength(1)
- await expect(f.call('read-secret', {id:entry.id})).rejects.toMatchObject({ code: 'vault_command_unknown' })
- await f.call('update', { id: entry.id, entry: { name: 'Updated', username: 'dummy', target: entry.target, revision: entry.revision } })
- expect((await f.call('status')).entries[0].name).toBe('Updated')
- await f.call('remove', { id: entry.id }); expect((await f.call('status')).entries).toEqual([])
- })
- it('binds one-use local execution to the owner and session and requires a configured usage policy',async()=>{
- const f=client();await f.call('initialize',{password});await f.call('unlock',{password})
- const entry=await f.call('add',{name:'Dummy',username:'dummy',target:{kind:'website',origin:'https://example.test'},secret})
- const input={credentialRef:entry.id,agentId:randomUUID(),workId:randomUUID(),runnerId:randomUUID(),runnerInstance:randomUUID(),runnerEpoch:randomUUID(),operation:'website.login',target:entry.target,seconds:60}
- const lease=await f.call('grant',input)
- await expect(f.call('execute',{id:lease.id,input},'another-owner')).rejects.toMatchObject({code:'vault_lease_revoked'})
- await expect(f.call('execute',{id:lease.id,input},'dummy-owner','b'.repeat(64))).rejects.toMatchObject({code:'vault_scope_forbidden'})
- expect(await f.call('execute',{id:lease.id,input})).toEqual({status:'manual_takeover_required',reason:'usage_policy_required',submitted:false})
- await expect(f.call('execute',{id:lease.id,input})).rejects.toMatchObject({code:'vault_lease_revoked'})
- })
- it('saves SSH offline, remembers the first host without authenticating, persists its pin and rejects a changed host before sending a password',async()=>{
- const f=client();await f.call('initialize',{password});await f.call('unlock',{password})
- let ssh=await sshFixture('password')
- try{
- const {hostKey:_initialKey,...target}=ssh.target
- await ssh.close() // The server is actually offline while the password is saved.
- const entry=await f.call('add',{name:'Dummy SSH',username:ssh.username,target,secret:ssh.secret,usage:{kind:'ssh.exec',command:'/usr/bin/uptime'}})
- ssh=await sshFixture('password',target.port);const hostKey=ssh.target.hostKey
- const input={credentialRef:entry.id,agentId:randomUUID(),workId:randomUUID(),runnerId:randomUUID(),runnerInstance:randomUUID(),runnerEpoch:randomUUID(),operation:'ssh.exec',target,seconds:60}
- expect(ssh.state.authMethods).toEqual([])
- await expect(f.call('grant',input)).rejects.toMatchObject({code:'vault_host_key_required'})
- await expect(f.call('prepare-host',{id:entry.id,revision:entry.revision},'dummy-owner','b'.repeat(64))).rejects.toMatchObject({code:'vault_locked'})
- const prepared=await f.call('prepare-host',{id:entry.id,revision:entry.revision})
- expect(prepared.target.hostKey).toBe(hostKey);expect(ssh.state.authMethods).toEqual([])
- await f.call('lock');await f.call('unlock',{password});expect((await f.call('status')).entries[0].target.hostKey).toBe(hostKey)
- const port=target.port;await ssh.close();ssh=await sshFixture('password',port)
- expect(ssh.target.hostKey).not.toBe(hostKey)
- expect((await f.call('prepare-host',{id:entry.id,revision:prepared.revision})).target.hostKey).toBe(hostKey)
- const bound={...input,target:prepared.target},lease=await f.call('grant',bound)
- await expect(f.call('execute',{id:lease.id,input:bound})).rejects.toMatchObject({code:'vault_host_key_rejected'})
- expect(ssh.state.authMethods).toEqual([]);expect(ssh.state.commands).toEqual([])
- expect((await f.call('status')).entries[0].target.hostKey).toBe(hostKey)
- }finally{await ssh.close()}
- },15000)
- it('isolates accounts and login sessions and starts locked after a server restart', async () => {
- const f = client(); await f.call('initialize', { password }); await f.call('unlock', { password })
- await f.call('add', { name: 'Dummy', username: 'dummy', target: { kind: 'website', origin: 'https://example.test' }, secret })
- expect(await f.call('status', undefined, 'another-owner')).toMatchObject({ initialized: false, entries: [] })
- expect(await f.call('status', undefined, 'dummy-owner', 'b'.repeat(64))).toMatchObject({ unlocked: false, entries: [] })
- await expect(f.call('add', {}, 'dummy-owner', 'b'.repeat(64))).rejects.toMatchObject({ code: 'vault_locked' })
- f.value.close(); await Promise.resolve()
- const restarted = client(f.home)
- expect(await restarted.call('status')).toMatchObject({ initialized: true, unlocked: false, entries: [] })
- await restarted.call('unlock', { password }); expect((await restarted.call('status')).entries).toHaveLength(1)
- })
- it('keeps an explicitly configured but unavailable private broker offline', async () => {
- const c = createCredentialVaultClient({ socket: '/missing/socket', tokenFile: '/missing/token', hermesUid: process.getuid!() + 1 }, '/unused')
- expect(c).not.toBeInstanceOf(LocalCredentialVaultClient)
- await expect(c.call('owner', session, 'status')).rejects.toMatchObject({ code: 'vault_offline' })
- c.close()
- })
- it('prevents concurrent brokers from writing the same local vault', async () => {
- const first = client(); await first.call('initialize', { password })
- const second = client(first.home)
- await expect(second.call('status')).rejects.toMatchObject({ code: 'vault_offline' })
- first.value.close(); await Promise.resolve()
- expect(await second.call('status')).toMatchObject({ initialized: true, unlocked: false })
- })
|