cascade_cli/config/
auth.rs

1use crate::errors::{CascadeError, Result};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::Path;
5
6#[derive(Debug, Clone, Serialize, Deserialize, Default)]
7pub struct AuthConfig {
8    pub bitbucket_tokens: std::collections::HashMap<String, String>,
9    pub default_server: Option<String>,
10}
11
12pub struct AuthManager {
13    config: AuthConfig,
14    config_path: std::path::PathBuf,
15}
16
17impl AuthManager {
18    /// Create a new AuthManager
19    pub fn new(config_dir: &Path) -> Result<Self> {
20        let config_path = config_dir.join("auth.json");
21        let config = if config_path.exists() {
22            AuthConfig::load_from_file(&config_path)?
23        } else {
24            AuthConfig::default()
25        };
26
27        Ok(Self {
28            config,
29            config_path,
30        })
31    }
32
33    /// Store an authentication token for a Bitbucket server
34    pub fn store_token(&mut self, server_url: &str, token: &str) -> Result<()> {
35        self.config
36            .bitbucket_tokens
37            .insert(server_url.to_string(), token.to_string());
38        self.save()?;
39        tracing::info!("Stored authentication token for {}", server_url);
40        Ok(())
41    }
42
43    /// Retrieve an authentication token for a Bitbucket server
44    pub fn get_token(&self, server_url: &str) -> Option<&String> {
45        self.config.bitbucket_tokens.get(server_url)
46    }
47
48    /// Remove an authentication token
49    pub fn remove_token(&mut self, server_url: &str) -> Result<bool> {
50        let removed = self.config.bitbucket_tokens.remove(server_url).is_some();
51        if removed {
52            self.save()?;
53            tracing::info!("Removed authentication token for {}", server_url);
54        }
55        Ok(removed)
56    }
57
58    /// List all configured servers
59    pub fn list_servers(&self) -> Vec<&String> {
60        self.config.bitbucket_tokens.keys().collect()
61    }
62
63    /// Set the default server
64    pub fn set_default_server(&mut self, server_url: &str) -> Result<()> {
65        if !self.config.bitbucket_tokens.contains_key(server_url) {
66            return Err(CascadeError::auth(format!(
67                "No token configured for server: {server_url}"
68            )));
69        }
70
71        self.config.default_server = Some(server_url.to_string());
72        self.save()?;
73        tracing::info!("Set default server to {}", server_url);
74        Ok(())
75    }
76
77    /// Get the default server
78    pub fn get_default_server(&self) -> Option<&String> {
79        self.config.default_server.as_ref()
80    }
81
82    /// Validate that we have authentication for a server
83    pub fn validate_auth(&self, server_url: &str) -> Result<()> {
84        if self.get_token(server_url).is_none() {
85            return Err(CascadeError::auth(format!(
86                "No authentication token configured for server: {server_url}. Use 'cc config set bitbucket.token <token>' to configure."
87            )));
88        }
89        Ok(())
90    }
91
92    /// Save the configuration to disk
93    fn save(&self) -> Result<()> {
94        self.config.save_to_file(&self.config_path)
95    }
96}
97
98impl AuthConfig {
99    /// Load authentication config from a file
100    pub fn load_from_file(path: &Path) -> Result<Self> {
101        if !path.exists() {
102            return Ok(Self::default());
103        }
104
105        let content = fs::read_to_string(path)
106            .map_err(|e| CascadeError::config(format!("Failed to read auth config: {e}")))?;
107
108        let config: AuthConfig = serde_json::from_str(&content)
109            .map_err(|e| CascadeError::config(format!("Failed to parse auth config: {e}")))?;
110
111        Ok(config)
112    }
113
114    /// Save authentication config to a file
115    pub fn save_to_file(&self, path: &Path) -> Result<()> {
116        // Ensure parent directory exists
117        if let Some(parent) = path.parent() {
118            fs::create_dir_all(parent).map_err(|e| {
119                CascadeError::config(format!("Failed to create config directory: {e}"))
120            })?;
121        }
122
123        let content = serde_json::to_string_pretty(self)
124            .map_err(|e| CascadeError::config(format!("Failed to serialize auth config: {e}")))?;
125
126        // Write to temporary file first, then rename for atomic write
127        let temp_path = path.with_extension("tmp");
128        fs::write(&temp_path, content)
129            .map_err(|e| CascadeError::config(format!("Failed to write auth config: {e}")))?;
130
131        fs::rename(&temp_path, path)
132            .map_err(|e| CascadeError::config(format!("Failed to finalize auth config: {e}")))?;
133
134        Ok(())
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use tempfile::TempDir;
142
143    #[test]
144    fn test_auth_manager_basic_operations() {
145        let temp_dir = TempDir::new().unwrap();
146        let config_dir = temp_dir.path();
147
148        let mut auth_manager = AuthManager::new(config_dir).unwrap();
149
150        // Test storing and retrieving tokens
151        auth_manager
152            .store_token("https://bitbucket.company.com", "test-token")
153            .unwrap();
154        assert_eq!(
155            auth_manager.get_token("https://bitbucket.company.com"),
156            Some(&"test-token".to_string())
157        );
158
159        // Test setting default server
160        auth_manager
161            .set_default_server("https://bitbucket.company.com")
162            .unwrap();
163        assert_eq!(
164            auth_manager.get_default_server(),
165            Some(&"https://bitbucket.company.com".to_string())
166        );
167
168        // Test validation
169        auth_manager
170            .validate_auth("https://bitbucket.company.com")
171            .unwrap();
172        assert!(auth_manager
173            .validate_auth("https://unknown.server.com")
174            .is_err());
175
176        // Test removing tokens
177        assert!(auth_manager
178            .remove_token("https://bitbucket.company.com")
179            .unwrap());
180        assert!(!auth_manager
181            .remove_token("https://bitbucket.company.com")
182            .unwrap());
183    }
184}