llm_optimizer_integrations/jira/
auth.rs1use 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#[derive(Debug, Clone)]
14pub struct AuthManager {
15 config: Arc<RwLock<JiraConfig>>,
16}
17
18impl AuthManager {
19 pub fn new(config: JiraConfig) -> Self {
21 Self {
22 config: Arc::new(RwLock::new(config)),
23 }
24 }
25
26 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 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 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(¶ms)
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 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 Ok(false)
154 }
155 }
156
157 pub async fn get_base_url(&self) -> String {
159 self.config.read().await.base_url.clone()
160 }
161
162 pub async fn get_timeout(&self) -> std::time::Duration {
164 std::time::Duration::from_secs(self.config.read().await.timeout_secs)
165 }
166
167 pub async fn get_max_retries(&self) -> u32 {
169 self.config.read().await.max_retries
170 }
171
172 pub async fn get_rate_limit(&self) -> u32 {
174 self.config.read().await.rate_limit_per_minute
175 }
176}
177
178mod 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}