Fence governed connector acquisitions
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict
|
||||
import hashlib
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Response, status
|
||||
from fastapi import APIRouter, Depends, Header, HTTPException, Query, Response, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.audit.logging import audit_event
|
||||
@@ -42,6 +44,10 @@ from govoplan_connectors.backend.schemas import (
|
||||
TabularSourceResponse,
|
||||
)
|
||||
from govoplan_connectors.backend.feeds import ConnectorFeedProvider, feed_rows
|
||||
from govoplan_connectors.backend.recovery import (
|
||||
ConnectorRecoveryError,
|
||||
begin_connector_read_snapshot,
|
||||
)
|
||||
from govoplan_connectors.backend.sanctions_sources import (
|
||||
SANCTIONS_READ_SCOPE,
|
||||
SANCTIONS_REFRESH_SCOPE,
|
||||
@@ -108,6 +114,18 @@ def _feed_http_error(exc: FeedCapabilityError) -> HTTPException:
|
||||
)
|
||||
|
||||
|
||||
def _recovery_http_error(exc: ConnectorRecoveryError) -> HTTPException:
|
||||
detail = str(exc)
|
||||
return HTTPException(
|
||||
status_code=(
|
||||
status.HTTP_409_CONFLICT
|
||||
if "already" in detail.casefold() or "active" in detail.casefold()
|
||||
else status.HTTP_503_SERVICE_UNAVAILABLE
|
||||
),
|
||||
detail=detail,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/feeds/preview", response_model=FeedDocumentResponse)
|
||||
def api_preview_feed(
|
||||
payload: FeedAcquireRequest,
|
||||
@@ -133,8 +151,45 @@ def api_import_feed_snapshot(
|
||||
payload: FeedImportRequest,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
idempotency_key: Annotated[
|
||||
str | None,
|
||||
Header(alias="Idempotency-Key", max_length=500),
|
||||
] = None,
|
||||
) -> TabularSourceResponse:
|
||||
_require_any_scope(principal, WRITE_SCOPE, ADMIN_SCOPE)
|
||||
try:
|
||||
recovery = begin_connector_read_snapshot(
|
||||
session,
|
||||
tenant_id=principal.tenant_id,
|
||||
provider_id="connectors.feed_snapshot",
|
||||
idempotency_key=idempotency_key,
|
||||
source_revision=None,
|
||||
cursor=None,
|
||||
dry_run_evidence={
|
||||
"performed": False,
|
||||
"reason": "read-only acquisition into an immutable snapshot",
|
||||
},
|
||||
request_metadata={
|
||||
"source_url_sha256": hashlib.sha256(
|
||||
payload.url.encode("utf-8")
|
||||
).hexdigest(),
|
||||
"source_name": payload.source_name,
|
||||
"max_entries": payload.max_entries,
|
||||
},
|
||||
resource_type="connector_tabular_source",
|
||||
)
|
||||
except ConnectorRecoveryError as exc:
|
||||
raise _recovery_http_error(exc) from exc
|
||||
if recovery.replayed:
|
||||
try:
|
||||
source = provider.get_source(
|
||||
session,
|
||||
principal,
|
||||
source_ref=f"snapshot:{recovery.resource_id}",
|
||||
)
|
||||
except TabularSourceError as exc:
|
||||
raise _http_error(exc) from exc
|
||||
return _source_response(source)
|
||||
try:
|
||||
document = feed_transport.fetch(
|
||||
payload.url,
|
||||
@@ -170,8 +225,14 @@ def api_import_feed_snapshot(
|
||||
},
|
||||
},
|
||||
),
|
||||
source_id=recovery.resource_id,
|
||||
)
|
||||
except (FeedCapabilityError, TabularSourceError) as exc:
|
||||
session.rollback()
|
||||
recovery.fail_without_projection(
|
||||
summary="The read-only feed import failed before a snapshot committed",
|
||||
code=exc.__class__.__name__,
|
||||
)
|
||||
if isinstance(exc, FeedCapabilityError):
|
||||
raise _feed_http_error(exc) from exc
|
||||
raise _http_error(exc) from exc
|
||||
@@ -190,7 +251,22 @@ def api_import_feed_snapshot(
|
||||
"row_count": source.row_count,
|
||||
},
|
||||
)
|
||||
session.commit()
|
||||
try:
|
||||
recovery.commit_success(
|
||||
session,
|
||||
evidence={
|
||||
"verified": True,
|
||||
"checks": {
|
||||
"snapshot_ref": source.ref,
|
||||
"snapshot_fingerprint": source.fingerprint,
|
||||
"feed_sha256": document.sha256,
|
||||
"row_count": source.row_count,
|
||||
"provider_mutation": False,
|
||||
},
|
||||
},
|
||||
)
|
||||
except ConnectorRecoveryError as exc:
|
||||
raise _recovery_http_error(exc) from exc
|
||||
return _source_response(source)
|
||||
|
||||
|
||||
@@ -390,6 +466,10 @@ def api_refresh_sanctions_source(
|
||||
provider_id: str,
|
||||
session: Session = Depends(get_session),
|
||||
principal: ApiPrincipal = Depends(get_api_principal),
|
||||
idempotency_key: Annotated[
|
||||
str | None,
|
||||
Header(alias="Idempotency-Key", max_length=500),
|
||||
] = None,
|
||||
) -> SanctionsRefreshResponse:
|
||||
_require_any_scope(
|
||||
principal,
|
||||
@@ -401,7 +481,10 @@ def api_refresh_sanctions_source(
|
||||
session,
|
||||
principal,
|
||||
provider_id=provider_id,
|
||||
idempotency_key=idempotency_key,
|
||||
)
|
||||
except ConnectorRecoveryError as exc:
|
||||
raise _recovery_http_error(exc) from exc
|
||||
except SanctionsSourceError as exc:
|
||||
raise _sanctions_http_error(exc) from exc
|
||||
audit_event(
|
||||
|
||||
Reference in New Issue
Block a user