Skip to main content

llm_optimizer_integrations/jira/
webhooks.rs

1//! Jira webhook event handler
2//!
3//! Provides webhook verification and event processing for Jira webhooks.
4
5use super::types::WebhookEvent;
6use anyhow::{anyhow, Context, Result};
7use async_trait::async_trait;
8use serde_json::Value;
9use std::sync::Arc;
10use tokio::sync::RwLock;
11use tracing::{debug, error, info, warn};
12
13/// Webhook event handler trait
14#[async_trait]
15pub trait WebhookHandler: Send + Sync {
16    /// Handle an issue created event
17    async fn on_issue_created(&self, event: &WebhookEvent) -> Result<()>;
18
19    /// Handle an issue updated event
20    async fn on_issue_updated(&self, event: &WebhookEvent) -> Result<()>;
21
22    /// Handle an issue deleted event
23    async fn on_issue_deleted(&self, event: &WebhookEvent) -> Result<()>;
24
25    /// Handle any other event type
26    async fn on_other_event(&self, event: &WebhookEvent) -> Result<()>;
27}
28
29/// Webhook processor for Jira events
30pub struct WebhookProcessor {
31    handlers: Arc<RwLock<Vec<Arc<dyn WebhookHandler>>>>,
32    /// Optional webhook secret for signature verification
33    webhook_secret: Option<String>,
34}
35
36impl WebhookProcessor {
37    /// Create a new webhook processor
38    ///
39    /// # Arguments
40    ///
41    /// * `webhook_secret` - Optional secret for webhook signature verification
42    pub fn new(webhook_secret: Option<String>) -> Self {
43        Self {
44            handlers: Arc::new(RwLock::new(Vec::new())),
45            webhook_secret,
46        }
47    }
48
49    /// Register a webhook event handler
50    ///
51    /// # Arguments
52    ///
53    /// * `handler` - Handler to register
54    pub async fn register_handler(&self, handler: Arc<dyn WebhookHandler>) {
55        let mut handlers = self.handlers.write().await;
56        handlers.push(handler);
57        info!("Registered webhook handler");
58    }
59
60    /// Verify webhook signature
61    ///
62    /// # Arguments
63    ///
64    /// * `signature` - Signature from webhook headers
65    /// * `payload` - Raw webhook payload
66    ///
67    /// # Returns
68    ///
69    /// Returns Ok(()) if signature is valid or not configured
70    pub fn verify_signature(&self, signature: Option<&str>, payload: &[u8]) -> Result<()> {
71        if let Some(secret) = &self.webhook_secret {
72            let sig = signature.ok_or_else(|| anyhow!("Missing webhook signature"))?;
73
74            // Compute expected signature using HMAC-SHA256
75            use sha2::{Digest, Sha256};
76            let mut hasher = Sha256::new();
77            hasher.update(secret.as_bytes());
78            hasher.update(payload);
79            let expected = format!("sha256={:x}", hasher.finalize());
80
81            // Constant-time comparison to prevent timing attacks
82            if !constant_time_compare(sig.as_bytes(), expected.as_bytes()) {
83                error!("Invalid webhook signature");
84                return Err(anyhow!("Invalid webhook signature"));
85            }
86
87            debug!("Webhook signature verified");
88        } else {
89            debug!("Webhook signature verification disabled (no secret configured)");
90        }
91
92        Ok(())
93    }
94
95    /// Process a webhook event
96    ///
97    /// # Arguments
98    ///
99    /// * `payload` - Raw webhook payload as JSON string
100    /// * `signature` - Optional signature from headers
101    ///
102    /// # Returns
103    ///
104    /// Returns Ok(()) if event was processed successfully
105    pub async fn process_event(
106        &self,
107        payload: &str,
108        signature: Option<&str>,
109    ) -> Result<()> {
110        // Verify signature if configured
111        self.verify_signature(signature, payload.as_bytes())?;
112
113        // Parse webhook event
114        let event: WebhookEvent = serde_json::from_str(payload)
115            .context("Failed to parse webhook event")?;
116
117        info!(
118            "Processing webhook event: {} at {}",
119            event.webhook_event, event.timestamp
120        );
121
122        // Route to appropriate handlers based on event type
123        let handlers = self.handlers.read().await;
124
125        if handlers.is_empty() {
126            warn!("No webhook handlers registered");
127            return Ok(());
128        }
129
130        let event_type = event.webhook_event.as_str();
131        let mut errors = Vec::new();
132
133        for handler in handlers.iter() {
134            let result = match event_type {
135                "jira:issue_created" => handler.on_issue_created(&event).await,
136                "jira:issue_updated" => handler.on_issue_updated(&event).await,
137                "jira:issue_deleted" => handler.on_issue_deleted(&event).await,
138                _ => handler.on_other_event(&event).await,
139            };
140
141            if let Err(e) = result {
142                error!("Handler error for {}: {}", event_type, e);
143                errors.push(e);
144            }
145        }
146
147        if !errors.is_empty() {
148            return Err(anyhow!(
149                "Some handlers failed: {} errors",
150                errors.len()
151            ));
152        }
153
154        info!("Successfully processed webhook event: {}", event_type);
155        Ok(())
156    }
157
158    /// Process a raw webhook payload (any JSON)
159    ///
160    /// # Arguments
161    ///
162    /// * `payload` - Raw JSON payload
163    /// * `signature` - Optional signature from headers
164    ///
165    /// # Returns
166    ///
167    /// Returns the parsed event data
168    pub async fn process_raw_event(
169        &self,
170        payload: &str,
171        signature: Option<&str>,
172    ) -> Result<Value> {
173        // Verify signature if configured
174        self.verify_signature(signature, payload.as_bytes())?;
175
176        let value: Value = serde_json::from_str(payload)
177            .context("Failed to parse webhook payload")?;
178
179        debug!("Received raw webhook event: {}", value["webhookEvent"].as_str().unwrap_or("unknown"));
180
181        Ok(value)
182    }
183
184    /// Validate webhook event structure
185    ///
186    /// # Arguments
187    ///
188    /// * `payload` - Raw webhook payload
189    ///
190    /// # Returns
191    ///
192    /// Returns Ok(()) if the event structure is valid
193    pub fn validate_event(&self, payload: &str) -> Result<()> {
194        let value: Value = serde_json::from_str(payload)
195            .context("Invalid JSON payload")?;
196
197        // Check required fields
198        if !value.get("webhookEvent").and_then(|v| v.as_str()).is_some() {
199            return Err(anyhow!("Missing webhookEvent field"));
200        }
201
202        if !value.get("timestamp").and_then(|v| v.as_i64()).is_some() {
203            return Err(anyhow!("Missing or invalid timestamp field"));
204        }
205
206        debug!("Webhook event structure is valid");
207        Ok(())
208    }
209}
210
211/// Constant-time string comparison to prevent timing attacks
212fn constant_time_compare(a: &[u8], b: &[u8]) -> bool {
213    if a.len() != b.len() {
214        return false;
215    }
216
217    let mut result = 0u8;
218    for (x, y) in a.iter().zip(b.iter()) {
219        result |= x ^ y;
220    }
221
222    result == 0
223}
224
225/// Example webhook handler implementation
226pub struct LoggingWebhookHandler;
227
228#[async_trait]
229impl WebhookHandler for LoggingWebhookHandler {
230    async fn on_issue_created(&self, event: &WebhookEvent) -> Result<()> {
231        if let Some(issue) = &event.issue {
232            info!(
233                "Issue created: {} - {}",
234                issue.key, issue.fields.summary
235            );
236        }
237        Ok(())
238    }
239
240    async fn on_issue_updated(&self, event: &WebhookEvent) -> Result<()> {
241        if let Some(issue) = &event.issue {
242            info!(
243                "Issue updated: {} - {}",
244                issue.key, issue.fields.summary
245            );
246
247            if let Some(changelog) = &event.changelog {
248                for item in &changelog.items {
249                    debug!(
250                        "  {} changed from '{}' to '{}'",
251                        item.field,
252                        item.from_string.as_deref().unwrap_or(""),
253                        item.to_string.as_deref().unwrap_or("")
254                    );
255                }
256            }
257        }
258        Ok(())
259    }
260
261    async fn on_issue_deleted(&self, event: &WebhookEvent) -> Result<()> {
262        if let Some(issue) = &event.issue {
263            info!("Issue deleted: {}", issue.key);
264        }
265        Ok(())
266    }
267
268    async fn on_other_event(&self, event: &WebhookEvent) -> Result<()> {
269        info!("Other event: {}", event.webhook_event);
270        Ok(())
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    #[tokio::test]
279    async fn test_webhook_processor_creation() {
280        let processor = WebhookProcessor::new(Some("secret".to_string()));
281        assert!(processor.webhook_secret.is_some());
282    }
283
284    #[tokio::test]
285    async fn test_constant_time_compare() {
286        assert!(constant_time_compare(b"test", b"test"));
287        assert!(!constant_time_compare(b"test", b"fail"));
288        assert!(!constant_time_compare(b"test", b"testing"));
289    }
290
291    #[tokio::test]
292    async fn test_validate_event() {
293        let processor = WebhookProcessor::new(None);
294
295        let valid_payload = r#"{
296            "webhookEvent": "jira:issue_created",
297            "timestamp": 1234567890
298        }"#;
299
300        assert!(processor.validate_event(valid_payload).is_ok());
301
302        let invalid_payload = r#"{"other": "data"}"#;
303        assert!(processor.validate_event(invalid_payload).is_err());
304    }
305
306    #[tokio::test]
307    async fn test_process_event() {
308        let processor = WebhookProcessor::new(None);
309        let handler = Arc::new(LoggingWebhookHandler);
310        processor.register_handler(handler).await;
311
312        let payload = r#"{
313            "webhookEvent": "jira:issue_created",
314            "timestamp": 1234567890,
315            "issue": {
316                "id": "10001",
317                "key": "TEST-1",
318                "self": "https://test.atlassian.net/rest/api/3/issue/10001",
319                "fields": {
320                    "summary": "Test Issue",
321                    "description": null,
322                    "issuetype": {
323                        "id": "1",
324                        "name": "Task",
325                        "description": null
326                    },
327                    "status": {
328                        "id": "1",
329                        "name": "To Do",
330                        "description": null,
331                        "statusCategory": {
332                            "id": 1,
333                            "key": "new",
334                            "name": "To Do",
335                            "colorName": "blue-gray"
336                        }
337                    },
338                    "project": {
339                        "id": "10000",
340                        "key": "TEST",
341                        "name": "Test Project",
342                        "description": null,
343                        "projectTypeKey": "software"
344                    },
345                    "created": "2024-01-01T00:00:00.000+0000",
346                    "updated": "2024-01-01T00:00:00.000+0000",
347                    "labels": [],
348                    "components": []
349                }
350            }
351        }"#;
352
353        let result = processor.process_event(payload, None).await;
354        assert!(result.is_ok());
355    }
356}