diff --git a/backend/routers/frontend.py b/backend/routers/frontend.py index e001908..f7b3b80 100644 --- a/backend/routers/frontend.py +++ b/backend/routers/frontend.py @@ -405,6 +405,28 @@ async def create_frontend(frontend: FrontendConfig, request: Request, authorizat await close_database_connection(conn) raise HTTPException(status_code=400, detail=f"Frontend '{frontend.name}' already exists") + # CRITICAL VALIDATION: Check if default_backend has active servers + # Use case: Prevent frontend from routing to DOWN backend (no servers = 503 errors) + # HAProxy allows this (syntax valid) but it's bad practice for production + if frontend.default_backend: + backend_has_servers = await conn.fetchval(""" + SELECT EXISTS( + SELECT 1 FROM backend_servers bs + JOIN backends b ON bs.backend_name = b.name AND bs.cluster_id = b.cluster_id + WHERE b.name = $1 + AND b.cluster_id = $2 + AND b.is_active = TRUE + AND bs.is_active = TRUE + ) + """, frontend.default_backend, frontend.cluster_id) + + if not backend_has_servers: + await close_database_connection(conn) + raise HTTPException( + status_code=400, + detail=f"Backend '{frontend.default_backend}' has no active servers. Please add at least one server to the backend before assigning it to a frontend." + ) + # ENTERPRISE DUAL-MODE: Save ssl_certificate_ids (NEW) and ssl_certificate_id (OLD - backward compat) # Convert ssl_certificate_ids to JSONB for database ssl_cert_ids_json = json.dumps(frontend.ssl_certificate_ids) if frontend.ssl_certificate_ids else '[]' @@ -625,6 +647,28 @@ async def update_frontend(frontend_id: int, frontend: FrontendConfig, request: R await close_database_connection(conn) raise HTTPException(status_code=400, detail=f"Frontend name '{frontend.name}' already exists") + # CRITICAL VALIDATION: Check if default_backend has active servers + # Use case: Prevent frontend from routing to DOWN backend (no servers = 503 errors) + # HAProxy allows this (syntax valid) but it's bad practice for production + if frontend.default_backend: + backend_has_servers = await conn.fetchval(""" + SELECT EXISTS( + SELECT 1 FROM backend_servers bs + JOIN backends b ON bs.backend_name = b.name AND bs.cluster_id = b.cluster_id + WHERE b.name = $1 + AND (b.cluster_id = $2 OR b.cluster_id IS NULL) + AND b.is_active = TRUE + AND bs.is_active = TRUE + ) + """, frontend.default_backend, cluster_id) + + if not backend_has_servers: + await close_database_connection(conn) + raise HTTPException( + status_code=400, + detail=f"Backend '{frontend.default_backend}' has no active servers. Please add at least one server to the backend before assigning it to a frontend." + ) + # CRITICAL FIX: Preserve SSL configuration if not explicitly changed # If SSL is currently enabled but incoming data has ssl_enabled=False or ssl_certificate_id=None, # check if this is an intentional change or just missing data from the form diff --git a/backend/routers/waf.py b/backend/routers/waf.py index ef126b7..20eae80 100644 --- a/backend/routers/waf.py +++ b/backend/routers/waf.py @@ -415,6 +415,16 @@ async def create_waf_rule(waf_rule_data: dict, cluster_id: Optional[int] = None, cluster_info = f" in cluster {cluster_id}" if cluster_id else "" raise HTTPException(status_code=400, detail=f"WAF rule '{waf_rule.name}' already exists{cluster_info}") + # CRITICAL VALIDATION: At least one frontend must be selected + # Use case: Prevent unintentional application to ALL frontends + # WAF rule without frontend = no config change = should not create pending version + if not waf_rule.frontend_ids or len(waf_rule.frontend_ids) == 0: + await close_database_connection(conn) + raise HTTPException( + status_code=400, + detail="At least one frontend must be selected for WAF rule. Please select target frontend(s) where this WAF rule should be applied." + ) + async with conn.transaction(): rule_id = await conn.fetchval(""" INSERT INTO waf_rules (name, rule_type, config, action, priority, description, enabled, is_active, cluster_id) @@ -673,6 +683,15 @@ async def update_waf_rule(rule_id: int, waf_rule_data: dict, request: Request, a frontend_ids_in_payload = 'frontend_ids' in waf_rule_data if frontend_ids_in_payload: + # CRITICAL VALIDATION: If frontend_ids explicitly provided, must have at least one + # Use case: Prevent clearing all frontends (WAF rule would apply to nothing) + if not waf_rule.frontend_ids or len(waf_rule.frontend_ids) == 0: + await close_database_connection(conn) + raise HTTPException( + status_code=400, + detail="At least one frontend must be selected for WAF rule. Cannot remove all frontend assignments. Please select target frontend(s)." + ) + # Frontend IDs were explicitly provided in the request (could be [] or [1,2,3]) await conn.execute("DELETE FROM frontend_waf_rules WHERE waf_rule_id = $1", rule_id) frontend_assignments, cluster_ids = await assign_frontends_and_get_clusters(conn, rule_id, waf_rule.frontend_ids or [], existing_rule.get("cluster_id")) diff --git a/frontend/src/components/WAFManagement.js b/frontend/src/components/WAFManagement.js index addc85d..cac084a 100644 --- a/frontend/src/components/WAFManagement.js +++ b/frontend/src/components/WAFManagement.js @@ -1473,13 +1473,31 @@ const WAFManagement = () => {