diff --git a/02-DECISIONS/0054-a-consumers-identity-fits-the-tightest-backend.md b/02-DECISIONS/0054-a-consumers-identity-fits-the-tightest-backend.md new file mode 100644 index 0000000..e242461 --- /dev/null +++ b/02-DECISIONS/0054-a-consumers-identity-fits-the-tightest-backend.md @@ -0,0 +1,104 @@ +--- +status: proposed +date: 2026-09-05 +deciders: jochen +reconstructed: false +--- + +# 54. A consumer's identity is bounded by the tightest backend that must accept it + +## Context + +The mesh says who a consumer is, once, and hands the same name to the provider (to create) and the +consumer (to present), so the two ends agree by construction rather than by two conventions (the +principle behind `ConsumerIdentity`, 04-ISSUES/023). The name is `mesh__`, cleaned to +lower-case letters, digits and underscore. + +Proving the provider contract per backend (ADR 0053) turned up 04-ISSUES/010: redis and postgres +create that name verbatim, but **minio refuses it** — an S3 access key is capped at 20 characters, +and `mesh_anchor_bucketuser` is 22. The provisioner then retries for ever, per consumer, and the +consumer holding that same too-long name could never present it either. + +Two things about the existing derivation decide most of this: + +- **The charset is already right.** `[^a-z0-9_]` is deliberately conservative, and its own comment + says it reaches "a PostgreSQL role, a MinIO access key, an LDAP uid and a Keycloak client without + quoting." That much is true. +- **The length is wrong.** `CheckIdentity` refuses names over `identityLimit = 63`, commented as + "the shortest identifier limit among the systems these names reach: PostgreSQL's". It is not the + shortest — S3's 20 is shorter — so the guard that was meant to catch exactly this lets it through, + and the failure lands at provision time as a silent retry instead of at assignment as a refusal. + +So this is a small wrong constant with a real cost attached: whatever bound we set, `mesh_` (5) plus +a node name plus `_` plus a module name has to fit inside it. + +## The options + +**A — Bound the identity by the true minimum, and refuse early.** Lower `identityLimit` to the real +shortest (20, S3's), so `CheckIdentity` refuses an over-long name *at assignment* with a clear +message, the way it already refuses over-63 names. The derivation does not change; long names are +simply rejected before anything is provisioned. +- *For:* smallest change; keeps "the mesh says the identity once, verbatim" intact; the failure + moves from a per-consumer provision-time retry to an up-front, legible refusal — which is what + `CheckIdentity` exists to do. +- *Against:* a hard budget. `mesh_` + node + `_` + module ≤ 20 means node + module ≤ 14 characters. + `anchor` + `bucketuser` (16) is already over. It pushes the constraint onto how machines and + modules are named, which is a real limitation on legible names. + +**B — Keep the readable name when it fits, compact it when it does not.** Below the bound, the name +is `mesh__` as today; over it, the mesh substitutes a deterministic short form (e.g. +`mesh_` + a truncated hash of node+module) — still one derivation, so both ends still agree. +- *For:* no naming constraint; short backends always satisfied; the common case stays legible. +- *Against:* some identities become opaque, and a provisioner tracing "whose login is this" loses + the answer for exactly the consumers that overflowed. The mesh now owns a fallback format and its + collision properties (a truncated hash is not free of collisions at 15 characters). + +**C — Let each interface declare its identifier bounds, and derive within the tightest a consumer +reaches.** `s3-bucket` states `identifier: { max: 20 }`; `postgres-database` states 63; the mesh +derives a name that fits the **minimum** bound across the providers a given consumer is granted. +- *For:* the most precise — each provision gets exactly the room it has, and a database consumer + keeps long legible names while an S3 consumer gets a short one; the constraint lives where the + fact does (on the interface). +- *Against:* the most work, and a consumer of two interfaces with different bounds must satisfy the + smaller — so its name shortens for both, reintroducing B's opacity in a narrower case. It also + means one consumer can hold **different** identities per provision, which the "said once" model + currently forbids. + +**D — Let the provider generate a backend-valid identity and hand it back (rejected).** minio mints +its own access key and returns it to the consumer. This is the data-provision return path this era +keeps meeting — but it directly contradicts 023 and ADR 0053: the identity would no longer be the +mesh's single derivation the two ends share, it would be a value one side invents and the other must +be told. Listed for completeness; not recommended. + +## Recommendation + +**A, with B held as the escape hatch.** Fixing the constant is what the code already says it meant +to do ("the shortest limit among the systems these names reach"), and moving the failure to an +honest assignment-time refusal is strictly better than a silent provision loop. Accept the naming +budget first; it is a forcing function toward short node/module names, which the mesh benefits from +elsewhere. If the ≤14 budget proves too tight in real use, add B's compact fallback for the overflow +case only — keeping the legible name for everyone it fits. Reach for C only if per-interface bounds +turn out to matter beyond this one 20-vs-63 gap; it is the right shape but more machinery than the +problem currently demands. + +## Consequences (of A) + +- `identityLimit` becomes 20; the comment stops claiming postgres is the shortest and names S3 as + the binding one. `CheckIdentity` refuses over-long names at `module add` / assignment, not at + provision. +- Existing node/module names longer than the budget must be shortened before they can consume a + provision — a migration cost paid once, surfaced as a clear refusal. +- minio (04-ISSUES/010) is unblocked without any provider-specific mapping, and the minio + per-backend e2e (skipped, pending this) can run. +- **How it is checked:** the minio grant e2e — a consumer with a name inside the budget reaches its + bucket with the credential the mesh delivered — and a unit test that `CheckIdentity` refuses a + name whose derived identity exceeds 20. + +## References + +- [04-ISSUES/010](../04-ISSUES/010-mesh-login-exceeds-s3-access-key-limit/00-report.md) — the + observation. +- ADR 0053 — a provider creates the credential the mesh minted; the identity it creates it under is + the one this decision bounds. +- `mesh-control` `internal/catalogue/identity.go` — `ConsumerIdentity`, `identityUnusable`, + `identityLimit`, `CheckIdentity` — where the constant and the check live. diff --git a/04-ISSUES/010-mesh-login-exceeds-s3-access-key-limit/00-report.md b/04-ISSUES/010-mesh-login-exceeds-s3-access-key-limit/00-report.md index ed0849a..2d8f338 100644 --- a/04-ISSUES/010-mesh-login-exceeds-s3-access-key-limit/00-report.md +++ b/04-ISSUES/010-mesh-login-exceeds-s3-access-key-limit/00-report.md @@ -61,3 +61,7 @@ and those are not always the same string. - Do redis/postgres actually want the long name, or did it only survive because they are permissive? If nothing needs it long, the cheap fix is to cap it. - Does this fold into the same decision as the data-provision return path, or is it separate? + +The options are sketched in **ADR 0054 (proposed)** — bound the identity by the true minimum and +refuse early (recommended), a compact fallback for overflow, per-interface bounds, or a +provider-generated identity (rejected). Awaiting ratification.