| 1 | from typing import List |
| 2 | from typing import Optional |
| 3 | |
| 4 | from fastapi import APIRouter |
| 5 | from fastapi import Depends |
| 6 | from fastapi import HTTPException |
| 7 | from fastapi import Query |
| 8 | from fastapi import Security |
| 9 | from loguru import logger |
| 10 | from sqlalchemy.ext.asyncio import AsyncSession |
| 11 | from sqlalchemy.future import select |
| 12 | |
| 13 | from app.auth.models.users import User |
| 14 | from app.auth.utils import AuthHandler |
| 15 | from app.db.db_session import get_db |
| 16 | from app.db.universal_models import Customers |
| 17 | from app.middleware.customer_access import customer_access_handler |
| 18 | from app.siem.schema.dashboards import DashboardCategoriesListResponse |
| 19 | from app.siem.schema.dashboards import DashboardCategoryDetailResponse |
| 20 | from app.siem.schema.dashboards import DisableDashboardResponse |
| 21 | from app.siem.schema.dashboards import EnableDashboardRequest |
| 22 | from app.siem.schema.dashboards import EnabledDashboardOperationResponse |
| 23 | from app.siem.schema.dashboards import EnabledDashboardResponse |
| 24 | from app.siem.schema.dashboards import EnabledDashboardsListResponse |
| 25 | from app.siem.schema.dashboards import PanelDataRequest |
| 26 | from app.siem.schema.dashboards import PanelDataResponse |
| 27 | from app.siem.services.dashboards import disable_dashboard |
| 28 | from app.siem.services.dashboards import enable_dashboard |
| 29 | from app.siem.services.dashboards import get_category_detail |
| 30 | from app.siem.services.dashboards import get_enabled_dashboards |
| 31 | from app.siem.services.dashboards import get_enabled_dashboards_for_customers |
| 32 | from app.siem.services.dashboards import get_panel_data |
| 33 | from app.siem.services.dashboards import list_categories |
| 34 | |
| 35 | dashboards_router = APIRouter() |
| 36 | |
| 37 | |
| 38 | async def verify_customer_exists(customer_code: str, db: AsyncSession) -> None: |
| 39 | result = await db.execute( |
| 40 | select(Customers).filter(Customers.customer_code == customer_code), |
| 41 | ) |
| 42 | if not result.scalars().first(): |
| 43 | raise HTTPException( |
| 44 | status_code=404, |
| 45 | detail=f"Customer with customer_code {customer_code} not found", |
| 46 | ) |
| 47 | |
| 48 | |
| 49 | # ── Browse available templates (filesystem) ───────────────────── |
| 50 | |
| 51 | |
| 52 | @dashboards_router.get( |
| 53 | "/templates", |
| 54 | response_model=DashboardCategoriesListResponse, |
| 55 | description="List all available dashboard categories (e.g. wazuh_edr, fortinet_edr)", |
| 56 | dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst", "customer_user"))], |
| 57 | ) |
| 58 | async def list_dashboard_categories() -> DashboardCategoriesListResponse: |
| 59 | logger.info("Listing dashboard categories") |
| 60 | categories = list_categories() |
| 61 | return DashboardCategoriesListResponse( |
| 62 | categories=categories, |
| 63 | success=True, |
| 64 | message="Dashboard categories retrieved successfully", |
| 65 | ) |
| 66 | |
| 67 | |
| 68 | @dashboards_router.get( |
| 69 | "/templates/{category_id}", |
| 70 | response_model=DashboardCategoryDetailResponse, |
| 71 | description="Get a dashboard category with all its template definitions", |
| 72 | dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst", "customer_user"))], |
| 73 | ) |
| 74 | async def get_dashboard_category(category_id: str) -> DashboardCategoryDetailResponse: |
| 75 | logger.info(f"Getting dashboard category {category_id}") |
| 76 | category = get_category_detail(category_id) |
| 77 | return DashboardCategoryDetailResponse( |
| 78 | category=category, |
| 79 | success=True, |
| 80 | message="Dashboard category retrieved successfully", |
| 81 | ) |
| 82 | |
| 83 | |
| 84 | # ── Enabled dashboards (per-customer, DB-backed) ──────────────── |
| 85 | |
| 86 | |
| 87 | @dashboards_router.get( |
| 88 | "/enabled", |
| 89 | response_model=EnabledDashboardsListResponse, |
| 90 | description="List enabled dashboards across the user's accessible customers (optionally narrowed to a subset via customer_codes).", |
| 91 | dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst", "customer_user"))], |
| 92 | ) |
| 93 | async def list_enabled_dashboards_multi( |
| 94 | customer_codes: Optional[List[str]] = Query(None, description="Optional subset of customer codes to scope the results to"), |
| 95 | current_user: User = Depends(AuthHandler().get_current_user), |
| 96 | db: AsyncSession = Depends(get_db), |
| 97 | ) -> EnabledDashboardsListResponse: |
| 98 | # Resolve the requested subset against the user's access (never widens scope). |
| 99 | effective_customers = await customer_access_handler.resolve_effective_customers(current_user, customer_codes, db) |
| 100 | rows = await get_enabled_dashboards_for_customers(effective_customers, db) |
| 101 | return EnabledDashboardsListResponse( |
| 102 | enabled_dashboards=[EnabledDashboardResponse.from_orm(r) for r in rows], |
| 103 | success=True, |
| 104 | message="Enabled dashboards retrieved successfully", |
| 105 | ) |
| 106 | |
| 107 | |
| 108 | @dashboards_router.get( |
| 109 | "/enabled/{customer_code}", |
| 110 | response_model=EnabledDashboardsListResponse, |
| 111 | description="List dashboards enabled for a customer", |
| 112 | dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst", "customer_user"))], |
| 113 | ) |
| 114 | async def list_enabled_dashboards( |
| 115 | customer_code: str, |
| 116 | current_user: User = Depends(AuthHandler().get_current_user), |
| 117 | db: AsyncSession = Depends(get_db), |
| 118 | ) -> EnabledDashboardsListResponse: |
| 119 | logger.info(f"Listing enabled dashboards for customer {customer_code}") |
| 120 | if not await customer_access_handler.check_customer_access(current_user, customer_code, db): |
| 121 | raise HTTPException(status_code=403, detail=f"Access denied to customer {customer_code}") |
| 122 | await verify_customer_exists(customer_code, db) |
| 123 | rows = await get_enabled_dashboards(customer_code, db) |
| 124 | return EnabledDashboardsListResponse( |
| 125 | enabled_dashboards=[EnabledDashboardResponse.from_orm(r) for r in rows], |
| 126 | success=True, |
| 127 | message="Enabled dashboards retrieved successfully", |
| 128 | ) |
| 129 | |
| 130 | |
| 131 | @dashboards_router.post( |
| 132 | "/enable", |
| 133 | response_model=EnabledDashboardOperationResponse, |
| 134 | description="Enable a dashboard template for a customer + event source", |
| 135 | dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))], |
| 136 | ) |
| 137 | async def enable_dashboard_endpoint( |
| 138 | request: EnableDashboardRequest, |
| 139 | db: AsyncSession = Depends(get_db), |
| 140 | ) -> EnabledDashboardOperationResponse: |
| 141 | logger.info(f"Enabling dashboard for customer {request.customer_code}") |
| 142 | await verify_customer_exists(request.customer_code, db) |
| 143 | row = await enable_dashboard(request, db) |
| 144 | return EnabledDashboardOperationResponse( |
| 145 | enabled_dashboard=EnabledDashboardResponse.from_orm(row), |
| 146 | success=True, |
| 147 | message="Dashboard enabled successfully", |
| 148 | ) |
| 149 | |
| 150 | |
| 151 | @dashboards_router.delete( |
| 152 | "/disable/{dashboard_id}", |
| 153 | response_model=DisableDashboardResponse, |
| 154 | description="Disable (remove) an enabled dashboard", |
| 155 | dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst"))], |
| 156 | ) |
| 157 | async def disable_dashboard_endpoint( |
| 158 | dashboard_id: int, |
| 159 | db: AsyncSession = Depends(get_db), |
| 160 | ) -> DisableDashboardResponse: |
| 161 | logger.info(f"Disabling dashboard {dashboard_id}") |
| 162 | await disable_dashboard(dashboard_id, db) |
| 163 | return DisableDashboardResponse( |
| 164 | success=True, |
| 165 | message="Dashboard disabled successfully", |
| 166 | ) |
| 167 | |
| 168 | |
| 169 | # ── Panel data (execute queries and return chart-ready data) ───── |
| 170 | |
| 171 | |
| 172 | @dashboards_router.post( |
| 173 | "/panel-data", |
| 174 | response_model=PanelDataResponse, |
| 175 | description="Execute all panel queries for an enabled dashboard and return chart-ready data", |
| 176 | dependencies=[Security(AuthHandler().require_any_scope("admin", "analyst", "customer_user"))], |
| 177 | ) |
| 178 | async def panel_data_endpoint( |
| 179 | request: PanelDataRequest, |
| 180 | db: AsyncSession = Depends(get_db), |
| 181 | ) -> PanelDataResponse: |
| 182 | logger.info(f"Fetching panel data for dashboard {request.dashboard_id} (timerange={request.timerange})") |
| 183 | data = await get_panel_data(request.dashboard_id, request.timerange, db) |
| 184 | return PanelDataResponse( |
| 185 | panels=data["results"], |
| 186 | template=data["template"], |
| 187 | dashboard_id=request.dashboard_id, |
| 188 | customer_code=data["customer_code"], |
| 189 | source_name=data["source_name"], |
| 190 | accent_color=data["accent_color"], |
| 191 | success=True, |
| 192 | message="Panel data retrieved successfully", |
| 193 | ) |