feat: add MSP workspace operator views

This commit is contained in:
Christian Krakau-Louis
2026-05-23 20:10:30 +02:00
parent cec72faf34
commit 5eea21c6f2
10 changed files with 579 additions and 1 deletions
@@ -0,0 +1,111 @@
"""MSP operator endpoints."""
from typing import Any, Dict, List
from fastapi import APIRouter, Depends, HTTPException, Request, status
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.security import require_admin_auth
from app.models.workspace import Workspace
from app.services.workspace_access import (
PERMISSION_AUDIT_READ,
PERMISSION_WORKSPACE_ADMIN,
require_workspace_permission,
)
from app.services.workspace_audit import record_workspace_audit_log
from app.services.workspace_operator import (
list_workspace_operator_summaries,
retention_to_dict,
workspace_operator_summary,
)
router = APIRouter()
class OperatorWorkspacesResponse(BaseModel):
"""Cross-workspace operator summaries."""
workspaces: List[Dict[str, Any]]
class WorkspaceRetentionUpdate(BaseModel):
"""Workspace retention controls."""
aggregate_reports_days: int = Field(..., ge=1, le=3650)
forensic_reports_days: int = Field(..., ge=1, le=3650)
tls_reports_days: int = Field(..., ge=1, le=3650)
class WorkspaceRetentionResponse(BaseModel):
"""Updated workspace retention response."""
workspace: Dict[str, Any]
retention: Dict[str, int]
def _workspace_or_404(db: Session, workspace_id: int) -> Workspace:
workspace = db.query(Workspace).filter(Workspace.id == workspace_id).first()
if workspace is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Workspace {workspace_id} not found",
)
return workspace
@router.get("/workspaces", response_model=OperatorWorkspacesResponse)
async def list_operator_workspaces(
db: Session = Depends(get_db),
_auth: dict = Depends(require_admin_auth),
) -> OperatorWorkspacesResponse:
"""Return safe cross-workspace health, drift, import, and retention summaries."""
require_workspace_permission(_auth, PERMISSION_AUDIT_READ)
return {"workspaces": list_workspace_operator_summaries(db)}
@router.get("/workspaces/{workspace_id}", response_model=Dict[str, Any])
async def get_operator_workspace(
workspace_id: int,
db: Session = Depends(get_db),
_auth: dict = Depends(require_admin_auth),
) -> Dict[str, Any]:
"""Return one workspace operator summary."""
require_workspace_permission(_auth, PERMISSION_AUDIT_READ)
return workspace_operator_summary(db, _workspace_or_404(db, workspace_id))
@router.put("/workspaces/{workspace_id}/retention", response_model=WorkspaceRetentionResponse)
async def update_workspace_retention(
workspace_id: int,
payload: WorkspaceRetentionUpdate,
request: Request,
db: Session = Depends(get_db),
_auth: dict = Depends(require_admin_auth),
) -> WorkspaceRetentionResponse:
"""Update workspace retention controls and audit the change."""
require_workspace_permission(_auth, PERMISSION_WORKSPACE_ADMIN)
workspace = _workspace_or_404(db, workspace_id)
old_retention = retention_to_dict(workspace)
workspace.report_retention_days = payload.aggregate_reports_days
workspace.forensic_retention_days = payload.forensic_reports_days
workspace.tls_report_retention_days = payload.tls_reports_days
new_retention = retention_to_dict(workspace)
record_workspace_audit_log(
db,
workspace=workspace,
action="workspace.retention_updated",
entity_type="workspace",
entity_id=workspace.id,
entity_name=workspace.slug,
details={"old": old_retention, "new": new_retention},
auth_context=_auth,
request=request,
)
db.commit()
db.refresh(workspace)
return {
"workspace": {"id": workspace.id, "slug": workspace.slug, "name": workspace.name},
"retention": retention_to_dict(workspace),
}