AshArcadic.DataLayer.Verifiers.ValidateRelationshipFk (AshArcadic v0.1.0)

Copy Markdown View Source

Compile-verifier (build-blocking under --warnings-as-errors): a relationship's LOCAL join attribute must not be sensitive. A sensitive attribute is app-side-encrypted binary (ValidateSensitive R2); an encrypted-binary FK cannot be IN-joined — Slice-5 relationship loading builds dest.<fk> IN [<plaintext pks>], so a sensitive join key silently breaks loading and leaks via filter presence/absence. Fail closed, value-free (names the attribute atom only).

Coverage boundary (verified at Slice-5 closeout, 2026-07-08)

This verifier runs per-resource and flags a join attribute only when it is BOTH in the current resource's own attribute set (local) AND its sensitive list. Coverage is therefore per-resource and LOCAL:

  • belongs_to.source_attribute — local to the source (the FK lives here) → caught directly.
  • many_to_many join-resource FKs — local to the JOIN resource, which idiomatically declares them as the source_attribute of its own belongs_to to each endpoint → caught by the JOIN resource's own run (NOT by the *_on_join_resource slots below, which name attributes remote to the declaring resource and so never satisfy the local check — they are kept as defensive belt-and-suspenders, not the coverage mechanism).

KNOWN LIMITATION (routed follow-up → Slice 6): a has_many/has_one destination_attribute names an attribute on the DESTINATION resource. It is caught here only when the destination independently declares a relationship using that attribute as a LOCAL join key (the idiomatic inverse belongs_to). A has_many/has_one whose sensitive destination_attribute has no such inverse is NOT caught at compile (a per-resource Spark verifier cannot read a sibling resource's sensitive list without compile-ordering fragility). Slice-6 adds a RUNTIME, LOAD-TIME guard (AshArcadic.Query.Filter rejects a value comparison — including the relationship-load dest.<fk> IN [pks] — on a sensitive or non-stored field), which converts the residual from a silent-[] load into a fail-closed loud %UnsupportedFilter{}. That guard is load-time, not compile-time: a misconfigured resource still compiles clean and fails only when the relationship is loaded.