Source code for aeat.adapters.persistence.storage.master_key._active_session
"""Active-bucket session resolution for the column-level encrypt path.The column-level :class:`TypeDecorator` set in``adapters/persistence/storage/crypto/_encrypted_columns.py`` cannotthread an explicit session reference through SQLAlchemy's:meth:`process_bind_param` signature (the method is invoked bySQLAlchemy's column machinery with a fixed ``(self, value, dialect)``shape). The substrate also forbids module-global mutable state thatcould survive a bucket switch — the :class:`BucketSession` instance isthe only legitimate owner of unlocked KEK and DEK bytes.This module composes both constraints with a ``ContextVar`` (PEP 567)holding the active :class:`BucketSession`. The CLI entry point opensa session and enters :func:`activate_session` as a contextmanager;every column-level decrypt or encrypt call inside the block resolvesthe active DEK through :func:`get_active_master_key`. On exit the``ContextVar`` token is reset to the previous value (``None`` at thetop of the stack), so no key material outlives the with-block.The pattern is per-thread and per-async-task by PEP 567 semantics.``asyncio.Task`` instances inherit a copy of the parent context atcreation time, so the active session crosses into spawned taskscorrectly. :class:`concurrent.futures.ThreadPoolExecutor` workers doNOT inherit ``ContextVar`` state by default; future code introducinga thread-pool worker on the encrypt path must propagate the activesession explicitly via :func:`contextvars.copy_context`."""from__future__importannotationsimportatexitas_atexitfromcollections.abcimportIteratorfromcontextlibimportcontextmanagerfromcontextvarsimportContextVarfromtypingimportoverridefrom.....core.errorsimportresolve_error_messagefrom.....core.loggingimportget_loggerfrom.....core.timeimportnowfrom..bucketimportBucketLockedErrorfrom..errorsimportSecretStoreErrorfrom._bucket_sessionimportBucketSession_log=get_logger(__name__)_active_session:ContextVar[BucketSession|None]=ContextVar("aeat_active_bucket_session",default=None,)
[docs]classNoActiveBucketSessionError(SecretStoreError):"""Raised when the encrypt path runs outside an active session block. Carries no payload — the diagnostic message names the canonical remediation verb so operators see how to recover without re-parsing the message. """def__init__(self,detail:str|None=None)->None:super().__init__(context={"detail":detail}ifdetailelseNone,translated_message="errors.refused.refused_storage_master_key_no_active_session",)self._detail=detail@overridedef__str__(self)->str:"""Render the locale-backed remediation message while keeping positional args empty."""returnresolve_error_message(self)
[docs]@contextmanagerdefactivate_session(session:BucketSession)->Iterator[None]:"""Bind ``session`` as the active :class:`BucketSession` for the block. The previous value of the :class:`ContextVar` is restored on exit via the :class:`contextvars.Token` returned by ``set()``, so nested activations stack and unwind cleanly. The session itself is not closed on exit — ownership of the :class:`BucketSession` lifecycle stays with the caller that opened it. Args: session: The unlocked :class:`BucketSession` whose DEK becomes the column-level encryption key for the duration of the block. """token=_active_session.set(session)try:yieldfinally:_active_session.reset(token)
[docs]defget_active_master_key()->bytes:"""Return the DEK bytes of the currently-active :class:`BucketSession`. Used by every column-level encrypt and decrypt operation in ``_encrypted_columns.py``. The DEK (not the KEK) is the AES-256-GCM key for the row-ciphertext layer — the KEK only ever unwraps the DEK during :meth:`BucketSession.open`. Returns: The 32-byte DEK used for AES-256-GCM column-level encryption. Raises: NoActiveBucketSessionError: When no :func:`activate_session` block is currently active on the calling thread or task. BucketLockedError: When the active session has expired. """session=_active_session.get()ifsessionisNone:raiseNoActiveBucketSessionError("no active bucket session; run `aeat config switch NAME` ""to unlock a profile before invoking commands that decrypt ""stored records.",)ifsession.is_expired(now()):bucket_id=session.bucket_idsession.close()raiseBucketLockedError(bucket_id=bucket_id)returnsession.dek
[docs]defhas_active_bucket_session()->bool:"""Return whether an active :class:`BucketSession` is bound."""return_active_session.get()isnotNone
[docs]defcurrent_active_bucket_session()->BucketSession|None:"""Return the currently-bound :class:`BucketSession`, or ``None``. Read-only observation of the active-session :class:`~contextvars.ContextVar` for callers (storage runtime readiness, per-request secure-object session gating) that need the live session's attributes (``bucket_id``, ``sealed``, idle deadline) rather than only its DEK (:func:`get_active_master_key`) or its presence (:func:`has_active_bucket_session`). Never mutates the context; only :func:`activate_session` and :func:`suspend_active_session` may bind or clear it. """return_active_session.get()
[docs]@contextmanagerdefsuspend_active_session()->Iterator[None]:"""Temporarily clear the active :class:`BucketSession` for the current context."""token=_active_session.set(None)try:yieldfinally:_active_session.reset(token)
def_close_active_session_at_exit()->None:"""Best-effort close of the active session on interpreter shutdown. Registered as an :func:`atexit.register` hook below. If a session is still bound when the interpreter exits (an interrupted CLI run, a crashed test, a long-lived REPL) this hook zeroises the key buffers in place so the memory footprint at shutdown does not leak cleartext key material. """session=_active_session.get()ifsessionisNone:returntry:session.close()exceptExceptionasexc:# Interpreter shutdown is a degraded environment; never raise# from an atexit hook, but keep a debug breadcrumb for audit._log.debug("active bucket session cleanup failed at interpreter exit error_type=%s",type(exc).__name__)return_atexit.register(_close_active_session_at_exit)__all__=["NoActiveBucketSessionError","activate_session","current_active_bucket_session","get_active_master_key","has_active_bucket_session","suspend_active_session",]