fix(automation): bound backup approval fingerprints

This commit is contained in:
ogt
2026-07-14 10:43:34 +08:00
parent 7282dad5a9
commit 0ec066b5c6
3 changed files with 73 additions and 5 deletions

View File

@@ -39,6 +39,7 @@ logger = get_logger("awoooi.backup_restore_alertmanager_ingress")
BACKUP_RESTORE_AGENT99_ROUTE_ID = "agent99:backup_health:BackupCheck"
BACKUP_RESTORE_DISPATCH_OWNER = "alertmanager_raw_signal"
BACKUP_RESTORE_OWNER_SCHEMA_VERSION = "backup_restore_ingress_owner_v1"
APPROVAL_FINGERPRINT_MAX_LENGTH = 64
BACKUP_RESTORE_PROHIBITED_ACTIONS = (
"run_backup",
"run_restore",
@@ -55,6 +56,15 @@ def _stable_digest(*parts: object) -> str:
return hashlib.sha256(value.encode("utf-8")).hexdigest()
def _approval_storage_fingerprint(source_fingerprint: str) -> str:
"""Fit a source identity into approval_records.fingerprint VARCHAR(64)."""
normalized = str(source_fingerprint or "").strip()
if len(normalized) <= APPROVAL_FINGERPRINT_MAX_LENGTH:
return normalized
return hashlib.sha256(normalized.encode("utf-8")).hexdigest()
def build_backup_restore_ingress_owner(
*,
project_id: str,
@@ -233,6 +243,9 @@ async def process_backup_restore_alertmanager_signal(
source_event_fingerprint=source_event_fingerprint,
source_started_at=source_started_at,
)
approval_storage_fingerprint = _approval_storage_fingerprint(
source_fingerprint
)
approval_service = get_approval_service()
owner_approval_id = UUID(str(owner["approval_id"]))
approval = await approval_service.get_approval(owner_approval_id)
@@ -240,7 +253,7 @@ async def process_backup_restore_alertmanager_signal(
legacy_incident_id = ""
if approval is None:
recent = await approval_service.find_by_fingerprint(
fingerprint=source_fingerprint,
fingerprint=approval_storage_fingerprint,
debounce_minutes=30,
)
legacy_incident_id = _value(getattr(recent, "incident_id", ""))
@@ -253,7 +266,7 @@ async def process_backup_restore_alertmanager_signal(
try:
approval = await approval_service.create_approval_with_fingerprint(
request=request,
fingerprint=source_fingerprint,
fingerprint=approval_storage_fingerprint,
)
except Exception:
# A concurrent producer may have inserted the deterministic owner.

View File

@@ -15,7 +15,7 @@ from sqlalchemy import text
from sqlalchemy.ext.asyncio import create_async_engine
from src.jobs import backup_restore_legacy_backfill_job as backfill
from src.services import platform_operator_service
from src.services import backup_restore_alertmanager_ingress, platform_operator_service
def _explicit_local_test_database_url() -> str:
@@ -146,6 +146,42 @@ async def test_backfill_run_inserts_satisfy_no_default_cost_column() -> None:
await engine.dispose()
@pytest.mark.asyncio
async def test_long_occurrence_fingerprint_fits_real_pg_varchar() -> None:
engine = create_async_engine(_explicit_local_test_database_url(), echo=False)
source_fingerprint = "legacy-backup-occurrence:" + ("a" * 64)
stored_fingerprint = (
backup_restore_alertmanager_ingress._approval_storage_fingerprint(
source_fingerprint
)
)
try:
async with engine.connect() as connection:
transaction = await connection.begin()
try:
await connection.execute(text("""
CREATE TEMP TABLE approval_records (
fingerprint VARCHAR(64) NOT NULL
) ON COMMIT DROP
"""))
await connection.execute(
text("""
INSERT INTO approval_records (fingerprint)
VALUES (:fingerprint)
"""),
{"fingerprint": stored_fingerprint},
)
persisted = await connection.scalar(
text("SELECT fingerprint FROM approval_records")
)
assert persisted == stored_fingerprint
assert len(persisted) == 64
finally:
await transaction.rollback()
finally:
await engine.dispose()
@pytest.mark.asyncio
async def test_direct_readback_rewrites_ssl_and_keeps_rls_context_in_transaction(
monkeypatch: pytest.MonkeyPatch,

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import hashlib
from datetime import UTC, datetime
from types import SimpleNamespace
from unittest.mock import AsyncMock
@@ -46,7 +47,7 @@ def _signal_kwargs() -> dict[str, object]:
"lane": "backup_restore_escrow_triage",
},
"annotations": {"summary": "readback verifier required"},
"source_fingerprint": "source-fingerprint",
"source_fingerprint": "legacy-backup-occurrence:" + ("a" * 64),
"source_event_fingerprint": "alertmanager-native-fingerprint",
"source_started_at": "2026-07-11T10:00:00Z",
"alert_category": "backup",
@@ -84,6 +85,17 @@ def test_backup_ingress_owner_is_stable_per_source_occurrence() -> None:
assert first["read_only"] is True
def test_backup_approval_fingerprint_fits_durable_varchar_boundary() -> None:
short = "source-fingerprint"
long = "legacy-backup-occurrence:" + ("a" * 64)
assert ingress._approval_storage_fingerprint(short) == short
stored = ingress._approval_storage_fingerprint(long)
assert stored == hashlib.sha256(long.encode("utf-8")).hexdigest()
assert len(stored) == ingress.APPROVAL_FINGERPRINT_MAX_LENGTH
assert ingress._approval_storage_fingerprint(long) == stored
@pytest.mark.asyncio
async def test_alertmanager_type1_backup_queues_durable_owner_before_info_shortcut(
monkeypatch: pytest.MonkeyPatch,
@@ -167,12 +179,18 @@ class _FakeApprovalService:
async def find_by_fingerprint(self, **_kwargs):
self.calls.append("legacy_owner_read")
expected = ingress._approval_storage_fingerprint(
str(_signal_kwargs()["source_fingerprint"])
)
assert _kwargs["fingerprint"] == expected
return None
async def create_approval_with_fingerprint(self, *, request, fingerprint):
self.calls.append("owner_create")
self.created_request = request
assert fingerprint == "source-fingerprint"
assert fingerprint == ingress._approval_storage_fingerprint(
str(_signal_kwargs()["source_fingerprint"])
)
self.approval = SimpleNamespace(
id=UUID(request.metadata["preallocated_approval_id"]),
incident_id=request.incident_id,
@@ -218,6 +236,7 @@ async def test_backup_ingress_binds_canonical_incident_then_dispatches_and_reads
calls.append("agent99_dispatch")
assert kwargs["incident_id"] == incident_id
assert kwargs["route_id"] == "agent99:backup_health:BackupCheck"
assert kwargs["fingerprint"] == _signal_kwargs()["source_fingerprint"]
identity.update({
"incident_id": incident_id,
"run_id": "12345678-1234-5678-9234-567812345678",