Issue 009 (fixed) + Issue 008 (resolved via ADR 0053) — module-runtime config & provider contract #21
@@ -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_<node>_<module>`, 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_<node>_<module>` 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.
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user