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 { username: String, api_key: String },
27}
28
29impl Credential {
30    /// Get the authorization header value for API requests
31    #[inline]
32    pub fn auth_header(&self) -> String {
33        match self {
34            Credential::OAuth { access_token, .. } => {
35                // Pre-allocate: "Bearer " (7) + token length
36                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                // Pre-calculate capacity: "Basic " (6) + base64 encoded length
44                // base64 length = ceil(input_len * 4/3)
45                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                // Encode directly into a buffer to avoid intermediate String
51                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    /// Get the credential type name for display
63    #[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    /// Check if the credential needs refresh (for OAuth)
72    #[inline]
73    pub fn needs_refresh(&self) -> bool {
74        match self {
75            Credential::OAuth {
76                expires_at: Some(expires),
77                ..
78            } => {
79                // Refresh if expiring within 5 minutes (300 seconds)
80                *expires < chrono::Utc::now().timestamp() + 300
81            }
82            _ => false,
83        }
84    }
85
86    /// Get username if available
87    #[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    /// Check if this is an OAuth credential
96    #[inline]
97    pub fn is_oauth(&self) -> bool {
98        matches!(self, Credential::OAuth { .. })
99    }
100
101    /// Check if this is an API key credential
102    #[inline]
103    pub fn is_api_key(&self) -> bool {
104        matches!(self, Credential::ApiKey { .. })
105    }
106}
107
108/// Authentication manager - uses file-based credential storage
109pub struct AuthManager {
110    store: FileStore,
111}
112
113impl AuthManager {
114    pub fn new() -> Result<Self> {
115        Ok(Self {
116            store: FileStore::new()?,
117        })
118    }
119
120    /// Get stored credentials
121    pub fn get_credentials(&self) -> Result<Option<Credential>> {
122        self.store.get_credential()
123    }
124
125    /// Store credentials
126    pub fn store_credentials(&self, credential: &Credential) -> Result<()> {
127        self.store.store_credential(credential)
128    }
129
130    /// Clear stored credentials
131    pub fn clear_credentials(&self) -> Result<()> {
132        self.store.delete_credential()
133    }
134
135    /// Check if authenticated
136    pub fn is_authenticated(&self) -> bool {
137        self.get_credentials().map(|c| c.is_some()).unwrap_or(false)
138    }
139}
140
141impl Default for AuthManager {
142    fn default() -> Self {
143        Self::new().expect("Failed to create auth manager")
144    }
145}