diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 493ff97..6a646d0 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -126,9 +126,9 @@ impl AuthConfig { #[derive(Debug, Clone, Deserialize, Serialize)] pub struct DockerConfig { #[serde(default)] - pub auths: HashMap, + pub auths: Option>, #[serde(rename = "credHelpers", default)] - pub cred_helpers: HashMap, + pub cred_helpers: Option>, #[serde(rename = "credsStore", skip_serializing_if = "Option::is_none")] pub creds_store: Option, } diff --git a/src/auth/simple.rs b/src/auth/simple.rs index 2686e6a..3ce2232 100644 --- a/src/auth/simple.rs +++ b/src/auth/simple.rs @@ -1,12 +1,243 @@ //! Simple authentication wrapper for registry authentication -use crate::registry::RegistryAuth; -use anyhow::Result; +use crate::registry::{ImageReference, RegistryAuth}; +use anyhow::{Context, Result}; +use serde::Deserialize; +use std::fs; +use std::path::PathBuf; +use std::process::Command; +use tracing::debug; + +use super::{DockerAuthEntry, DockerConfig}; /// Resolve authentication for a given resource using Docker config and credential helpers pub fn resolve_auth(resource: &str) -> Result { - // For now, return anonymous auth - // TODO: Implement actual credential resolution from Docker config - let _ = resource; // Silence unused warning + debug!("Resolving auth for resource: {}", resource); + + // Parse the resource to extract registry + let registry = if let Ok(image_ref) = ImageReference::parse(resource) { + image_ref.registry + } else if resource.contains('/') { + // If it looks like a repository (registry/repo), extract registry part + resource.split('/').next().unwrap_or(resource).to_string() + } else { + // Just use the resource as-is (might be a registry hostname) + resource.to_string() + }; + + debug!("Extracted registry from resource: {}", registry); + + // Try to read Docker config + if let Ok(auth) = read_docker_config(®istry) { + debug!("Found auth in Docker config for registry: {}", registry); + return Ok(auth); + } + + // Try credential helpers + if let Ok(auth) = try_credential_helpers(®istry) { + debug!( + "Found auth via credential helper for registry: {}", + registry + ); + return Ok(auth); + } + + debug!("No auth found, using anonymous for registry: {}", registry); Ok(RegistryAuth::Anonymous) } + +fn read_docker_config(registry: &str) -> Result { + let config_paths = get_docker_config_paths(); + + for config_path in config_paths { + if let Ok(config_content) = fs::read_to_string(&config_path) { + debug!("Reading Docker config from: {:?}", config_path); + + if let Ok(config) = serde_json::from_str::(&config_content) { + if let Some(auths) = &config.auths { + // Try exact registry match first + if let Some(auth_entry) = auths.get(registry) { + debug!("Found exact registry match for: {}", registry); + return parse_auth_entry(auth_entry); + } + + // Try with https:// prefix (common in Docker config) + let https_registry = format!("https://{}", registry); + if let Some(auth_entry) = auths.get(&https_registry) { + debug!("Found https registry match for: {}", https_registry); + return parse_auth_entry(auth_entry); + } + + // Try registry-1.docker.io for docker.io + if registry == "docker.io" || registry == "registry-1.docker.io" { + for key in &[ + "docker.io", + "registry-1.docker.io", + "https://index.docker.io/v1/", + ] { + if let Some(auth_entry) = auths.get(*key) { + debug!("Found Docker Hub match with key: {}", key); + return parse_auth_entry(auth_entry); + } + } + } + } + } + } + } + + anyhow::bail!("No auth found in Docker config") +} + +fn parse_auth_entry(auth_entry: &DockerAuthEntry) -> Result { + // Check for bearer token first + if let Some(token) = &auth_entry.registry_token { + debug!("Using bearer token auth"); + return Ok(RegistryAuth::Bearer { + token: token.clone(), + }); + } + + // Check for basic auth credentials + if let (Some(username), Some(password)) = (&auth_entry.username, &auth_entry.password) { + debug!("Using basic auth with username/password"); + return Ok(RegistryAuth::Basic { + username: username.clone(), + password: password.clone(), + }); + } + + // Check for base64 encoded auth + if let Some(auth_b64) = &auth_entry.auth { + debug!("Using base64 encoded auth"); + use base64::Engine; + let decoded = base64::engine::general_purpose::STANDARD + .decode(auth_b64) + .context("Failed to decode base64 auth")?; + let auth_str = String::from_utf8(decoded).context("Auth is not valid UTF-8")?; + + if let Some((username, password)) = auth_str.split_once(':') { + return Ok(RegistryAuth::Basic { + username: username.to_string(), + password: password.to_string(), + }); + } + } + + anyhow::bail!("No valid auth found in auth entry") +} + +fn get_docker_config_paths() -> Vec { + let mut paths = Vec::new(); + + // Check DOCKER_CONFIG environment variable + if let Ok(docker_config) = std::env::var("DOCKER_CONFIG") { + paths.push(PathBuf::from(docker_config).join("config.json")); + } + + // Check HOME/.docker/config.json + if let Ok(home) = std::env::var("HOME") { + paths.push(PathBuf::from(home).join(".docker").join("config.json")); + } + + // Check XDG_RUNTIME_DIR for rootless Docker + if let Ok(xdg_runtime) = std::env::var("XDG_RUNTIME_DIR") { + paths.push( + PathBuf::from(xdg_runtime) + .join("containers") + .join("auth.json"), + ); + } + + paths +} + +fn try_credential_helpers(registry: &str) -> Result { + let config_paths = get_docker_config_paths(); + + for config_path in config_paths { + if let Ok(config_content) = fs::read_to_string(&config_path) { + if let Ok(config) = serde_json::from_str::(&config_content) { + // Check specific credential helpers first + if let Some(cred_helpers) = &config.cred_helpers { + if let Some(helper) = cred_helpers.get(registry) { + debug!( + "Trying credential helper '{}' for registry: {}", + helper, registry + ); + if let Ok(auth) = call_credential_helper(helper, registry) { + return Ok(auth); + } + } + } + + // Try default credential store + if let Some(helper) = &config.creds_store { + debug!( + "Trying default credential helper '{}' for registry: {}", + helper, registry + ); + if let Ok(auth) = call_credential_helper(helper, registry) { + return Ok(auth); + } + } + } + } + } + + anyhow::bail!("No credential helpers found") +} + +#[derive(Debug, Deserialize)] +struct CredentialHelperResponse { + #[serde(rename = "Username")] + username: String, + #[serde(rename = "Secret")] + secret: String, +} + +fn call_credential_helper(helper: &str, registry: &str) -> Result { + use std::io::Write; + use std::process::Stdio; + + let helper_name = format!("docker-credential-{}", helper); + + debug!("Calling credential helper: {}", helper_name); + + let mut child = Command::new(&helper_name) + .arg("get") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .context(format!( + "Failed to execute credential helper: {}", + helper_name + ))?; + + if let Some(mut stdin) = child.stdin.take() { + stdin + .write_all(registry.as_bytes()) + .context("Failed to write to credential helper stdin")?; + } + + let output = child + .wait_with_output() + .context("Failed to wait for credential helper")?; + + if !output.status.success() { + anyhow::bail!( + "Credential helper {} failed: {}", + helper_name, + String::from_utf8_lossy(&output.stderr) + ); + } + + let response: CredentialHelperResponse = serde_json::from_slice(&output.stdout) + .context("Failed to parse credential helper response")?; + + Ok(RegistryAuth::Basic { + username: response.username, + password: response.secret, + }) +} diff --git a/src/auth/tests.rs b/src/auth/tests.rs index d4e1b0d..8fc17b7 100644 --- a/src/auth/tests.rs +++ b/src/auth/tests.rs @@ -23,20 +23,22 @@ fn test_docker_config_parsing() { let config: DockerConfig = serde_json::from_str(config_json).unwrap(); - assert_eq!(config.auths.len(), 2); - assert!(config.auths.contains_key("docker.io")); - assert!(config.auths.contains_key("gcr.io")); + let auths = config.auths.as_ref().unwrap(); + assert_eq!(auths.len(), 2); + assert!(auths.contains_key("docker.io")); + assert!(auths.contains_key("gcr.io")); - let docker_auth = &config.auths["docker.io"]; + let docker_auth = &auths["docker.io"]; assert_eq!(docker_auth.auth, Some("dXNlcjpwYXNz".to_string())); - let gcr_auth = &config.auths["gcr.io"]; + let gcr_auth = &auths["gcr.io"]; assert_eq!(gcr_auth.username, Some("oauth2accesstoken".to_string())); assert_eq!(gcr_auth.password, Some("ya29.token".to_string())); assert_eq!(gcr_auth.registry_token, Some("bearer-token".to_string())); - assert_eq!(config.cred_helpers.len(), 1); - assert_eq!(config.cred_helpers["ecr.amazonaws.com"], "ecr-login"); + let cred_helpers = config.cred_helpers.as_ref().unwrap(); + assert_eq!(cred_helpers.len(), 1); + assert_eq!(cred_helpers["ecr.amazonaws.com"], "ecr-login"); assert_eq!(config.creds_store, Some("osxkeychain".to_string())); } diff --git a/tests/auth_integration_test.rs b/tests/auth_integration_test.rs index 6000ee7..8358c56 100644 --- a/tests/auth_integration_test.rs +++ b/tests/auth_integration_test.rs @@ -31,16 +31,25 @@ fn test_auth_integration_with_docker_config() -> Result<()> { // Set HOME to temp directory std::env::set_var("HOME", temp_dir.path()); - // TODO: Implement actual credential resolution from Docker config - // For now, all auth resolves to anonymous until we implement the actual credential logic - - // Test GitHub Container Registry auth (currently returns anonymous) + // Test GitHub Container Registry auth (should resolve from config) let ghcr_auth = resolve_auth("ghcr.io/user/image:tag")?; - assert!(matches!(ghcr_auth, RegistryAuth::Anonymous)); + match ghcr_auth { + RegistryAuth::Basic { username, password } => { + assert_eq!(username, "test"); + assert_eq!(password, "test123"); + } + _ => panic!("Expected Basic auth for ghcr.io, got: {:?}", ghcr_auth), + } - // Test Docker Hub auth (currently returns anonymous) + // Test Docker Hub auth (should resolve from config) let docker_auth = resolve_auth("docker.io/library/ubuntu:latest")?; - assert!(matches!(docker_auth, RegistryAuth::Anonymous)); + match docker_auth { + RegistryAuth::Basic { username, password } => { + assert_eq!(username, "testuser"); + assert_eq!(password, "testpass"); + } + _ => panic!("Expected Basic auth for docker.io, got: {:?}", docker_auth), + } // Test unknown registry returns anonymous let unknown_auth = resolve_auth("unknown.registry.io/image:tag")?; diff --git a/tests/credential_helper_integration_test.rs b/tests/credential_helper_integration_test.rs index 1361723..7606e57 100644 --- a/tests/credential_helper_integration_test.rs +++ b/tests/credential_helper_integration_test.rs @@ -100,10 +100,15 @@ fn test_resolve_auth_from_config() -> Result<()> { env::remove_var("XDG_RUNTIME_DIR"); env::set_var("HOME", tmp_dir.path()); - // TODO: Implement actual credential resolution from Docker config - // For now, should resolve to anonymous auth + // Should resolve credentials from config let auth = resolve_auth("test.registry.io/myimage")?; - assert!(matches!(auth, RegistryAuth::Anonymous)); + match auth { + RegistryAuth::Basic { username, password } => { + assert_eq!(username, "testuser"); + assert_eq!(password, "testpass"); + } + _ => panic!("Expected Basic auth for test.registry.io, got: {:?}", auth), + } // Restore env vars if let Some(val) = old_docker_config { @@ -158,10 +163,14 @@ fn test_resolve_auth_bearer_token() -> Result<()> { env::remove_var("XDG_RUNTIME_DIR"); env::set_var("HOME", tmp_dir.path()); - // TODO: Implement actual credential resolution from Docker config - // For now, should resolve to anonymous auth + // Should resolve bearer token from config let auth = resolve_auth("ghcr.io/user/image")?; - assert!(matches!(auth, RegistryAuth::Anonymous)); + match auth { + RegistryAuth::Bearer { token } => { + assert_eq!(token, "test-bearer-token"); + } + _ => panic!("Expected Bearer token auth for ghcr.io, got: {:?}", auth), + } // Restore env vars if let Some(val) = old_docker_config { diff --git a/tests/testdata/auth_base64.txt b/tests/testdata/auth_base64.txt new file mode 100644 index 0000000..becbb68 --- /dev/null +++ b/tests/testdata/auth_base64.txt @@ -0,0 +1,34 @@ +# Test base64 encoded auth from Docker config + +# Create a test project +-- Cargo.toml -- +[package] +name = "auth-b64-test" +version = "0.1.0" +edition = "2021" + +[dependencies] +-- src/main.rs -- +fn main() { + println!("Testing base64 auth"); +} + +# Create Docker config with base64 auth (testuser:testpass = dGVzdHVzZXI6dGVzdHBhc3M=) +mkdir .docker +-- .docker/config.json -- +{ + "auths": { + "ghcr.io": { + "auth": "dGVzdHVzZXI6dGVzdHBhc3M=" + } + } +} + +# Set HOME to use our config +env HOME=$WORK +env RUST_LOG=debug + +# Try to build (will fail at registry, but auth should be resolved) +! exec ./krust build --platform linux/amd64 --image ghcr.io/user/app:latest . +stderr 'Resolving auth' +stderr 'ghcr.io' diff --git a/tests/testdata/auth_credential_helper.txt b/tests/testdata/auth_credential_helper.txt new file mode 100644 index 0000000..b63622a --- /dev/null +++ b/tests/testdata/auth_credential_helper.txt @@ -0,0 +1,62 @@ +# Test credential helper execution + +# Create a mock credential helper that logs when it's called +-- docker-credential-mock -- +#!/bin/sh +# Mock credential helper that logs execution and returns test credentials + +# Log that we were called +echo "MOCK_HELPER_CALLED" >&2 + +case "$1" in + get) + # Read registry from stdin + read registry + echo "MOCK_HELPER_REGISTRY=$registry" >&2 + + # Return credentials + cat <