zlayer-secrets 0.14.1

Secure secrets management for ZLayer container workloads
Documentation
//! Registry-credential resolution shared by the API pull handlers and the
//! agent's supervisor recreate path.
//!
//! This lives in `zlayer-secrets` (rather than `zlayer-api`, its historical
//! home) because it is fundamentally a credential-store concern and both
//! consumers — `zlayer-api` (the `/images/pull` + container-create handlers)
//! and `zlayer-agent` (the supervisor's `pull_and_refresh_digest`) — already
//! depend on `zlayer-secrets`, whereas the agent cannot depend on `zlayer-api`
//! (that edge runs the other way). `zlayer-api` re-exports both functions from
//! `zlayer_api::auth` for source compatibility.

use crate::{RegistryCredentialStore, SecretsStore};

/// Resolve registry authentication asynchronously, handling
/// [`zlayer_core::auth::AuthSource::SecretStore`] via the provided credential
/// store.
///
/// For all other `AuthSource` variants this delegates to the synchronous
/// [`zlayer_core::auth::AuthResolver::resolve_source`] method. The `SecretStore`
/// variant performs an async lookup against the `RegistryCredentialStore`.
///
/// # Arguments
///
/// * `resolver` - The sync auth resolver (owns per-registry config)
/// * `registry` - Registry hostname to resolve credentials for
/// * `cred_store` - Optional credential store; if `None` and the source is
///   `SecretStore`, returns `Anonymous` with a warning
pub async fn resolve_registry_auth_async<S: SecretsStore>(
    resolver: &zlayer_core::auth::AuthResolver,
    registry: &str,
    cred_store: Option<&RegistryCredentialStore<S>>,
) -> oci_client::secrets::RegistryAuth {
    let source = resolver.source_for_registry(registry);

    match source {
        zlayer_core::auth::AuthSource::SecretStore { credential_id } => {
            let Some(store) = cred_store else {
                tracing::warn!(
                    credential_id,
                    "No credential store provided for SecretStore auth"
                );
                return oci_client::secrets::RegistryAuth::Anonymous;
            };
            match store.get(credential_id).await {
                Ok(Some(cred)) => match store.get_password(credential_id).await {
                    Ok(secret) => oci_client::secrets::RegistryAuth::Basic(
                        cred.username.clone(),
                        secret.expose().to_string(),
                    ),
                    Err(e) => {
                        tracing::error!(
                            error = %e,
                            credential_id,
                            "Failed to retrieve registry password"
                        );
                        oci_client::secrets::RegistryAuth::Anonymous
                    }
                },
                Ok(None) => {
                    tracing::warn!(credential_id, "Registry credential not found");
                    oci_client::secrets::RegistryAuth::Anonymous
                }
                Err(e) => {
                    tracing::error!(
                        error = %e,
                        credential_id,
                        "Failed to look up registry credential"
                    );
                    oci_client::secrets::RegistryAuth::Anonymous
                }
            }
        }
        other => resolver.resolve_source(other, registry),
    }
}

/// Resolve registry auth for an image's host **from the stored credential
/// store**, with a Docker-config fallback.
///
/// This is the "`zlayer login <host>` then plain `pull <host>/img`" path: the
/// pull request carries neither inline `registry_auth` nor an explicit
/// `registry_credential_id`, so we consult the [`RegistryCredentialStore`] by
/// hostname. Every stored credential is mapped into a per-registry
/// [`zlayer_core::auth::AuthSource::SecretStore`] entry keyed by its normalized
/// registry host, the resolver's default is left as
/// [`zlayer_core::auth::AuthSource::DockerConfig`], and resolution is delegated
/// to [`resolve_registry_auth_async`]. The effective precedence for the returned
/// auth is therefore:
///
/// 1. **Stored credential** whose registry matches the image host (from
///    `zlayer login`), then
/// 2. **`~/.docker/config.json`** (the `DockerConfig` default), then
/// 3. **anonymous** (returned as `None`).
///
/// Inline `registry_auth` precedence is enforced by the *callers* (the pull /
/// create handlers short-circuit on inline auth before ever calling this), so
/// this function is only reached when no explicit credential was supplied.
///
/// Returns `None` when resolution yields anonymous access, so the caller can
/// pass `None` straight through to the runtime (whose own resolver then performs
/// the identical Docker-config fallback — harmless, never a private-registry
/// regression).
///
/// If two stored credentials normalize to the same host, the last one wins
/// (`HashMap` insertion order); `zlayer login` overwrites conceptually, so this
/// matches the "most recent login for a host" intent.
pub async fn resolve_stored_registry_auth<S: SecretsStore>(
    image: &str,
    cred_store: &RegistryCredentialStore<S>,
) -> Option<zlayer_spec::RegistryAuth> {
    use zlayer_core::auth::{AuthConfig, AuthResolver, AuthSource, RegistryAuthConfig};

    let creds = match cred_store.list().await {
        Ok(creds) => creds,
        Err(e) => {
            tracing::error!(error = %e, "failed to list registry credentials for host-match auth");
            return None;
        }
    };

    let registries: Vec<RegistryAuthConfig> = creds
        .into_iter()
        .map(|c| RegistryAuthConfig {
            registry: AuthResolver::normalize_registry(&c.registry),
            source: AuthSource::SecretStore {
                credential_id: c.id,
            },
        })
        .collect();

    let config = AuthConfig {
        registries,
        // Fall back to ~/.docker/config.json for hosts without a stored
        // credential, preserving the existing DockerConfig behaviour.
        default: AuthSource::DockerConfig,
        docker_config_path: None,
    };
    let resolver = AuthResolver::new(config);

    let host = AuthResolver::normalize_registry(&AuthResolver::extract_registry(image));
    match resolve_registry_auth_async(&resolver, &host, Some(cred_store)).await {
        oci_client::secrets::RegistryAuth::Basic(username, password) => {
            Some(zlayer_spec::RegistryAuth {
                username,
                password,
                // oci Basic is what both Basic and Token creds reduce to on the
                // wire (`spec_auth_to_oci`), so Basic is the faithful tag here.
                auth_type: zlayer_spec::RegistryAuthType::Basic,
            })
        }
        _ => None,
    }
}