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 pub auths: HashMap<String, String>,
15}
16
17impl AuthConfig {
18 pub async fn load_default() -> Result<Self> {
21 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 pub async fn save_default(&self) -> Result<()> {
33 self.save(&Self::default_path()?).await
34 }
35
36 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 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}