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 <noreply@anthropic.com>
This commit is contained in:
Jason Staack
2026-03-12 22:59:37 -05:00
parent f7d5aec4ec
commit 5c56344d74
2 changed files with 85 additions and 0 deletions

View File

@@ -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")

View File

@@ -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,
)