bitbucket_cli/auth/
mod.rs1pub 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#[derive(Debug, Clone, Serialize, Deserialize)]
17pub enum Credential {
18 OAuth {
20 access_token: String,
21 refresh_token: Option<String>,
22 expires_at: Option<i64>,
23 },
24 ApiKey { username: String, api_key: String },
27}
28
29impl Credential {
30 #[inline]
32 pub fn auth_header(&self) -> String {
33 match self {
34 Credential::OAuth { access_token, .. } => {
35 let mut result = String::with_capacity(7 + access_token.len());
37 result.push_str("Bearer ");
38 result.push_str(access_token);
39 result
40 }
41 Credential::ApiKey { username, api_key } => {
42 use base64::Engine;
43 let input_len = username.len() + 1 + api_key.len();
46 let base64_len = input_len.div_ceil(3) * 4;
47 let mut result = String::with_capacity(6 + base64_len);
48 result.push_str("Basic ");
49
50 let mut credentials = Vec::with_capacity(input_len);
52 credentials.extend_from_slice(username.as_bytes());
53 credentials.push(b':');
54 credentials.extend_from_slice(api_key.as_bytes());
55
56 base64::engine::general_purpose::STANDARD.encode_string(&credentials, &mut result);
57 result
58 }
59 }
60 }
61
62 #[inline]
64 pub fn type_name(&self) -> &'static str {
65 match self {
66 Credential::OAuth { .. } => "OAuth 2.0",
67 Credential::ApiKey { .. } => "API Key",
68 }
69 }
70
71 #[inline]
73 pub fn needs_refresh(&self) -> bool {
74 match self {
75 Credential::OAuth {
76 expires_at: Some(expires),
77 ..
78 } => {
79 *expires < chrono::Utc::now().timestamp() + 300
81 }
82 _ => false,
83 }
84 }
85
86 #[inline]
88 pub fn username(&self) -> Option<&str> {
89 match self {
90 Credential::ApiKey { username, .. } => Some(username),
91 Credential::OAuth { .. } => None,
92 }
93 }
94
95 #[inline]
97 pub fn is_oauth(&self) -> bool {
98 matches!(self, Credential::OAuth { .. })
99 }
100
101 #[inline]
103 pub fn is_api_key(&self) -> bool {
104 matches!(self, Credential::ApiKey { .. })
105 }
106}
107
108enum StorageBackend {
110 Keyring(KeyringStore),
111 File(FileStore),
112}
113
114pub struct AuthManager {
116 backend: StorageBackend,
117 file_fallback: Option<FileStore>,
119}
120
121impl AuthManager {
122 pub fn new() -> Result<Self> {
123 let use_file_storage = Self::should_use_file_storage();
125
126 let (backend, file_fallback) = if use_file_storage {
127 (StorageBackend::File(FileStore::new()?), None)
129 } else {
130 match KeyringStore::new() {
132 Ok(keyring) => {
133 let fallback = FileStore::new().ok();
135 (StorageBackend::Keyring(keyring), fallback)
136 }
137 Err(_) => (StorageBackend::File(FileStore::new()?), None),
138 }
139 };
140
141 Ok(Self {
142 backend,
143 file_fallback,
144 })
145 }
146
147 fn should_use_file_storage() -> bool {
149 if std::env::var("BITBUCKET_USE_FILE_STORAGE").is_ok() {
151 return true;
152 }
153
154 if Self::is_wsl() {
156 return true;
157 }
158
159 if Self::is_container() {
161 return true;
162 }
163
164 !Self::test_keyring()
166 }
167
168 fn is_wsl() -> bool {
170 if let Ok(version) = std::fs::read_to_string("/proc/version") {
172 if version.to_lowercase().contains("microsoft")
173 || version.to_lowercase().contains("wsl")
174 {
175 return true;
176 }
177 }
178
179 std::env::var("WSL_DISTRO_NAME").is_ok() || std::env::var("WSL_INTEROP").is_ok()
181 }
182
183 fn is_container() -> bool {
185 if std::path::Path::new("/.dockerenv").exists() {
187 return true;
188 }
189
190 if let Ok(cgroup) = std::fs::read_to_string("/proc/1/cgroup") {
192 if cgroup.contains("docker") || cgroup.contains("lxc") || cgroup.contains("kubepods") {
193 return true;
194 }
195 }
196
197 false
198 }
199
200 fn test_keyring() -> bool {
203 match keyring::Entry::new("bitbucket-cli", "test-probe") {
206 Ok(entry) => {
207 if entry.set_password("test").is_ok() {
209 let can_read = entry.get_password().is_ok();
210 let _ = entry.delete_credential(); can_read
212 } else {
213 false
214 }
215 }
216 Err(_) => false,
217 }
218 }
219
220 pub fn get_credentials(&self) -> Result<Option<Credential>> {
223 match &self.backend {
224 StorageBackend::Keyring(store) => {
225 match store.get_credential() {
226 Ok(Some(cred)) => Ok(Some(cred)),
227 Ok(None) | Err(_) => {
228 if let Some(ref file_store) = self.file_fallback {
230 file_store.get_credential()
231 } else {
232 Ok(None)
233 }
234 }
235 }
236 }
237 StorageBackend::File(store) => store.get_credential(),
238 }
239 }
240
241 pub fn store_credentials(&self, credential: &Credential) -> Result<()> {
244 match &self.backend {
245 StorageBackend::Keyring(store) => {
246 match store.store_credential(credential) {
247 Ok(()) => Ok(()),
248 Err(e) => {
249 if let Some(ref file_store) = self.file_fallback {
251 eprintln!(
252 "⚠️ Keyring storage failed ({}), falling back to file storage",
253 e
254 );
255 file_store.store_credential(credential)
256 } else {
257 Err(e)
258 }
259 }
260 }
261 }
262 StorageBackend::File(store) => store.store_credential(credential),
263 }
264 }
265
266 pub fn clear_credentials(&self) -> Result<()> {
268 let primary_result = match &self.backend {
270 StorageBackend::Keyring(store) => store.delete_credential(),
271 StorageBackend::File(store) => store.delete_credential(),
272 };
273
274 if let Some(ref file_store) = self.file_fallback {
276 let _ = file_store.delete_credential(); }
278
279 primary_result
280 }
281
282 pub fn is_authenticated(&self) -> bool {
284 self.get_credentials().map(|c| c.is_some()).unwrap_or(false)
285 }
286}
287
288impl Default for AuthManager {
289 fn default() -> Self {
290 Self::new().expect("Failed to create auth manager")
291 }
292}