Skip to main content

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        /// Persisted OAuth consumer credentials for re-authentication
24        #[serde(default, skip_serializing_if = "Option::is_none")]
25        client_id: Option<String>,
26        #[serde(default, skip_serializing_if = "Option::is_none")]
27        client_secret: Option<String>,
28    },
29    /// Bitbucket API key (fallback for automation/CI)
30    /// Note: App passwords are deprecated by Atlassian
31    ApiKey { username: String, api_key: String },
32}
33
34impl Credential {
35    /// Get the authorization header value for API requests
36    #[inline]
37    pub fn auth_header(&self) -> String {
38        match self {
39            Credential::OAuth { access_token, .. } => {
40                // Pre-allocate: "Bearer " (7) + token length
41                let mut result = String::with_capacity(7 + access_token.len());
42                result.push_str("Bearer ");
43                result.push_str(access_token);
44                result
45            }
46            Credential::ApiKey { username, api_key } => {
47                use base64::Engine;
48                // Pre-calculate capacity: "Basic " (6) + base64 encoded length
49                // base64 length = ceil(input_len * 4/3)
50                let input_len = username.len() + 1 + api_key.len();
51                let base64_len = input_len.div_ceil(3) * 4;
52                let mut result = String::with_capacity(6 + base64_len);
53                result.push_str("Basic ");
54
55                // Encode directly into a buffer to avoid intermediate String
56                let mut credentials = Vec::with_capacity(input_len);
57                credentials.extend_from_slice(username.as_bytes());
58                credentials.push(b':');
59                credentials.extend_from_slice(api_key.as_bytes());
60
61                base64::engine::general_purpose::STANDARD.encode_string(&credentials, &mut result);
62                result
63            }
64        }
65    }
66
67    /// Get the credential type name for display
68    #[inline]
69    pub fn type_name(&self) -> &'static str {
70        match self {
71            Credential::OAuth { .. } => "OAuth 2.0",
72            Credential::ApiKey { .. } => "API Key",
73        }
74    }
75
76    /// Check if the credential needs refresh (for OAuth)
77    #[inline]
78    pub fn needs_refresh(&self) -> bool {
79        match self {
80            Credential::OAuth {
81                expires_at: Some(expires),
82                ..
83            } => {
84                // Refresh if expiring within 5 minutes (300 seconds)
85                *expires < chrono::Utc::now().timestamp() + 300
86            }
87            _ => false,
88        }
89    }
90
91    /// Get username if available
92    #[inline]
93    pub fn username(&self) -> Option<&str> {
94        match self {
95            Credential::ApiKey { username, .. } => Some(username),
96            Credential::OAuth { .. } => None,
97        }
98    }
99
100    /// Check if this is an OAuth credential
101    #[inline]
102    pub fn is_oauth(&self) -> bool {
103        matches!(self, Credential::OAuth { .. })
104    }
105
106    /// Check if this is an API key credential
107    #[inline]
108    pub fn is_api_key(&self) -> bool {
109        matches!(self, Credential::ApiKey { .. })
110    }
111
112    /// Get stored OAuth consumer credentials (client_id, client_secret)
113    pub fn oauth_consumer_credentials(&self) -> Option<(&str, &str)> {
114        match self {
115            Credential::OAuth {
116                client_id: Some(id),
117                client_secret: Some(secret),
118                ..
119            } => Some((id, secret)),
120            _ => None,
121        }
122    }
123}
124
125/// Authentication manager - uses file-based credential storage
126pub struct AuthManager {
127    store: FileStore,
128}
129
130impl AuthManager {
131    pub fn new() -> Result<Self> {
132        Ok(Self {
133            store: FileStore::new()?,
134        })
135    }
136
137    /// Get stored credentials
138    pub fn get_credentials(&self) -> Result<Option<Credential>> {
139        self.store.get_credential()
140    }
141
142    /// Store credentials
143    pub fn store_credentials(&self, credential: &Credential) -> Result<()> {
144        self.store.store_credential(credential)
145    }
146
147    /// Clear stored credentials
148    pub fn clear_credentials(&self) -> Result<()> {
149        self.store.delete_credential()
150    }
151
152    /// Check if authenticated
153    pub fn is_authenticated(&self) -> bool {
154        self.get_credentials().map(|c| c.is_some()).unwrap_or(false)
155    }
156}
157
158impl Default for AuthManager {
159    fn default() -> Self {
160        Self::new().expect("Failed to create auth manager")
161    }
162}