Source code for aeat.adapters.persistence.storage.bucket._manifest
"""Strict pydantic v2 record for the per-bucket plaintext manifest.The bucket manifest sits at ``<aeat-root>/buckets/<bucket-id>/manifest.toml``and carries only non-sensitive metadata: the bucket identifier and label,creation and unlock timestamps, the Argon2id KDF parameters and salt(salt is public per Argon2 design), the recovery-enrollment flag, and aschema version. The manifest never contains the derived key, the wrappedkey, the passphrase, or any byte derivable from them; those artefactstravel through the separate master-key surface.The :class:`ManifestKdfParams` nested model carried here is themanifest-side shape (algorithm tag, parameter version, the fourArgon2id cost parameters, salt). The canonical OWASP-pinnedconstructor and the parameter-window validators live alongside itunder ``master_key/_kdf_params.py``; manifest I/O wires the twotogether."""from__future__importannotationsfromdatetimeimportdatetimefromenumimportStrEnumfromtypingimportAnnotatedfrompydanticimportBaseModel,Field,field_serializer,field_validatorfrom.....coreimportSTRICT_FROZEN_CONFIGas_STRICT_FROZENfrom.....core.timeimportvalidate_utc_awarefrom.._kdf_saltimportdecode_kdf_salt,encode_kdf_salt,require_kdf_salt_length
[docs]classManifestKdfParams(BaseModel):"""Argon2id parameters and salt as carried in the bucket manifest. Strict pydantic v2 record. The canonical OWASP-baseline constructor is declared in :mod:`adapters.persistence.storage.master_key._kdf_params`; this manifest-side record holds whatever parameters the bucket was enrolled under so a future cost-bump can be non-breaking. """model_config=_STRICT_FROZENalgorithm:str=Field(min_length=1)version:int=Field(ge=1)memory_cost:int=Field(ge=1)time_cost:int=Field(ge=1)parallelism:int=Field(ge=1)salt:bytesoutput_length:int=Field(ge=16)@field_validator("salt")@classmethoddef_check_salt_length(cls,value:bytes)->bytes:returnrequire_kdf_salt_length(value)@field_serializer("salt")def_serialise_salt(self,value:bytes)->str:returnencode_kdf_salt(value)@field_validator("salt",mode="before")@classmethoddef_decode_salt(cls,value:object)->bytes:returndecode_kdf_salt(value)
[docs]classBucketLifecycleStatus(StrEnum):"""Plaintext lifecycle marker carried on the bucket manifest. The encrypted :class:`~domain.user_profile.UserProfileStatus` record is the lifecycle authority, but reading it costs a bucket decryption. The manifest mirrors that status as a plaintext marker so the manifest scan can exclude a tombstoned profile from every live operator surface (``list`` / ``switch`` / name-uniqueness) without unlocking the bucket. The :class:`ProfileRepository` is the sole writer of both stores and keeps the two in lockstep. Values match :class:`UserProfileStatus` so the application-layer repository maps the two enums one-to-one. """ACTIVE="active"TOMBSTONED="tombstoned"
[docs]classBucketKeySchedule(StrEnum):"""Data-key schedule used by encrypted records in this bucket."""BUCKET_DEK_V1="bucket-dek-v1"
[docs]classBucketManifest(BaseModel):"""Plaintext manifest for one per-bucket directory. The manifest never carries sensitive bytes; see :class:`ManifestKdfParams` for the salt contract. """model_config=_STRICT_FROZENbucket_id:Annotated[str,Field(min_length=1)]label:strcreated_at:datetimelast_unlocked_at:datetime|Nonekdf_params:ManifestKdfParamsrecovery_enrolled:boolidle_lock_minutes:int|None=Field(default=None,gt=0)key_schedule:BucketKeySchedule=BucketKeySchedule.BUCKET_DEK_V1schema_version:int=Field(ge=1)status:BucketLifecycleStatus"""Plaintext mirror of the encrypted record's lifecycle status. Required, with no default. A manifest that omits ``status`` is rejected at the read boundary (fail-closed) rather than silently hydrating as a live profile — a silent default would risk leaking a tombstoned bucket back onto the operator surface. The :class:`ProfileRepository` sets it explicitly on every write: ``ACTIVE`` at creation, ``TOMBSTONED`` in the same write that tombstones the encrypted record. """@field_validator("status",mode="before")@classmethoddef_coerce_status(cls,value:object)->BucketLifecycleStatus:"""Accept the TOML-native string form of the lifecycle marker. The strict model rejects a bare ``str`` for the enum field, but the on-disk TOML manifest stores ``status`` as a plain string. This pre-validator parses that string back into the enum so the manifest round-trips while the public constructor still type-checks an explicit :class:`BucketLifecycleStatus`. """ifisinstance(value,BucketLifecycleStatus):returnvalueifisinstance(value,str):returnBucketLifecycleStatus(value)raiseValueError("status must be a BucketLifecycleStatus or its string value")@field_validator("key_schedule",mode="before")@classmethoddef_coerce_key_schedule(cls,value:object)->BucketKeySchedule:ifisinstance(value,BucketKeySchedule):returnvalueifisinstance(value,str):returnBucketKeySchedule(value)raiseValueError("key_schedule must be a BucketKeySchedule or its string value")@field_validator("created_at")@classmethoddef_check_created_at(cls,value:datetime)->datetime:returnvalidate_utc_aware(value)@field_validator("last_unlocked_at")@classmethoddef_check_last_unlocked_at(cls,value:datetime|None)->datetime|None:ifvalueisNone:returnNonereturnvalidate_utc_aware(value)