From 5c56344d74480f7007396b9660f23ca0f2b7a439 Mon Sep 17 00:00:00 2001 From: Jason Staack Date: Thu, 12 Mar 2026 22:59:37 -0500 Subject: [PATCH] feat(06-01): add config-history endpoint with RBAC and main.py registration - GET /api/tenants/{tid}/devices/{did}/config-history endpoint - Viewer+ RBAC with config:read scope - Pagination via limit/offset query params (defaults 50/0) - Router registered in main.py Co-Authored-By: Claude Opus 4.6 --- backend/app/main.py | 2 + backend/app/routers/config_history.py | 83 +++++++++++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 backend/app/routers/config_history.py diff --git a/backend/app/main.py b/backend/app/main.py index 057adc9..fe3fe4c 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -281,6 +281,7 @@ def create_app() -> FastAPI: from app.routers.sse import router as sse_router from app.routers.config_backups import router as config_router from app.routers.config_editor import router as config_editor_router + from app.routers.config_history import router as config_history_router from app.routers.device_groups import router as device_groups_router from app.routers.device_tags import router as device_tags_router from app.routers.devices import router as devices_router @@ -311,6 +312,7 @@ def create_app() -> FastAPI: app.include_router(device_tags_router, prefix="/api") app.include_router(metrics_router, prefix="/api") app.include_router(config_router, prefix="/api") + app.include_router(config_history_router, prefix="/api") app.include_router(firmware_router, prefix="/api") app.include_router(alerts_router, prefix="/api") app.include_router(config_editor_router, prefix="/api") diff --git a/backend/app/routers/config_history.py b/backend/app/routers/config_history.py new file mode 100644 index 0000000..d7422cb --- /dev/null +++ b/backend/app/routers/config_history.py @@ -0,0 +1,83 @@ +"""Config history timeline API endpoint. + +Provides: + - GET /tenants/{tenant_id}/devices/{device_id}/config-history + Paginated timeline of config changes for a device. + +RBAC: viewer+ can read. Scope: config:read. +""" + +import logging +import uuid +from typing import Any + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.database import get_db +from app.middleware.rbac import require_min_role, require_scope +from app.middleware.tenant_context import CurrentUser, get_current_user +from app.services.config_history_service import get_config_history + +logger = logging.getLogger(__name__) + +router = APIRouter(tags=["config-history"]) + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +async def _check_tenant_access( + current_user: CurrentUser, tenant_id: uuid.UUID, db: AsyncSession +) -> None: + """Verify the current user is allowed to access the given tenant. + + - super_admin can access any tenant -- re-sets DB tenant context to target tenant. + - All other roles must match their own tenant_id. + """ + if current_user.is_super_admin: + from app.database import set_tenant_context + await set_tenant_context(db, str(tenant_id)) + return + if current_user.tenant_id != tenant_id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="Access denied: you do not belong to this tenant.", + ) + + +# --------------------------------------------------------------------------- +# Endpoints +# --------------------------------------------------------------------------- + + +@router.get( + "/tenants/{tenant_id}/devices/{device_id}/config-history", + summary="Get config change timeline for a device", + dependencies=[require_scope("config:read")], +) +async def list_config_history( + tenant_id: uuid.UUID, + device_id: uuid.UUID, + limit: int = Query(default=50, ge=1, le=200), + offset: int = Query(default=0, ge=0), + current_user: CurrentUser = Depends(get_current_user), + _role: CurrentUser = Depends(require_min_role("viewer")), + db: AsyncSession = Depends(get_db), +) -> list[dict[str, Any]]: + """Return paginated config change timeline for a device, newest first. + + Each entry includes: id, component, summary, created_at, + diff_id, lines_added, lines_removed, snapshot_id. + """ + await _check_tenant_access(current_user, tenant_id, db) + + return await get_config_history( + device_id=str(device_id), + tenant_id=str(tenant_id), + session=db, + limit=limit, + offset=offset, + )