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 {
27 username: String,
28 api_key: String,
29 },
30}
31
32impl Credential {
33 #[inline]
35 pub fn auth_header(&self) -> String {
36 match self {
37 Credential::OAuth { access_token, .. } => {
38 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 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 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 #[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 #[inline]
76 pub fn needs_refresh(&self) -> bool {
77 match self {
78 Credential::OAuth {
79 expires_at: Some(expires),
80 ..
81 } => {
82 *expires < chrono::Utc::now().timestamp() + 300
84 }
85 _ => false,
86 }
87 }
88
89 #[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 #[inline]
100 pub fn is_oauth(&self) -> bool {
101 matches!(self, Credential::OAuth { .. })
102 }
103
104 #[inline]
106 pub fn is_api_key(&self) -> bool {
107 matches!(self, Credential::ApiKey { .. })
108 }
109}
110
111enum StorageBackend {
113 Keyring(KeyringStore),
114 File(FileStore),
115}
116
117pub struct AuthManager {
119 backend: StorageBackend,
120}
121
122impl AuthManager {
123 pub fn new() -> Result<Self> {
124 let use_file_storage = Self::should_use_file_storage();
126
127 let backend = if use_file_storage {
128 StorageBackend::File(FileStore::new()?)
130 } else {
131 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 fn should_use_file_storage() -> bool {
143 if std::env::var("BITBUCKET_USE_FILE_STORAGE").is_ok() {
145 return true;
146 }
147
148 if Self::is_wsl() {
150 return true;
151 }
152
153 if Self::is_container() {
155 return true;
156 }
157
158 !Self::test_keyring()
160 }
161
162 fn is_wsl() -> bool {
164 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 std::env::var("WSL_DISTRO_NAME").is_ok() || std::env::var("WSL_INTEROP").is_ok()
173 }
174
175 fn is_container() -> bool {
177 if std::path::Path::new("/.dockerenv").exists() {
179 return true;
180 }
181
182 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 fn test_keyring() -> bool {
194 match keyring::Entry::new("bitbucket-cli-test", "test") {
196 Ok(entry) => {
197 if entry.set_password("test").is_ok() {
199 let can_read = entry.get_password().is_ok();
200 let _ = entry.delete_credential(); can_read
202 } else {
203 false
204 }
205 }
206 Err(_) => false,
207 }
208 }
209
210 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 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 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 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}