/** * 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' } }) }