diff --git a/server/routes/account_management.py b/server/routes/account_management.py index ac30ae6d0..61d7308bc 100644 --- a/server/routes/account_management.py +++ b/server/routes/account_management.py @@ -1,7 +1,6 @@ """Account management routes for connected accounts.""" import logging from flask import Blueprint, request, jsonify -from utils.web.cors_utils import create_cors_response from utils.auth.rbac_decorators import require_auth_only from utils.db.db_utils import connect_to_db_as_admin, connect_to_db_as_user from utils.auth.token_management import get_token_data @@ -34,11 +33,6 @@ def _validate_provider_connection(provider: str, token_data: dict) -> bool: return False -@account_management_bp.route("/api/connected-accounts/", methods=["OPTIONS"]) -def get_connected_accounts_options(target_user_id): - return create_cors_response() - - @account_management_bp.route("/api/connected-accounts/", methods=["GET"]) @require_auth_only def get_connected_accounts(user_id, target_user_id): @@ -212,11 +206,6 @@ def _resolve_row(row): conn.close() -@account_management_bp.route("/api/connected-accounts//", methods=["OPTIONS"]) -def delete_connected_account_options(target_user_id, provider): - return create_cors_response() - - @account_management_bp.route("/api/connected-accounts//", methods=["DELETE"]) @require_auth_only def delete_connected_account(user_id, target_user_id, provider): @@ -355,11 +344,6 @@ def delete_connected_account(user_id, target_user_id, provider): return jsonify({"error": "Failed to delete connected account"}), 500 -@account_management_bp.route("/api/getUserId", methods=["OPTIONS"]) -def get_user_id_options(): - return create_cors_response() - - @account_management_bp.route("/api/getUserId", methods=["GET"]) @require_auth_only def get_user_id(user_id): @@ -372,11 +356,6 @@ def get_user_id(user_id): return jsonify({"error": "Failed to get user ID"}), 500 -@account_management_bp.route("/user_tokens", methods=["OPTIONS"]) -def get_user_tokens_options(): - return create_cors_response() - - @account_management_bp.route("/user_tokens", methods=["GET"]) @require_auth_only def get_user_tokens(user_id): diff --git a/server/routes/atlassian/atlassian_routes.py b/server/routes/atlassian/atlassian_routes.py index c28ceb587..4c51eb5a0 100644 --- a/server/routes/atlassian/atlassian_routes.py +++ b/server/routes/atlassian/atlassian_routes.py @@ -117,7 +117,7 @@ def _validate_jsm_ops(access_token: str, cloud_id: str) -> Optional[Dict[str, An # POST /atlassian/connect # ------------------------------------------------------------------ -@atlassian_bp.route("/connect", methods=["POST", "OPTIONS"]) +@atlassian_bp.route("/connect", methods=["POST"]) @require_permission("connectors", "write") def connect(user_id): """Unified connect for Atlassian products (Confluence/Jira/both).""" @@ -288,7 +288,7 @@ def connect(user_id): # GET /atlassian/status # ------------------------------------------------------------------ -@atlassian_bp.route("/status", methods=["GET", "OPTIONS"]) +@atlassian_bp.route("/status", methods=["GET"]) @require_permission("connectors", "read") def status(user_id): """Return connection status for all Atlassian products.""" @@ -363,7 +363,7 @@ def status(user_id): # POST /atlassian/disconnect # ------------------------------------------------------------------ -@atlassian_bp.route("/disconnect", methods=["POST", "OPTIONS"]) +@atlassian_bp.route("/disconnect", methods=["POST"]) @require_permission("connectors", "write") def disconnect(user_id): """Disconnect one or all Atlassian products.""" diff --git a/server/routes/auth_routes.py b/server/routes/auth_routes.py index c8edc07e5..900803be8 100644 --- a/server/routes/auth_routes.py +++ b/server/routes/auth_routes.py @@ -10,7 +10,6 @@ from utils.db.db_utils import connect_to_db_as_user from utils.db.connection_pool import db_pool from utils.auth.rbac_decorators import require_auth_only -from utils.web.cors_utils import create_cors_response import os auth_bp = Blueprint('auth', __name__, url_prefix='/api/auth') @@ -35,11 +34,11 @@ def add_cors_headers(response): origin = request.headers.get('Origin', FRONTEND_URL) response.headers['Access-Control-Allow-Origin'] = origin response.headers['Access-Control-Allow-Credentials'] = 'true' - response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS' + response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS' response.headers['Access-Control-Allow-Headers'] = 'Content-Type, X-Provider, X-Requested-With, X-User-ID, Authorization' return response -@auth_bp.route('/register', methods=['POST', 'OPTIONS']) +@auth_bp.route('/register', methods=['POST']) def register(): """Register a new organization with its first admin user. @@ -48,9 +47,6 @@ def register(): - Users within an existing org are created by an admin via /api/admin/users (invite-only). """ - if request.method == 'OPTIONS': - return create_cors_response() - try: data = request.get_json() if not data: @@ -162,15 +158,13 @@ def register(): return jsonify({"error": "Registration failed"}), 500 -@auth_bp.route('/setup-org', methods=['POST', 'OPTIONS']) +@auth_bp.route('/setup-org', methods=['POST']) @require_auth_only def setup_org(user_id): """Create an organization for an authenticated user who doesn't have one. Body: { org_name } """ - if request.method == 'OPTIONS': - return create_cors_response() try: data = request.get_json() if not data: @@ -273,12 +267,9 @@ def setup_org(user_id): return jsonify({"error": "Organization setup failed"}), 500 -@auth_bp.route('/login', methods=['POST', 'OPTIONS']) +@auth_bp.route('/login', methods=['POST']) def login(): """Authenticate user with email and password.""" - if request.method == 'OPTIONS': - return create_cors_response() - try: data = request.get_json() if not data: @@ -339,12 +330,10 @@ def login(): return jsonify({"error": "Login failed"}), 500 -@auth_bp.route('/change-password', methods=['POST', 'OPTIONS']) +@auth_bp.route('/change-password', methods=['POST']) @require_auth_only def change_password(user_id): """Change user password (requires authentication).""" - if request.method == 'OPTIONS': - return create_cors_response() try: data = request.get_json() if not data: diff --git a/server/routes/aws/auth.py b/server/routes/aws/auth.py index 7b42ba6fc..7296745de 100644 --- a/server/routes/aws/auth.py +++ b/server/routes/aws/auth.py @@ -6,7 +6,6 @@ import flask import boto3 from botocore.exceptions import ClientError -from utils.web.cors_utils import create_cors_response from utils.auth.rbac_decorators import require_permission from utils.auth.stateless_auth import get_org_id_from_request from utils.logging.secure_logging import mask_credential_value @@ -18,7 +17,7 @@ auth_bp = Blueprint("aws_auth_bp", __name__) -@auth_bp.route('/get-credentials', methods=['POST', 'OPTIONS']) +@auth_bp.route('/get-credentials', methods=['POST']) @require_permission("connectors", "read") def aws_get_credentials(user_id): """Retrieve AWS credentials stored for the user.""" @@ -77,21 +76,18 @@ def aws_get_credentials(user_id): return jsonify({"error": "Failed to retrieve AWS credentials"}), 500 -@auth_bp.route('/auth', methods=['POST', 'OPTIONS']) +@auth_bp.route('/auth', methods=['POST']) @require_permission("connectors", "write") def auth(user_id): """ AWS authentication endpoint using IAM role assumption. - + Requires External ID that matches the workspace's External ID for security. Legacy flow without External ID is no longer supported. """ - if flask.request.method == 'OPTIONS': - return create_cors_response() - logging.info("=== AWS AUTH ENDPOINT STARTED ===") try: - data = flask.request.get_json() + data = flask.request.get_json(silent=True) or {} role_arn = data.get('role_arn') read_only_role_arn = data.get('read_only_role_arn') or data.get('readOnlyRoleArn') external_id = data.get('external_id') diff --git a/server/routes/aws/aws_routes.py b/server/routes/aws/aws_routes.py index b9dc5b303..0b369fc49 100644 --- a/server/routes/aws/aws_routes.py +++ b/server/routes/aws/aws_routes.py @@ -4,24 +4,19 @@ import logging from flask import Blueprint, jsonify import flask -from utils.web.cors_utils import create_cors_response import os aws_bp = Blueprint("aws_bp", __name__) -@aws_bp.route("/setup-script", methods=["GET", "OPTIONS"]) +@aws_bp.route("/setup-script", methods=["GET"]) def aws_setup_script(): """Legacy endpoint - redirects to role-based setup""" - if flask.request.method == 'OPTIONS': - return create_cors_response() # Redirect to new role-based script return flask.redirect("/aws/setup-role", code=301) -@aws_bp.route("/aws/setup-role", methods=["GET", "OPTIONS"]) +@aws_bp.route("/aws/setup-role", methods=["GET"]) def aws_setup_role_script(): """Serve the new role-based setup script""" - if flask.request.method == 'OPTIONS': - return create_cors_response() try: script_path = os.path.join(os.path.dirname(__file__), "..", "..", "connectors", "aws_connector", "setup-aurora-role.sh") if os.path.exists(script_path): @@ -36,19 +31,15 @@ def aws_setup_role_script(): return jsonify({"error": "Failed to serve setup script"}), 500 -@aws_bp.route("/setup-script-ps1", methods=["GET", "OPTIONS"]) +@aws_bp.route("/setup-script-ps1", methods=["GET"]) def aws_setup_script_ps1(): """Legacy endpoint - redirects to role-based setup""" - if flask.request.method == 'OPTIONS': - return create_cors_response() # Redirect to new role-based script return flask.redirect("/aws/setup-role-ps1", code=301) -@aws_bp.route("/aws/setup-role-ps1", methods=["GET", "OPTIONS"]) +@aws_bp.route("/aws/setup-role-ps1", methods=["GET"]) def aws_setup_role_script_ps1(): """Serve the new role-based PowerShell setup script""" - if flask.request.method == 'OPTIONS': - return create_cors_response() try: script_path = os.path.join(os.path.dirname(__file__), "..", "..", "connectors", "aws_connector", "setup-aurora-role.ps1") if os.path.exists(script_path): diff --git a/server/routes/aws/onboarding.py b/server/routes/aws/onboarding.py index d930ac8c9..c09b6d332 100644 --- a/server/routes/aws/onboarding.py +++ b/server/routes/aws/onboarding.py @@ -6,7 +6,6 @@ import logging import os from flask import Blueprint, request, jsonify, Response -from utils.web.cors_utils import create_cors_response from utils.auth.rbac_decorators import require_permission from utils.log_sanitizer import sanitize from utils.workspace.workspace_utils import ( @@ -24,13 +23,13 @@ onboarding_bp = Blueprint("aws_onboarding_bp", __name__) -@onboarding_bp.route('/aws/env/check', methods=['GET', 'OPTIONS']) +@onboarding_bp.route('/aws/env/check', methods=['GET']) @require_permission("connectors", "read") def check_aws_environment(_user_id): """ Check if Aurora has AWS credentials available via any method (env vars, IRSA web identity, instance profile, etc.). - + Returns: { "configured": bool, @@ -39,9 +38,6 @@ def check_aws_environment(_user_id): "accountId": str | null # Only if credentials are configured and valid } """ - if request.method == 'OPTIONS': - return create_cors_response() - try: access_key_id = os.getenv('AWS_ACCESS_KEY_ID') secret_access_key = os.getenv('AWS_SECRET_ACCESS_KEY') @@ -69,7 +65,7 @@ def check_aws_environment(_user_id): return jsonify({"error": "Internal server error"}), 500 -@onboarding_bp.route('/workspaces//aws/links', methods=['GET', 'OPTIONS']) +@onboarding_bp.route('/workspaces//aws/links', methods=['GET']) @require_permission("connectors", "read") def get_aws_onboarding_links(user_id, workspace_id): """ @@ -77,9 +73,6 @@ def get_aws_onboarding_links(user_id, workspace_id): Returns basic information needed for manual role setup. """ - if request.method == 'OPTIONS': - return create_cors_response() - try: workspace = get_workspace_by_id(workspace_id) if not workspace: @@ -113,21 +106,18 @@ def get_aws_onboarding_links(user_id, workspace_id): return jsonify({"error": "Internal server error"}), 500 -@onboarding_bp.route('/workspaces//aws/role', methods=['POST', 'OPTIONS']) +@onboarding_bp.route('/workspaces//aws/role', methods=['POST']) @require_permission("connectors", "write") def set_aws_role(user_id, workspace_id): """ Manually set the AWS role ARN for a workspace. - + Expected payload: { "roleArn": "arn:aws:iam::123456789012:role/AuroraRole", "readOnlyRoleArn": "arn:aws:iam::123456789012:role/AuroraReadOnly" // optional } """ - if request.method == 'OPTIONS': - return create_cors_response() - try: workspace = get_workspace_by_id(workspace_id) if not workspace: @@ -230,15 +220,12 @@ def set_aws_role(user_id, workspace_id): return jsonify({"error": "Internal server error"}), 500 -@onboarding_bp.route('/workspaces//aws/status', methods=['GET', 'OPTIONS']) +@onboarding_bp.route('/workspaces//aws/status', methods=['GET']) @require_permission("connectors", "read") def get_aws_onboarding_status(user_id, workspace_id): """ Get current AWS onboarding status for a workspace. """ - if request.method == 'OPTIONS': - return create_cors_response() - try: workspace = get_workspace_by_id(workspace_id) if not workspace: @@ -268,13 +255,10 @@ def get_aws_onboarding_status(user_id, workspace_id): -@onboarding_bp.route('/users//workspaces', methods=['GET', 'OPTIONS']) +@onboarding_bp.route('/users//workspaces', methods=['GET']) @require_permission("connectors", "read") def list_user_workspaces(authenticated_user_id, user_id): """Get user workspaces.""" - if request.method == 'OPTIONS': - return create_cors_response() - try: if authenticated_user_id != user_id: return jsonify({"error": "Access denied"}), 403 @@ -306,16 +290,13 @@ def create_user_workspace(authenticated_user_id, user_id): return jsonify({"error": "Internal server error"}), 500 - -@onboarding_bp.route('/workspaces//aws/cleanup', methods=['POST', 'OPTIONS']) +@onboarding_bp.route('/workspaces//aws/cleanup', methods=['POST']) @require_permission("connectors", "write") def workspace_cleanup(user_id, workspace_id): """Disconnect AWS connection by removing it from user_connections (single source of truth). - + Users must manually remove IAM roles and other AWS resources in their AWS console. """ - if request.method == 'OPTIONS': - return create_cors_response() try: workspace = get_workspace_by_id(workspace_id) @@ -377,12 +358,10 @@ def workspace_cleanup(user_id, workspace_id): # --------------------------------------------------------------------------- -@onboarding_bp.route('/workspaces//aws/accounts', methods=['GET', 'OPTIONS']) +@onboarding_bp.route('/workspaces//aws/accounts', methods=['GET']) @require_permission("connectors", "read") def list_aws_accounts(user_id, workspace_id): """Return all active AWS accounts connected to this workspace's owner.""" - if request.method == 'OPTIONS': - return create_cors_response() try: workspace = get_workspace_by_id(workspace_id) @@ -398,7 +377,7 @@ def list_aws_accounts(user_id, workspace_id): return jsonify({"error": "Internal server error"}), 500 -@onboarding_bp.route('/workspaces//aws/accounts/bulk', methods=['POST', 'OPTIONS']) +@onboarding_bp.route('/workspaces//aws/accounts/bulk', methods=['POST']) @require_permission("connectors", "write") def bulk_register_aws_accounts(user_id, workspace_id): """Register multiple AWS accounts at once. @@ -416,9 +395,6 @@ def bulk_register_aws_accounts(user_id, workspace_id): Returns per-account success/failure so partially-successful bulk imports are surfaced clearly to the caller. """ - if request.method == 'OPTIONS': - return create_cors_response() - try: workspace = get_workspace_by_id(workspace_id) if not workspace or workspace['user_id'] != user_id: @@ -505,13 +481,10 @@ def bulk_register_aws_accounts(user_id, workspace_id): return jsonify({"error": "Internal server error"}), 500 -@onboarding_bp.route('/workspaces//aws/accounts/', methods=['DELETE', 'OPTIONS']) +@onboarding_bp.route('/workspaces//aws/accounts/', methods=['DELETE']) @require_permission("connectors", "write") def delete_aws_account(user_id, workspace_id, account_id): """Disconnect a single AWS account from the workspace.""" - if request.method == 'OPTIONS': - return create_cors_response() - try: workspace = get_workspace_by_id(workspace_id) if not workspace or workspace['user_id'] != user_id: @@ -529,7 +502,7 @@ def delete_aws_account(user_id, workspace_id, account_id): return jsonify({"error": "Internal server error"}), 500 -@onboarding_bp.route('/workspaces//aws/accounts/inactive', methods=['GET', 'OPTIONS']) +@onboarding_bp.route('/workspaces//aws/accounts/inactive', methods=['GET']) @require_permission("connectors", "read") def list_inactive_aws_accounts(user_id, workspace_id): """Return recently disconnected AWS accounts that can be reconnected. @@ -537,8 +510,6 @@ def list_inactive_aws_accounts(user_id, workspace_id): The IAM role likely still exists in these accounts, so the user can reconnect without redeploying the CloudFormation template. """ - if request.method == 'OPTIONS': - return create_cors_response() try: workspace = get_workspace_by_id(workspace_id) @@ -554,7 +525,7 @@ def list_inactive_aws_accounts(user_id, workspace_id): return jsonify({"error": "Internal server error"}), 500 -@onboarding_bp.route('/workspaces//aws/accounts//reconnect', methods=['POST', 'OPTIONS']) +@onboarding_bp.route('/workspaces//aws/accounts//reconnect', methods=['POST']) @require_permission("connectors", "write") def reconnect_aws_account(user_id, workspace_id, account_id): """Reconnect a previously disconnected AWS account. @@ -562,9 +533,6 @@ def reconnect_aws_account(user_id, workspace_id, account_id): Validates the role still works via STS AssumeRole, then re-activates the connection. No CloudFormation redeployment needed. """ - if request.method == 'OPTIONS': - return create_cors_response() - try: workspace = get_workspace_by_id(workspace_id) if not workspace or workspace['user_id'] != user_id: @@ -622,7 +590,7 @@ def reconnect_aws_account(user_id, workspace_id, account_id): # --------------------------------------------------------------------------- -@onboarding_bp.route('/workspaces//aws/cfn-template', methods=['GET', 'OPTIONS']) +@onboarding_bp.route('/workspaces//aws/cfn-template', methods=['GET']) @require_permission("connectors", "read") def get_cfn_template(user_id, workspace_id): """Return the CloudFormation template with ExternalId and Aurora account ID pre-filled. @@ -630,8 +598,6 @@ def get_cfn_template(user_id, workspace_id): Query params: format: 'raw' returns plain YAML (default), 'json' returns JSON wrapper """ - if request.method == 'OPTIONS': - return create_cors_response() try: workspace = get_workspace_by_id(workspace_id) @@ -720,7 +686,7 @@ def _cfn_representer(dumper, data): return jsonify({"error": "Internal server error"}), 500 -@onboarding_bp.route('/workspaces//aws/cfn-quickcreate', methods=['GET', 'OPTIONS']) +@onboarding_bp.route('/workspaces//aws/cfn-quickcreate', methods=['GET']) @require_permission("connectors", "read") def get_cfn_quickcreate_link(user_id, workspace_id): """Return a CloudFormation Quick-Create URL that opens the AWS Console @@ -737,8 +703,6 @@ def get_cfn_quickcreate_link(user_id, workspace_id): templateUrl: override the S3 URL for the template (optional, for self-hosted deployments that upload the template to S3) """ - if request.method == 'OPTIONS': - return create_cors_response() try: workspace = get_workspace_by_id(workspace_id) diff --git a/server/routes/azure/azure_routes.py b/server/routes/azure/azure_routes.py index c32f9fd2c..8a402862f 100644 --- a/server/routes/azure/azure_routes.py +++ b/server/routes/azure/azure_routes.py @@ -10,16 +10,15 @@ ) from utils.logging.secure_logging import mask_credential_value from utils.auth.token_management import get_token_data +from utils.log_sanitizer import sanitize import json azure_bp = Blueprint("azure_bp", __name__) # ---- Azure Routes ------------------------------------------------------# -@azure_bp.route("/azure/login", methods=["POST", "GET", "OPTIONS"]) +@azure_bp.route("/azure/login", methods=["POST"]) @require_permission("connectors", "write") def azure_login_route(user_id): - if flask.request.method == 'OPTIONS': - return create_cors_response() return azure_login() @@ -76,7 +75,7 @@ def azure_callback_route(): return azure_callback() -@azure_bp.route("/azure/fetch_data", methods=["GET", "POST", "OPTIONS"]) +@azure_bp.route("/azure/fetch_data", methods=["GET", "OPTIONS"]) @require_permission("connectors", "read") def fetch_data(user_id): if flask.request.method == 'OPTIONS': @@ -147,31 +146,36 @@ def azure_clusters(user_id): return jsonify({"error": "Failed to fetch AKS clusters"}), 500 -@azure_bp.route("/api/azure-subscriptions", methods=["GET", "POST", "OPTIONS"]) +@azure_bp.route("/api/azure-subscriptions", methods=["GET"]) @require_permission("connectors", "read") -def azure_subscriptions(user_id): - if request.method == "OPTIONS": - return create_cors_response() +def azure_subscriptions_get(user_id): + try: + from utils.auth.stateless_auth import get_org_id_from_request + org_id = get_org_id_from_request() + token_data = get_token_data(user_id, "azure", org_id=org_id) + if not token_data: + logging.warning("[AZURE API] No Azure token data found for user %s", sanitize(user_id)) + return jsonify({"error": "No Azure credentials found. Please authenticate with Azure."}), 401 + subscription_id = token_data.get("subscription_id") + subscription_name = token_data.get("subscription_name", "Azure Subscription") + if not subscription_id: + logging.warning("[AZURE API] No Azure subscription found for user %s", sanitize(user_id)) + return jsonify({"error": "No Azure subscription found. Please configure your Azure subscription."}), 401 + projects = [{"projectId": subscription_id, "name": subscription_name, "enabled": True}] + return jsonify({"projects": projects}), 200 + except Exception as e: + logging.error("Error in azure_subscriptions_get", exc_info=e) + return jsonify({"error": "Failed to process Azure subscriptions"}), 500 + + +@azure_bp.route("/api/azure-subscriptions", methods=["POST"]) +@require_permission("connectors", "write") +def azure_subscriptions_post(user_id): try: - if request.method == "GET": - from utils.auth.stateless_auth import get_org_id_from_request - org_id = get_org_id_from_request() - token_data = get_token_data(user_id, "azure", org_id=org_id) - if not token_data: - logging.warning(f"[AZURE API] No Azure token data found for user {user_id}") - return jsonify({"error": "No Azure credentials found. Please authenticate with Azure."}), 401 - subscription_id = token_data.get("subscription_id") - subscription_name = token_data.get("subscription_name", "Azure Subscription") - if not subscription_id: - logging.warning(f"[AZURE API] No Azure subscription found for user {user_id}") - return jsonify({"error": "No Azure subscription found. Please configure your Azure subscription."}), 401 - projects = [{"projectId": subscription_id, "name": subscription_name, "enabled": True}] - return jsonify({"projects": projects}), 200 - else: - data = request.get_json() - projects = data.get("projects", []) - logging.info(f"Azure subscription selection update received: {projects}") - return jsonify({"status": "success"}) + data = request.get_json() or {} + projects = data.get("projects", []) + logging.info("Azure subscription selection update received (count=%d)", len(projects)) + return jsonify({"status": "success"}) except Exception as e: - logging.error("Error in azure_subscriptions", exc_info=e) + logging.error("Error in azure_subscriptions_post", exc_info=e) return jsonify({"error": "Failed to process Azure subscriptions"}), 500 diff --git a/server/routes/bigpanda/bigpanda_routes.py b/server/routes/bigpanda/bigpanda_routes.py index 11240cede..ce0a31c94 100644 --- a/server/routes/bigpanda/bigpanda_routes.py +++ b/server/routes/bigpanda/bigpanda_routes.py @@ -13,7 +13,6 @@ from connectors.bigpanda_connector.api_client import BigPandaClient, BigPandaAPIError from utils.db.connection_pool import db_pool -from utils.web.cors_utils import create_cors_response from utils.auth.token_management import get_token_data, store_tokens_in_db from utils.auth.rbac_decorators import require_permission from utils.auth.stateless_auth import set_rls_context @@ -33,7 +32,7 @@ def _get_stored_credentials(user_id: str) -> dict | None: return None -@bigpanda_bp.route("/connect", methods=["POST", "OPTIONS"]) +@bigpanda_bp.route("/connect", methods=["POST"]) @require_permission("connectors", "write") def connect(user_id): data = request.get_json(force=True, silent=True) or {} @@ -69,7 +68,7 @@ def connect(user_id): return jsonify({"success": True, "connected": True, "environmentCount": validation.get("environment_count", 0)}) -@bigpanda_bp.route("/status", methods=["GET", "OPTIONS"]) +@bigpanda_bp.route("/status", methods=["GET"]) @require_permission("connectors", "read") def status(user_id): creds = _get_stored_credentials(user_id) @@ -82,7 +81,7 @@ def status(user_id): }) -@bigpanda_bp.route("/disconnect", methods=["POST", "DELETE", "OPTIONS"]) +@bigpanda_bp.route("/disconnect", methods=["POST", "DELETE"]) @require_permission("connectors", "write") def disconnect(user_id): try: @@ -116,11 +115,8 @@ def _verify_webhook_user(user_id: str) -> bool: return False -@bigpanda_bp.route("/webhook/", methods=["POST", "OPTIONS"]) +@bigpanda_bp.route("/webhook/", methods=["POST"]) def webhook(user_id: str): - if request.method == "OPTIONS": - return create_cors_response() - if not user_id: return jsonify({"error": "user_id is required"}), 400 @@ -167,7 +163,7 @@ def webhook(user_id: str): return jsonify({"error": "Failed to process webhook"}), 503 -@bigpanda_bp.route("/webhook-url", methods=["GET", "OPTIONS"]) +@bigpanda_bp.route("/webhook-url", methods=["GET"]) @require_permission("connectors", "read") def get_webhook_url(user_id): ngrok_url = os.getenv("NGROK_URL", "").rstrip("/") diff --git a/server/routes/bitbucket/bitbucket.py b/server/routes/bitbucket/bitbucket.py index 2f884b4cc..c76621746 100644 --- a/server/routes/bitbucket/bitbucket.py +++ b/server/routes/bitbucket/bitbucket.py @@ -9,7 +9,6 @@ from flask import Blueprint, jsonify, render_template, request from utils.auth.stateless_auth import get_credentials_from_db -from utils.web.cors_utils import create_cors_response from utils.auth.rbac_decorators import require_permission from utils.log_sanitizer import sanitize @@ -19,13 +18,10 @@ FRONTEND_URL = os.getenv("FRONTEND_URL") -@bitbucket_bp.route("/login", methods=["POST", "OPTIONS"]) +@bitbucket_bp.route("/login", methods=["POST"]) @require_permission("connectors", "write") def bitbucket_login(user_id): """Handle Bitbucket login - either API token or OAuth initiation.""" - if request.method == "OPTIONS": - return create_cors_response() - try: data = request.get_json() or {} @@ -218,13 +214,10 @@ def bitbucket_callback(): ) -@bitbucket_bp.route("/status", methods=["GET", "OPTIONS"]) +@bitbucket_bp.route("/status", methods=["GET"]) @require_permission("connectors", "read") def bitbucket_status(user_id): """Check Bitbucket connection status for a user.""" - if request.method == "OPTIONS": - return create_cors_response() - try: bb_creds = get_credentials_from_db(user_id, "bitbucket") if not bb_creds or not bb_creds.get("access_token"): @@ -272,13 +265,10 @@ def bitbucket_status(user_id): return jsonify({"connected": False, "error": "Failed to check Bitbucket status"}), 500 -@bitbucket_bp.route("/disconnect", methods=["POST", "OPTIONS"]) +@bitbucket_bp.route("/disconnect", methods=["POST"]) @require_permission("connectors", "write") def bitbucket_disconnect(user_id): """Disconnect Bitbucket account for a user.""" - if request.method == "OPTIONS": - return create_cors_response() - try: from utils.secrets.secret_ref_utils import delete_user_secret diff --git a/server/routes/bitbucket/bitbucket_browsing.py b/server/routes/bitbucket/bitbucket_browsing.py index 1ea268d02..82969d2b5 100644 --- a/server/routes/bitbucket/bitbucket_browsing.py +++ b/server/routes/bitbucket/bitbucket_browsing.py @@ -7,7 +7,6 @@ from flask import Blueprint, request, jsonify from utils.auth.stateless_auth import get_credentials_from_db -from utils.web.cors_utils import create_cors_response from utils.auth.rbac_decorators import require_permission bitbucket_browsing_bp = Blueprint("bitbucket_browsing", __name__) @@ -50,12 +49,10 @@ def _get_bb_client(user_id): ) -@bitbucket_browsing_bp.route("/workspaces", methods=["GET", "OPTIONS"]) +@bitbucket_browsing_bp.route("/workspaces", methods=["GET"]) @require_permission("connectors", "read") def list_workspaces(user_id): """List Bitbucket workspaces for the authenticated user.""" - if request.method == "OPTIONS": - return create_cors_response() try: client = _get_bb_client(user_id) @@ -72,12 +69,10 @@ def list_workspaces(user_id): return jsonify({"error": "Failed to list workspaces"}), 500 -@bitbucket_browsing_bp.route("/projects/", methods=["GET", "OPTIONS"]) +@bitbucket_browsing_bp.route("/projects/", methods=["GET"]) @require_permission("connectors", "read") def list_projects(user_id, workspace): """List projects in a Bitbucket workspace.""" - if request.method == "OPTIONS": - return create_cors_response() try: client = _get_bb_client(user_id) @@ -92,12 +87,10 @@ def list_projects(user_id, workspace): return jsonify({"error": "Failed to list projects"}), 500 -@bitbucket_browsing_bp.route("/repos/", methods=["GET", "OPTIONS"]) +@bitbucket_browsing_bp.route("/repos/", methods=["GET"]) @require_permission("connectors", "read") def list_repos(user_id, workspace): """List repositories in a Bitbucket workspace, optionally filtered by project.""" - if request.method == "OPTIONS": - return create_cors_response() try: client = _get_bb_client(user_id) @@ -121,12 +114,10 @@ def list_repos(user_id, workspace): return jsonify({"error": "Failed to list repositories"}), 500 -@bitbucket_browsing_bp.route("/branches//", methods=["GET", "OPTIONS"]) +@bitbucket_browsing_bp.route("/branches//", methods=["GET"]) @require_permission("connectors", "read") def list_branches(user_id, workspace, repo_slug): """List branches for a Bitbucket repository.""" - if request.method == "OPTIONS": - return create_cors_response() try: client = _get_bb_client(user_id) @@ -141,12 +132,10 @@ def list_branches(user_id, workspace, repo_slug): return jsonify({"error": "Failed to list branches"}), 500 -@bitbucket_browsing_bp.route("/pull-requests//", methods=["GET", "OPTIONS"]) +@bitbucket_browsing_bp.route("/pull-requests//", methods=["GET"]) @require_permission("connectors", "read") def list_pull_requests(user_id, workspace, repo_slug): """List pull requests for a Bitbucket repository.""" - if request.method == "OPTIONS": - return create_cors_response() try: client = _get_bb_client(user_id) @@ -162,12 +151,10 @@ def list_pull_requests(user_id, workspace, repo_slug): return jsonify({"error": "Failed to list pull requests"}), 500 -@bitbucket_browsing_bp.route("/issues//", methods=["GET", "OPTIONS"]) +@bitbucket_browsing_bp.route("/issues//", methods=["GET"]) @require_permission("connectors", "read") def list_issues(user_id, workspace, repo_slug): """List issues for a Bitbucket repository.""" - if request.method == "OPTIONS": - return create_cors_response() try: client = _get_bb_client(user_id) diff --git a/server/routes/bitbucket/bitbucket_selection.py b/server/routes/bitbucket/bitbucket_selection.py index fdf529da8..c584dc1a6 100644 --- a/server/routes/bitbucket/bitbucket_selection.py +++ b/server/routes/bitbucket/bitbucket_selection.py @@ -8,20 +8,16 @@ from utils.auth.stateless_auth import get_credentials_from_db from utils.auth.token_management import store_tokens_in_db -from utils.web.cors_utils import create_cors_response from utils.auth.rbac_decorators import require_permission bitbucket_selection_bp = Blueprint("bitbucket_selection", __name__) logger = logging.getLogger(__name__) -@bitbucket_selection_bp.route("/workspace-selection", methods=["GET", "OPTIONS"]) +@bitbucket_selection_bp.route("/workspace-selection", methods=["GET"]) @require_permission("connectors", "read") def get_workspace_selection(user_id): """Get the stored Bitbucket workspace selection for a user.""" - if request.method == "OPTIONS": - return create_cors_response() - try: selection = get_credentials_from_db(user_id, "bitbucket_workspace_selection") or {} @@ -36,13 +32,10 @@ def get_workspace_selection(user_id): return jsonify({"error": "Failed to get workspace selection"}), 500 -@bitbucket_selection_bp.route("/workspace-selection", methods=["POST", "PUT", "OPTIONS"]) +@bitbucket_selection_bp.route("/workspace-selection", methods=["POST", "PUT"]) @require_permission("connectors", "write") def save_workspace_selection(user_id): """Save the Bitbucket workspace selection for a user.""" - if request.method == "OPTIONS": - return create_cors_response() - try: data = request.get_json() if not data: @@ -80,13 +73,10 @@ def save_workspace_selection(user_id): return jsonify({"error": "Failed to save workspace selection"}), 500 -@bitbucket_selection_bp.route("/workspace-selection", methods=["DELETE", "OPTIONS"]) +@bitbucket_selection_bp.route("/workspace-selection", methods=["DELETE"]) @require_permission("connectors", "write") def clear_workspace_selection(user_id): """Clear the Bitbucket workspace selection for a user.""" - if request.method == "OPTIONS": - return create_cors_response() - try: from utils.secrets.secret_ref_utils import delete_user_secret diff --git a/server/routes/ci_shared.py b/server/routes/ci_shared.py index b9d35817d..443df0256 100644 --- a/server/routes/ci_shared.py +++ b/server/routes/ci_shared.py @@ -6,7 +6,6 @@ from utils.auth.stateless_auth import get_user_preference, store_user_preference from utils.log_sanitizer import sanitize -from utils.web.cors_utils import create_cors_response from utils.auth.rbac_decorators import require_permission logger = logging.getLogger(__name__) @@ -16,13 +15,13 @@ def register_rca_settings_routes(blueprint, provider: str, preference_key: str): """Register GET/PUT /rca-settings routes on the given blueprint.""" label = provider.upper() - @blueprint.route("/rca-settings", methods=["GET", "OPTIONS"]) + @blueprint.route("/rca-settings", methods=["GET"]) @require_permission("connectors", "read") def get_rca_settings(user_id): rca_enabled = get_user_preference(user_id, preference_key, default=True) return jsonify({"rcaEnabled": rca_enabled}) - @blueprint.route("/rca-settings", methods=["PUT", "OPTIONS"]) + @blueprint.route("/rca-settings", methods=["PUT"]) @require_permission("connectors", "write") def update_rca_settings(user_id): try: diff --git a/server/routes/cloudbees/cloudbees_routes.py b/server/routes/cloudbees/cloudbees_routes.py index c7d32a2d3..e9c8275b9 100644 --- a/server/routes/cloudbees/cloudbees_routes.py +++ b/server/routes/cloudbees/cloudbees_routes.py @@ -13,7 +13,6 @@ from connectors.jenkins_connector.api_client import JenkinsClient from utils.db.connection_pool import db_pool -from utils.web.cors_utils import create_cors_response from utils.web.webhook_signature import SIGNATURE_HEADER, verify_webhook_signature from utils.auth.token_management import get_token_data, store_tokens_in_db from utils.auth.rbac_decorators import require_permission @@ -45,7 +44,7 @@ def _build_client(creds: Dict[str, Any]) -> Optional[JenkinsClient]: return JenkinsClient(base_url=base_url, username=username, api_token=api_token) -@cloudbees_bp.route("/connect", methods=["POST", "OPTIONS"]) +@cloudbees_bp.route("/connect", methods=["POST"]) @require_permission("connectors", "write") def connect(user_id): """Validate and store CloudBees CI credentials.""" @@ -113,7 +112,7 @@ def connect(user_id): }) -@cloudbees_bp.route("/status", methods=["GET", "OPTIONS"]) +@cloudbees_bp.route("/status", methods=["GET"]) @require_permission("connectors", "read") def status(user_id): """Check whether CloudBees CI is connected and return summary dashboard data.""" @@ -208,7 +207,7 @@ def status(user_id): }) -@cloudbees_bp.route("/disconnect", methods=["POST", "DELETE", "OPTIONS"]) +@cloudbees_bp.route("/disconnect", methods=["POST", "DELETE"]) @require_permission("connectors", "write") def disconnect(user_id): """Disconnect CloudBees CI by removing stored credentials.""" @@ -256,16 +255,13 @@ def _verify_webhook_user(user_id: str) -> bool: return False -@cloudbees_bp.route("/webhook/", methods=["POST", "OPTIONS"]) +@cloudbees_bp.route("/webhook/", methods=["POST"]) def deployment_webhook(user_id: str): """Receive a deployment event webhook from a CloudBees CI pipeline. Security: validates per-user HMAC-SHA256 signature via X-Aurora-Signature header. Falls back to user verification only when no webhook secret is configured (pre-upgrade). """ - if request.method == "OPTIONS": - return create_cors_response() - if not user_id: return jsonify({"error": "user_id is required"}), 400 @@ -305,7 +301,7 @@ def deployment_webhook(user_id: str): return jsonify({"received": True}) -@cloudbees_bp.route("/webhook-url", methods=["GET", "OPTIONS"]) +@cloudbees_bp.route("/webhook-url", methods=["GET"]) @require_permission("connectors", "read") def get_webhook_url(user_id): """Return the webhook URL and Jenkinsfile snippets for the authenticated user.""" @@ -384,7 +380,7 @@ def sig = mac.doFinal(payload.bytes).encodeHex().toString() }) -@cloudbees_bp.route("/deployments", methods=["GET", "OPTIONS"]) +@cloudbees_bp.route("/deployments", methods=["GET"]) @require_permission("connectors", "read") def list_deployments(user_id): """List recent CloudBees CI deployment events for the authenticated user.""" diff --git a/server/routes/cloudflare/cloudflare_routes.py b/server/routes/cloudflare/cloudflare_routes.py index f64963171..c041320d3 100644 --- a/server/routes/cloudflare/cloudflare_routes.py +++ b/server/routes/cloudflare/cloudflare_routes.py @@ -30,7 +30,7 @@ logger = logging.getLogger(__name__) -@cloudflare_bp.route('/cloudflare/connect', methods=['POST', 'OPTIONS']) +@cloudflare_bp.route('/cloudflare/connect', methods=['POST']) @limiter.limit("10 per minute;50 per hour") @require_permission("connectors", "write") def cloudflare_connect(user_id): @@ -120,7 +120,7 @@ def cloudflare_connect(user_id): return jsonify({"error": "Failed to connect Cloudflare"}), 500 -@cloudflare_bp.route('/cloudflare/zones', methods=['GET', 'OPTIONS']) +@cloudflare_bp.route('/cloudflare/zones', methods=['GET']) @limiter.limit("30 per minute") @require_permission("connectors", "read") def cloudflare_zones_get(user_id): @@ -175,7 +175,7 @@ def cloudflare_zones_get(user_id): return jsonify({"error": "Failed to fetch zones"}), 500 -@cloudflare_bp.route('/cloudflare/zones', methods=['POST', 'OPTIONS']) +@cloudflare_bp.route('/cloudflare/zones', methods=['POST']) @limiter.limit("30 per minute") @require_permission("connectors", "write") def cloudflare_zones_post(user_id): @@ -207,7 +207,7 @@ def cloudflare_zones_post(user_id): return jsonify({"error": "Failed to save zones"}), 500 -@cloudflare_bp.route('/cloudflare/status', methods=['GET', 'OPTIONS']) +@cloudflare_bp.route('/cloudflare/status', methods=['GET']) @limiter.limit("60 per minute") @require_permission("connectors", "read") def cloudflare_status(user_id): @@ -265,7 +265,7 @@ def cloudflare_status(user_id): return jsonify({"connected": has_creds, "provider": "cloudflare"}), 200 -@cloudflare_bp.route('/cloudflare/disconnect', methods=['POST', 'OPTIONS']) +@cloudflare_bp.route('/cloudflare/disconnect', methods=['POST']) @limiter.limit("10 per minute") @require_permission("connectors", "write") def cloudflare_disconnect(user_id): diff --git a/server/routes/command_policies.py b/server/routes/command_policies.py index 6493de2a6..319a2af34 100644 --- a/server/routes/command_policies.py +++ b/server/routes/command_policies.py @@ -43,12 +43,9 @@ def _list_states(org_id: str) -> dict: } -@command_policies_bp.route("/command-policies", methods=["GET", "OPTIONS"]) +@command_policies_bp.route("/command-policies", methods=["GET"]) @require_auth_only def list_policies(user_id): - if request.method == "OPTIONS": - return jsonify({}), 200 - org_id = get_org_id_from_request() if not org_id: return jsonify({"error": "No organization context"}), 403 @@ -134,12 +131,9 @@ def create_policy(user_id): return jsonify({"id": new_id, "status": "created"}), 201 -@command_policies_bp.route("/command-policies/", methods=["PUT", "OPTIONS"]) +@command_policies_bp.route("/command-policies/", methods=["PUT"]) @require_permission("admin", "access") def update_policy(user_id, rule_id): - if request.method == "OPTIONS": - return jsonify({}), 200 - org_id = get_org_id_from_request() if not org_id: return jsonify({"error": "No organization context"}), 403 @@ -212,12 +206,9 @@ def delete_policy(user_id, rule_id): return jsonify({"status": "deleted"}) -@command_policies_bp.route("/command-policies/test", methods=["POST", "OPTIONS"]) +@command_policies_bp.route("/command-policies/test", methods=["POST"]) @require_auth_only def test_command(user_id): - if request.method == "OPTIONS": - return jsonify({}), 200 - org_id = get_org_id_from_request() if not org_id: return jsonify({"error": "No organization context"}), 403 @@ -235,12 +226,9 @@ def test_command(user_id): }) -@command_policies_bp.route("/command-policy-toggle", methods=["PUT", "OPTIONS"]) +@command_policies_bp.route("/command-policy-toggle", methods=["PUT"]) @require_permission("admin", "access") def toggle_list(user_id): - if request.method == "OPTIONS": - return jsonify({}), 200 - org_id = get_org_id_from_request() if not org_id: return jsonify({"error": "No organization context"}), 403 @@ -292,12 +280,9 @@ def toggle_list(user_id): # Template library endpoints # --------------------------------------------------------------------------- -@command_policies_bp.route("/command-policy-templates", methods=["GET", "OPTIONS"]) +@command_policies_bp.route("/command-policy-templates", methods=["GET"]) @require_auth_only def list_templates(user_id): - if request.method == "OPTIONS": - return jsonify({}), 200 - templates = get_policy_templates() result = [] for tpl in templates: @@ -313,12 +298,9 @@ def list_templates(user_id): return jsonify(result) -@command_policies_bp.route("/command-policy-templates/apply", methods=["POST", "OPTIONS"]) +@command_policies_bp.route("/command-policy-templates/apply", methods=["POST"]) @require_permission("admin", "access") def apply_template(user_id): - if request.method == "OPTIONS": - return jsonify({}), 200 - org_id = get_org_id_from_request() if not org_id: return jsonify({"error": "No organization context"}), 403 @@ -366,12 +348,9 @@ def apply_template(user_id): return jsonify({"status": "applied", "template_id": template_id, **_list_states(org_id)}) -@command_policies_bp.route("/command-policy-templates/active", methods=["DELETE", "OPTIONS"]) +@command_policies_bp.route("/command-policy-templates/active", methods=["DELETE"]) @require_permission("admin", "access") def clear_active_template(user_id): - if request.method == "OPTIONS": - return jsonify({}), 200 - org_id = get_org_id_from_request() if not org_id: return jsonify({"error": "No organization context"}), 403 diff --git a/server/routes/confluence/confluence_routes.py b/server/routes/confluence/confluence_routes.py index 5b9f5ed68..dd96c5da2 100644 --- a/server/routes/confluence/confluence_routes.py +++ b/server/routes/confluence/confluence_routes.py @@ -22,7 +22,6 @@ parse_confluence_page_id, ) from connectors.confluence_connector.runbook_parser import parse_confluence_runbook -from utils.web.cors_utils import create_cors_response from utils.auth.oauth2_state_cache import retrieve_oauth2_state, store_oauth2_state from utils.auth.token_management import get_token_data, store_tokens_in_db from utils.auth.rbac_decorators import require_permission @@ -141,13 +140,10 @@ def _fetch_page_payload( return page_payload, creds, None, None -@confluence_bp.route("/connect", methods=["POST", "OPTIONS"]) +@confluence_bp.route("/connect", methods=["POST"]) @require_permission("connectors", "write") def connect(user_id): """Connect Confluence Cloud (OAuth) or Data Center (PAT).""" - if request.method == "OPTIONS": - return create_cors_response() - try: data = request.get_json(force=True, silent=True) or {} except Exception: @@ -272,13 +268,10 @@ def connect(user_id): }) -@confluence_bp.route("/status", methods=["GET", "OPTIONS"]) +@confluence_bp.route("/status", methods=["GET"]) @require_permission("connectors", "read") def status(user_id): """Check Confluence connection status.""" - if request.method == "OPTIONS": - return create_cors_response() - creds = _get_stored_confluence_credentials(user_id) if not creds: return jsonify({"connected": False}) @@ -327,13 +320,10 @@ def status(user_id): }) -@confluence_bp.route("/disconnect", methods=["POST", "DELETE", "OPTIONS"]) +@confluence_bp.route("/disconnect", methods=["POST", "DELETE"]) @require_permission("connectors", "write") def disconnect(user_id): """Disconnect Confluence by removing stored credentials.""" - if request.method == "OPTIONS": - return create_cors_response() - try: success, deleted_count = delete_user_secret(user_id, "confluence") if not success: @@ -347,13 +337,10 @@ def disconnect(user_id): return jsonify({"error": "Failed to disconnect Confluence"}), 500 -@confluence_bp.route("/fetch", methods=["POST", "OPTIONS"]) +@confluence_bp.route("/fetch", methods=["POST"]) @require_permission("connectors", "read") def fetch_page(user_id): """Fetch a Confluence page by URL and return the raw page payload.""" - if request.method == "OPTIONS": - return create_cors_response() - try: data = request.get_json(force=True, silent=True) or {} except Exception: @@ -369,13 +356,10 @@ def fetch_page(user_id): return jsonify(page_payload) -@confluence_bp.route("/parse", methods=["POST", "OPTIONS"]) +@confluence_bp.route("/parse", methods=["POST"]) @require_permission("connectors", "read") def parse_page(user_id): """Fetch a Confluence page and return cleaned runbook content.""" - if request.method == "OPTIONS": - return create_cors_response() - try: data = request.get_json(force=True, silent=True) or {} except Exception: diff --git a/server/routes/connector_status.py b/server/routes/connector_status.py index 7c4ffc709..516e38b4b 100644 --- a/server/routes/connector_status.py +++ b/server/routes/connector_status.py @@ -713,7 +713,7 @@ def _check_netdata(creds: Dict[str, Any]) -> Dict[str, Any]: # ── Route + batch logic ───────────────────────────────────────────── -@connector_status_bp.route("/api/connectors/status", methods=["GET", "OPTIONS"]) +@connector_status_bp.route("/api/connectors/status", methods=["GET"]) @require_permission("connectors", "read") def all_connector_status(user_id): org_id = get_org_id_from_request() or "" diff --git a/server/routes/coroot/coroot_routes.py b/server/routes/coroot/coroot_routes.py index 5136d983b..7cd9f018a 100644 --- a/server/routes/coroot/coroot_routes.py +++ b/server/routes/coroot/coroot_routes.py @@ -13,7 +13,6 @@ from chat.backend.agent.tools.mcp_tools import clear_credentials_cache from utils.auth.token_management import get_token_data, store_tokens_in_db from utils.secrets.secret_ref_utils import delete_user_secret -from utils.web.cors_utils import create_cors_response from utils.auth.rbac_decorators import require_permission logger = logging.getLogger(__name__) @@ -55,7 +54,7 @@ def _build_client_from_creds( # Connection management # ------------------------------------------------------------------ -@coroot_bp.route("/connect", methods=["POST", "OPTIONS"]) +@coroot_bp.route("/connect", methods=["POST"]) @require_permission("connectors", "write") def connect(user_id): try: @@ -112,7 +111,7 @@ def connect(user_id): }) -@coroot_bp.route("/status", methods=["GET", "OPTIONS"]) +@coroot_bp.route("/status", methods=["GET"]) @require_permission("connectors", "read") def status(user_id): creds = _get_stored_coroot_credentials(user_id) @@ -141,7 +140,7 @@ def status(user_id): }) -@coroot_bp.route("/disconnect", methods=["DELETE", "POST", "OPTIONS"]) +@coroot_bp.route("/disconnect", methods=["DELETE", "POST"]) @require_permission("connectors", "write") def disconnect(user_id): try: diff --git a/server/routes/datadog/datadog_routes.py b/server/routes/datadog/datadog_routes.py index b5cd54f1c..9e5f1c50e 100644 --- a/server/routes/datadog/datadog_routes.py +++ b/server/routes/datadog/datadog_routes.py @@ -9,7 +9,6 @@ from routes.datadog.tasks import process_datadog_event from utils.db.connection_pool import db_pool from utils.log_sanitizer import sanitize, hash_for_log -from utils.web.cors_utils import create_cors_response from utils.auth.token_management import get_token_data, store_tokens_in_db from utils.auth.rbac_decorators import require_permission from utils.auth.stateless_auth import get_org_id_from_request, set_rls_context @@ -281,7 +280,7 @@ def _build_client_from_creds(creds: Dict[str, Any]) -> Optional[DatadogClient]: return DatadogClient(api_key=api_key, app_key=app_key, site=site) -@datadog_bp.route("/connect", methods=["POST", "OPTIONS"]) +@datadog_bp.route("/connect", methods=["POST"]) @require_permission("connectors", "write") def connect(user_id): try: @@ -349,7 +348,7 @@ def connect(user_id): return jsonify(response) -@datadog_bp.route("/status", methods=["GET", "OPTIONS"]) +@datadog_bp.route("/status", methods=["GET"]) @require_permission("connectors", "read") def status(user_id): creds = _get_stored_datadog_credentials(user_id) @@ -381,7 +380,7 @@ def status(user_id): }) -@datadog_bp.route("/disconnect", methods=["DELETE", "POST", "OPTIONS"]) +@datadog_bp.route("/disconnect", methods=["DELETE", "POST"]) @require_permission("connectors", "write") def disconnect(user_id): try: @@ -411,7 +410,7 @@ def disconnect(user_id): return jsonify({"error": "Failed to disconnect Datadog"}), 500 -@datadog_bp.route("/logs/search", methods=["POST", "OPTIONS"]) +@datadog_bp.route("/logs/search", methods=["POST"]) @require_permission("connectors", "read") def search_logs(user_id): creds = _get_stored_datadog_credentials(user_id) @@ -441,7 +440,7 @@ def search_logs(user_id): return jsonify({"error": "Failed to search Datadog logs"}), 502 -@datadog_bp.route("/metrics/query", methods=["POST", "OPTIONS"]) +@datadog_bp.route("/metrics/query", methods=["POST"]) @require_permission("connectors", "read") def query_metrics(user_id): creds = _get_stored_datadog_credentials(user_id) @@ -474,7 +473,7 @@ def query_metrics(user_id): return jsonify({"error": "Failed to query Datadog metrics"}), 502 -@datadog_bp.route("/events", methods=["GET", "OPTIONS"]) +@datadog_bp.route("/events", methods=["GET"]) @require_permission("connectors", "read") def list_events(user_id): creds = _get_stored_datadog_credentials(user_id) @@ -509,7 +508,7 @@ def list_events(user_id): return jsonify({"error": "Failed to list Datadog events"}), 502 -@datadog_bp.route("/monitors", methods=["GET", "OPTIONS"]) +@datadog_bp.route("/monitors", methods=["GET"]) @require_permission("connectors", "read") def list_monitors(user_id): creds = _get_stored_datadog_credentials(user_id) @@ -538,7 +537,7 @@ def list_monitors(user_id): return jsonify({"error": "Failed to list Datadog monitors"}), 502 -@datadog_bp.route("/events/ingested", methods=["GET", "OPTIONS"]) +@datadog_bp.route("/events/ingested", methods=["GET"]) @require_permission("connectors", "read") def list_ingested_events(user_id): org_id = get_org_id_from_request() @@ -607,11 +606,8 @@ def list_ingested_events(user_id): return jsonify({"error": "Failed to load Datadog webhook events"}), 500 -@datadog_bp.route("/webhook/", methods=["POST", "OPTIONS"]) +@datadog_bp.route("/webhook/", methods=["POST"]) def webhook(user_id: str): - if request.method == "OPTIONS": - return create_cors_response() - if not user_id: logger.warning("[DATADOG] Webhook received without user_id") return jsonify({"error": "user_id is required"}), 400 @@ -636,7 +632,7 @@ def webhook(user_id: str): return jsonify({"received": True}) -@datadog_bp.route("/webhook-url", methods=["GET", "OPTIONS"]) +@datadog_bp.route("/webhook-url", methods=["GET"]) @require_permission("connectors", "read") def webhook_url(user_id): # Use ngrok URL for development if available, otherwise use backend URL diff --git a/server/routes/debug/debug_routes.py b/server/routes/debug/debug_routes.py index 4a609def3..87189ae6c 100644 --- a/server/routes/debug/debug_routes.py +++ b/server/routes/debug/debug_routes.py @@ -1,19 +1,15 @@ import logging from datetime import datetime, timezone from flask import Blueprint, request, jsonify, session, Response -import flask -from utils.web.cors_utils import create_cors_response from utils.auth.rbac_decorators import require_auth_only from utils.auth.token_management import get_token_data debug_util_bp = Blueprint("debug_util_bp", __name__) -@debug_util_bp.route("/debug/user-info", methods=["GET", "OPTIONS"]) +@debug_util_bp.route("/debug/user-info", methods=["GET"]) @require_auth_only def debug_user_info(user_id): - if request.method == "OPTIONS": - return create_cors_response() try: debug_info = { "time": datetime.now(timezone.utc).isoformat(), @@ -30,10 +26,8 @@ def debug_user_info(user_id): return jsonify({"error": "Failed to retrieve debug info"}), 500 -@debug_util_bp.route("/test-endpoint", methods=["GET", "OPTIONS"]) +@debug_util_bp.route("/test-endpoint", methods=["GET"]) def test_endpoint(): - if flask.request.method == 'OPTIONS': - return create_cors_response() return jsonify({ "message": "Test endpoint working", "method": request.method, diff --git a/server/routes/dynatrace/dynatrace_routes.py b/server/routes/dynatrace/dynatrace_routes.py index e61630577..9f95cd24d 100644 --- a/server/routes/dynatrace/dynatrace_routes.py +++ b/server/routes/dynatrace/dynatrace_routes.py @@ -11,7 +11,6 @@ from routes.dynatrace.tasks import process_dynatrace_problem from utils.db.connection_pool import db_pool from utils.log_sanitizer import sanitize -from utils.web.cors_utils import create_cors_response from utils.auth.stateless_auth import ( get_org_id_from_request, get_user_preference, @@ -99,7 +98,7 @@ def _get_stored_credentials(user_id: str) -> dict[str, Any] | None: return None -@dynatrace_bp.route("/connect", methods=["POST", "OPTIONS"]) +@dynatrace_bp.route("/connect", methods=["POST"]) @require_permission("connectors", "write") def connect(user_id): data = request.get_json(force=True, silent=True) or {} @@ -133,7 +132,7 @@ def connect(user_id): return jsonify({"success": True, "environmentUrl": environment_url, "version": version}) -@dynatrace_bp.route("/status", methods=["GET", "OPTIONS"]) +@dynatrace_bp.route("/status", methods=["GET"]) @require_permission("connectors", "read") def status(user_id): creds = _get_stored_credentials(user_id) @@ -147,7 +146,7 @@ def status(user_id): }) -@dynatrace_bp.route("/disconnect", methods=["POST", "DELETE", "OPTIONS"]) +@dynatrace_bp.route("/disconnect", methods=["POST", "DELETE"]) @require_permission("connectors", "write") def disconnect(user_id): try: @@ -163,11 +162,8 @@ def disconnect(user_id): return jsonify({"error": "Failed to disconnect Dynatrace"}), 500 -@dynatrace_bp.route("/webhook/", methods=["POST", "OPTIONS"]) +@dynatrace_bp.route("/webhook/", methods=["POST"]) def webhook(user_id: str): - if request.method == "OPTIONS": - return create_cors_response() - creds = get_token_data(user_id, "dynatrace") if not creds: logger.warning("[DYNATRACE] Webhook received for user %s with no connection", sanitize(user_id)) @@ -186,7 +182,7 @@ def webhook(user_id: str): return jsonify({"received": True}) -@dynatrace_bp.route("/alerts", methods=["GET", "OPTIONS"]) +@dynatrace_bp.route("/alerts", methods=["GET"]) @require_permission("connectors", "read") def get_alerts(user_id): org_id = get_org_id_from_request() @@ -235,7 +231,7 @@ def get_alerts(user_id): return jsonify({"error": "Failed to fetch alerts"}), 500 -@dynatrace_bp.route("/webhook-url", methods=["GET", "OPTIONS"]) +@dynatrace_bp.route("/webhook-url", methods=["GET"]) @require_permission("connectors", "read") def get_webhook_url(user_id): ngrok_url = os.getenv("NGROK_URL", "").rstrip("/") @@ -266,13 +262,13 @@ def get_webhook_url(user_id): }) -@dynatrace_bp.route("/rca-settings", methods=["GET", "OPTIONS"]) +@dynatrace_bp.route("/rca-settings", methods=["GET"]) @require_permission("connectors", "read") def get_rca_settings(user_id): return jsonify({"rcaEnabled": get_user_preference(user_id, "dynatrace_rca_enabled", default=False)}) -@dynatrace_bp.route("/rca-settings", methods=["PUT", "OPTIONS"]) +@dynatrace_bp.route("/rca-settings", methods=["PUT"]) @require_permission("connectors", "write") def update_rca_settings(user_id): data = request.get_json(force=True, silent=True) or {} diff --git a/server/routes/gcp/auth.py b/server/routes/gcp/auth.py index 9bae12140..41bfbbc03 100644 --- a/server/routes/gcp/auth.py +++ b/server/routes/gcp/auth.py @@ -226,7 +226,7 @@ def force_disconnect_gcp(user_id): return jsonify({"success": True, "message": "GCP disconnected successfully"}), 200 -@gcp_auth_bp.route("/api/gcp/service-account/connect", methods=["POST", "OPTIONS"]) +@gcp_auth_bp.route("/api/gcp/service-account/connect", methods=["POST"]) @require_permission("connectors", "write") def connect_service_account(user_id): """Connect a GCP service account by uploading its JSON key. diff --git a/server/routes/gcp/projects.py b/server/routes/gcp/projects.py index 784e5535a..78c7879dd 100644 --- a/server/routes/gcp/projects.py +++ b/server/routes/gcp/projects.py @@ -1,12 +1,12 @@ """GCP project management routes.""" import logging from flask import Blueprint, request, jsonify -from utils.web.cors_utils import create_cors_response from utils.auth.stateless_auth import get_user_preference from utils.auth.rbac_decorators import require_permission from utils.auth.token_refresh import refresh_token_if_needed from connectors.gcp_connector.auth.oauth import get_credentials from utils.auth.token_management import get_token_data +from utils.log_sanitizer import sanitize from connectors.gcp_connector.gcp.projects import get_project_list from connectors.gcp_connector.auth.service_accounts import ( get_aurora_service_account_email, @@ -19,13 +19,10 @@ gcp_projects_bp = Blueprint("gcp_projects", __name__) -@gcp_projects_bp.route("/api/gcp/projects", methods=["POST", "OPTIONS"]) +@gcp_projects_bp.route("/api/gcp/projects", methods=["POST"]) @require_permission("connectors", "read") def get_projects(user_id): """Get all GCP projects with billing status for the authenticated user.""" - if request.method == "OPTIONS": - return create_cors_response() - try: logging.info("Fetching GCP projects with billing status") provider = "gcp" @@ -100,136 +97,159 @@ def get_projects(user_id): return jsonify({"error": "Failed to fetch GCP projects"}), 500 -@gcp_projects_bp.route("/api/gcp/sa-project-access", methods=["GET", "POST", "OPTIONS"]) -@require_permission("connectors", "write") -def sa_project_access(user_id): - """GET -> list projects with SA access flag. - POST -> update SA access based on payload {projects:[{projectId, enabled}]} - """ - if request.method == "OPTIONS": - return create_cors_response() +def _load_gcp_token(user_id): + """Fetch GCP token data; return (token_data, None) or (None, error_response).""" + token_data = get_token_data(user_id, "gcp") + if not token_data: + logging.warning("No token data found for user_id: %s, provider: gcp", sanitize(user_id)) + return None, (jsonify({"error": "No GCP credentials found. Please authenticate with GCP."}), 401) + return token_data, None + +def _refresh_and_reload_gcp_token(user_id): + """Refresh GCP token if needed, then re-fetch token_data; return (token_data, None) or (None, error_response).""" try: - provider = "gcp" - token_data = get_token_data(user_id, provider) - if not token_data: - logging.warning(f"No token data found for user_id: {user_id}, provider: {provider}") - return jsonify({"error": "No GCP credentials found. Please authenticate with GCP."}), 401 + refresh_token_if_needed(user_id, "gcp") + except Exception as e: + logging.error(f"Token refresh failed: {e}", exc_info=True) + return None, (jsonify({"error": "Token refresh failed"}), 401) + return get_token_data(user_id, "gcp"), None + + +def _sa_mode_project_list(token_data, root_project): + """Build project list for service-account mode (no IAM enumeration).""" + accessible = token_data.get("accessible_projects") or [] + result = [] + for proj in accessible: + pid = proj.get("project_id") + if not pid: + continue + result.append({ + "projectId": pid, + "name": proj.get("name") or pid, + "enabled": True, + "hasPermission": True, + "isRootProject": pid == root_project, + }) + result.sort(key=lambda x: x['name']) + return result + + +def _check_project_iam(crm_service, pid, name, member_sa, root_project): + """Check IAM access for one project; return project info dict.""" + has_permission = True + enabled = False + try: + policy = crm_service.projects().getIamPolicy(resource=pid, body={}).execute() + for binding in policy.get('bindings', []): + if member_sa in binding.get('members', []): + enabled = True + break + except Exception as e: + logging.warning(f"Cannot read IAM policy for project {pid}: {e}") + has_permission = False + enabled = False + + return { + "projectId": pid, + "name": name, + "enabled": enabled, + "hasPermission": has_permission, + "isRootProject": pid == root_project, + } + + +def _oauth_mode_project_list(credentials, sa_email, root_project): + """Build project list for OAuth mode by enumerating projects + IAM.""" + projects = get_project_list(credentials) + crm_service = build('cloudresourcemanager', 'v1', credentials=credentials) + member_sa = f"serviceAccount:{sa_email}" + + result = [] + for proj in projects: + pid = proj.get('projectId') + if not pid: + continue + result.append(_check_project_iam( + crm_service, pid, proj.get('name', pid), member_sa, root_project, + )) + result.sort(key=lambda x: x['name']) + return result + + +@gcp_projects_bp.route("/api/gcp/sa-project-access", methods=["GET"]) +@require_permission("connectors", "read") +def sa_project_access_get(user_id): + """List projects with SA access flag.""" + try: + token_data, err = _load_gcp_token(user_id) + if err: + return err - # Service-account mode: Aurora never created a per-user SA to manage, - # so there are no IAM bindings to toggle. The uploaded SA already has - # whatever roles the user granted it directly in GCP. GET surfaces - # the auto-discovered accessible_projects list with all entries - # marked enabled; POST is a no-op (selection is an OAuth-only - # concept). - if get_gcp_auth_type(token_data) == GCP_AUTH_TYPE_SA: - if request.method == "GET": - accessible = token_data.get("accessible_projects") or [] - root_project = get_user_preference(user_id, 'gcp_root_project') - result = [] - for proj in accessible: - pid = proj.get("project_id") - if not pid: - continue - result.append({ - "projectId": pid, - "name": proj.get("name") or pid, - "enabled": True, - "hasPermission": True, - "isRootProject": pid == root_project, - }) - result.sort(key=lambda x: x['name']) - return jsonify({"projects": result, "root_project": root_project}), 200 - # POST: nothing to persist — Aurora does not manage IAM in SA mode. - return jsonify({"success": True}), 200 + root_project = get_user_preference(user_id, 'gcp_root_project') - if request.method == "GET": - try: - refresh_token_if_needed(user_id, provider) - except Exception as e: - return jsonify({"error": "Token refresh failed"}), 401 - - credentials = get_credentials(token_data) - - # Determine SA email (root project logic inside helper) - sa_email = get_aurora_service_account_email(user_id) - - # Fetch all projects - projects = get_project_list(credentials) - - crm_service = build('cloudresourcemanager', 'v1', credentials=credentials) - member_sa = f"serviceAccount:{sa_email}" - - result = [] - for proj in projects: - pid = proj.get('projectId') - if not pid: - continue - name = proj.get('name', pid) - - # Try to get IAM policy, but handle permission errors gracefully - has_permission = True - enabled = False - try: - policy = crm_service.projects().getIamPolicy(resource=pid, body={}).execute() - sa_roles = [] - for binding in policy.get('bindings', []): - if member_sa in binding.get('members', []): - sa_roles.append(binding.get('role')) - enabled = len(sa_roles) > 0 - except Exception as e: - # If we can't read IAM policy (403, etc), mark as no permission - logging.warning(f"Cannot read IAM policy for project {pid}: {e}") - has_permission = False - enabled = False - - result.append({ - "projectId": pid, - "name": name, - "enabled": enabled, - "hasPermission": has_permission, - }) - - # Get current root project preference - root_project = get_user_preference(user_id, 'gcp_root_project') - - # Mark which project is the root project - for project in result: - project['isRootProject'] = project['projectId'] == root_project - - # sort alphabetical - result.sort(key=lambda x: x['name']) + # SA mode: surface auto-discovered accessible_projects (no IAM enumeration). + # Aurora doesn't manage IAM bindings in SA mode — the uploaded SA already + # has whatever roles the user granted it directly in GCP. + if get_gcp_auth_type(token_data) == GCP_AUTH_TYPE_SA: + result = _sa_mode_project_list(token_data, root_project) return jsonify({"projects": result, "root_project": root_project}), 200 - elif request.method == "POST": - data = request.get_json() - projects = data.get("projects") # list of {projectId, enabled} - if projects is None: - return jsonify({"error": "projects required"}), 400 + token_data, err = _refresh_and_reload_gcp_token(user_id) + if err: + return err - selections = {} - for p in projects: - pid = p.get('projectId') or p.get('id') - enabled = bool(p.get('enabled')) - if pid: - selections[pid] = enabled + credentials = get_credentials(token_data) + sa_email = get_aurora_service_account_email(user_id) + result = _oauth_mode_project_list(credentials, sa_email, root_project) + return jsonify({"projects": result, "root_project": root_project}), 200 - try: - refresh_token_if_needed(user_id, provider) - except Exception as e: - return jsonify({"error": "Token refresh failed"}), 401 - credentials = get_credentials(token_data) - sa_email = get_aurora_service_account_email(user_id) + except ValueError as e: + logging.warning(f"Validation error in sa_project_access_get: {e}") + return jsonify({"error": "Invalid request parameters"}), 400 + except Exception as e: + logging.error(f"Error in sa_project_access_get: {e}", exc_info=True) + return jsonify({"error": "Failed to process service account project access"}), 500 - update_service_account_project_access(credentials, sa_email, selections) +@gcp_projects_bp.route("/api/gcp/sa-project-access", methods=["POST"]) +@require_permission("connectors", "write") +def sa_project_access_post(user_id): + """Update SA access based on payload {projects:[{projectId, enabled}]}.""" + try: + token_data, err = _load_gcp_token(user_id) + if err: + return err + + # SA mode: nothing to persist — Aurora does not manage IAM bindings. + if get_gcp_auth_type(token_data) == GCP_AUTH_TYPE_SA: return jsonify({"success": True}), 200 - return jsonify({"error": "Method not allowed"}), 405 + data = request.get_json() or {} + projects = data.get("projects") + if projects is None: + return jsonify({"error": "projects required"}), 400 + + selections = {} + for p in projects: + pid = p.get('projectId') or p.get('id') + enabled = bool(p.get('enabled')) + if pid: + selections[pid] = enabled + + token_data, err = _refresh_and_reload_gcp_token(user_id) + if err: + return err + + credentials = get_credentials(token_data) + sa_email = get_aurora_service_account_email(user_id) + update_service_account_project_access(credentials, sa_email, selections) + + return jsonify({"success": True}), 200 except ValueError as e: - logging.warning(f"Validation error in sa_project_access: {e}") + logging.warning(f"Validation error in sa_project_access_post: {e}") return jsonify({"error": "Invalid request parameters"}), 400 except Exception as e: - logging.error(f"Error in sa_project_access: {e}", exc_info=True) + logging.error(f"Error in sa_project_access_post: {e}", exc_info=True) return jsonify({"error": "Failed to process service account project access"}), 500 diff --git a/server/routes/gcp/root_project.py b/server/routes/gcp/root_project.py index 69dc0f0d8..23ed1fc18 100644 --- a/server/routes/gcp/root_project.py +++ b/server/routes/gcp/root_project.py @@ -20,7 +20,7 @@ root_project_bp = Blueprint('root_project', __name__) -@root_project_bp.route('/api/gcp/root-project', methods=['GET', 'OPTIONS']) +@root_project_bp.route('/api/gcp/root-project', methods=['GET']) @require_permission("connectors", "read") def get_root_project(user_id): """Get the currently selected root project for the user.""" diff --git a/server/routes/github/github.py b/server/routes/github/github.py index 4a87fb065..83277add3 100644 --- a/server/routes/github/github.py +++ b/server/routes/github/github.py @@ -3,7 +3,6 @@ import flask from flask import Blueprint, request, jsonify, Response import os -from utils.web.cors_utils import create_cors_response from utils.auth.rbac_decorators import require_permission github_bp = Blueprint("github", __name__) @@ -11,13 +10,10 @@ FRONTEND_URL = os.getenv("FRONTEND_URL") GITHUB_TIMEOUT = 20 -@github_bp.route("/login", methods=["POST", "OPTIONS"]) +@github_bp.route("/login", methods=["POST"]) @require_permission("connectors", "write") def github_login(user_id): """Handle GitHub OAuth login initiation and manual token storage""" - if request.method == 'OPTIONS': - return create_cors_response() - try: data = request.get_json() @@ -107,13 +103,10 @@ def github_login(user_id): logging.error(f"Error in GitHub login: {e}", exc_info=True) return jsonify({"error": "Failed to process GitHub login"}), 500 -@github_bp.route("/status", methods=["GET", "OPTIONS"]) +@github_bp.route("/status", methods=["GET"]) @require_permission("connectors", "read") def github_status(user_id): """Check GitHub connection status for a user""" - if request.method == 'OPTIONS': - return create_cors_response() - try: # Check if user has GitHub credentials stored @@ -132,15 +125,11 @@ def github_status(user_id): logging.error(f"Error checking GitHub status: {e}", exc_info=True) return jsonify({"connected": False, "error": "Failed to check GitHub status"}), 500 -@github_bp.route("/disconnect", methods=["POST", "OPTIONS"]) +@github_bp.route("/disconnect", methods=["POST"]) @require_permission("connectors", "write") def github_disconnect(user_id): """Disconnect GitHub account for a user""" - if request.method == 'OPTIONS': - return create_cors_response() - try: - # Remove GitHub credentials from database and Vault from utils.secrets.secret_ref_utils import delete_user_secret @@ -278,14 +267,10 @@ def github_callback(): error="An unexpected error occurred during GitHub authentication", frontend_url=FRONTEND_URL) -@github_bp.route("/repos", methods=["GET", "OPTIONS"]) +@github_bp.route("/repos", methods=["GET"]) @require_permission("connectors", "read") def get_github_repos(user_id): """Fetch repositories for an authenticated GitHub user""" - # Handle preflight OPTIONS request - if request.method == 'OPTIONS': - return create_cors_response() - try: # Get the GitHub token from Authorization header auth_header = request.headers.get("Authorization", "") @@ -385,13 +370,10 @@ def get_github_repos(user_id): logging.error(f"Error fetching GitHub repos: {e}", exc_info=True) return jsonify({"error": "Failed to fetch GitHub repositories"}), 500 -@github_bp.route("/token-info", methods=["GET", "OPTIONS"]) +@github_bp.route("/token-info", methods=["GET"]) @require_permission("connectors", "read") def github_token_info(user_id): """Debug endpoint to check token information""" - if request.method == 'OPTIONS': - return create_cors_response() - try: # Get the GitHub token from Authorization header auth_header = request.headers.get("Authorization", "") @@ -447,13 +429,10 @@ def github_token_info(user_id): logging.error(f"Error in token info endpoint: {e}", exc_info=True) return jsonify({"error": "Failed to retrieve token info"}), 500 -@github_bp.route("/download-repo", methods=["POST", "OPTIONS"]) +@github_bp.route("/download-repo", methods=["POST"]) @require_permission("connectors", "read") def download_github_repo(user_id): """Download a GitHub repository as a zip file and return it""" - if request.method == 'OPTIONS': - return create_cors_response() - try: # Get request data data = request.get_json() diff --git a/server/routes/github/github_user_repos.py b/server/routes/github/github_user_repos.py index da6528eeb..6b8ff40f5 100644 --- a/server/routes/github/github_user_repos.py +++ b/server/routes/github/github_user_repos.py @@ -30,13 +30,10 @@ def create_cors_response(data=None, status=200): response.headers['Access-Control-Allow-Credentials'] = 'true' return response -@github_user_repos_bp.route("/user-repos", methods=["GET", "OPTIONS"]) +@github_user_repos_bp.route("/user-repos", methods=["GET"]) @require_permission("connectors", "read") def get_user_repos(user_id): """Fetch repositories for a user using their stored GitHub credentials""" - if request.method == 'OPTIONS': - return create_cors_response() - t0 = time.time() try: # Get stored GitHub credentials for this user @@ -115,13 +112,10 @@ def get_user_repos(user_id): logger.error(f"Error fetching user repositories: {e}", exc_info=True) return create_cors_response({"error": "Failed to fetch repositories", "repos": []}, 500) -@github_user_repos_bp.route("/user-branches/", methods=["GET", "OPTIONS"]) +@github_user_repos_bp.route("/user-branches/", methods=["GET"]) @require_permission("connectors", "read") def get_user_branches(user_id, repo_full_name): """Fetch branches for a repository using stored GitHub credentials""" - if request.method == 'OPTIONS': - return create_cors_response() - _REPO_NAME_RE = re.compile(r'[a-zA-Z0-9][a-zA-Z0-9._-]{0,99}/[a-zA-Z0-9][a-zA-Z0-9._-]{0,99}') if not _REPO_NAME_RE.fullmatch(repo_full_name): return create_cors_response({"error": "Invalid repository name format", "branches": []}, 400) diff --git a/server/routes/grafana/grafana_routes.py b/server/routes/grafana/grafana_routes.py index 618ccc2ac..52cc5c187 100644 --- a/server/routes/grafana/grafana_routes.py +++ b/server/routes/grafana/grafana_routes.py @@ -7,7 +7,6 @@ from routes.grafana.tasks import process_grafana_alert from utils.db.connection_pool import db_pool from utils.log_sanitizer import sanitize -from utils.web.cors_utils import create_cors_response from utils.auth.token_management import store_tokens_in_db from utils.auth.rbac_decorators import require_permission from utils.auth.stateless_auth import get_org_id_from_request, validate_user_exists, set_rls_context @@ -58,7 +57,7 @@ def _set_grafana_active(user_id: str, active: bool) -> bool: return False -@grafana_bp.route("/status", methods=["GET", "OPTIONS"]) +@grafana_bp.route("/status", methods=["GET"]) @require_permission("connectors", "read") def status(user_id): row_exists, is_active = _has_grafana_row(user_id) @@ -69,7 +68,7 @@ def status(user_id): return jsonify({"connected": True}) -@grafana_bp.route("/disconnect", methods=["POST", "DELETE", "OPTIONS"]) +@grafana_bp.route("/disconnect", methods=["POST", "DELETE"]) @require_permission("connectors", "write") def disconnect(user_id): """Disconnect Grafana by deactivating the stored connection.""" @@ -91,16 +90,13 @@ def disconnect(user_id): return jsonify({"error": "Failed to disconnect Grafana"}), 500 -@grafana_bp.route("/alerts/webhook/", methods=["POST", "OPTIONS"]) +@grafana_bp.route("/alerts/webhook/", methods=["POST"]) def alert_webhook(user_id: str): """Receive alert webhook from Grafana for a specific user. Auto-creates or re-activates a connection record when needed. Always stores the alert; skips RCA for connection webhooks. """ - if request.method == "OPTIONS": - return create_cors_response() - if not user_id: logger.warning("[GRAFANA] Webhook received without user_id") return jsonify({"error": "user_id is required"}), 400 @@ -141,7 +137,7 @@ def alert_webhook(user_id: str): return jsonify({"received": True}) -@grafana_bp.route("/alerts", methods=["GET", "OPTIONS"]) +@grafana_bp.route("/alerts", methods=["GET"]) @require_permission("connectors", "read") def get_alerts(user_id): """Fetch Grafana alerts for the authenticated user.""" @@ -221,7 +217,7 @@ def get_alerts(user_id): return jsonify({"error": "Failed to fetch alerts"}), 500 -@grafana_bp.route("/alerts/webhook-url", methods=["GET", "OPTIONS"]) +@grafana_bp.route("/alerts/webhook-url", methods=["GET"]) @require_permission("connectors", "read") def get_webhook_url(user_id): """Get the webhook URL that should be configured in Grafana.""" diff --git a/server/routes/jenkins/jenkins_routes.py b/server/routes/jenkins/jenkins_routes.py index 622a24be3..37c7f6370 100644 --- a/server/routes/jenkins/jenkins_routes.py +++ b/server/routes/jenkins/jenkins_routes.py @@ -8,7 +8,6 @@ from connectors.jenkins_connector.api_client import JenkinsClient from utils.db.connection_pool import db_pool from utils.log_sanitizer import sanitize -from utils.web.cors_utils import create_cors_response from utils.web.webhook_signature import SIGNATURE_HEADER, verify_webhook_signature from utils.auth.token_management import get_token_data, store_tokens_in_db from utils.auth.rbac_decorators import require_permission @@ -39,7 +38,7 @@ def _build_client(creds: Dict[str, Any]) -> Optional[JenkinsClient]: return JenkinsClient(base_url=base_url, username=username, api_token=api_token) -@jenkins_bp.route("/connect", methods=["POST", "OPTIONS"]) +@jenkins_bp.route("/connect", methods=["POST"]) @require_permission("connectors", "write") def connect(user_id): """Validate and store Jenkins credentials.""" @@ -107,7 +106,7 @@ def connect(user_id): }) -@jenkins_bp.route("/status", methods=["GET", "OPTIONS"]) +@jenkins_bp.route("/status", methods=["GET"]) @require_permission("connectors", "read") def status(user_id): """Check whether Jenkins is connected and return summary dashboard data.""" @@ -203,7 +202,7 @@ def status(user_id): }) -@jenkins_bp.route("/disconnect", methods=["POST", "DELETE", "OPTIONS"]) +@jenkins_bp.route("/disconnect", methods=["POST", "DELETE"]) @require_permission("connectors", "write") def disconnect(user_id): """Disconnect Jenkins by removing stored credentials.""" @@ -250,16 +249,13 @@ def _verify_webhook_user(user_id: str) -> bool: return False -@jenkins_bp.route("/webhook/", methods=["POST", "OPTIONS"]) +@jenkins_bp.route("/webhook/", methods=["POST"]) def deployment_webhook(user_id: str): """Receive a deployment event webhook from a Jenkins pipeline. - + Security: validates per-user HMAC-SHA256 signature via X-Aurora-Signature header. Falls back to user verification only when no webhook secret is configured (pre-upgrade). """ - if request.method == "OPTIONS": - return create_cors_response() - if not user_id: return jsonify({"error": "user_id is required"}), 400 @@ -300,7 +296,7 @@ def deployment_webhook(user_id: str): return jsonify({"received": True}) -@jenkins_bp.route("/webhook-url", methods=["GET", "OPTIONS"]) +@jenkins_bp.route("/webhook-url", methods=["GET"]) @require_permission("connectors", "read") def get_webhook_url(user_id): """Return the webhook URL and Jenkinsfile snippets for the authenticated user.""" @@ -381,7 +377,7 @@ def sig = mac.doFinal(payload.bytes).encodeHex().toString() }) -@jenkins_bp.route("/deployments", methods=["GET", "OPTIONS"]) +@jenkins_bp.route("/deployments", methods=["GET"]) @require_permission("connectors", "read") def list_deployments(user_id): """List recent Jenkins deployment events for the authenticated user.""" diff --git a/server/routes/jira/jira_routes.py b/server/routes/jira/jira_routes.py index 053a3da6c..6bc5465f3 100644 --- a/server/routes/jira/jira_routes.py +++ b/server/routes/jira/jira_routes.py @@ -102,7 +102,7 @@ def _refresh_jira_credentials(user_id: str, creds: Dict[str, Any]) -> Optional[D # POST /jira/search # ------------------------------------------------------------------ -@jira_bp.route("/search", methods=["POST", "OPTIONS"]) +@jira_bp.route("/search", methods=["POST"]) @require_permission("connectors", "read") def search(user_id): client, creds, error = _get_jira_client(user_id) @@ -128,7 +128,7 @@ def search(user_id): # GET /jira/issue/ # ------------------------------------------------------------------ -@jira_bp.route("/issue/", methods=["GET", "OPTIONS"]) +@jira_bp.route("/issue/", methods=["GET"]) @require_permission("connectors", "read") def get_issue(user_id, issue_key): client, creds, error = _get_jira_client(user_id) @@ -147,7 +147,7 @@ def get_issue(user_id, issue_key): # POST /jira/issue (create) # ------------------------------------------------------------------ -@jira_bp.route("/issue", methods=["POST", "OPTIONS"]) +@jira_bp.route("/issue", methods=["POST"]) @require_permission("connectors", "write") def create_issue(user_id): client, creds, error = _get_jira_client(user_id) @@ -185,7 +185,7 @@ def create_issue(user_id): # PATCH /jira/issue/ (update) # ------------------------------------------------------------------ -@jira_bp.route("/issue/", methods=["PATCH", "OPTIONS"]) +@jira_bp.route("/issue/", methods=["PATCH"]) @require_permission("connectors", "write") def update_issue(user_id, issue_key): client, creds, error = _get_jira_client(user_id) @@ -209,7 +209,7 @@ def update_issue(user_id, issue_key): # POST /jira/issue//comment # ------------------------------------------------------------------ -@jira_bp.route("/issue//comment", methods=["POST", "OPTIONS"]) +@jira_bp.route("/issue//comment", methods=["POST"]) @require_permission("connectors", "write") def add_comment(user_id, issue_key): client, creds, error = _get_jira_client(user_id) @@ -235,7 +235,7 @@ def add_comment(user_id, issue_key): # POST /jira/issue/link # ------------------------------------------------------------------ -@jira_bp.route("/issue/link", methods=["POST", "OPTIONS"]) +@jira_bp.route("/issue/link", methods=["POST"]) @require_permission("connectors", "write") def link_issues(user_id): client, creds, error = _get_jira_client(user_id) @@ -266,7 +266,7 @@ def link_issues(user_id): VALID_MODES = ("full", "comment_only") -@jira_bp.route("/settings", methods=["GET", "OPTIONS"]) +@jira_bp.route("/settings", methods=["GET"]) @require_permission("connectors", "read") def get_settings(user_id): mode = get_user_preference(user_id, JIRA_MODE_KEY, default="comment_only") @@ -277,7 +277,7 @@ def get_settings(user_id): # PUT /jira/settings # ------------------------------------------------------------------ -@jira_bp.route("/settings", methods=["PUT", "OPTIONS"]) +@jira_bp.route("/settings", methods=["PUT"]) @require_permission("connectors", "write") def update_settings(user_id): data = request.get_json(force=True, silent=True) or {} @@ -296,7 +296,7 @@ def update_settings(user_id): # GET /jira/status # ------------------------------------------------------------------ -@jira_bp.route("/status", methods=["GET", "OPTIONS"]) +@jira_bp.route("/status", methods=["GET"]) @require_permission("connectors", "read") def jira_status(user_id): """Check Jira connection status.""" @@ -341,7 +341,7 @@ def jira_status(user_id): # POST|DELETE /jira/disconnect # ------------------------------------------------------------------ -@jira_bp.route("/disconnect", methods=["POST", "DELETE", "OPTIONS"]) +@jira_bp.route("/disconnect", methods=["POST", "DELETE"]) @require_permission("connectors", "write") def disconnect(user_id): """Disconnect Jira by removing stored credentials.""" diff --git a/server/routes/knowledge_base/routes.py b/server/routes/knowledge_base/routes.py index 7d489b582..9cb1f6001 100644 --- a/server/routes/knowledge_base/routes.py +++ b/server/routes/knowledge_base/routes.py @@ -16,7 +16,6 @@ from utils.db.connection_pool import db_pool from utils.log_sanitizer import sanitize -from utils.web.cors_utils import create_cors_response from utils.auth.rbac_decorators import require_permission from utils.auth.stateless_auth import get_org_id_from_request, set_rls_context @@ -65,13 +64,10 @@ def serialize_document(row: tuple) -> dict[str, Any]: # Memory Endpoints # ============================================================================= -@knowledge_base_bp.route("/memory", methods=["GET", "OPTIONS"]) +@knowledge_base_bp.route("/memory", methods=["GET"]) @require_permission("knowledge_base", "read") def get_memory(user_id): """Get the org's knowledge base memory content.""" - if request.method == "OPTIONS": - return create_cors_response() - org_id = get_org_id_from_request() try: @@ -107,13 +103,10 @@ def get_memory(user_id): return jsonify({"error": "Failed to retrieve memory"}), 500 -@knowledge_base_bp.route("/memory", methods=["PUT", "OPTIONS"]) +@knowledge_base_bp.route("/memory", methods=["PUT"]) @require_permission("knowledge_base", "write") def update_memory(user_id): """Update user's knowledge base memory content.""" - if request.method == "OPTIONS": - return create_cors_response() - try: data = request.get_json(force=True, silent=True) or {} except Exception: @@ -165,13 +158,10 @@ def update_memory(user_id): # Document Endpoints # ============================================================================= -@knowledge_base_bp.route("/documents", methods=["GET", "OPTIONS"]) +@knowledge_base_bp.route("/documents", methods=["GET"]) @require_permission("knowledge_base", "read") def list_documents(user_id): """List all documents for the user.""" - if request.method == "OPTIONS": - return create_cors_response() - org_id = get_org_id_from_request() try: @@ -207,13 +197,10 @@ def list_documents(user_id): return jsonify({"error": "Failed to list documents"}), 500 -@knowledge_base_bp.route("/upload", methods=["POST", "OPTIONS"]) +@knowledge_base_bp.route("/upload", methods=["POST"]) @require_permission("knowledge_base", "write") def upload_document(user_id): """Upload a new document for processing.""" - if request.method == "OPTIONS": - return create_cors_response() - org_id = get_org_id_from_request() # Check if file is in request @@ -370,13 +357,10 @@ def upload_document(user_id): return jsonify({"error": "Failed to upload document"}), 500 -@knowledge_base_bp.route("/documents/", methods=["GET", "OPTIONS"]) +@knowledge_base_bp.route("/documents/", methods=["GET"]) @require_permission("knowledge_base", "read") def get_document(user_id, doc_id: str): """Get a specific document's details.""" - if request.method == "OPTIONS": - return create_cors_response() - org_id = get_org_id_from_request() try: @@ -405,13 +389,10 @@ def get_document(user_id, doc_id: str): return jsonify({"error": "Failed to get document"}), 500 -@knowledge_base_bp.route("/documents/", methods=["DELETE", "OPTIONS"]) +@knowledge_base_bp.route("/documents/", methods=["DELETE"]) @require_permission("knowledge_base", "write") def delete_document(user_id, doc_id: str): """Delete a document and its chunks.""" - if request.method == "OPTIONS": - return create_cors_response() - org_id = get_org_id_from_request() try: @@ -471,13 +452,10 @@ def delete_document(user_id, doc_id: str): return jsonify({"error": "Failed to delete document"}), 500 -@knowledge_base_bp.route("/search", methods=["POST", "OPTIONS"]) +@knowledge_base_bp.route("/search", methods=["POST"]) @require_permission("knowledge_base", "read") def search_documents(user_id): """Search the knowledge base (for direct API usage, not agent tool).""" - if request.method == "OPTIONS": - return create_cors_response() - try: data = request.get_json(force=True, silent=True) or {} except Exception: diff --git a/server/routes/llm_usage_routes.py b/server/routes/llm_usage_routes.py index ab31f720e..ff63446c4 100644 --- a/server/routes/llm_usage_routes.py +++ b/server/routes/llm_usage_routes.py @@ -5,18 +5,12 @@ from utils.auth.stateless_auth import get_org_id_from_request, set_rls_context from utils.db.connection_pool import db_pool from utils.log_sanitizer import sanitize -from utils.web.cors_utils import create_cors_response logger = logging.getLogger(__name__) llm_usage_bp = Blueprint('llm_usage', __name__) _LOG_PREFIX = "[LLMUsage]" -@llm_usage_bp.route('/api/llm-usage/models', methods=['OPTIONS']) -def get_available_models_options(): - return create_cors_response() - - @llm_usage_bp.route('/api/llm-usage/models', methods=['GET']) @require_permission("llm_usage", "read") def get_available_models(user_id): @@ -106,21 +100,6 @@ def get_available_models(user_id): return jsonify({"error": "Failed to retrieve models"}), 500 -@llm_usage_bp.route('/api/llm-usage/session/', methods=['OPTIONS']) -def get_session_usage_options(session_id): - return create_cors_response() - - -@llm_usage_bp.route('/api/llm-usage/cost-over-time', methods=['OPTIONS']) -def get_cost_over_time_options(): - return create_cors_response() - - -@llm_usage_bp.route('/api/llm-usage/summary', methods=['OPTIONS']) -def get_usage_summary_options(): - return create_cors_response() - - @llm_usage_bp.route('/api/llm-usage/cost-over-time', methods=['GET']) @require_permission("llm_usage", "read") def get_cost_over_time(user_id): diff --git a/server/routes/netdata/netdata_routes.py b/server/routes/netdata/netdata_routes.py index 90669737b..bc7bcb5e2 100644 --- a/server/routes/netdata/netdata_routes.py +++ b/server/routes/netdata/netdata_routes.py @@ -9,7 +9,6 @@ from routes.netdata.tasks import process_netdata_alert from utils.db.connection_pool import db_pool from utils.log_sanitizer import sanitize -from utils.web.cors_utils import create_cors_response from utils.auth.token_management import get_token_data, store_tokens_in_db from utils.auth.rbac_decorators import require_permission from utils.auth.stateless_auth import get_org_id_from_request, set_rls_context @@ -29,7 +28,7 @@ def _get_stored_netdata_credentials(user_id: str) -> Optional[Dict[str, Any]]: return None -@netdata_bp.route("/connect", methods=["POST", "OPTIONS"]) +@netdata_bp.route("/connect", methods=["POST"]) @require_permission("connectors", "write") def connect(user_id): """Store Netdata API token and space info.""" @@ -68,7 +67,7 @@ def connect(user_id): }) -@netdata_bp.route("/status", methods=["GET", "OPTIONS"]) +@netdata_bp.route("/status", methods=["GET"]) @require_permission("connectors", "read") def status(user_id): """Check Netdata connection status.""" @@ -94,7 +93,7 @@ def status(user_id): }) -@netdata_bp.route("/disconnect", methods=["POST", "DELETE", "OPTIONS"]) +@netdata_bp.route("/disconnect", methods=["POST", "DELETE"]) @require_permission("connectors", "write") def disconnect(user_id): """Disconnect Netdata by removing stored credentials.""" @@ -128,12 +127,9 @@ def disconnect(user_id): return jsonify({"error": "Failed to disconnect Netdata"}), 500 -@netdata_bp.route("/alerts/webhook/", methods=["POST", "OPTIONS"]) +@netdata_bp.route("/alerts/webhook/", methods=["POST"]) def alert_webhook(user_id: str): """Receive alert webhook from Netdata.""" - if request.method == "OPTIONS": - return create_cors_response() - if not user_id: logger.warning("[NETDATA] Webhook received without user_id") return jsonify({"error": "user_id is required"}), 400 @@ -183,7 +179,7 @@ def alert_webhook(user_id: str): return jsonify({"received": True}) -@netdata_bp.route("/alerts", methods=["GET", "OPTIONS"]) +@netdata_bp.route("/alerts", methods=["GET"]) @require_permission("connectors", "read") def get_alerts(user_id): """Fetch stored Netdata alerts for user.""" @@ -264,7 +260,7 @@ def get_alerts(user_id): return jsonify({"error": "Failed to fetch alerts"}), 500 -@netdata_bp.route("/alerts/webhook-url", methods=["GET", "OPTIONS"]) +@netdata_bp.route("/alerts/webhook-url", methods=["GET"]) @require_permission("connectors", "read") def get_webhook_url(user_id): """Get the webhook URL and verification token for Netdata configuration.""" diff --git a/server/routes/newrelic/newrelic_routes.py b/server/routes/newrelic/newrelic_routes.py index 7dc2f4c0e..42a843760 100644 --- a/server/routes/newrelic/newrelic_routes.py +++ b/server/routes/newrelic/newrelic_routes.py @@ -77,7 +77,7 @@ def _build_client_from_creds(creds: Dict[str, Any]) -> Optional[NewRelicClient]: # ------------------------------------------------------------------ -@newrelic_bp.route("/connect", methods=["POST", "OPTIONS"]) +@newrelic_bp.route("/connect", methods=["POST"]) @require_permission("connectors", "write") def connect(user_id): """Store and validate New Relic credentials.""" @@ -159,7 +159,7 @@ def connect(user_id): }) -@newrelic_bp.route("/status", methods=["GET", "OPTIONS"]) +@newrelic_bp.route("/status", methods=["GET"]) @require_permission("connectors", "read") def status(user_id): """Check connection status by validating stored credentials.""" @@ -191,7 +191,7 @@ def status(user_id): }) -@newrelic_bp.route("/disconnect", methods=["DELETE", "POST", "OPTIONS"]) +@newrelic_bp.route("/disconnect", methods=["DELETE", "POST"]) @require_permission("connectors", "write") def disconnect(user_id): """Remove stored New Relic credentials and backing Vault secrets.""" @@ -217,7 +217,7 @@ def disconnect(user_id): # ------------------------------------------------------------------ -@newrelic_bp.route("/webhook-url", methods=["GET", "OPTIONS"]) +@newrelic_bp.route("/webhook-url", methods=["GET"]) @require_permission("connectors", "read") def webhook_url(user_id): """Get the webhook URL to configure in New Relic.""" @@ -251,7 +251,7 @@ def webhook_url(user_id): # ------------------------------------------------------------------ -@newrelic_bp.route("/issues", methods=["GET", "OPTIONS"]) +@newrelic_bp.route("/issues", methods=["GET"]) @require_permission("connectors", "read") def list_issues(user_id): """Fetch active alert issues from New Relic via NerdGraph.""" @@ -283,7 +283,7 @@ def list_issues(user_id): # ------------------------------------------------------------------ -@newrelic_bp.route("/events/ingested", methods=["GET", "OPTIONS"]) +@newrelic_bp.route("/events/ingested", methods=["GET"]) @require_permission("connectors", "read") def list_ingested_events(user_id): """List New Relic webhook events stored in the database.""" @@ -353,12 +353,9 @@ def list_ingested_events(user_id): # ------------------------------------------------------------------ -@newrelic_bp.route("/webhook/", methods=["POST", "OPTIONS"]) +@newrelic_bp.route("/webhook/", methods=["POST"]) def webhook(user_id: str): """Receive alert notifications from New Relic and enqueue RCA processing.""" - if request.method == "OPTIONS": - return jsonify({}), 200 - if not user_id: return jsonify({"error": "user_id is required"}), 400 diff --git a/server/routes/notion/notion_routes.py b/server/routes/notion/notion_routes.py index 51b992994..eb0c43827 100644 --- a/server/routes/notion/notion_routes.py +++ b/server/routes/notion/notion_routes.py @@ -100,7 +100,7 @@ def _handle_oauth_callback( # ── Routes ────────────────────────────────────────────────────────── -@notion_bp.route("/connect", methods=["POST", "OPTIONS"]) +@notion_bp.route("/connect", methods=["POST"]) @require_permission("connectors", "write") def connect(user_id): """Dual-purpose: OAuth handshake (start + callback) and IIT submission.""" @@ -191,7 +191,7 @@ def connect(user_id): return _handle_oauth_callback(user_id, code, data.get("state")) -@notion_bp.route("/oauth/callback", methods=["POST", "OPTIONS"]) +@notion_bp.route("/oauth/callback", methods=["POST"]) @require_permission("connectors", "write") def oauth_callback(user_id): """OAuth callback endpoint — frontend exchanges ?code/?state from popup here.""" @@ -203,7 +203,7 @@ def oauth_callback(user_id): return _handle_oauth_callback(user_id, data.get("code"), data.get("state")) -@notion_bp.route("/status", methods=["GET", "OPTIONS"]) +@notion_bp.route("/status", methods=["GET"]) @require_permission("connectors", "read") def status(user_id): """Check Notion connection status. @@ -272,7 +272,7 @@ def status(user_id): return jsonify(result) -@notion_bp.route("/disconnect", methods=["POST", "DELETE", "OPTIONS"]) +@notion_bp.route("/disconnect", methods=["POST", "DELETE"]) @require_permission("connectors", "write") def disconnect(user_id): """Disconnect Notion by removing stored credentials (and revoking OAuth token).""" @@ -320,7 +320,7 @@ def disconnect(user_id): ) -@notion_bp.route("/databases", methods=["GET", "OPTIONS"]) +@notion_bp.route("/databases", methods=["GET"]) @require_permission("connectors", "read") def list_databases(user_id): """List Notion databases matching an optional search query (for DB picker).""" @@ -383,7 +383,7 @@ def list_databases(user_id): return jsonify(payload) -@notion_bp.route("/databases/", methods=["GET", "OPTIONS"]) +@notion_bp.route("/databases/", methods=["GET"]) @require_permission("connectors", "read") def get_database(user_id, db_id: str): """Return a shallow summary of a Notion database (for property-mapping UI).""" diff --git a/server/routes/opsgenie/opsgenie_routes.py b/server/routes/opsgenie/opsgenie_routes.py index 2d19f8180..0714cd987 100644 --- a/server/routes/opsgenie/opsgenie_routes.py +++ b/server/routes/opsgenie/opsgenie_routes.py @@ -11,7 +11,6 @@ from routes.opsgenie.tasks import process_opsgenie_event from utils.db.connection_pool import db_pool from utils.log_sanitizer import sanitize -from utils.web.cors_utils import create_cors_response from utils.auth.token_management import get_token_data, store_tokens_in_db from utils.auth.rbac_decorators import require_permission from utils.auth.stateless_auth import get_org_id_from_request, set_rls_context @@ -454,7 +453,7 @@ def _build_client_from_creds(creds: Dict[str, Any]) -> Optional[Union[OpsGenieCl # ── Routes ──────────────────────────────────────────────────────────── -@opsgenie_bp.route("/connect", methods=["POST", "OPTIONS"]) +@opsgenie_bp.route("/connect", methods=["POST"]) @require_permission("connectors", "write") def connect(user_id): try: @@ -572,7 +571,7 @@ def connect(user_id): return jsonify(response) -@opsgenie_bp.route("/status", methods=["GET", "OPTIONS"]) +@opsgenie_bp.route("/status", methods=["GET"]) @require_permission("connectors", "read") def status(user_id): creds = _get_stored_opsgenie_credentials(user_id) @@ -605,7 +604,7 @@ def status(user_id): return jsonify(result) -@opsgenie_bp.route("/disconnect", methods=["DELETE", "POST", "OPTIONS"]) +@opsgenie_bp.route("/disconnect", methods=["DELETE", "POST"]) @require_permission("connectors", "write") def disconnect(user_id): try: @@ -635,11 +634,8 @@ def disconnect(user_id): return jsonify({"error": "Failed to disconnect OpsGenie"}), 500 -@opsgenie_bp.route("/webhook/", methods=["POST", "OPTIONS"]) +@opsgenie_bp.route("/webhook/", methods=["POST"]) def webhook(user_id: str): - if request.method == "OPTIONS": - return create_cors_response() - # Check if user has OpsGenie connected creds = get_token_data(user_id, "opsgenie") if not creds: @@ -657,7 +653,7 @@ def webhook(user_id: str): return jsonify({"received": True}) -@opsgenie_bp.route("/webhook-url", methods=["GET", "OPTIONS"]) +@opsgenie_bp.route("/webhook-url", methods=["GET"]) @require_permission("connectors", "read") def webhook_url(user_id): # Use ngrok URL for development if available, otherwise use backend URL @@ -705,7 +701,7 @@ def webhook_url(user_id): }) -@opsgenie_bp.route("/events/ingested", methods=["GET", "OPTIONS"]) +@opsgenie_bp.route("/events/ingested", methods=["GET"]) @require_permission("connectors", "read") def list_ingested_events(user_id): org_id = get_org_id_from_request() diff --git a/server/routes/org_routes.py b/server/routes/org_routes.py index 1142b2d33..de980a76c 100644 --- a/server/routes/org_routes.py +++ b/server/routes/org_routes.py @@ -241,12 +241,10 @@ def _transfer_user_to_org(cursor, user_id: str, old_org_id, new_org_id: str, new return cursor.fetchone() -@org_bp.route("/current", methods=["GET", "OPTIONS"]) +@org_bp.route("/current", methods=["GET"]) @require_auth_only def get_current_org(user_id): """Get the current user's organization details and member list.""" - if request.method == "OPTIONS": - return jsonify({}), 200 org_id = get_org_id_from_request() if not org_id: @@ -295,12 +293,10 @@ def get_current_org(user_id): return jsonify({"error": "Failed to fetch organization"}), 500 -@org_bp.route("", methods=["PATCH", "OPTIONS"]) +@org_bp.route("", methods=["PATCH"]) @require_permission("org", "manage") def update_org(user_id): """Update organization name or slug (admin only).""" - if request.method == "OPTIONS": - return jsonify({}), 200 org_id = get_org_id_from_request() if not org_id: @@ -368,13 +364,10 @@ def update_org(user_id): return jsonify({"error": "Failed to update organization"}), 500 -@org_bp.route("/members", methods=["POST", "OPTIONS"]) +@org_bp.route("/members", methods=["POST"]) @require_permission("users", "manage") def add_member(user_id): """Add an existing user to this org with a role (admin only).""" - if request.method == "OPTIONS": - return jsonify({}), 200 - org_id = get_org_id_from_request() if not org_id: return jsonify({"error": "No organization found"}), 404 @@ -437,13 +430,10 @@ def add_member(user_id): return jsonify({"error": "Failed to add member"}), 500 -@org_bp.route("/members/", methods=["DELETE", "OPTIONS"]) +@org_bp.route("/members/", methods=["DELETE"]) @require_permission("users", "manage") def remove_member(user_id, target_user_id): """Remove a user from this org (admin only).""" - if request.method == "OPTIONS": - return jsonify({}), 200 - org_id = get_org_id_from_request() if not org_id: return jsonify({"error": "No organization found"}), 404 @@ -505,12 +495,10 @@ def remove_member(user_id, target_user_id): return jsonify({"error": "Failed to remove member"}), 500 -@org_bp.route("/my-invitations", methods=["GET", "OPTIONS"]) +@org_bp.route("/my-invitations", methods=["GET"]) @require_auth_only def my_invitations(user_id): """Return pending invitations addressed to the current user's email.""" - if request.method == "OPTIONS": - return jsonify({}), 200 try: with db_pool.get_admin_connection() as conn: @@ -559,13 +547,10 @@ def my_invitations(user_id): return jsonify({"error": "Failed to fetch invitations"}), 500 -@org_bp.route("/my-invitations//decline", methods=["POST", "OPTIONS"]) +@org_bp.route("/my-invitations//decline", methods=["POST"]) @require_auth_only def decline_invitation(user_id, invitation_id): """Decline a pending invitation addressed to the current user.""" - if request.method == "OPTIONS": - return jsonify({}), 200 - try: with db_pool.get_admin_connection() as conn: with conn.cursor() as cursor: @@ -595,13 +580,10 @@ def decline_invitation(user_id, invitation_id): return jsonify({"error": "Failed to decline invitation"}), 500 -@org_bp.route("/invitations//cancel", methods=["POST", "OPTIONS"]) +@org_bp.route("/invitations//cancel", methods=["POST"]) @require_permission("users", "manage") def cancel_invitation(user_id, invitation_id): """Cancel a pending invitation (admin only).""" - if request.method == "OPTIONS": - return jsonify({}), 200 - org_id = get_org_id_from_request() if not org_id: return jsonify({"error": "No organization found"}), 404 @@ -630,20 +612,23 @@ def cancel_invitation(user_id, invitation_id): return jsonify({"error": "Failed to cancel invitation"}), 500 -@org_bp.route("/invitations", methods=["GET", "POST", "OPTIONS"]) +@org_bp.route("/invitations", methods=["GET"]) @require_permission("users", "manage") -def invitations(user_id): - """Create or list invitations for this org (admin only).""" - if request.method == "OPTIONS": - return jsonify({}), 200 - +def list_invitations(user_id): + """List pending invitations for this org (admin only).""" org_id = get_org_id_from_request() if not org_id: return jsonify({"error": "No organization found"}), 404 + return _list_invitations(org_id) - if request.method == "GET": - return _list_invitations(org_id) +@org_bp.route("/invitations", methods=["POST"]) +@require_permission("users", "manage") +def create_invitation(user_id): + """Create a new invitation for this org (admin only).""" + org_id = get_org_id_from_request() + if not org_id: + return jsonify({"error": "No organization found"}), 404 return _create_invitation(org_id, user_id) @@ -756,13 +741,10 @@ def _create_invitation(org_id: str, user_id: str): return jsonify({"error": "Failed to create invitation"}), 500 -@org_bp.route("/join", methods=["POST", "OPTIONS"]) +@org_bp.route("/join", methods=["POST"]) @require_auth_only def join_org(user_id): """Accept an invitation and transfer user data to the new org.""" - if request.method == "OPTIONS": - return jsonify({}), 200 - data = request.get_json() or {} invitation_id = data.get("invitation_id") direct_org_id = data.get("org_id") @@ -889,12 +871,10 @@ def join_org(user_id): return jsonify({"error": "Failed to join organization"}), 500 -@org_bp.route("/stats", methods=["GET", "OPTIONS"]) +@org_bp.route("/stats", methods=["GET"]) @require_auth_only def get_org_stats(user_id): """Return aggregate stats for the current org.""" - if request.method == "OPTIONS": - return jsonify({}), 200 org_id = get_org_id_from_request() if not org_id: @@ -937,12 +917,10 @@ def get_org_stats(user_id): return jsonify({"error": "Failed to fetch stats"}), 500 -@org_bp.route("/activity", methods=["GET", "OPTIONS"]) +@org_bp.route("/activity", methods=["GET"]) @require_auth_only def get_org_activity(user_id): """Return recent activity events for the org (member joins, role changes).""" - if request.method == "OPTIONS": - return jsonify({}), 200 org_id = get_org_id_from_request() if not org_id: @@ -1048,12 +1026,10 @@ def get_org_activity(user_id): return jsonify({"error": "Failed to fetch activity"}), 500 -@org_bp.route("/preferences", methods=["GET", "OPTIONS"]) +@org_bp.route("/preferences", methods=["GET"]) @require_auth_only def get_org_preferences(user_id): """Get org-level preferences stored in user_preferences with user_id='__org__'.""" - if request.method == "OPTIONS": - return jsonify({}), 200 org_id = get_org_id_from_request() if not org_id: @@ -1083,13 +1059,10 @@ def get_org_preferences(user_id): return jsonify({"error": "Failed to fetch preferences"}), 500 -@org_bp.route("/preferences", methods=["PUT", "OPTIONS"]) +@org_bp.route("/preferences", methods=["PUT"]) @require_permission("org", "manage") def update_org_preferences(user_id): """Update org-level preferences (admin only).""" - if request.method == "OPTIONS": - return jsonify({}), 200 - org_id = get_org_id_from_request() if not org_id: return jsonify({"error": "No organization found"}), 404 diff --git a/server/routes/ovh/oauth2_auth_code_flow.py b/server/routes/ovh/oauth2_auth_code_flow.py index a1ffa676e..41fda70bc 100644 --- a/server/routes/ovh/oauth2_auth_code_flow.py +++ b/server/routes/ovh/oauth2_auth_code_flow.py @@ -60,7 +60,7 @@ } -@ovh_bp.route('/ovh/oauth2/initiate', methods=['POST', 'OPTIONS']) +@ovh_bp.route('/ovh/oauth2/initiate', methods=['POST']) @limiter.limit("5 per minute;20 per hour;100 per day") @require_permission("connectors", "write") def ovh_oauth2_initiate(user_id): @@ -81,11 +81,6 @@ def ovh_oauth2_initiate(user_id): "authorizationUrl": "https://www.ovh.com/auth/oauth2/authorize?..." } """ - # Handle CORS preflight - if request.method == 'OPTIONS': - from utils.web.cors_utils import create_cors_response - return create_cors_response() - try: data = request.get_json() or {} endpoint = data.get('endpoint') @@ -149,7 +144,7 @@ def ovh_oauth2_initiate(user_id): return jsonify({"error": "Failed to initiate OAuth2 flow"}), 500 -@ovh_bp.route('/ovh/oauth2/callback', methods=['GET', 'POST', 'OPTIONS']) +@ovh_bp.route('/ovh/oauth2/callback', methods=['GET', 'POST']) @limiter.limit("5 per minute;20 per hour;100 per day") def ovh_oauth2_callback(): """ @@ -165,11 +160,6 @@ def ovh_oauth2_callback(): code: authorization-code-from-ovh state: csrf-state-token """ - # Handle CORS preflight - if request.method == 'OPTIONS': - from utils.web.cors_utils import create_cors_response - return create_cors_response() - # Determine frontend URL for redirects frontend_url = os.environ.get('FRONTEND_URL') is_get_request = request.method == 'GET' diff --git a/server/routes/ovh/ovh_api_routes.py b/server/routes/ovh/ovh_api_routes.py index dd80630b5..cfb50b4ad 100644 --- a/server/routes/ovh/ovh_api_routes.py +++ b/server/routes/ovh/ovh_api_routes.py @@ -26,7 +26,6 @@ from utils.secrets.secret_ref_utils import has_user_credentials, delete_user_secret from utils.db.connection_utils import set_connection_status from utils.log_sanitizer import sanitize -from utils.web.cors_utils import create_cors_response from utils.auth.token_management import get_token_data, store_tokens_in_db from utils.ssh.ssh_utils import ( delete_ssh_credentials, @@ -47,13 +46,11 @@ } -@ovh_bp.route('/ovh/projects', methods=['GET', 'OPTIONS']) +@ovh_bp.route('/ovh/projects', methods=['GET']) @limiter.limit(OVH_READ_LIMITS) @require_permission("connectors", "read") def ovh_projects_read(user_id): """GET /ovh_api/ovh/projects - Fetch list of OVH cloud projects.""" - if request.method == 'OPTIONS': - return create_cors_response() try: logger.info(f"Fetching OVH projects for user: {sanitize(user_id)} (from header: {sanitize(request.headers.get('X-User-ID'))})") @@ -189,7 +186,7 @@ def ovh_projects_write(user_id): return jsonify({"error": "Failed to save project preferences"}), 500 -@ovh_bp.route('/ovh/instances', methods=['GET', 'OPTIONS']) +@ovh_bp.route('/ovh/instances', methods=['GET']) @limiter.limit(OVH_READ_LIMITS) @require_permission("connectors", "read") def ovh_instances(user_id): @@ -211,9 +208,6 @@ def ovh_instances(user_id): } """ # Handle CORS preflight - if request.method == 'OPTIONS': - return create_cors_response() - try: logger.info(f"Fetching OVH instances for user: {user_id}") @@ -471,14 +465,11 @@ def save_ovh_ssh_keys(user_id, instance_id): }) -@ovh_bp.route('/ovh/root-project', methods=['GET', 'OPTIONS']) +@ovh_bp.route('/ovh/root-project', methods=['GET']) @limiter.limit(OVH_READ_LIMITS) @require_permission("connectors", "read") def ovh_root_project_read(user_id): """GET /ovh_api/ovh/root-project - Get current root project.""" - if request.method == 'OPTIONS': - return create_cors_response() - try: root_project = get_user_preference(user_id, 'ovh_root_project') return jsonify({"root_project": root_project}) @@ -715,7 +706,7 @@ def ovh_grant_access(user_id): return jsonify({"error": "Failed to grant OVH access"}), 500 -@ovh_bp.route('/ovh/status', methods=['GET', 'OPTIONS']) +@ovh_bp.route('/ovh/status', methods=['GET']) @limiter.limit(OVH_READ_LIMITS) @require_permission("connectors", "read") def ovh_connection_status(user_id): @@ -732,9 +723,6 @@ def ovh_connection_status(user_id): } """ # Handle CORS preflight - if request.method == 'OPTIONS': - return create_cors_response() - try: # Check if user has OVH credentials stored @@ -810,7 +798,7 @@ def ovh_connection_status(user_id): return jsonify({"error": "Failed to check OVH status"}), 500 -@ovh_bp.route('/ovh/disconnect', methods=['POST', 'OPTIONS']) +@ovh_bp.route('/ovh/disconnect', methods=['POST']) @limiter.limit("5 per minute;20 per hour") @require_permission("connectors", "write") def ovh_disconnect(user_id): @@ -825,10 +813,6 @@ def ovh_disconnect(user_id): "message": "OVH account disconnected successfully" } """ - # Handle CORS preflight - if request.method == 'OPTIONS': - return create_cors_response() - try: logger.info(f"Disconnecting OVH account for user: {user_id}") diff --git a/server/routes/pagerduty/pagerduty_routes.py b/server/routes/pagerduty/pagerduty_routes.py index 8db5d87f8..d41951594 100644 --- a/server/routes/pagerduty/pagerduty_routes.py +++ b/server/routes/pagerduty/pagerduty_routes.py @@ -10,7 +10,6 @@ import urllib.parse from flask import Blueprint, jsonify, request, redirect -from utils.web.cors_utils import create_cors_response from utils.flags.feature_flags import is_pagerduty_oauth_enabled from utils.auth.token_management import get_token_data, store_tokens_in_db from utils.auth.rbac_decorators import require_permission @@ -53,13 +52,10 @@ def _validate_v3_webhook(payload: dict) -> tuple[bool, str]: return True, "" -@pagerduty_bp.route("", methods=["GET", "OPTIONS"]) +@pagerduty_bp.route("", methods=["GET"]) @require_permission("connectors", "read") def pagerduty_status(user_id): """Get PagerDuty connection status.""" - if request.method == "OPTIONS": - return create_cors_response() - creds = get_token_data(user_id, "pagerduty") if not creds: return jsonify({"connected": False}) @@ -143,13 +139,10 @@ def pagerduty_disconnect(user_id): return jsonify({"error": "Disconnect failed"}), 500 -@pagerduty_bp.route("/oauth/login", methods=["POST", "OPTIONS"]) +@pagerduty_bp.route("/oauth/login", methods=["POST"]) @require_permission("connectors", "write") def oauth_login(user_id): """Initiate OAuth flow.""" - if request.method == "OPTIONS": - return create_cors_response() - if not is_pagerduty_oauth_enabled(): return jsonify({"error": "PagerDuty OAuth is not enabled"}), 403 @@ -208,13 +201,10 @@ def oauth_callback(): return redirect(f"{callback_url}?oauth=failed&error=unexpected") -@pagerduty_bp.route("/webhook-url", methods=["GET", "OPTIONS"]) +@pagerduty_bp.route("/webhook-url", methods=["GET"]) @require_permission("connectors", "read") def get_webhook_url(user_id): """Get the webhook URL that should be configured in PagerDuty.""" - if request.method == "OPTIONS": - return create_cors_response() - # Use ngrok URL for development if available, otherwise use backend URL ngrok_url = os.getenv("NGROK_URL", "").rstrip("/") backend_url = os.getenv("NEXT_PUBLIC_BACKEND_URL", "").rstrip("/") @@ -240,12 +230,9 @@ def get_webhook_url(user_id): }) -@pagerduty_bp.route("/webhook/", methods=["POST", "OPTIONS"]) +@pagerduty_bp.route("/webhook/", methods=["POST"]) def webhook(user_id: str): """Receive V3 webhook events from PagerDuty.""" - if request.method == "OPTIONS": - return create_cors_response() - if not user_id: return jsonify({"error": "user_id is required"}), 400 diff --git a/server/routes/scaleway/scaleway_routes.py b/server/routes/scaleway/scaleway_routes.py index 7cf472eb9..030c64e6a 100644 --- a/server/routes/scaleway/scaleway_routes.py +++ b/server/routes/scaleway/scaleway_routes.py @@ -30,7 +30,6 @@ from utils.db.connection_utils import set_connection_status from utils.web.limiter_ext import limiter from utils.logging.secure_logging import mask_credential_value -from utils.web.cors_utils import create_cors_response from utils.ssh.ssh_utils import ( check_if_user_has_vms, delete_ssh_credentials, @@ -64,7 +63,7 @@ UUID_PATTERN = re.compile(r'^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$') -@scaleway_bp.route('/scaleway/connect', methods=['POST', 'OPTIONS']) +@scaleway_bp.route('/scaleway/connect', methods=['POST']) @limiter.limit("5 per minute") @require_permission("connectors", "write") def scaleway_connect(user_id): @@ -155,14 +154,11 @@ def scaleway_connect(user_id): return jsonify({"error": "Failed to connect Scaleway"}), 500 -@scaleway_bp.route('/scaleway/projects', methods=['GET', 'OPTIONS']) +@scaleway_bp.route('/scaleway/projects', methods=['GET']) @limiter.limit("30 per minute") @require_permission("connectors", "read") def scaleway_projects_read(user_id): """GET - Fetch Scaleway projects.""" - if request.method == 'OPTIONS': - return create_cors_response() - try: token_data = get_token_data(user_id, "scaleway") if not token_data: @@ -235,14 +231,11 @@ def scaleway_projects_write(user_id): return jsonify({"error": "Failed to save projects"}), 500 -@scaleway_bp.route('/scaleway/status', methods=['GET', 'OPTIONS']) +@scaleway_bp.route('/scaleway/status', methods=['GET']) @limiter.limit("60 per minute") @require_permission("connectors", "read") def scaleway_status(user_id): """Check Scaleway connection status and validate credentials with API call.""" - if request.method == 'OPTIONS': - return create_cors_response() - try: has_creds = has_user_credentials(user_id, "scaleway") @@ -282,14 +275,11 @@ def scaleway_status(user_id): return jsonify({"connected": has_creds, "provider": "scaleway"}), 200 -@scaleway_bp.route('/scaleway/disconnect', methods=['POST', 'OPTIONS']) +@scaleway_bp.route('/scaleway/disconnect', methods=['POST']) @limiter.limit("10 per minute") @require_permission("connectors", "write") def scaleway_disconnect(user_id): """Disconnect Scaleway account.""" - if request.method == 'OPTIONS': - return create_cors_response() - try: # Get access_key before deleting for status update @@ -312,14 +302,11 @@ def scaleway_disconnect(user_id): return jsonify({"error": "Failed to disconnect Scaleway"}), 500 -@scaleway_bp.route('/scaleway/root-project', methods=['GET', 'OPTIONS']) +@scaleway_bp.route('/scaleway/root-project', methods=['GET']) @limiter.limit("30 per minute") @require_permission("connectors", "read") def scaleway_root_project_read(user_id): """Get the current root project for Scaleway.""" - if request.method == 'OPTIONS': - return create_cors_response() - try: root_project = get_user_preference(user_id, 'scaleway_root_project') @@ -367,7 +354,7 @@ def scaleway_root_project_write(user_id): return jsonify({"error": "Failed to set root project"}), 500 -@scaleway_bp.route('/scaleway/instances', methods=['GET', 'OPTIONS']) +@scaleway_bp.route('/scaleway/instances', methods=['GET']) @limiter.limit("30 per minute") @require_permission("connectors", "read") def scaleway_instances(user_id): @@ -388,9 +375,6 @@ def scaleway_instances(user_id): ] } """ - if request.method == 'OPTIONS': - return create_cors_response() - try: logger.info(f"Fetching Scaleway instances for user: {user_id}") @@ -487,7 +471,7 @@ def scaleway_instances(user_id): return jsonify({"error": "Failed to fetch Scaleway instances"}), 500 -@scaleway_bp.route('/scaleway/instances//ssh-keys', methods=['POST', 'DELETE', 'OPTIONS']) +@scaleway_bp.route('/scaleway/instances//ssh-keys', methods=['POST', 'DELETE']) @limiter.limit("10 per minute") @require_permission("connectors", "write") def save_scaleway_ssh_keys(user_id, server_id): @@ -507,9 +491,7 @@ def save_scaleway_ssh_keys(user_id, server_id): "message": "SSH key saved successfully" } """ - if request.method == 'OPTIONS': - return create_cors_response() - + try: # Validate server_id format diff --git a/server/routes/sharepoint/sharepoint_routes.py b/server/routes/sharepoint/sharepoint_routes.py index 3a8d035c8..bbb46fe1e 100644 --- a/server/routes/sharepoint/sharepoint_routes.py +++ b/server/routes/sharepoint/sharepoint_routes.py @@ -118,7 +118,7 @@ def _refresh_sharepoint_credentials(user_id: str, creds: Dict[str, Any]) -> Opti return updated_creds -@sharepoint_bp.route("/connect", methods=["POST", "OPTIONS"]) +@sharepoint_bp.route("/connect", methods=["POST"]) @require_permission("connectors", "write") def connect(user_id): """Connect SharePoint via Microsoft OAuth2.""" @@ -190,7 +190,7 @@ def _exchange_oauth_code(user_id: str, data: dict, code: str): }) -@sharepoint_bp.route("/status", methods=["GET", "OPTIONS"]) +@sharepoint_bp.route("/status", methods=["GET"]) @require_permission("connectors", "read") def status(user_id): """Check SharePoint connection status.""" @@ -241,7 +241,7 @@ def _validate_sharepoint_token(user_id: str, creds: Dict[str, Any]) -> Optional[ return None -@sharepoint_bp.route("/disconnect", methods=["POST", "DELETE", "OPTIONS"]) +@sharepoint_bp.route("/disconnect", methods=["POST", "DELETE"]) @require_permission("connectors", "write") def disconnect(user_id): """Disconnect SharePoint by removing stored credentials.""" @@ -279,7 +279,7 @@ def disconnect(user_id): return jsonify({"error": "Failed to disconnect SharePoint"}), 500 -@sharepoint_bp.route("/search", methods=["POST", "OPTIONS"]) +@sharepoint_bp.route("/search", methods=["POST"]) @require_permission("connectors", "read") def search(user_id): """Search SharePoint for content matching query.""" @@ -307,7 +307,7 @@ def search(user_id): return _safe_json_response({"results": results, "count": len(results)}) -@sharepoint_bp.route("/fetch-page", methods=["POST", "OPTIONS"]) +@sharepoint_bp.route("/fetch-page", methods=["POST"]) @require_permission("connectors", "read") def fetch_page(user_id): """Fetch a SharePoint page and return its content as markdown.""" @@ -333,7 +333,7 @@ def fetch_page(user_id): return _safe_json_response(result) -@sharepoint_bp.route("/fetch-document", methods=["POST", "OPTIONS"]) +@sharepoint_bp.route("/fetch-document", methods=["POST"]) @require_permission("connectors", "read") def fetch_document(user_id): """Fetch a SharePoint document and return extracted text.""" @@ -359,7 +359,7 @@ def fetch_document(user_id): return _safe_json_response(result) -@sharepoint_bp.route("/create-page", methods=["POST", "OPTIONS"]) +@sharepoint_bp.route("/create-page", methods=["POST"]) @require_permission("connectors", "write") def create_page(user_id): """Create a new SharePoint page.""" @@ -387,7 +387,7 @@ def create_page(user_id): return _safe_json_response(result) -@sharepoint_bp.route("/sites", methods=["GET", "OPTIONS"]) +@sharepoint_bp.route("/sites", methods=["GET"]) @require_permission("connectors", "read") def list_sites(user_id): """List SharePoint sites, optionally filtered by search query.""" diff --git a/server/routes/spinnaker/spinnaker_routes.py b/server/routes/spinnaker/spinnaker_routes.py index b7b16181b..9c331f3d0 100644 --- a/server/routes/spinnaker/spinnaker_routes.py +++ b/server/routes/spinnaker/spinnaker_routes.py @@ -14,7 +14,6 @@ invalidate_spinnaker_client, ) from utils.db.connection_pool import db_pool -from utils.web.cors_utils import create_cors_response from utils.web.webhook_signature import SIGNATURE_HEADER, verify_webhook_signature from utils.auth.stateless_auth import get_org_id_from_request, set_rls_context from utils.auth.token_management import get_token_data, store_tokens_in_db @@ -47,7 +46,7 @@ def _get_cached_client(user_id: str) -> Optional["SpinnakerClient"]: # ------------------------------------------------------------------ -@spinnaker_bp.route("/connect", methods=["POST", "OPTIONS"]) +@spinnaker_bp.route("/connect", methods=["POST"]) @require_permission("connectors", "write") def connect(user_id): """Validate and store Spinnaker credentials (token or x509).""" @@ -136,7 +135,7 @@ def connect(user_id): }) -@spinnaker_bp.route("/status", methods=["GET", "OPTIONS"]) +@spinnaker_bp.route("/status", methods=["GET"]) @require_permission("connectors", "read") def status(user_id): """Check whether Spinnaker is connected and return summary data.""" @@ -166,7 +165,7 @@ def status(user_id): }) -@spinnaker_bp.route("/disconnect", methods=["POST", "DELETE", "OPTIONS"]) +@spinnaker_bp.route("/disconnect", methods=["POST", "DELETE"]) @require_permission("connectors", "write") def disconnect(user_id): """Disconnect Spinnaker by removing stored credentials.""" @@ -189,7 +188,7 @@ def disconnect(user_id): # ------------------------------------------------------------------ -@spinnaker_bp.route("/applications", methods=["GET", "OPTIONS"]) +@spinnaker_bp.route("/applications", methods=["GET"]) @require_permission("connectors", "read") def list_applications(user_id): """List Spinnaker applications.""" @@ -205,7 +204,7 @@ def list_applications(user_id): return jsonify({"error": "Spinnaker API request failed"}), 502 -@spinnaker_bp.route("/applications//pipelines", methods=["GET", "OPTIONS"]) +@spinnaker_bp.route("/applications//pipelines", methods=["GET"]) @require_permission("connectors", "read") def list_pipelines(user_id, app: str): """List pipeline executions for an application.""" @@ -224,7 +223,7 @@ def list_pipelines(user_id, app: str): return jsonify({"error": "Spinnaker API request failed"}), 502 -@spinnaker_bp.route("/applications//pipeline-configs", methods=["GET", "OPTIONS"]) +@spinnaker_bp.route("/applications//pipeline-configs", methods=["GET"]) @require_permission("connectors", "read") def list_pipeline_configs(user_id, app: str): """List pipeline definitions for an application.""" @@ -240,7 +239,7 @@ def list_pipeline_configs(user_id, app: str): return jsonify({"error": "Spinnaker API request failed"}), 502 -@spinnaker_bp.route("/applications//pipelines//trigger", methods=["POST", "OPTIONS"]) +@spinnaker_bp.route("/applications//pipelines//trigger", methods=["POST"]) @require_permission("connectors", "write") def trigger_pipeline(user_id, app: str, name: str): """Trigger a named pipeline for an application.""" @@ -259,7 +258,7 @@ def trigger_pipeline(user_id, app: str, name: str): return jsonify({"error": "Spinnaker API request failed"}), 502 -@spinnaker_bp.route("/applications//health", methods=["GET", "OPTIONS"]) +@spinnaker_bp.route("/applications//health", methods=["GET"]) @require_permission("connectors", "read") def application_health(user_id, app: str): """Get cluster + server group health for an application.""" @@ -280,7 +279,7 @@ def application_health(user_id, app: str): # ------------------------------------------------------------------ -@spinnaker_bp.route("/webhook/", methods=["POST", "OPTIONS"], strict_slashes=False) +@spinnaker_bp.route("/webhook/", methods=["POST"], strict_slashes=False) def deployment_webhook(user_id: str): """Receive a deployment event webhook from Spinnaker Echo. @@ -288,9 +287,6 @@ def deployment_webhook(user_id: str): when present. Echo does not support HMAC signing, so the signature check is only enforced when the header is actually provided. """ - if request.method == "OPTIONS": - return create_cors_response() - if not user_id or len(user_id) > 255: return jsonify({"error": "user_id is required"}), 400 @@ -351,7 +347,7 @@ def deployment_webhook(user_id: str): return jsonify({"received": True}) -@spinnaker_bp.route("/webhook-url", methods=["GET", "OPTIONS"]) +@spinnaker_bp.route("/webhook-url", methods=["GET"]) @require_permission("connectors", "read") def get_webhook_url(user_id): """Return the webhook URL and Spinnaker Echo config snippets.""" @@ -389,7 +385,7 @@ def get_webhook_url(user_id): # ------------------------------------------------------------------ -@spinnaker_bp.route("/deployments", methods=["GET", "OPTIONS"]) +@spinnaker_bp.route("/deployments", methods=["GET"]) @require_permission("connectors", "read") def list_deployments(user_id): """List recent Spinnaker deployment events for the authenticated user.""" diff --git a/server/routes/splunk/search_routes.py b/server/routes/splunk/search_routes.py index a89ddbecd..bfcbca3cf 100644 --- a/server/routes/splunk/search_routes.py +++ b/server/routes/splunk/search_routes.py @@ -8,7 +8,6 @@ import requests from flask import Blueprint, Response, jsonify, request, stream_with_context -from utils.web.cors_utils import create_cors_response from utils.auth.token_management import get_token_data from utils.auth.rbac_decorators import require_permission from utils.log_sanitizer import sanitize @@ -62,7 +61,7 @@ def _splunk_headers(api_token: str) -> Dict[str, str]: } -@search_bp.route("/search", methods=["POST", "OPTIONS"]) +@search_bp.route("/search", methods=["POST"]) @require_permission("connectors", "read") def search_sync(user_id): """Execute a synchronous SPL search (oneshot mode).""" @@ -150,7 +149,7 @@ def search_sync(user_id): return jsonify({"error": "Search request failed"}), 502 -@search_bp.route("/search/jobs", methods=["POST", "OPTIONS"]) +@search_bp.route("/search/jobs", methods=["POST"]) @require_permission("connectors", "read") def create_search_job(user_id): """Create an asynchronous search job.""" @@ -216,7 +215,7 @@ def create_search_job(user_id): return jsonify({"error": "Failed to create search job"}), 502 -@search_bp.route("/search/jobs/", methods=["GET", "OPTIONS"]) +@search_bp.route("/search/jobs/", methods=["GET"]) @require_permission("connectors", "read") def get_job_status(user_id, sid: str): """Get the status of a search job.""" @@ -269,7 +268,7 @@ def get_job_status(user_id, sid: str): return jsonify({"error": "Failed to get job status"}), 502 -@search_bp.route("/search/jobs//results", methods=["GET", "OPTIONS"]) +@search_bp.route("/search/jobs//results", methods=["GET"]) @require_permission("connectors", "read") def get_job_results(user_id, sid: str): """Get the results of a completed search job.""" @@ -323,7 +322,7 @@ def get_job_results(user_id, sid: str): return jsonify({"error": "Failed to get search results"}), 502 -@search_bp.route("/search/jobs/", methods=["DELETE", "OPTIONS"]) +@search_bp.route("/search/jobs/", methods=["DELETE"]) @require_permission("connectors", "write") def cancel_job(user_id, sid: str): """Cancel a running search job.""" diff --git a/server/routes/splunk/splunk_routes.py b/server/routes/splunk/splunk_routes.py index 22dbda2a5..9f685e315 100644 --- a/server/routes/splunk/splunk_routes.py +++ b/server/routes/splunk/splunk_routes.py @@ -9,7 +9,6 @@ from routes.splunk.tasks import process_splunk_alert from utils.db.connection_pool import db_pool -from utils.web.cors_utils import create_cors_response from utils.auth.stateless_auth import ( get_org_id_from_request, get_user_preference, @@ -125,7 +124,7 @@ def _get_stored_splunk_credentials(user_id: str) -> Optional[Dict[str, Any]]: return None -@splunk_bp.route("/connect", methods=["POST", "OPTIONS"]) +@splunk_bp.route("/connect", methods=["POST"]) @require_permission("connectors", "write") def connect(user_id): """Store Splunk API token and validate connectivity.""" @@ -195,7 +194,7 @@ def connect(user_id): }) -@splunk_bp.route("/status", methods=["GET", "OPTIONS"]) +@splunk_bp.route("/status", methods=["GET"]) @require_permission("connectors", "read") def status(user_id): """Check Splunk connection status.""" @@ -234,7 +233,7 @@ def status(user_id): }) -@splunk_bp.route("/disconnect", methods=["POST", "DELETE", "OPTIONS"]) +@splunk_bp.route("/disconnect", methods=["POST", "DELETE"]) @require_permission("connectors", "write") def disconnect(user_id): """Disconnect Splunk by removing stored credentials.""" @@ -257,12 +256,9 @@ def disconnect(user_id): return jsonify({"error": "Failed to disconnect Splunk"}), 500 -@splunk_bp.route("/alerts/webhook/", methods=["POST", "OPTIONS"]) +@splunk_bp.route("/alerts/webhook/", methods=["POST"]) def alert_webhook(user_id: str): """Receive alert webhook from Splunk for a specific user.""" - if request.method == "OPTIONS": - return create_cors_response() - if not user_id: logger.warning("[SPLUNK] Webhook received without user_id") return jsonify({"error": "user_id is required"}), 400 @@ -296,7 +292,7 @@ def alert_webhook(user_id: str): return jsonify({"received": True}) -@splunk_bp.route("/alerts", methods=["GET", "OPTIONS"]) +@splunk_bp.route("/alerts", methods=["GET"]) @require_permission("connectors", "read") def get_alerts(user_id): """Fetch Splunk alerts for the authenticated user.""" @@ -376,7 +372,7 @@ def get_alerts(user_id): return jsonify({"error": "Failed to fetch alerts"}), 500 -@splunk_bp.route("/alerts/webhook-url", methods=["GET", "OPTIONS"]) +@splunk_bp.route("/alerts/webhook-url", methods=["GET"]) @require_permission("connectors", "read") def get_webhook_url(user_id): """Get the webhook URL that should be configured in Splunk.""" @@ -406,7 +402,7 @@ def get_webhook_url(user_id): }) -@splunk_bp.route("/rca-settings", methods=["GET", "OPTIONS"]) +@splunk_bp.route("/rca-settings", methods=["GET"]) @require_permission("connectors", "read") def get_rca_settings(user_id): """Get Splunk RCA settings for the authenticated user.""" @@ -417,7 +413,7 @@ def get_rca_settings(user_id): }) -@splunk_bp.route("/rca-settings", methods=["PUT", "OPTIONS"]) +@splunk_bp.route("/rca-settings", methods=["PUT"]) @require_permission("connectors", "write") def update_rca_settings(user_id): """Update Splunk RCA settings for the authenticated user.""" diff --git a/server/routes/thousandeyes/thousandeyes_routes.py b/server/routes/thousandeyes/thousandeyes_routes.py index fcd5da403..bb67df63f 100644 --- a/server/routes/thousandeyes/thousandeyes_routes.py +++ b/server/routes/thousandeyes/thousandeyes_routes.py @@ -12,7 +12,6 @@ from chat.backend.agent.tools.mcp_tools import clear_credentials_cache from utils.auth.token_management import get_token_data, store_tokens_in_db from utils.secrets.secret_ref_utils import delete_user_secret -from utils.web.cors_utils import create_cors_response from utils.auth.rbac_decorators import require_permission logger = logging.getLogger(__name__) @@ -37,7 +36,7 @@ def _get_stored_credentials(user_id: str) -> Optional[Dict[str, Any]]: # Connection management # ------------------------------------------------------------------ -@thousandeyes_bp.route("/connect", methods=["POST", "OPTIONS"]) +@thousandeyes_bp.route("/connect", methods=["POST"]) @require_permission("connectors", "write") def connect(user_id): payload = request.get_json(force=True, silent=True) or {} @@ -79,7 +78,7 @@ def connect(user_id): }) -@thousandeyes_bp.route("/status", methods=["GET", "OPTIONS"]) +@thousandeyes_bp.route("/status", methods=["GET"]) @require_permission("connectors", "read") def status(user_id): creds = _get_stored_credentials(user_id) @@ -112,7 +111,7 @@ def status(user_id): }) -@thousandeyes_bp.route("/disconnect", methods=["DELETE", "POST", "OPTIONS"]) +@thousandeyes_bp.route("/disconnect", methods=["DELETE", "POST"]) @require_permission("connectors", "write") def disconnect(user_id): try: diff --git a/server/routes/user_connections.py b/server/routes/user_connections.py index 8b9882b10..824564735 100644 --- a/server/routes/user_connections.py +++ b/server/routes/user_connections.py @@ -1,7 +1,6 @@ from flask import Blueprint, jsonify, request import logging from utils.auth.rbac_decorators import require_permission -from utils.web.cors_utils import create_cors_response from utils.db.connection_utils import list_active_connections, set_connection_status user_connections_bp = Blueprint('user_connections_bp', __name__) @@ -12,11 +11,6 @@ def get_user_connections_from_db(user_id: str): """Return active connections using user_connections table.""" return list_active_connections(user_id) -@user_connections_bp.route('/api/user_connections', methods=['OPTIONS']) -def get_user_connections_options(): - return create_cors_response() - - @user_connections_bp.route('/api/user_connections', methods=['GET']) @require_permission("connectors", "read") def get_user_connections(user_id): diff --git a/server/routes/user_preferences.py b/server/routes/user_preferences.py index 2d229facf..77e88d758 100644 --- a/server/routes/user_preferences.py +++ b/server/routes/user_preferences.py @@ -8,7 +8,6 @@ set_rls_context, ) from utils.log_sanitizer import sanitize -from utils.web.cors_utils import create_cors_response from utils.auth.rbac_decorators import require_permission import json @@ -17,11 +16,6 @@ user_preferences_bp = Blueprint('user_preferences', __name__) -@user_preferences_bp.route('/api/user-preferences', methods=['OPTIONS']) -def handle_user_preferences_options(): - return create_cors_response() - - @user_preferences_bp.route('/api/user-preferences', methods=['GET']) @require_permission("user_preferences", "read") def get_user_preferences(user_id): @@ -52,11 +46,6 @@ def set_user_preferences(user_id): logger.info(f"Stored preference {key} for user {user_id}") return jsonify({"status": "success"}) -@user_preferences_bp.route('/api/clear-session', methods=['OPTIONS']) -def clear_session_options(): - return create_cors_response() - - @user_preferences_bp.route('/api/clear-session', methods=['POST']) @require_permission("user_preferences", "write") def clear_session(user_id): @@ -86,11 +75,6 @@ def clear_session(user_id): if 'conn' in locals() and conn: conn.close() -@user_preferences_bp.route('/api/credentials/', methods=['OPTIONS']) -def get_credentials_options(provider): - return create_cors_response() - - @user_preferences_bp.route('/api/credentials/', methods=['GET']) @require_permission("user_preferences", "read") def get_credentials(user_id, provider): @@ -103,11 +87,6 @@ def get_credentials(user_id, provider): logger.warning(f"No {sanitize(provider)} credentials found for user {sanitize(user_id)}") return jsonify({"error": f"No {provider} credentials found"}), 404 -@user_preferences_bp.route('/api/user-preferences/batch', methods=['OPTIONS']) -def handle_batch_preferences_options(): - return create_cors_response() - - @user_preferences_bp.route('/api/user-preferences/batch', methods=['GET']) @require_permission("user_preferences", "read") def get_batch_preferences(user_id): @@ -180,11 +159,6 @@ def set_batch_preferences(user_id): logger.error(f"Error storing batch preferences for user {user_id}: {e}") return jsonify({"error": "Failed to store preferences"}), 500 -@user_preferences_bp.route('/api/terraform/clear-state', methods=['OPTIONS']) -def clear_terraform_state_options(): - return create_cors_response() - - @user_preferences_bp.route('/api/terraform/clear-state', methods=['POST']) @require_permission("incidents", "write") def clear_terraform_state(user_id):