fix(installer): delete provider secrets before retirement

This commit is contained in:
2026-07-21 15:46:24 +02:00
parent 39b199d7e6
commit 7843fe88e5
3 changed files with 124 additions and 9 deletions

View File

@@ -1,7 +1,10 @@
from __future__ import annotations
from dataclasses import replace
from pathlib import Path
from sqlalchemy import inspect
from govoplan_calendar.backend.db import models as calendar_models # noqa: F401 - populate Calendar ORM metadata
from govoplan_core.core.access import CAPABILITY_AUTH_PERMISSION_EVALUATOR, CAPABILITY_AUTH_PRINCIPAL_RESOLVER
from govoplan_core.core.calendar import (
@@ -24,6 +27,41 @@ from govoplan_core.core.modules import (
from govoplan_core.db.base import Base
_calendar_table_retirement_provider = drop_table_retirement_provider(
calendar_models.CalendarCollection,
calendar_models.CalendarEvent,
calendar_models.CalendarOutboxOperation,
calendar_models.CalendarSyncCredential,
calendar_models.CalendarSyncSource,
label="Calendar",
)
def _calendar_retirement_provider(session: object | None, module_id: str):
plan = _calendar_table_retirement_provider(session, module_id)
base_executor = plan.destroy_data_executor
if base_executor is None:
return plan
def executor(execute_session: object, execute_module_id: str) -> None:
if not hasattr(execute_session, "get_bind") or not hasattr(execute_session, "query"):
raise RuntimeError("No database session is available for Calendar credential retirement.")
if inspect(execute_session.get_bind()).has_table(calendar_models.CalendarSyncCredential.__tablename__):
from govoplan_calendar.backend.service import delete_calendar_credentials_for_retirement
delete_calendar_credentials_for_retirement(execute_session)
base_executor(execute_session, execute_module_id)
return replace(
plan,
destroy_data_warnings=(
*plan.destroy_data_warnings,
"Calendar-owned credentials are deleted immediately before tables are dropped; retirement fails if an external secret provider is unavailable.",
),
destroy_data_executor=executor,
)
def _permission(scope: str, label: str, description: str) -> PermissionDefinition:
module_id, resource, action = scope.split(":", 2)
return PermissionDefinition(
@@ -163,14 +201,7 @@ manifest = ModuleManifest(
metadata=Base.metadata,
script_location=str(Path(__file__).with_name("migrations") / "versions"),
retirement_supported=True,
retirement_provider=drop_table_retirement_provider(
calendar_models.CalendarCollection,
calendar_models.CalendarEvent,
calendar_models.CalendarOutboxOperation,
calendar_models.CalendarSyncCredential,
calendar_models.CalendarSyncSource,
label="Calendar",
),
retirement_provider=_calendar_retirement_provider,
retirement_notes="Destructive retirement drops calendar-owned database tables after the installer captures a database snapshot.",
),
uninstall_guard_providers=(

View File

@@ -1655,6 +1655,30 @@ def _provider_secret_is_absent(provider: Any, provider_ref: str) -> bool:
return False
def delete_calendar_credentials_for_retirement(session: Session) -> int:
"""Delete all Calendar-owned secrets before destructive module retirement.
This includes pre-hardening soft-deleted rows whose provider references or
ciphertext were retained. Provider deletion is intentionally fail-closed;
the caller must not drop Calendar tables if any external deletion fails.
"""
credentials = session.query(CalendarSyncCredential).order_by(CalendarSyncCredential.id.asc()).all()
deleted = 0
for credential in credentials:
metadata = credential.metadata_ if isinstance(credential.metadata_, dict) else {}
source_id = metadata.get("source_id")
if _delete_calendar_owned_credential(
session,
credential=credential,
source_id=str(source_id) if source_id else None,
deletion_reason="module_data_retired",
):
deleted += 1
session.flush()
return deleted
def resolve_caldav_secret(
session: Session,
*,