spin_oci/
auth.rs

1use std::{
2    collections::HashMap,
3    path::{Path, PathBuf},
4};
5
6use anyhow::{bail, Context, Result};
7use oci_distribution::secrets::RegistryAuth;
8use serde::{Deserialize, Serialize};
9use spin_common::ui::quoted_path;
10
11#[derive(Serialize, Deserialize)]
12pub struct AuthConfig {
13    /// Map between registry server and base64 encoded username:password credential set.
14    pub auths: HashMap<String, String>,
15}
16
17impl AuthConfig {
18    /// Load the authentication configuration from the default location
19    /// ($XDG_CONFIG_HOME/fermyon/registry-auth.json).
20    pub async fn load_default() -> Result<Self> {
21        // TODO: add a way to override this path.
22        match Self::load(&Self::default_path()?).await {
23            Ok(s) => Ok(s),
24            Err(_) => Ok(Self {
25                auths: HashMap::new(),
26            }),
27        }
28    }
29
30    /// Save the authentication configuration to the default location
31    /// ($XDG_CONFIG_HOME/fermyon/registry-auth.json).
32    pub async fn save_default(&self) -> Result<()> {
33        self.save(&Self::default_path()?).await
34    }
35
36    /// Insert the new credentials into the auths file, with the server as the key and base64
37    /// encoded username:password as the value.
38    pub fn insert(
39        &mut self,
40        server: impl AsRef<str>,
41        username: impl AsRef<str>,
42        password: impl AsRef<str>,
43    ) -> Result<()> {
44        let encoded = base64::Engine::encode(
45            &base64::engine::general_purpose::STANDARD,
46            format!("{}:{}", username.as_ref(), password.as_ref()),
47        );
48        self.auths.insert(server.as_ref().to_string(), encoded);
49
50        Ok(())
51    }
52
53    fn default_path() -> Result<PathBuf> {
54        Ok(dirs::config_dir()
55            .context("Cannot find configuration directory")?
56            .join("fermyon")
57            .join("registry-auth.json"))
58    }
59
60    /// Get the registry authentication for a given registry from the default location.
61    pub async fn get_auth_from_default(server: impl AsRef<str>) -> Result<RegistryAuth> {
62        let auths = Self::load_default().await?;
63        let encoded = match auths.auths.get(server.as_ref()) {
64            Some(e) => e,
65            None => bail!(format!("no credentials stored for {}", server.as_ref())),
66        };
67
68        let bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, encoded)?;
69        let decoded = std::str::from_utf8(&bytes)?;
70        let parts: Vec<&str> = decoded.splitn(2, ':').collect();
71
72        tracing::trace!("Decoded registry credentials from the Spin configuration.");
73        Ok(RegistryAuth::Basic(
74            parts
75                .first()
76                .context("expected username as first element of the decoded auth")?
77                .to_string(),
78            parts
79                .get(1)
80                .context("expected secret as second element of the decoded auth")?
81                .to_string(),
82        ))
83    }
84
85    async fn load(p: &Path) -> Result<Self> {
86        let contents = tokio::fs::read_to_string(&p).await?;
87        serde_json::from_str(&contents)
88            .with_context(|| format!("cannot load authentication file {}", quoted_path(p)))
89    }
90
91    async fn save(&self, p: &Path) -> Result<()> {
92        if let Some(parent_dir) = p.parent() {
93            tokio::fs::create_dir_all(parent_dir)
94                .await
95                .with_context(|| format!("Failed to create config dir {}", parent_dir.display()))?;
96        }
97        tokio::fs::write(&p, &serde_json::to_vec_pretty(&self)?)
98            .await
99            .with_context(|| format!("cannot save authentication file {}", quoted_path(p)))
100    }
101}