From c20da6d744f2425d74db4ecb6b0fb9c7953d78b8 Mon Sep 17 00:00:00 2001 From: ZdenekSrotyr Date: Thu, 9 Apr 2026 07:13:20 +0200 Subject: [PATCH] Remove dead Flask Blueprint from Jira connector The Flask-based webhook endpoint at connectors/jira/webhook.py is no longer used. FastAPI handles webhooks via app/api/jira_webhooks.py which is already integrated into the application. This removes the redundant Flask code. --- connectors/jira/webhook.py | 206 ------------------------------------- 1 file changed, 206 deletions(-) delete mode 100644 connectors/jira/webhook.py diff --git a/connectors/jira/webhook.py b/connectors/jira/webhook.py deleted file mode 100644 index 4046f0b..0000000 --- a/connectors/jira/webhook.py +++ /dev/null @@ -1,206 +0,0 @@ -""" -Jira webhook endpoint for receiving issue change notifications. - -Handles incoming webhooks from Atlassian Jira, verifies HMAC signatures, -and triggers issue data fetching. -""" - -import hashlib -import hmac -import json -import logging -from datetime import datetime, timezone - -from flask import Blueprint, abort, jsonify, request - -from .service import Config, get_jira_service - -logger = logging.getLogger(__name__) - -jira_bp = Blueprint("jira", __name__, url_prefix="/webhooks") - -# Path for storing raw webhook events (for debugging/audit) -WEBHOOK_LOG_DIR = Config.JIRA_DATA_DIR / "webhook_events" - - -def verify_signature(payload: bytes, signature: str | None) -> bool: - """ - Verify HMAC-SHA256 signature from Jira webhook. - - Args: - payload: Raw request body bytes - signature: Signature from X-Hub-Signature header - - Returns: - True if signature is valid or if no secret is configured (dev mode) - """ - secret = Config.JIRA_WEBHOOK_SECRET - - # If no secret configured, skip verification (not recommended for production) - if not secret: - logger.warning("JIRA_WEBHOOK_SECRET not configured, skipping signature verification") - return True - - if not signature: - logger.warning("No signature provided in webhook request") - return False - - # Jira may send signature with or without algorithm prefix - if signature.startswith("sha256="): - signature = signature[7:] - - # Compute expected signature - expected = hmac.new( - secret.encode("utf-8"), - payload, - hashlib.sha256, - ).hexdigest() - - # Constant-time comparison to prevent timing attacks - return hmac.compare_digest(signature, expected) - - -def log_webhook_event(event_data: dict) -> None: - """ - Log webhook event to file for debugging/audit. - - Args: - event_data: Webhook payload - """ - try: - WEBHOOK_LOG_DIR.mkdir(parents=True, exist_ok=True) - timestamp = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S_%f") - event_type = event_data.get("webhookEvent", "unknown").replace(":", "_") - filename = f"{timestamp}_{event_type}.json" - filepath = WEBHOOK_LOG_DIR / filename - - with open(filepath, "w") as f: - json.dump(event_data, f, indent=2, default=str) - - except Exception as e: - logger.warning(f"Failed to log webhook event: {e}") - - -@jira_bp.route("/jira", methods=["POST"]) -def receive_jira_webhook(): - """ - Receive and process Jira webhook notifications. - - Jira sends POST requests with JSON payload containing: - - webhookEvent: Event type (e.g., "jira:issue_created", "jira:issue_updated") - - issue: Issue data (may be partial) - - comment: Comment data (for comment events) - - changelog: List of field changes (for update events) - - Returns: - JSON response with processing status - """ - # Get raw payload for signature verification - payload = request.get_data() - - # Verify signature (Jira uses X-Hub-Signature or X-Hub-Signature-256) - signature = request.headers.get("X-Hub-Signature-256") or request.headers.get("X-Hub-Signature") - - if not verify_signature(payload, signature): - logger.warning(f"Invalid webhook signature from {request.remote_addr}") - abort(401, "Invalid signature") - - # Parse JSON payload - try: - event_data = request.get_json(force=True) - except Exception as e: - logger.error(f"Failed to parse webhook JSON: {e}") - abort(400, "Invalid JSON payload") - - if not event_data: - abort(400, "Empty payload") - - # Log the event for debugging - log_webhook_event(event_data) - - # Extract event info - webhook_event = event_data.get("webhookEvent", "unknown") - issue = event_data.get("issue", {}) - issue_key = issue.get("key", "unknown") - - logger.info(f"Received webhook: {webhook_event} for issue {issue_key}") - - # Process the event - jira_service = get_jira_service() - - if not jira_service.is_configured(): - logger.error("Jira service not configured, cannot process webhook") - return jsonify({ - "status": "error", - "message": "Jira service not configured", - }), 503 - - # Process asynchronously would be better, but for now process synchronously - success = jira_service.process_webhook_event(event_data) - - if success: - return jsonify({ - "status": "ok", - "event": webhook_event, - "issue": issue_key, - }) - else: - return jsonify({ - "status": "error", - "message": "Failed to process event", - "event": webhook_event, - "issue": issue_key, - }), 500 - - -@jira_bp.route("/jira/health", methods=["GET"]) -def jira_webhook_health(): - """ - Health check for Jira webhook endpoint. - - Returns configuration status without exposing secrets. - """ - jira_service = get_jira_service() - - return jsonify({ - "status": "ok", - "configured": jira_service.is_configured(), - "webhook_secret_set": bool(Config.JIRA_WEBHOOK_SECRET), - "jira_domain": Config.JIRA_DOMAIN or "(not set)", - }) - - -@jira_bp.route("/jira/test", methods=["POST"]) -def test_jira_fetch(): - """ - Test endpoint to manually fetch an issue (for debugging). - - Requires JSON body: {"issue_key": "KSP-123"} - Only available if FLASK_DEBUG is true. - """ - if not Config.DEBUG: - abort(404) - - data = request.get_json(silent=True) or {} - issue_key = data.get("issue_key") - - if not issue_key: - return jsonify({"error": "issue_key is required"}), 400 - - jira_service = get_jira_service() - - if not jira_service.is_configured(): - return jsonify({"error": "Jira service not configured"}), 503 - - issue_data = jira_service.fetch_issue(issue_key) - - if issue_data: - saved_path = jira_service.save_issue(issue_data) - return jsonify({ - "status": "ok", - "issue_key": issue_key, - "saved_to": str(saved_path) if saved_path else None, - "fields_count": len(issue_data.get("fields", {})), - }) - else: - return jsonify({"error": f"Failed to fetch issue {issue_key}"}), 500