cadrumo.adapters.persistence.storage.envelope.secure_bound_repository module¶
Generic SQL-backed envelope repository for secure objects.
Concrete domain and application repositories that wrap
adapters.persistence.storage.sql.secure_objects.SecureObjectRepository
all share the same boilerplate:
adapters.persistence.storage.envelope.contract.Envelope wrapping, namespace,
sensitivity, schema-version, Pydantic payload type, and a function that
extracts the natural id from the payload.
This module provides
adapters.persistence.storage.envelope.secure_bound_repository.SecureBoundRepository, a
generic base class that captures that shared shape exactly once. Concrete
subclasses override the four class-level descriptors (namespace,
payload_type, sensitivity, schema_version) and implement
extract_identifier; they inherit envelope_path_for,
lock_target_for, load, save, to_secure_object_write,
delete, iter_ids, and iter_records for free.
The base class does NOT replace
adapters.persistence.storage.sql.secure_objects.SecureObjectRepository; it
composes one.
- class SecureBoundRepository(*, bucket_id=None, objects=None, settings=None)[source]¶
Bases:
GenericGeneric repository over encrypted SQL-backed envelopes for one payload type.
Subclasses MUST set class attributes:
namespace: theadapters.persistence.storage.sql.secure_objects.SecureObjectRepositorynamespace string for this payload family (e.g."cadrumo.domain.filing.drafts").payload_type: the typed Pydantic model class wrapped by the envelope.sensitivity: theSensitivityClassthat every row in this namespace MUST carry; mismatches raiseadapters.persistence.storage.errors.ClassificationError.schema_version: the current envelope schema version this consumer expects; rows whose version differs from it raiseadapters.persistence.storage.errors.EnvelopeVersionError.
Subclasses MUST implement
extract_identifier()so thatsave()anditer_ids()can recover the natural id from the decrypted payload.- Parameters:
bucket_id (str | None)
objects (SecureObjectRepository | None)
settings (Settings | None)
-
sensitivity:
ClassVar[SensitivityClass]¶
- extract_identifier(payload)[source]¶
Return the natural id for
payload(used as the SQL object key).Subclasses MUST override. The base implementation raises
NotImplementedError.- Return type:
str
- Parameters:
payload (T)
- classmethod payload_model()[source]¶
Return the concrete Pydantic payload model for this repository.
- Return type:
type[T]
- envelope_path_for(identifier)[source]¶
Return a logical path marker for
identifier.- Return type:
Path- Parameters:
identifier (str)
- lock_target_for(identifier)[source]¶
Return a logical lock-target marker for
identifier.SQL transactions govern actual write atomicity; this is only surfaced for diagnostic parity with file-backed repositories.
- Return type:
Path- Parameters:
identifier (str)
- property secure_object_repository: SecureObjectRepository¶
Return the concrete secure-object backend.
- Returns:
The
adapters.persistence.storage.sql.secure_objects.SecureObjectRepositorybacking this logical repository.
- load(identifier)[source]¶
Return the persisted payload or
Noneif absent.The payload’s own natural identity is compared with the key it was looked up under.
savederives the row key from the payload viaextract_identifier(), so the two are meant to be one fact in two encodings – but nothing on the read path checked that, and a row written under a different key returned its payload unremarked. A caller asking for one record was handed another and had no way to tell: the returned object describes itself truthfully, it simply is not the one that was requested.- Raises:
ClassificationError – Refused by the shared envelope gate.
EnvelopeVersionError – Refused by the shared envelope gate.
SecureObjectRowIdentityError – The payload’s natural identity differs from
identifier. Raised rather than returningNone, because the row exists – reporting it absent would hide a real inconsistency behind an ordinary miss.
- Return type:
T | None
- Parameters:
identifier (str)
- save(payload)[source]¶
Persist
payloadas an encrypted envelope row.The natural id is recovered from the payload via
extract_identifier().- Return type:
None
- Parameters:
payload (T)
- to_secure_object_write(payload, *, expected_revision_id=None)[source]¶
Return the prepared upsert for
payloadwithout committing it.The caller hands the returned
SecureObjectWritetoadapters.persistence.storage.sql._secure_object_writes.SecureObjectWriteOperations.save_many()(orapply_batch) alongside a sibling repository’s write, so both land in ONE SQL unit of work and a crash between them is impossible. That is the same co-emit shape the filing path already uses to keep the participation index from drifting from the filing catalogue; the catalogue repositories exposeto_secure_object_writefor exactly this, and this method extends it to every envelope-bound repository.The write carries the identical
adapters.persistence.storage.envelope.contract.Envelope, classification and schema versionsave()would persist directly — both build it through_identified_envelope(), so the committed and the prepared forms cannot drift apart.
- to_secure_object_deletion(identifier)[source]¶
Return the prepared removal for
identifierwithout committing it.The deletion counterpart of
to_secure_object_write(), so a caller that must remove some rows and write others can hand both toadapters.persistence.storage.sql._secure_object_writes.SecureObjectWriteOperations.apply_batch()and land the whole change in ONE SQL unit of work.adapters.persistence.storage.sql.secure_object_records.SecureObjectDeletionaddresses a row by its stored HMAC digest, so the natural id is bound here with the same digest the storage column uses.- Return type:
- Parameters:
identifier (str)
- replace_records(replacements, stale_identifiers)[source]¶
Commit
replacementsand removestale_identifiersin one transaction.The atomic set-replace primitive: a caller that must clear a window of rows and write its successor set gets all-or-nothing semantics instead of a delete loop followed by a save loop, where a failure part-way through leaves the store holding neither the old set nor the new one.
An identifier that is BOTH stale and replaced is written, not deleted:
adapters.persistence.storage.sql._secure_object_writes.SecureObjectWriteOperations.apply_batch()applies writes before deletions, so a row carried across the replacement would otherwise be upserted and then removed in the same transaction.- Parameters:
replacements (Sequence[T]) – The payloads that constitute the new set.
stale_identifiers (Iterable[str]) – Natural ids of rows to remove. Ids also carried by
replacementsare ignored.
- Return type:
None
- iter_ids()[source]¶
Yield every persisted identifier in storage order.
Order is the secure-object storage order (the
object_keydigest order), not the natural-id order: a caller that needs a specific order sorts the result itself. Streams one identifier at a time rather than buffering and sorting the whole namespace in memory.Every id is the one its own row is filed under: the scan refuses a row whose payload rebuilds a different key rather than reporting an id the store does not actually hold that record at.
- Raises:
SecureObjectRowIdentityError – A row’s payload rebuilds a different natural id than the key it is filed under.
- Return type:
- iter_records()[source]¶
Yield every persisted payload whose identity matches the key it is filed under.
Streams each payload straight from
adapters.persistence.storage.sql.secure_objects.SecureObjectRepository.list_records()without buffering the whole namespace or sorting it in memory. Order is storage-defined (theobject_keydigest order), not the natural-id order; a caller that needs a specific order sorts the result itself.Parses each row’s decrypted payload bytes directly from
list_recordsrather than routing throughload(). The SQLWHERE object_key = ?lookup insideload()cannot match the stored ciphertext whenobject_keyis an randomized ciphertext column (AES-256-GCM uses a random nonce, so the bind-parameter ciphertext differs from the stored ciphertext every time). Iterating directly over the decrypted rows is the correct pattern for full-scan enumeration.Verification is the DEFAULT, not an opt-in a caller must remember: a scan that filters on payload fields trusts each record to describe the key it is stored under, and the callers most exposed to that – window counts, carry-forward reads, reconciliation history – are the ones least able to notice a foreign row. An opt-in variant only protects the callers that already suspected the problem.
- Raises:
SecureObjectRowIdentityError – A row’s payload rebuilds a different natural id than the key it is filed under.
- Return type:
Iterator[T]