fix(secrets): audit connector credential deletion
This commit is contained in:
@@ -12,6 +12,7 @@ from sqlalchemy import and_, false, func, or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from govoplan_core.auth import ApiPrincipal
|
||||
from govoplan_core.audit.logging import audit_event
|
||||
from govoplan_core.core.change_sequence import record_change
|
||||
from govoplan_core.db.base import utcnow
|
||||
from govoplan_core.security.secrets import decrypt_secret, encrypt_secret
|
||||
@@ -352,9 +353,70 @@ def delete_sync_source(session: Session, principal: ApiPrincipal, sync_source_id
|
||||
book.sync_status = None
|
||||
book.sync_error = None
|
||||
book.updated_by_account_id = _account_id(principal)
|
||||
_audit_sync_credential_deletion(session, principal, sync_source)
|
||||
session.delete(sync_source)
|
||||
|
||||
|
||||
def _audit_sync_credential_deletion(
|
||||
session: Session,
|
||||
principal: ApiPrincipal,
|
||||
sync_source: AddressSyncSource,
|
||||
) -> None:
|
||||
"""Audit removal of source-owned credential material in the DB transaction.
|
||||
|
||||
CardDAV credentials are encrypted inside the sync-source row. Deleting that
|
||||
row therefore deletes the credential atomically with the connector. Legacy
|
||||
credential references are detached from the source, but are never resolved
|
||||
or sent to an external provider because ownership cannot be proven.
|
||||
"""
|
||||
|
||||
tenant_id = _tenant_id_or_none(principal)
|
||||
user = getattr(principal, "user", None)
|
||||
_record_sync_credential_deletion_audit(
|
||||
session,
|
||||
sync_source=sync_source,
|
||||
tenant_id=tenant_id,
|
||||
user_id=getattr(user, "id", None),
|
||||
api_key_id=getattr(principal, "api_key_id", None),
|
||||
deletion_reason="sync_source_deleted",
|
||||
)
|
||||
|
||||
|
||||
def _record_sync_credential_deletion_audit(
|
||||
session: Session,
|
||||
*,
|
||||
sync_source: AddressSyncSource,
|
||||
tenant_id: str | None,
|
||||
user_id: str | None,
|
||||
api_key_id: str | None,
|
||||
deletion_reason: str,
|
||||
) -> bool:
|
||||
auth = _carddav_auth_metadata(sync_source.metadata_)
|
||||
if auth.get("secret_encrypted"):
|
||||
storage_backend = "encrypted_database"
|
||||
elif auth.get("credential_ref"):
|
||||
storage_backend = "legacy_reference"
|
||||
else:
|
||||
return False
|
||||
audit_event(
|
||||
session,
|
||||
tenant_id=tenant_id,
|
||||
user_id=user_id,
|
||||
api_key_id=api_key_id,
|
||||
action="addresses.sync_credential_deleted",
|
||||
scope="tenant" if tenant_id is not None else "system",
|
||||
object_type="address_sync_credential",
|
||||
object_id=sync_source.id,
|
||||
details={
|
||||
"sync_source_id": sync_source.id,
|
||||
"connector_type": sync_source.connector_type,
|
||||
"storage_backend": storage_backend,
|
||||
"deletion_reason": deletion_reason,
|
||||
},
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
def start_sync_attempt(session: Session, principal: ApiPrincipal, sync_source_id: str) -> AddressSyncSource:
|
||||
sync_source = get_visible_sync_source(session, principal, sync_source_id)
|
||||
if not sync_source.enabled:
|
||||
|
||||
Reference in New Issue
Block a user