cadrumo.adapters.persistence.storage.master_key.active_session module

Active-bucket session resolution for the column-level encrypt path.

The column-level TypeDecorator set in adapters/persistence/storage/crypto/encrypted_columns.py cannot thread an explicit session reference through SQLAlchemy’s process_bind_param() signature (the method is invoked by SQLAlchemy’s column machinery with a fixed (self, value, dialect) shape). The substrate also forbids module-global mutable state that could survive a bucket switch – the BucketSession instance is the only legitimate owner of unlocked DEK bytes.

This module composes both constraints with a ProcessScopedBinding holding the active BucketSession. The binding holds the session OBJECT, never its key bytes, so the session remains the sole owner of unlocked DEK material and BucketSession.close() remains the sole zeroisation boundary.

Who logged in is a fact about the PROCESS, not about one span of work. Exactly one profile is unlocked per process, and every surface that reads encrypted storage – a parsed CLI invocation, a Textual credential screen running on a thread worker, a second asyncio.run() frame in the same program – must observe the same answer. A plain ContextVar cannot carry that: a login performed inside a copied context is discarded when that context ends, which reaches the operator as a correct authentication followed by a surface that cannot find the profile it just unlocked.

So bind_active_bucket_session() publishes process-wide, and activate_session() still shadows that publication for the duration of a block – nested activations stack and unwind exactly as before, and no binding made by the context manager outlives its with. close_active_bucket_session() is the explicit eviction boundary: it closes the current session before removing that exact binding.

active_session: ProcessScopedBinding[BucketSession]

The one live BucketSession this process has unlocked, if any.

exception NoActiveBucketSessionError(detail=None)[source]

Bases: SecretStoreError

Raised when the encrypt path runs outside an active session block.

The adapter establishes only the observed session state. The CLI boundary decides whether a public profile label makes the canonical login action resolvable; this substrate must not manufacture that action from custody internals.

Parameters:

detail (str | None)

Return type:

None

code
activate_session(session)[source]

Bind session as the active BucketSession for the block.

The binding this installs is scoped to the block: nested activations stack and unwind cleanly, and on exit the caller observes exactly what it observed before, whether that was an enclosing activation or the process-wide binding bind_active_bucket_session() published. The session itself is not closed on exit — ownership of the BucketSession lifecycle stays with the caller that opened it.

Parameters:

session (BucketSession) – The unlocked BucketSession whose DEK becomes the column-level encryption key for the duration of the block.

Return type:

Generator[None]

bind_active_bucket_session(session)[source]

Publish session as this PROCESS’s active session.

The unscoped counterpart of activate_session(), for the caller shape that has no enclosing with block: an authenticated or resumed profile session, whose binding must outlive the function that opened it and is evicted explicitly by close_active_bucket_session() (or by the interpreter-exit hook).

The publication is process-wide because the fact is: one profile is unlocked per process, and a credential surface that authenticates on a thread worker or inside its own event loop must be visible to the surface that reads storage afterwards. Callers that DO have a scope, and mean to shadow this binding only for that scope, use activate_session() instead.

Parameters:

session (BucketSession) – The unlocked BucketSession to bind.

Return type:

None

get_active_master_key()[source]

Return the DEK bytes of the currently-active 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 while the session is being opened.

Return type:

bytes

Returns:

The 32-byte DEK used for AES-256-GCM column-level encryption.

Raises:
get_active_hmac_subkey(context)[source]

Return the active session’s memoised HKDF sub-key for context.

The keyed-lookup digest path (HashedLookup and the secure-object key digest built on it) derives a per-consumer sub-key from the active DEK before HMAC-ing its material. That derivation depends only on (DEK, context), so it is memoised on the BucketSession (see BucketSession.hmac_subkey()) and resolved here under exactly the freshness checks get_active_master_key() applies.

Parameters:

context (bytes) – Stable per-consumer HKDF info bytes.

Return type:

bytes

Returns:

The 32-byte derived sub-key.

Raises:
has_active_bucket_session()[source]

Return whether an active BucketSession is bound.

Return type:

bool

class ActiveProfileSessionPresenceAdapter(*args, **kwargs)[source]

Bases: ActiveProfileSessionPresencePort

Expose active-session presence through the application probe contract.

is_bound()[source]

Return the current active bucket-session presence.

Return type:

bool

current_active_bucket_session()[source]

Return the currently-bound BucketSession, or None.

Read-only observation of the active-session 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 (get_active_master_key()) or its presence (has_active_bucket_session()). Never mutates the context; activate_session(), suspend_active_session(), and close_active_bucket_session() own binding changes.

Return type:

BucketSession | None

session_serves_bucket(session, bucket_id)[source]

Return whether session is open for exactly bucket_id.

The single bucket-identity comparison for session reuse. Callers holding an explicitly-passed session (the auth scope resolves one, then layers storage-root and explicit-routing agreement on top) use this; callers reading the ambient binding use active_bucket_session_serves(), which is this function applied to the active-session ContextVar.

One comparison, two entry points: an injected-session caller must not re-derive session.bucket_id == bucket_id locally, because that is the comparison whose omission produces the cross-bucket read.

Typed as a TypeGuard so a caller that passes the guard may read the session’s own attributes without a redundant is not None re-check. TypeIs would be unsound here: this returns False for a perfectly non-None session bound to a different bucket, which must not narrow the negative branch to None.

Return type:

TypeGuard[BucketSession]

Parameters:
active_bucket_session_serves(bucket_id)[source]

Return whether the bound session is open for exactly bucket_id.

The single reuse predicate for callers deciding whether an ambient session already serves the bucket they are about to read or write, rather than opening a second span over it.

has_active_bucket_session() answers a strictly weaker question – whether any session is bound – and a caller that resolves a target bucket and then reuses on presence alone will operate against whichever bucket happens to be bound. The two differ exactly when a session for one bucket is ambient while the caller targets another, which is the case that reads or writes one profile’s encrypted store under another profile’s identity. Callers that hold a target bucket MUST use this predicate; has_active_bucket_session() remains correct only for callers with no target to compare against.

Storage-root and explicit-routing agreement are a separate, stricter concern layered on top of this by active_profile_storage_span(); this function owns the bucket-identity half that every caller needs.

Return type:

bool

Parameters:

bucket_id (str)

close_active_bucket_session()[source]

Close and evict the currently bound BucketSession.

The existing BucketSession.close() boundary owns key zeroisation, sealing, and engine disposal. This function adds only active-context eviction. It is idempotent when no session is bound or when the bound session is already sealed.

The binding is cleared in finally so an unexpected close failure cannot leave a key-owning or sealed object advertised as active. An identity check preserves a replacement binding installed reentrantly during cleanup.

Return type:

None

suspend_active_session()[source]

Temporarily clear the active BucketSession for the current context.

Return type:

Generator[None]