1
0
Fork 0
mirror of https://github.com/imjasonh/krust synced 2026-07-07 22:35:25 +00:00

Merge pull request #44 from imjasonh/auth

implement auth
This commit is contained in:
Jason Hall 2025-10-15 08:57:10 -04:00 committed by GitHub
commit d095c26871
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 410 additions and 27 deletions

View file

@ -126,9 +126,9 @@ impl AuthConfig {
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DockerConfig {
#[serde(default)]
pub auths: HashMap<String, DockerAuthEntry>,
pub auths: Option<HashMap<String, DockerAuthEntry>>,
#[serde(rename = "credHelpers", default)]
pub cred_helpers: HashMap<String, String>,
pub cred_helpers: Option<HashMap<String, String>>,
#[serde(rename = "credsStore", skip_serializing_if = "Option::is_none")]
pub creds_store: Option<String>,
}

View file

@ -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<RegistryAuth> {
// 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(&registry) {
debug!("Found auth in Docker config for registry: {}", registry);
return Ok(auth);
}
// Try credential helpers
if let Ok(auth) = try_credential_helpers(&registry) {
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<RegistryAuth> {
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::<DockerConfig>(&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<RegistryAuth> {
// 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<PathBuf> {
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<RegistryAuth> {
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::<DockerConfig>(&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<RegistryAuth> {
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,
})
}

View file

@ -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()));
}

View file

@ -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")?;

View file

@ -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 {

34
tests/testdata/auth_base64.txt vendored Normal file
View file

@ -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'

View file

@ -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 <<EOF
{
"Username": "helper-user",
"Secret": "helper-pass"
}
EOF
;;
*)
exit 1
;;
esac
-- Cargo.toml --
[package]
name = "cred-helper-test"
version = "0.1.0"
edition = "2021"
[dependencies]
-- src/main.rs --
fn main() {
println!("Testing credential helper");
}
# Make the helper executable
chmod +x docker-credential-mock
# Create Docker config that uses our mock helper
mkdir .docker
-- .docker/config.json --
{
"credHelpers": {
"mock.registry.io": "mock"
}
}
# Add current dir to PATH and set HOME
env HOME=$WORK
env PATH=$WORK:$PATH
env RUST_LOG=debug
# Try to push to mock.registry.io (will fail at network level, but should call helper)
# Using --no-push to avoid actual network call
! exec ./krust build --platform linux/amd64 --image mock.registry.io/test:latest .
# Should see the helper being attempted
stderr 'credential helper'

View file

@ -0,0 +1,36 @@
# Test basic auth from Docker config
# Create a test project
-- Cargo.toml --
[package]
name = "auth-basic-test"
version = "0.1.0"
edition = "2021"
[dependencies]
-- src/main.rs --
fn main() {
println!("Testing basic auth");
}
# Create Docker config with username/password
mkdir .docker
-- .docker/config.json --
{
"auths": {
"test.example.com": {
"username": "myuser",
"password": "mypass"
}
}
}
# Set HOME to use our config
env HOME=$WORK
env RUST_LOG=debug
# Try to build and push (will fail at registry level, but auth should be resolved)
# We can verify auth was read from config in debug logs
! exec ./krust build --platform linux/amd64 --image test.example.com/app:latest .
stderr 'Resolving auth'
stderr 'test.example.com'