Source code for cadrumo.adapters.outbound.storage._key_validation
"""One admissibility rule for the object key every storage backend receives.``object_key_hmac`` is a CONTRACT-level value, not a backend-level one: it isthe same HMAC of the same object key whichever backend stores it, and itscharacter set is fixed by the digest that produces it rather than by anythingthe filesystem or Drive can express. So the two backends have no businessdisagreeing about which keys are admissible -- and before this module they did.Local enforced ``[alnum-_]``; Drive enforced only non-blank.That asymmetry mattered because of what happens downstream.:func:`~adapters.outbound.storage._object_name.provider_object_hmac_prefix` isa bare slice with no sanitisation, deliberately: its module delegatesadmissibility to the caller, and the ``sanitize_provider_object_label`` helperone function below it -- which DOES sanitise -- shows the delegation is adesign choice rather than an oversight. Local discharged the delegated duty andDrive did not, while Drive interpolates that unsanitised prefix into a querystring (``name contains '<prefix>--'``).**Severity, stated honestly: latent, not exploitable.** Every productionproducer emits a value that already satisfies the strict rule --``remote_mirror_object_key_hmac`` and the mirror manifest's own key are both``sha256_hex`` (64 lowercase hex), and both providers' ``probe`` sentinels are``"00000000probe"``. No path existed by which a hostile key reached the query.What is closed here is a divergence between two backends implementing onecontract, not a live injection.The rule is the STRICTER of the two former behaviours. Tightening is the safedirection: Drive begins refusing keys nothing legitimate produces, whereasloosening local would let a backend accept a key its sibling rejects andreintroduce the split. Confirmed before landing that no key any writer in thisrepository can produce fails the tightened rule.Namespace validation deliberately stays per-backend and is NOT lifted here --see :func:`assert_admissible_object_key_hmac`'s note on the one divergence thatis legitimate."""from__future__importannotationsfromcollections.abcimportMappingfrom....application.operator_actions.preconditionsimportno_action_precondition_verdictfrom....core.operator_action_enumsimportActionEvidenceProvenance,NoRecoveryOutcomefrom.errorsimportOutboundStorageValidationErrordef_validation_verdict(condition:str,facts:Mapping[str,str|bool]):returnno_action_precondition_verdict(condition_id=condition,facts=facts,provenance=ActionEvidenceProvenance.RUNTIME_OBSERVATION,outcome=NoRecoveryOutcome.OPERATOR_DECISION,)#: Backend-specific prefix for the translated-message keys, so a refusal keeps#: the identity of the backend that refused. Parameterised rather than#: collapsed to one shared message for the reason the AEAT representation gate#: is: an operator reading a refusal needs to know WHICH backend refused, and#: flattening two error identities into one loses that._MESSAGE_ROOT="adapters.outbound.storage"
[docs]defassert_admissible_object_key_hmac(object_key_hmac:str,*,backend:str)->str:"""Return ``object_key_hmac`` stripped, or refuse it for every backend alike. Admissible characters are alphanumerics, ``-`` and ``_`` -- the set a base16/base64url digest can produce and nothing more. A blank value is refused separately so the refusal names the actual problem. Args: object_key_hmac: The candidate key as the caller received it. backend: Backend token used to build the translated-message key (``"local"`` or ``"google_drive"``), so each backend's refusal keeps its own operator-facing identity. Returns: The stripped, admissible key. Raises: OutboundStorageValidationError: When the value is blank, or carries a character outside the admissible set. Note: NAMESPACE validation is deliberately not centralised alongside this. The local backend refuses a namespace beginning with ``.`` and Drive does not, and that is a genuine capability difference rather than drift: a leading dot makes a hidden file on a filesystem and means nothing in a Drive folder name. Divergence with a stated reason is fine; it is divergence by silence that this module exists to remove. """cleaned=object_key_hmac.strip()ifnotcleaned:raiseOutboundStorageValidationError("object_key_hmac must not be blank",translated_message=f"{_MESSAGE_ROOT}.{backend}.errors.object_key_hmac_blank",precondition_verdict=_validation_verdict("storage.key.present",{"backend":backend,"field":"object_key_hmac","valid":False},),)ifnotall(character.isalnum()orcharacterin"-_"forcharacterincleaned):raiseOutboundStorageValidationError(f"object_key_hmac {object_key_hmac!r} contains forbidden characters",context={"object_key_hmac":object_key_hmac},translated_message=f"{_MESSAGE_ROOT}.{backend}.errors.object_key_hmac_forbidden_characters",precondition_verdict=_validation_verdict("storage.key.admissible",{"backend":backend,"field":"object_key_hmac","valid":False},),)returncleaned