Files
peardock/server/handlers/vault.js
T
Raven Scott e51de4ad1d Add full registry manager and image pull/push with vault credentials.
Wire credential-aware pull/push, session auth, and an Images Registries tab for vault management.
2026-07-15 14:54:30 -04:00

198 lines
6.3 KiB
JavaScript

/**
* Registry credential vault RPC handlers.
*/
import * as vault from '../core/registry-vault.js'
import * as validation from '../utils/validation.js'
import { docker } from '../services/docker.js'
import logger from '../utils/logger.js'
import { getSessionAuthconfig, setSessionAuthconfig, clearSessionAuthconfig } from './system.js'
/**
* Resolve Docker authconfig for pull/push from vault id, inline auth, or session.
* @param {import('../rpc/session.js').PeerSession} session
* @param {{ credentialId?: string, auth?: object, autoVault?: boolean, image?: string }} args
* @returns {{ username: string, password: string, serveraddress: string }|null}
*/
export function resolveRegistryAuth(session, args = {}) {
if (args.credentialId) {
const cred = vault.getCredential(String(args.credentialId))
if (!cred) throw Object.assign(new Error('Vault credential not found'), { code: 'VAULT_NOT_FOUND' })
return {
username: cred.username,
password: cred.password,
serveraddress: cred.serveraddress,
}
}
if (args.auth && args.auth.username && args.auth.password) {
return {
username: String(args.auth.username),
password: String(args.auth.password),
serveraddress:
args.auth.serveraddress || 'https://index.docker.io/v1/',
}
}
const sessionAuth = getSessionAuthconfig(session)
if (sessionAuth) {
return {
username: sessionAuth.username,
password: sessionAuth.password,
serveraddress: sessionAuth.serveraddress,
}
}
// Optional: match vault entry from image registry host
if (args.autoVault !== false && args.image) {
const host = registryHostFromImage(args.image)
if (host) {
const found = vault.findCredentialForServer(host)
if (found) {
return {
username: found.username,
password: found.password,
serveraddress: found.serveraddress,
}
}
}
}
return null
}
/**
* @param {string} image
* @returns {string|null}
*/
export function registryHostFromImage(image) {
if (!image || typeof image !== 'string') return null
const ref = image.split('@')[0]
const withoutTag = ref.includes('/')
? ref.replace(/:[^/]+$/, '')
: ref
// docker.io short names: nginx, library/nginx, user/app
const parts = withoutTag.split('/')
if (parts.length === 1) return 'docker.io'
if (parts.length === 2 && !parts[0].includes('.') && !parts[0].includes(':') && parts[0] !== 'localhost') {
return 'docker.io'
}
return parts[0].split(':')[0] || null
}
async function checkDockerAuth(authconfig) {
return new Promise((resolve, reject) => {
docker.checkAuth(authconfig, (err, res) => (err ? reject(err) : resolve(res)))
})
}
export function registerVaultHandlers(session) {
session.respond('listVaultCredentials', async () => {
return {
success: true,
type: 'vaultCredentials',
data: vault.listCredentials(),
}
})
session.respond('vaultStoreCredential', async (args) => {
const username = validation.sanitizeString(args.username, 128)
const password = args.password
const serveraddress = validation.sanitizeString(
args.serveraddress || 'https://index.docker.io/v1/',
512
)
const label = validation.sanitizeString(args.label || '', 128)
if (!username || !password) throw new Error('username and password required')
// Optional verify against engine
if (args.verify !== false) {
try {
await checkDockerAuth({ username, password, serveraddress })
} catch (err) {
if (args.requireAuth) throw new Error(`Auth failed: ${err.message}`)
logger.warn('vault store: checkAuth soft-fail', { error: err.message })
}
}
const stored = vault.storeCredential({
id: args.id,
username,
password,
serveraddress,
label: label || username,
})
return { success: true, message: 'Credential stored encrypted at rest', data: stored }
})
session.respond('vaultDeleteCredential', async (args) => {
if (!args.id) throw new Error('credential id required')
return vault.deleteCredential(args.id)
})
session.respond('vaultUseCredential', async (args) => {
if (!args.id) throw new Error('credential id required')
const listed = vault.listCredentials().find((c) => c.id === args.id)
const cred = vault.getCredential(args.id)
if (!cred) throw new Error('Credential not found')
const auth = {
username: cred.username,
password: cred.password,
serveraddress: cred.serveraddress,
credentialId: args.id,
label: listed?.label || cred.username,
}
setSessionAuthconfig(session, auth)
return {
success: true,
message: `Using vault credential for ${cred.username}`,
data: {
id: args.id,
username: cred.username,
serveraddress: cred.serveraddress,
label: listed?.label || null,
},
}
})
session.respond('vaultTestCredential', async (args) => {
let authconfig = null
if (args.id) {
const cred = vault.getCredential(String(args.id))
if (!cred) throw new Error('Credential not found')
authconfig = {
username: cred.username,
password: cred.password,
serveraddress: cred.serveraddress,
}
} else {
const username = validation.sanitizeString(args.username, 128)
const password = args.password
const serveraddress = validation.sanitizeString(
args.serveraddress || 'https://index.docker.io/v1/',
512
)
if (!username || !password) throw new Error('id or username+password required')
authconfig = { username, password, serveraddress }
}
try {
const res = await checkDockerAuth(authconfig)
return {
success: true,
ok: true,
message: `Authenticated as ${authconfig.username}`,
data: {
username: authconfig.username,
serveraddress: authconfig.serveraddress,
identityToken: Boolean(res?.IdentityToken),
status: res?.Status || null,
},
}
} catch (err) {
throw Object.assign(new Error(`Auth failed: ${err.message}`), {
code: 'REGISTRY_AUTH_FAILED',
})
}
})
session.respond('vaultClearSession', async () => {
clearSessionAuthconfig(session)
return { success: true, message: 'Registry session credentials cleared' }
})
}