bitbucket_cli/auth/
mod.rs

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