Add governed Files integrity operations UI

This commit is contained in:
2026-08-04 01:04:39 +02:00
parent 04882f1628
commit 7e6be4b017
15 changed files with 841 additions and 16 deletions
+56 -6
View File
@@ -18,6 +18,7 @@ from govoplan_files.backend.schemas import (
FileIntegrityFindingResponse,
FileIntegrityFindingsResponse,
FileIntegrityScanCreateRequest,
FileIntegrityScanRunRequest,
FileIntegrityScanResponse,
FileIntegrityScansResponse,
)
@@ -93,10 +94,17 @@ def create_scan(
@router.post("/scans/{scan_id}/run", response_model=FileIntegrityScanResponse)
def run_scan_batch(
scan_id: str,
payload: FileIntegrityScanRunRequest,
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(require_scope("files:file:admin")),
) -> FileIntegrityScanResponse:
scan = _scan_for_tenant(session, scan_id, principal.tenant_id)
scan = _scan_for_tenant(
session,
scan_id,
principal.tenant_id,
for_update=True,
)
_assert_expected_revision(scan.revision, payload.expected_revision)
previous_status = scan.status
try:
run_integrity_scan_batch(session, scan)
@@ -117,7 +125,12 @@ def run_scan_batch(
return _scan_response(scan)
except (FileStorageError, StorageBackendError) as exc:
session.rollback()
scan = _scan_for_tenant(session, scan_id, principal.tenant_id)
scan = _scan_for_tenant(
session,
scan_id,
principal.tenant_id,
for_update=True,
)
mark_integrity_scan_failed(scan, error=exc)
audit_from_principal(
session,
@@ -172,7 +185,13 @@ def recheck_finding(
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(require_scope("files:file:admin")),
) -> FileIntegrityActionResponse:
finding = _finding_for_tenant(session, finding_id, principal.tenant_id)
finding = _finding_for_tenant(
session,
finding_id,
principal.tenant_id,
for_update=True,
)
_assert_expected_revision(finding.revision, payload.expected_revision)
try:
result = recheck_integrity_finding(
session,
@@ -198,7 +217,13 @@ def cleanup_finding(
session: Session = Depends(get_session),
principal: ApiPrincipal = Depends(require_scope("files:file:admin")),
) -> FileIntegrityActionResponse:
finding = _finding_for_tenant(session, finding_id, principal.tenant_id)
finding = _finding_for_tenant(
session,
finding_id,
principal.tenant_id,
for_update=True,
)
_assert_expected_revision(finding.revision, payload.expected_revision)
try:
result = cleanup_orphan_finding(
session,
@@ -218,8 +243,13 @@ def _scan_for_tenant(
session: Session,
scan_id: str,
tenant_id: str,
*,
for_update: bool = False,
) -> FileIntegrityScan:
scan = session.get(FileIntegrityScan, scan_id)
query = session.query(FileIntegrityScan).filter(FileIntegrityScan.id == scan_id)
if for_update:
query = query.populate_existing().with_for_update()
scan = query.one_or_none()
if scan is None or scan.tenant_id != tenant_id:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -232,8 +262,15 @@ def _finding_for_tenant(
session: Session,
finding_id: str,
tenant_id: str,
*,
for_update: bool = False,
) -> FileIntegrityFinding:
finding = session.get(FileIntegrityFinding, finding_id)
query = session.query(FileIntegrityFinding).filter(
FileIntegrityFinding.id == finding_id
)
if for_update:
query = query.populate_existing().with_for_update()
finding = query.one_or_none()
if finding is None or finding.tenant_id != tenant_id:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -242,6 +279,17 @@ def _finding_for_tenant(
return finding
def _assert_expected_revision(current: int, expected: int) -> None:
if current != expected:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=(
"The integrity record changed after it was loaded; reload before "
"performing this action."
),
)
def _audit_integrity_action(session, principal, result) -> None:
audit_from_principal(
session,
@@ -271,6 +319,7 @@ def _scan_response(scan: FileIntegrityScan) -> FileIntegrityScanResponse:
storage_backend=scan.storage_backend,
storage_prefix=scan.storage_prefix,
status=scan.status,
revision=scan.revision,
phase=scan.phase,
verify_checksums=scan.verify_checksums,
batch_size=scan.batch_size,
@@ -299,6 +348,7 @@ def _finding_response(
tenant_id=finding.tenant_id,
kind=finding.kind,
state=finding.state,
revision=finding.revision,
blob_id=finding.blob_id,
storage_key=finding.storage_key,
expected_size_bytes=finding.expected_size_bytes,