AshReplicant.Resource.Verifiers.ValidateMultitenancy (AshReplicant v0.3.0)

Copy Markdown View Source

Compile-verifier for the replicant tenant sources (surfaced as a Spark diagnostic; build-blocking under --warnings-as-errors). Two arms.

tenant_attribute

When a tenant_attribute is set it is the per-row tenant discriminator Ash injects as a plaintext filter and force-set. It fails closed unless it is:

  • not classified sensitive — an encrypted discriminator would match nothing (fail-open isolation), and ash_replicant holds no key material;
  • not listed in skip — a skipped discriminator is never written, so the tenant filter matches nothing (fail-open isolation);
  • a declared attribute — an undeclared discriminator cannot be resolved or force-set;
  • not binary-storage-typed — the discriminator is a plaintext comparator; a tag/base64 discriminator would scope inconsistently; and
  • backed by an Ash multitenancy block — the sink passes the resolved per-row tenant to Ash as the tenant: option, which Ash HONORS only under declared multitenancy. With no multitenancy block, tenant: is silently ignored and every tenant's rows are mirrored UNSCOPED (fail-open isolation).

tenant_mfa

An mfa-resolved tenant is passed to Ash as the same tenant: option, so it has the identical "no multitenancy block ⇒ tenant: ignored ⇒ every tenant mirrored unscoped" fail-open. This arm requires an Ash multitenancy block when tenant_mfa is declared — any strategy satisfies (both :attribute and :context honor tenant:; :context is the typical mfa pairing, since the mfa value is a computed function result, not a stored attribute). The shape checks above do not apply to the mfa arm — the tenant is a function result, not a declared column.

Multitenancy block :attribute shape

Independent of the tenant source, when the resource declares strategy :attribute the multitenancy block's OWN attribute is force-set to the plaintext tenant on write and filtered on read. It must be a plaintext, non-sensitive comparator — a sensitive-classified or binary-storage-typed discriminator would store/compare a mismatched value and silently mis-scope (reads return empty). This runs for both tenant sources (and a global :attribute resource). An AshCloak-encrypted attribute is already rejected by Ash's own multitenancy verifier (the cloak transform removes the plain attribute), so it is not re-checked here.

Messages are value-free: they name schema structure (the attribute name or the tenant_mfa key), never a row value or the mfa target's arguments.