Skip to main content

llm_optimizer_integrations/jira/
auth.rs

1//! Jira authentication module
2//!
3//! Handles various authentication methods for Jira API.
4
5use super::types::{JiraAuth, JiraConfig};
6use anyhow::{Context, Result};
7use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, CONTENT_TYPE};
8use std::sync::Arc;
9use tokio::sync::RwLock;
10use tracing::{debug, warn};
11
12/// Authentication manager for Jira API
13#[derive(Debug, Clone)]
14pub struct AuthManager {
15    config: Arc<RwLock<JiraConfig>>,
16}
17
18impl AuthManager {
19    /// Create a new authentication manager
20    pub fn new(config: JiraConfig) -> Self {
21        Self {
22            config: Arc::new(RwLock::new(config)),
23        }
24    }
25
26    /// Get authorization headers for API requests
27    ///
28    /// # Returns
29    ///
30    /// Returns a HeaderMap with appropriate authentication headers
31    pub async fn get_auth_headers(&self) -> Result<HeaderMap> {
32        let config = self.config.read().await;
33        let mut headers = HeaderMap::new();
34
35        headers.insert(
36            CONTENT_TYPE,
37            HeaderValue::from_static("application/json"),
38        );
39
40        match &config.auth {
41            JiraAuth::Basic { email, api_token } => {
42                let credentials = format!("{}:{}", email, api_token);
43                let encoded = base64::encode(&credentials);
44                let auth_value = format!("Basic {}", encoded);
45
46                headers.insert(
47                    AUTHORIZATION,
48                    HeaderValue::from_str(&auth_value)
49                        .context("Failed to create Basic auth header")?,
50                );
51
52                debug!("Using Basic authentication for user: {}", email);
53            }
54            JiraAuth::OAuth2 { access_token, .. } => {
55                let auth_value = format!("Bearer {}", access_token);
56
57                headers.insert(
58                    AUTHORIZATION,
59                    HeaderValue::from_str(&auth_value)
60                        .context("Failed to create OAuth2 auth header")?,
61                );
62
63                debug!("Using OAuth2 authentication");
64            }
65            JiraAuth::PersonalAccessToken { token } => {
66                let auth_value = format!("Bearer {}", token);
67
68                headers.insert(
69                    AUTHORIZATION,
70                    HeaderValue::from_str(&auth_value)
71                        .context("Failed to create PAT auth header")?,
72                );
73
74                debug!("Using Personal Access Token authentication");
75            }
76        }
77
78        Ok(headers)
79    }
80
81    /// Refresh OAuth2 token if needed
82    ///
83    /// # Arguments
84    ///
85    /// * `client` - HTTP client to use for token refresh
86    ///
87    /// # Returns
88    ///
89    /// Returns Ok(true) if token was refreshed, Ok(false) if no refresh was needed
90    pub async fn refresh_token_if_needed(
91        &self,
92        client: &reqwest::Client,
93    ) -> Result<bool> {
94        let mut config = self.config.write().await;
95
96        if let JiraAuth::OAuth2 {
97            client_id,
98            client_secret,
99            refresh_token: Some(refresh_token),
100            ..
101        } = &config.auth
102        {
103            debug!("Attempting to refresh OAuth2 token");
104
105            // OAuth2 token refresh endpoint
106            let token_url = format!("{}/rest/oauth2/token", config.base_url);
107
108            let params = [
109                ("grant_type", "refresh_token"),
110                ("client_id", client_id),
111                ("client_secret", client_secret),
112                ("refresh_token", refresh_token),
113            ];
114
115            let response = client
116                .post(&token_url)
117                .form(&params)
118                .send()
119                .await
120                .context("Failed to send token refresh request")?;
121
122            if response.status().is_success() {
123                #[derive(serde::Deserialize)]
124                struct TokenResponse {
125                    access_token: String,
126                    refresh_token: Option<String>,
127                }
128
129                let token_response: TokenResponse = response
130                    .json()
131                    .await
132                    .context("Failed to parse token response")?;
133
134                // Update the stored tokens
135                config.auth = JiraAuth::OAuth2 {
136                    client_id: client_id.clone(),
137                    client_secret: client_secret.clone(),
138                    access_token: token_response.access_token,
139                    refresh_token: token_response.refresh_token.or_else(|| Some(refresh_token.clone())),
140                };
141
142                debug!("Successfully refreshed OAuth2 token");
143                Ok(true)
144            } else {
145                warn!(
146                    "Failed to refresh OAuth2 token: {}",
147                    response.status()
148                );
149                Ok(false)
150            }
151        } else {
152            // No OAuth2 or no refresh token
153            Ok(false)
154        }
155    }
156
157    /// Get the base URL
158    pub async fn get_base_url(&self) -> String {
159        self.config.read().await.base_url.clone()
160    }
161
162    /// Get timeout duration
163    pub async fn get_timeout(&self) -> std::time::Duration {
164        std::time::Duration::from_secs(self.config.read().await.timeout_secs)
165    }
166
167    /// Get max retries
168    pub async fn get_max_retries(&self) -> u32 {
169        self.config.read().await.max_retries
170    }
171
172    /// Get rate limit
173    pub async fn get_rate_limit(&self) -> u32 {
174        self.config.read().await.rate_limit_per_minute
175    }
176}
177
178// Note: base64 is a placeholder - will be added to Cargo.toml
179mod base64 {
180    pub fn encode(data: &str) -> String {
181        use std::fmt::Write;
182        let bytes = data.as_bytes();
183        let mut result = String::new();
184
185        const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
186
187        for chunk in bytes.chunks(3) {
188            let mut buf = [0u8; 3];
189            for (i, &b) in chunk.iter().enumerate() {
190                buf[i] = b;
191            }
192
193            let b1 = (buf[0] >> 2) as usize;
194            let b2 = (((buf[0] & 0x03) << 4) | (buf[1] >> 4)) as usize;
195            let b3 = (((buf[1] & 0x0F) << 2) | (buf[2] >> 6)) as usize;
196            let b4 = (buf[2] & 0x3F) as usize;
197
198            write!(&mut result, "{}", CHARSET[b1] as char).unwrap();
199            write!(&mut result, "{}", CHARSET[b2] as char).unwrap();
200
201            if chunk.len() > 1 {
202                write!(&mut result, "{}", CHARSET[b3] as char).unwrap();
203            } else {
204                result.push('=');
205            }
206
207            if chunk.len() > 2 {
208                write!(&mut result, "{}", CHARSET[b4] as char).unwrap();
209            } else {
210                result.push('=');
211            }
212        }
213
214        result
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[tokio::test]
223    async fn test_basic_auth_headers() {
224        let config = JiraConfig {
225            base_url: "https://test.atlassian.net".to_string(),
226            auth: JiraAuth::Basic {
227                email: "test@example.com".to_string(),
228                api_token: "test-token".to_string(),
229            },
230            timeout_secs: 30,
231            max_retries: 3,
232            rate_limit_per_minute: 100,
233        };
234
235        let manager = AuthManager::new(config);
236        let headers = manager.get_auth_headers().await.unwrap();
237
238        assert!(headers.contains_key(AUTHORIZATION));
239        assert!(headers.contains_key(CONTENT_TYPE));
240    }
241
242    #[tokio::test]
243    async fn test_oauth2_auth_headers() {
244        let config = JiraConfig {
245            base_url: "https://test.atlassian.net".to_string(),
246            auth: JiraAuth::OAuth2 {
247                client_id: "client-id".to_string(),
248                client_secret: "client-secret".to_string(),
249                access_token: "access-token".to_string(),
250                refresh_token: Some("refresh-token".to_string()),
251            },
252            timeout_secs: 30,
253            max_retries: 3,
254            rate_limit_per_minute: 100,
255        };
256
257        let manager = AuthManager::new(config);
258        let headers = manager.get_auth_headers().await.unwrap();
259
260        assert!(headers.contains_key(AUTHORIZATION));
261        let auth_header = headers.get(AUTHORIZATION).unwrap().to_str().unwrap();
262        assert!(auth_header.starts_with("Bearer "));
263    }
264
265    #[tokio::test]
266    async fn test_pat_auth_headers() {
267        let config = JiraConfig {
268            base_url: "https://test.atlassian.net".to_string(),
269            auth: JiraAuth::PersonalAccessToken {
270                token: "pat-token".to_string(),
271            },
272            timeout_secs: 30,
273            max_retries: 3,
274            rate_limit_per_minute: 100,
275        };
276
277        let manager = AuthManager::new(config);
278        let headers = manager.get_auth_headers().await.unwrap();
279
280        assert!(headers.contains_key(AUTHORIZATION));
281    }
282}