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
BucketSessionthis process has unlocked, if any.
- exception NoActiveBucketSessionError(detail=None)[source]¶
Bases:
SecretStoreErrorRaised 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
sessionas the activeBucketSessionfor 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 theBucketSessionlifecycle stays with the caller that opened it.- Parameters:
session (
BucketSession) – The unlockedBucketSessionwhose DEK becomes the column-level encryption key for the duration of the block.- Return type:
- bind_active_bucket_session(session)[source]¶
Publish
sessionas this PROCESS’s active session.The unscoped counterpart of
activate_session(), for the caller shape that has no enclosingwithblock: an authenticated or resumed profile session, whose binding must outlive the function that opened it and is evicted explicitly byclose_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 unlockedBucketSessionto bind.- Return type:
- 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:
- Returns:
The 32-byte DEK used for AES-256-GCM column-level encryption.
- Raises:
NoActiveBucketSessionError – When no
activate_session()block is currently active on the calling thread or task.BucketLockedError – When the active session has expired.
- get_active_hmac_subkey(context)[source]¶
Return the active session’s memoised HKDF sub-key for
context.The keyed-lookup digest path (
HashedLookupand 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 theBucketSession(seeBucketSession.hmac_subkey()) and resolved here under exactly the freshness checksget_active_master_key()applies.- Parameters:
context (
bytes) – Stable per-consumer HKDF info bytes.- Return type:
- Returns:
The 32-byte derived sub-key.
- Raises:
NoActiveBucketSessionError – When no
activate_session()block is currently active on the calling thread or task.BucketLockedError – When the active session has expired.
- has_active_bucket_session()[source]¶
Return whether an active
BucketSessionis bound.- Return type:
- class ActiveProfileSessionPresenceAdapter(*args, **kwargs)[source]¶
Bases:
ActiveProfileSessionPresencePortExpose active-session presence through the application probe contract.
- current_active_bucket_session()[source]¶
Return the currently-bound
BucketSession, orNone.Read-only observation of the active-session
ContextVarfor 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(), andclose_active_bucket_session()own binding changes.- Return type:
- session_serves_bucket(session, bucket_id)[source]¶
Return whether
sessionis open for exactlybucket_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-sessionContextVar.One comparison, two entry points: an injected-session caller must not re-derive
session.bucket_id == bucket_idlocally, because that is the comparison whose omission produces the cross-bucket read.Typed as a
TypeGuardso a caller that passes the guard may read the session’s own attributes without a redundantis not Nonere-check.TypeIswould be unsound here: this returnsFalsefor a perfectly non-Nonesession bound to a different bucket, which must not narrow the negative branch toNone.- Return type:
TypeGuard[BucketSession]- Parameters:
session (BucketSession | None)
bucket_id (str)
- 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.
- 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
finallyso 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:
- suspend_active_session()[source]¶
Temporarily clear the active
BucketSessionfor the current context.