Skip to main content

spin_oci/
auth.rs

1use std::{
2    collections::HashMap,
3    path::{Path, PathBuf},
4};
5
6use anyhow::{Context, Result, bail};
7use oci_client::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 (username, secret) = decoded
71            .split_once(':')
72            .context("expected secret as second element of the decoded auth")?;
73
74        tracing::trace!("Decoded registry credentials from the Spin configuration.");
75        Ok(RegistryAuth::Basic(
76            username.to_string(),
77            secret.to_string(),
78        ))
79    }
80
81    async fn load(p: &Path) -> Result<Self> {
82        let contents = tokio::fs::read_to_string(&p).await?;
83        serde_json::from_str(&contents)
84            .with_context(|| format!("cannot load authentication file {}", quoted_path(p)))
85    }
86
87    async fn save(&self, p: &Path) -> Result<()> {
88        if let Some(parent_dir) = p.parent() {
89            tokio::fs::create_dir_all(parent_dir)
90                .await
91                .with_context(|| format!("Failed to create config dir {}", parent_dir.display()))?;
92        }
93        tokio::fs::write(&p, &serde_json::to_vec_pretty(&self)?)
94            .await
95            .with_context(|| format!("cannot save authentication file {}", quoted_path(p)))
96    }
97}