bitbucket_cli/auth/
mod.rs

1pub mod api_key;
2pub mod file_store;
3pub mod keyring_store;
4pub mod oauth;
5
6use anyhow::Result;
7use serde::{Deserialize, Serialize};
8
9pub use api_key::*;
10pub use file_store::*;
11pub use keyring_store::*;
12pub use oauth::*;
13
14/// Credential types supported by the CLI
15/// OAuth2 is preferred, with API key as fallback
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub enum Credential {
18    /// OAuth 2.0 token (PREFERRED)
19    OAuth {
20        access_token: String,
21        refresh_token: Option<String>,
22        expires_at: Option<i64>,
23    },
24    /// Bitbucket API key (fallback for automation/CI)
25    /// Note: App passwords are deprecated by Atlassian
26    ApiKey {
27        username: String,
28        api_key: String,
29    },
30}
31
32impl Credential {
33    /// Get the authorization header value for API requests
34    #[inline]
35    pub fn auth_header(&self) -> String {
36        match self {
37            Credential::OAuth { access_token, .. } => {
38                // Pre-allocate: "Bearer " (7) + token length
39                let mut result = String::with_capacity(7 + access_token.len());
40                result.push_str("Bearer ");
41                result.push_str(access_token);
42                result
43            }
44            Credential::ApiKey { username, api_key } => {
45                use base64::Engine;
46                // Pre-calculate capacity: "Basic " (6) + base64 encoded length
47                // base64 length = ceil(input_len * 4/3)
48                let input_len = username.len() + 1 + api_key.len();
49                let base64_len = input_len.div_ceil(3) * 4;
50                let mut result = String::with_capacity(6 + base64_len);
51                result.push_str("Basic ");
52
53                // Encode directly into a buffer to avoid intermediate String
54                let mut credentials = Vec::with_capacity(input_len);
55                credentials.extend_from_slice(username.as_bytes());
56                credentials.push(b':');
57                credentials.extend_from_slice(api_key.as_bytes());
58
59                base64::engine::general_purpose::STANDARD.encode_string(&credentials, &mut result);
60                result
61            }
62        }
63    }
64    
65    /// Get the credential type name for display
66    #[inline]
67    pub fn type_name(&self) -> &'static str {
68        match self {
69            Credential::OAuth { .. } => "OAuth 2.0",
70            Credential::ApiKey { .. } => "API Key",
71        }
72    }
73
74    /// Check if the credential needs refresh (for OAuth)
75    #[inline]
76    pub fn needs_refresh(&self) -> bool {
77        match self {
78            Credential::OAuth {
79                expires_at: Some(expires),
80                ..
81            } => {
82                // Refresh if expiring within 5 minutes (300 seconds)
83                *expires < chrono::Utc::now().timestamp() + 300
84            }
85            _ => false,
86        }
87    }
88
89    /// Get username if available
90    #[inline]
91    pub fn username(&self) -> Option<&str> {
92        match self {
93            Credential::ApiKey { username, .. } => Some(username),
94            Credential::OAuth { .. } => None,
95        }
96    }
97    
98    /// Check if this is an OAuth credential
99    #[inline]
100    pub fn is_oauth(&self) -> bool {
101        matches!(self, Credential::OAuth { .. })
102    }
103    
104    /// Check if this is an API key credential
105    #[inline]
106    pub fn is_api_key(&self) -> bool {
107        matches!(self, Credential::ApiKey { .. })
108    }
109}
110
111/// Credential storage backend
112enum StorageBackend {
113    Keyring(KeyringStore),
114    File(FileStore),
115}
116
117/// Authentication manager
118pub struct AuthManager {
119    backend: StorageBackend,
120}
121
122impl AuthManager {
123    pub fn new() -> Result<Self> {
124        // Automatically detect if we should use file storage
125        let use_file_storage = Self::should_use_file_storage();
126        
127        let backend = if use_file_storage {
128            // Use file storage silently - no need to warn the user
129            StorageBackend::File(FileStore::new()?)
130        } else {
131            // Try keyring, but fall back to file if it fails
132            match KeyringStore::new() {
133                Ok(keyring) => StorageBackend::Keyring(keyring),
134                Err(_) => StorageBackend::File(FileStore::new()?),
135            }
136        };
137
138        Ok(Self { backend })
139    }
140
141    /// Determine if we should use file storage instead of keyring
142    fn should_use_file_storage() -> bool {
143        // Allow manual override
144        if std::env::var("BITBUCKET_USE_FILE_STORAGE").is_ok() {
145            return true;
146        }
147        
148        // Detect WSL
149        if Self::is_wsl() {
150            return true;
151        }
152        
153        // Detect if in a container
154        if Self::is_container() {
155            return true;
156        }
157        
158        // Test if keyring actually works
159        !Self::test_keyring()
160    }
161
162    /// Check if running in WSL
163    fn is_wsl() -> bool {
164        // Check for WSL in /proc/version
165        if let Ok(version) = std::fs::read_to_string("/proc/version") {
166            if version.to_lowercase().contains("microsoft") || version.to_lowercase().contains("wsl") {
167                return true;
168            }
169        }
170        
171        // Check WSL environment variables
172        std::env::var("WSL_DISTRO_NAME").is_ok() || std::env::var("WSL_INTEROP").is_ok()
173    }
174
175    /// Check if running in a container
176    fn is_container() -> bool {
177        // Check for /.dockerenv file
178        if std::path::Path::new("/.dockerenv").exists() {
179            return true;
180        }
181        
182        // Check for container in /proc/1/cgroup
183        if let Ok(cgroup) = std::fs::read_to_string("/proc/1/cgroup") {
184            if cgroup.contains("docker") || cgroup.contains("lxc") || cgroup.contains("kubepods") {
185                return true;
186            }
187        }
188        
189        false
190    }
191
192    /// Test if keyring is actually available and working
193    fn test_keyring() -> bool {
194        // Try to create a test entry
195        match keyring::Entry::new("bitbucket-cli-test", "test") {
196            Ok(entry) => {
197                // Try to set and get a test value
198                if entry.set_password("test").is_ok() {
199                    let can_read = entry.get_password().is_ok();
200                    let _ = entry.delete_credential(); // Clean up
201                    can_read
202                } else {
203                    false
204                }
205            }
206            Err(_) => false,
207        }
208    }
209
210    /// Get stored credentials
211    pub fn get_credentials(&self) -> Result<Option<Credential>> {
212        match &self.backend {
213            StorageBackend::Keyring(store) => store.get_credential(),
214            StorageBackend::File(store) => store.get_credential(),
215        }
216    }
217
218    /// Store credentials
219    pub fn store_credentials(&self, credential: &Credential) -> Result<()> {
220        match &self.backend {
221            StorageBackend::Keyring(store) => store.store_credential(credential),
222            StorageBackend::File(store) => store.store_credential(credential),
223        }
224    }
225
226    /// Clear stored credentials
227    pub fn clear_credentials(&self) -> Result<()> {
228        match &self.backend {
229            StorageBackend::Keyring(store) => store.delete_credential(),
230            StorageBackend::File(store) => store.delete_credential(),
231        }
232    }
233
234    /// Check if authenticated
235    pub fn is_authenticated(&self) -> bool {
236        self.get_credentials().map(|c| c.is_some()).unwrap_or(false)
237    }
238}
239
240impl Default for AuthManager {
241    fn default() -> Self {
242        Self::new().expect("Failed to create auth manager")
243    }
244}