Source code for aeat.adapters.persistence.storage.errors

"""Storage-layer exceptions.

All storage errors inherit from :class:`core.errors.AeatError` so callers can
catch domain-wide failures with a single base class.

The class tree:

- :class:`StorageError` — base for every storage error.
- :class:`PersistenceError` — base for the at-rest crypto, secret store,
  blob store, envelope, file lock, path containment, and audit-redaction
  surfaces. Subclass of :class:`StorageError` so existing catchers
  continue to work.
"""

from __future__ import annotations

from collections.abc import Mapping

from ....core.errors import AeatError


[docs] class SecureStorageError(AeatError): """Base class for secure-storage failures. This named base keeps the encrypted persistence, secret-store, bucket-session, and per-bucket lifecycle surfaces catchable as one family while still deriving from the central AEAT error registry. """
[docs] class StorageError(SecureStorageError): """Base class for every error raised by :mod:`adapters.persistence.storage`."""
[docs] class RepositoryError(StorageError): """Raised when a repository operation fails (not-found, integrity, etc.)."""
[docs] class RepositorySetupError(RepositoryError): """Raised when a concrete repository subclass is missing a required class attribute. Programming-contract guard: the attribute must be declared on the subclass before instantiation. Unlike a plain :class:`TypeError`, this error is enrolled in the AEAT error registry so it produces a structured envelope rather than an opaque interpreter-level exception. """
[docs] class SecureObjectRevisionConflictError(RepositoryError): """Raised when a revision-aware secure-object write sees a stale revision."""
[docs] class PersistenceError(StorageError): """Base class for governed-persistence error subtypes. Errors raised by the at-rest crypto primitives, the secret store, the encrypted blob store, the schema-version envelope, the file-lock helper, the path containment helper, and the audit-sink redaction contract all inherit from this class. """
[docs] class StorageValidationError(PersistenceError, ValueError): """Raised when a storage parameter fails validation (e.g. key length). Inherits from both :class:`PersistenceError` and :class:`ValueError` to remain compatible with Pydantic's validator-failure contract while allowing catch-all :class:`StorageError` handlers to detect integrity failures. """
_STORAGE_VALIDATION_MESSAGE_KEY = "errors.integrity.integrity_storage_validation"
[docs] def storage_validation_error(message: str) -> StorageValidationError: """Build a :class:`StorageValidationError` carrying the shared integrity message key. Single canonical factory for the storage-validation error that the persistence-storage submodules (crypto, envelope, runtime, secret store, and the master-key helpers) previously each declared identically. """ return StorageValidationError(message, translated_message=_STORAGE_VALIDATION_MESSAGE_KEY)
[docs] class EncryptionError(PersistenceError): """Base class for AEAD encryption / decryption failures."""
[docs] class DecryptionError(EncryptionError): """Raised when AEAD decryption fails (tag mismatch, malformed input)."""
[docs] class SecureObjectUnreadableError(DecryptionError): """Raised when one stored secure object cannot be decrypted under the current master key. Distinct from the generic :class:`DecryptionError` so iterator-shaped consumers can surface a structured per-row failure (namespace, row id, underlying cause) without aborting the iteration. The plaintext bound to such a row is cryptographically unrecoverable from this process: the master key under which it was sealed is no longer available. """ def __init__(self, namespace: str, row_id: int, *, cause: BaseException | None = None) -> None: """Construct the error, binding the affected namespace, row identifier, and optional root cause.""" super().__init__( context={"namespace": namespace, "row_id": row_id}, translated_message="errors.integrity.integrity_storage_secure_object_unreadable", ) self.namespace = namespace self.row_id = row_id self.__cause__ = cause
[docs] class KeyDerivationError(EncryptionError): """Raised when a key-derivation step fails."""
[docs] class NonceCollisionError(EncryptionError): """Raised on a defensive nonce-uniqueness invariant violation."""
[docs] class SecretStoreError(PersistenceError): """Base class for secret-store I/O failures."""
[docs] class SessionExpiredError(SecretStoreError): """Raised when the active :class:`BucketSession` has crossed its idle deadline. The session was opened earlier in the process lifetime but the operator did not act before the configured idle-lock window elapsed. The session is sealed; the operator must re-activate by running ``aeat config switch NAME`` (or a subsequent bootstrap-exempt verb that opens a fresh session). """
[docs] class PassphraseTooShortError(SecretStoreError): """Raised when an operator-supplied passphrase falls below the NIST floor. NIST SP 800-63B §5.1.1.1 mandates that verifiers SHALL require user- chosen memorized secrets to be at least 8 characters in length. The :class:`FileFallbackMasterKeyProvider` rejects shorter passphrases at resolution time. """
[docs] class KeyringUnavailableError(SecretStoreError): """Raised when the OS keychain backend is unusable. Either no backend is registered (e.g. headless Linux without libsecret), the backend rejected the operation, or the configured backend is the no-op ``null`` keyring. """
[docs] class MasterKeyUnavailableError(SecretStoreError): """Raised when no master key can be acquired from any provider."""
[docs] class MasterKeyKdfVersionError(MasterKeyUnavailableError): """Raised when the on-disk ``master.kdf`` declares a KDF version this build cannot consume. The substrate gates the master.kdf parameters by version. Mismatch means the operator's passphrase may be correct, but the on-disk parameters do not match this build's supported key-derivation contract. """
[docs] class MasterKeyKeychainLockedError(MasterKeyUnavailableError): """Raised when the OS keychain is reachable but the entry is locked. Distinct from :class:`KeyringUnavailableError` (no usable backend at all). This class signals a recoverable state: the operator unlocks the OS keychain (Touch ID / Windows Hello / desktop-wallet unlock) and retries. The CLI's error envelope renders the actionable hint. """
[docs] class MasterKeyPassphraseMismatchError(MasterKeyUnavailableError): """Raised when the file-fallback passphrase does not unwrap ``master.key``. Recoverable by re-entering the passphrase. If the passphrase has been forgotten, the operator can use ``aeat config recover`` to re-mint the master key from a recovery-key backup. The CLI's error envelope distinguishes this case from :class:`MasterKeyMaterialMissingError` so retries do not waste backoff budget on missing-file errors. """
[docs] class MasterKeyMaterialMissingError(MasterKeyUnavailableError): """Raised when no master-key material exists at all. Neither the keyring entry nor the file-fallback artefacts (``master.key`` / ``master.kdf`` / ``salt``) are present. The substrate has not been provisioned. The operator's actionable next step is ``aeat config profile create NAME`` or, if a recovery key is available, ``aeat config recover``. Raised by canonical read paths to distinguish "not provisioned" from "wrong passphrase" without minting key material. Explicit profile creation is responsible for provisioning; ordinary load paths fail closed with this class when material is absent. """
[docs] class UnsecuredModeRefusedError(SecretStoreError): """Raised when the unsecured backend is requested without proper gating. Two refusal classes: 1. The unsecured backend was selected (``aeat_secret_store_backend=unsecured``) but the operator did not set ``AEAT_ALLOW_UNENCRYPTED=1``. The hostile- named env var is the legible-and-embarrassing opt-out gate. 2. The unsecured backend is active AND the operator profile carries a real NIF/NIE/CIF (NIF-canary). Real tax data is incompatible with a published deterministic master key; the substrate refuses to write such records into the unsecured store. """
[docs] class ClassificationError(PersistenceError): """Raised when a record's declared sensitivity class is incompatible with its repository. Example: writing a CORPUS-class blob through the encrypted-blob path, or loading an envelope under a different classification than the one persisted on disk. """
[docs] class EnvelopeVersionError(PersistenceError): """Raised when an on-disk envelope version differs from the consumer contract."""
[docs] class PathContainmentError(PersistenceError, ValueError): """Raised when a computed path escapes its configured root directory.""" def __init__( self, message: str | None = None, *, context: Mapping[str, object] | None = None, ) -> None: """Construct a path-containment error with localized operator output.""" super().__init__( message, context=context, translated_message="errors.integrity.integrity_storage_path_containment", )
[docs] class BlobNotFoundError(PersistenceError): """Raised when a blob lookup misses on the encrypted blob store."""
[docs] class BlobIntegrityError(PersistenceError): """Raised when a blob's on-disk SHA-256 disagrees with its manifest."""
[docs] class SecretNotFoundError(SecretStoreError): """Raised when a secret-store ``get`` does not find a record for the requested key."""
[docs] class SecretAlreadyExistsError(SecretStoreError): """Raised when a secret-store ``put`` would overwrite an existing key without ``overwrite=True``."""
[docs] class RetentionPolicyError(PersistenceError): """Raised when a record's retention metadata violates its classification policy."""
[docs] class NamespaceRegistryError(StorageError, ValueError): """Raised when a namespace-registry key or definition violates a boot-time invariant. Fires from Pydantic field and model validators on :class:`~adapters.persistence.storage.SecureObjectNamespaceDefinition`, :class:`~adapters.persistence.storage.StoragePathDefinition`, and :class:`~adapters.persistence.storage.StorageHierarchyRegistry` when a registry key, namespace slug, path segment, or uniqueness constraint is violated at construction time. Inherits from :class:`StorageError` and ultimately from :class:`~core.errors.AeatError` so callers can catch it without importing Pydantic internals. Because these validators are called by Pydantic during model construction the exception propagates wrapped inside a :class:`pydantic.ValidationError` when raised from a field validator; direct callers of :class:`~adapters.persistence.storage.StorageHierarchyRegistry` model validators receive the raw :class:`NamespaceRegistryError`. """