Skip to main content

zlayer_secrets/
registry_auth.rs

1//! Registry-credential resolution shared by the API pull handlers and the
2//! agent's supervisor recreate path.
3//!
4//! This lives in `zlayer-secrets` (rather than `zlayer-api`, its historical
5//! home) because it is fundamentally a credential-store concern and both
6//! consumers — `zlayer-api` (the `/images/pull` + container-create handlers)
7//! and `zlayer-agent` (the supervisor's `pull_and_refresh_digest`) — already
8//! depend on `zlayer-secrets`, whereas the agent cannot depend on `zlayer-api`
9//! (that edge runs the other way). `zlayer-api` re-exports both functions from
10//! `zlayer_api::auth` for source compatibility.
11
12use crate::{RegistryCredentialStore, SecretsStore};
13
14/// Resolve registry authentication asynchronously, handling
15/// [`zlayer_core::auth::AuthSource::SecretStore`] via the provided credential
16/// store.
17///
18/// For all other `AuthSource` variants this delegates to the synchronous
19/// [`zlayer_core::auth::AuthResolver::resolve_source`] method. The `SecretStore`
20/// variant performs an async lookup against the `RegistryCredentialStore`.
21///
22/// # Arguments
23///
24/// * `resolver` - The sync auth resolver (owns per-registry config)
25/// * `registry` - Registry hostname to resolve credentials for
26/// * `cred_store` - Optional credential store; if `None` and the source is
27///   `SecretStore`, returns `Anonymous` with a warning
28pub async fn resolve_registry_auth_async<S: SecretsStore>(
29    resolver: &zlayer_core::auth::AuthResolver,
30    registry: &str,
31    cred_store: Option<&RegistryCredentialStore<S>>,
32) -> oci_client::secrets::RegistryAuth {
33    let source = resolver.source_for_registry(registry);
34
35    match source {
36        zlayer_core::auth::AuthSource::SecretStore { credential_id } => {
37            let Some(store) = cred_store else {
38                tracing::warn!(
39                    credential_id,
40                    "No credential store provided for SecretStore auth"
41                );
42                return oci_client::secrets::RegistryAuth::Anonymous;
43            };
44            match store.get(credential_id).await {
45                Ok(Some(cred)) => match store.get_password(credential_id).await {
46                    Ok(secret) => oci_client::secrets::RegistryAuth::Basic(
47                        cred.username.clone(),
48                        secret.expose().to_string(),
49                    ),
50                    Err(e) => {
51                        tracing::error!(
52                            error = %e,
53                            credential_id,
54                            "Failed to retrieve registry password"
55                        );
56                        oci_client::secrets::RegistryAuth::Anonymous
57                    }
58                },
59                Ok(None) => {
60                    tracing::warn!(credential_id, "Registry credential not found");
61                    oci_client::secrets::RegistryAuth::Anonymous
62                }
63                Err(e) => {
64                    tracing::error!(
65                        error = %e,
66                        credential_id,
67                        "Failed to look up registry credential"
68                    );
69                    oci_client::secrets::RegistryAuth::Anonymous
70                }
71            }
72        }
73        other => resolver.resolve_source(other, registry),
74    }
75}
76
77/// Resolve registry auth for an image's host **from the stored credential
78/// store**, with a Docker-config fallback.
79///
80/// This is the "`zlayer login <host>` then plain `pull <host>/img`" path: the
81/// pull request carries neither inline `registry_auth` nor an explicit
82/// `registry_credential_id`, so we consult the [`RegistryCredentialStore`] by
83/// hostname. Every stored credential is mapped into a per-registry
84/// [`zlayer_core::auth::AuthSource::SecretStore`] entry keyed by its normalized
85/// registry host, the resolver's default is left as
86/// [`zlayer_core::auth::AuthSource::DockerConfig`], and resolution is delegated
87/// to [`resolve_registry_auth_async`]. The effective precedence for the returned
88/// auth is therefore:
89///
90/// 1. **Stored credential** whose registry matches the image host (from
91///    `zlayer login`), then
92/// 2. **`~/.docker/config.json`** (the `DockerConfig` default), then
93/// 3. **anonymous** (returned as `None`).
94///
95/// Inline `registry_auth` precedence is enforced by the *callers* (the pull /
96/// create handlers short-circuit on inline auth before ever calling this), so
97/// this function is only reached when no explicit credential was supplied.
98///
99/// Returns `None` when resolution yields anonymous access, so the caller can
100/// pass `None` straight through to the runtime (whose own resolver then performs
101/// the identical Docker-config fallback — harmless, never a private-registry
102/// regression).
103///
104/// If two stored credentials normalize to the same host, the last one wins
105/// (`HashMap` insertion order); `zlayer login` overwrites conceptually, so this
106/// matches the "most recent login for a host" intent.
107pub async fn resolve_stored_registry_auth<S: SecretsStore>(
108    image: &str,
109    cred_store: &RegistryCredentialStore<S>,
110) -> Option<zlayer_spec::RegistryAuth> {
111    use zlayer_core::auth::{AuthConfig, AuthResolver, AuthSource, RegistryAuthConfig};
112
113    let creds = match cred_store.list().await {
114        Ok(creds) => creds,
115        Err(e) => {
116            tracing::error!(error = %e, "failed to list registry credentials for host-match auth");
117            return None;
118        }
119    };
120
121    let registries: Vec<RegistryAuthConfig> = creds
122        .into_iter()
123        .map(|c| RegistryAuthConfig {
124            registry: AuthResolver::normalize_registry(&c.registry),
125            source: AuthSource::SecretStore {
126                credential_id: c.id,
127            },
128        })
129        .collect();
130
131    let config = AuthConfig {
132        registries,
133        // Fall back to ~/.docker/config.json for hosts without a stored
134        // credential, preserving the existing DockerConfig behaviour.
135        default: AuthSource::DockerConfig,
136        docker_config_path: None,
137    };
138    let resolver = AuthResolver::new(config);
139
140    let host = AuthResolver::normalize_registry(&AuthResolver::extract_registry(image));
141    match resolve_registry_auth_async(&resolver, &host, Some(cred_store)).await {
142        oci_client::secrets::RegistryAuth::Basic(username, password) => {
143            Some(zlayer_spec::RegistryAuth {
144                username,
145                password,
146                // oci Basic is what both Basic and Token creds reduce to on the
147                // wire (`spec_auth_to_oci`), so Basic is the faithful tag here.
148                auth_type: zlayer_spec::RegistryAuthType::Basic,
149            })
150        }
151        _ => None,
152    }
153}