bitbucket_cli/auth/
mod.rs

1pub mod api_key;
2pub mod keyring_store;
3pub mod oauth;
4
5use anyhow::Result;
6use serde::{Deserialize, Serialize};
7
8pub use api_key::*;
9pub use keyring_store::*;
10pub use oauth::*;
11
12/// Credential types supported by the CLI
13/// OAuth2 is preferred, with API key as fallback
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub enum Credential {
16    /// OAuth 2.0 token (PREFERRED)
17    OAuth {
18        access_token: String,
19        refresh_token: Option<String>,
20        expires_at: Option<i64>,
21    },
22    /// Bitbucket API key (fallback for automation/CI)
23    /// Note: App passwords are deprecated by Atlassian
24    ApiKey {
25        username: String,
26        api_key: String,
27    },
28}
29
30impl Credential {
31    /// Get the authorization header value for API requests
32    #[inline]
33    pub fn auth_header(&self) -> String {
34        match self {
35            Credential::OAuth { access_token, .. } => {
36                // Pre-allocate: "Bearer " (7) + token length
37                let mut result = String::with_capacity(7 + access_token.len());
38                result.push_str("Bearer ");
39                result.push_str(access_token);
40                result
41            }
42            Credential::ApiKey { username, api_key } => {
43                use base64::Engine;
44                // Pre-calculate capacity: "Basic " (6) + base64 encoded length
45                // base64 length = ceil(input_len * 4/3)
46                let input_len = username.len() + 1 + api_key.len();
47                let base64_len = input_len.div_ceil(3) * 4;
48                let mut result = String::with_capacity(6 + base64_len);
49                result.push_str("Basic ");
50
51                // Encode directly into a buffer to avoid intermediate String
52                let mut credentials = Vec::with_capacity(input_len);
53                credentials.extend_from_slice(username.as_bytes());
54                credentials.push(b':');
55                credentials.extend_from_slice(api_key.as_bytes());
56
57                base64::engine::general_purpose::STANDARD.encode_string(&credentials, &mut result);
58                result
59            }
60        }
61    }
62    
63    /// Get the credential type name for display
64    #[inline]
65    pub fn type_name(&self) -> &'static str {
66        match self {
67            Credential::OAuth { .. } => "OAuth 2.0",
68            Credential::ApiKey { .. } => "API Key",
69        }
70    }
71
72    /// Check if the credential needs refresh (for OAuth)
73    #[inline]
74    pub fn needs_refresh(&self) -> bool {
75        match self {
76            Credential::OAuth {
77                expires_at: Some(expires),
78                ..
79            } => {
80                // Refresh if expiring within 5 minutes (300 seconds)
81                *expires < chrono::Utc::now().timestamp() + 300
82            }
83            _ => false,
84        }
85    }
86
87    /// Get username if available
88    #[inline]
89    pub fn username(&self) -> Option<&str> {
90        match self {
91            Credential::ApiKey { username, .. } => Some(username),
92            Credential::OAuth { .. } => None,
93        }
94    }
95    
96    /// Check if this is an OAuth credential
97    #[inline]
98    pub fn is_oauth(&self) -> bool {
99        matches!(self, Credential::OAuth { .. })
100    }
101    
102    /// Check if this is an API key credential
103    #[inline]
104    pub fn is_api_key(&self) -> bool {
105        matches!(self, Credential::ApiKey { .. })
106    }
107}
108
109/// Authentication manager
110pub struct AuthManager {
111    keyring: KeyringStore,
112}
113
114impl AuthManager {
115    pub fn new() -> Result<Self> {
116        Ok(Self {
117            keyring: KeyringStore::new()?,
118        })
119    }
120
121    /// Get stored credentials
122    pub fn get_credentials(&self) -> Result<Option<Credential>> {
123        self.keyring.get_credential()
124    }
125
126    /// Store credentials
127    pub fn store_credentials(&self, credential: &Credential) -> Result<()> {
128        self.keyring.store_credential(credential)
129    }
130
131    /// Clear stored credentials
132    pub fn clear_credentials(&self) -> Result<()> {
133        self.keyring.delete_credential()
134    }
135
136    /// Check if authenticated
137    pub fn is_authenticated(&self) -> bool {
138        self.get_credentials().map(|c| c.is_some()).unwrap_or(false)
139    }
140}
141
142impl Default for AuthManager {
143    fn default() -> Self {
144        Self::new().expect("Failed to create auth manager")
145    }
146}