Source code for aeat.application.user_profile._integrity
"""Cross-store integrity validation for a logical profile.A logical profile fragments across a bucket directory, a plaintextmanifest, and an encrypted secure-object row. Each store independentlyrecords the profile UUID; a torn write, a hand-edited manifest, or ahalf-rolled-back create can leave them disagreeing. Serving such aprofile silently is the ``missing_profile_record`` / ghost-profiledefect class.:func:`verify_profile_integrity` is the read-time gate:class:`ProfileRepository.load` runs on every load. It checks that themanifest ``bucket_id``, the on-disk directory name, and the:class:`UserProfileRecord.profile_id` all agree on the profile UUID.Decryption and pydantic validation of the record happen earlier in``load`` itself — by the time this gate runs the record is already avalidated object, so this function is a pure cross-store identitycomparison. It raises :class:`ProfileIntegrityError` on anydisagreement — the repository never returns an inconsistent aggregate."""from__future__importannotationsfrom...domain.user_profileimportProfileNotFoundError_INTEGRITY_IDENTITY_DRIFT_MESSAGE="profile physical stores disagree on identity"_INTEGRITY_STATUS_DRIFT_MESSAGE="profile physical stores disagree on lifecycle status"_INTEGRITY_LABEL_DRIFT_MESSAGE="profile physical stores disagree on label"
[docs]classProfileIntegrityError(ProfileNotFoundError):"""Raised when a profile's physical stores disagree on its identity. Inherits from :class:`ProfileNotFoundError` so existing handlers that catch the broader profile-resolution family also catch cross-store drift; an inconsistent profile is, for every consumer purpose, not a usable profile. """
[docs]defverify_profile_integrity(*,profile_id:str,directory_name:str,manifest_bucket_id:str,record_profile_id:str,manifest_status:str,record_status:str,manifest_label:str,record_display_name:str,)->None:"""Assert every physical store agrees on the profile UUID and status. The three cross-store identity claims are: - ``directory_name`` — the ``<root>/buckets/<name>/`` directory the bucket lives in. - ``manifest_bucket_id`` — the ``bucket_id`` key in the plaintext ``manifest.toml``. - ``record_profile_id`` — the ``profile_id`` on the decrypted :class:`UserProfileRecord`. All three must equal ``profile_id``. The lifecycle status is denormalised: the encrypted :class:`UserProfileRecord` carries the authoritative status and the plaintext manifest mirrors it so the manifest scan can exclude a tombstoned profile without decryption. ``manifest_status`` and ``record_status`` are the two copies of that mirror, compared by their string value. A disagreement is the drift state that re-opens the tombstone leak — a manifest saying ``active`` over a tombstoned record would let ``list`` / ``switch`` serve a deleted profile — so it must be surfaced, never served. The display label is denormalised the same way: ``rename`` writes the encrypted record ``display_name`` and the plaintext manifest ``label`` as two copies of one value. A crash between those two writes leaves them divergent, and without this gate ``load`` would silently serve the stale manifest label over the renamed record forever, with no signal for ``repair``. ``manifest_label`` and ``record_display_name`` are compared by their string value and a disagreement is surfaced, never served. A mismatch raises :class:`ProfileIntegrityError` with sanitized context naming the disagreeing stores so a ``repair`` surface can act on a concrete delta without echoing raw profile identifiers. Args: profile_id: The canonical UUID the caller expects all stores to agree on. directory_name: The ``<root>/buckets/<name>/`` directory name. manifest_bucket_id: The ``bucket_id`` key from the plaintext manifest. record_profile_id: The ``profile_id`` from the decrypted record. manifest_status: The lifecycle status mirrored in the manifest. record_status: The authoritative lifecycle status from the record. manifest_label: The display label mirrored in the manifest. record_display_name: The authoritative display label from the record. Raises: ProfileIntegrityError: If any store disagrees on the UUID, the manifest and record disagree on the lifecycle status, or they disagree on the display label. """mismatches:list[str]=[]ifdirectory_name!=profile_id:mismatches.append("bucket_directory")ifmanifest_bucket_id!=profile_id:mismatches.append("manifest_bucket_id")ifrecord_profile_id!=profile_id:mismatches.append("secure_record_profile_id")ifmismatches:raiseProfileIntegrityError(_INTEGRITY_IDENTITY_DRIFT_MESSAGE,translated_message="application.user_profile.errors.profile_integrity_identity_mismatch",context={"mismatches":tuple(mismatches)},)ifmanifest_status!=record_status:raiseProfileIntegrityError(_INTEGRITY_STATUS_DRIFT_MESSAGE,translated_message="application.user_profile.errors.profile_integrity_status_mismatch",context={"mismatches":("manifest_status","secure_record_status")},)ifmanifest_label!=record_display_name:raiseProfileIntegrityError(_INTEGRITY_LABEL_DRIFT_MESSAGE,translated_message="application.user_profile.errors.profile_integrity_label_mismatch",context={"mismatches":("manifest_label","secure_record_display_name")},)