Release rolling / release (push) Has been cancelled
Wire credential-aware pull/push, session auth, and an Images Registries tab for vault management.
198 lines
6.3 KiB
JavaScript
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' }
|
|
})
|
|
}
|