Re-home this session's new ADRs (0039-0049) and issues (032-037) onto the consolidated scheme; flip issue 003; port repos.md sdk line + feature-branches playbook (07); regenerate index
Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
---
|
||||
topic: what runs on it
|
||||
status: accepted
|
||||
date: 2026-09-05
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
---
|
||||
|
||||
# 49. 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 0048) turned up 04-ISSUES/034: 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 0048: 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.
|
||||
|
||||
**E — A module (and a node) may declare a short slug; the identity is built from it.** The identity
|
||||
becomes `mesh_<node-slug|node-name>_<module-slug|module-name>`: where a slug is declared it is used,
|
||||
otherwise the cleaned name. A slug is a deliberately short, operator-chosen identifier — `kc` for
|
||||
keycloak, `wkstn` for a workstation. It is optional: short names (`anchor`, `redis`) need none.
|
||||
- *For:* this is the escape hatch B wanted to be, without the opacity. The name stays legible — a
|
||||
provisioner can read `mesh_wkstn_kc` and know who is asking — because a person chose it, not a
|
||||
hash function. And it makes an early refusal *palatable*: if even the slug-built identity overflows,
|
||||
the refusal points at the slug, a field made for exactly this, rather than at the machine's name.
|
||||
Both ends still derive it from one declared thing, so they agree by construction.
|
||||
- *Against:* a new optional manifest field, and someone must pick the slug — but only for names that
|
||||
would otherwise overflow, and picking a short legible identifier is a better job than being handed
|
||||
a hash.
|
||||
|
||||
## What implementing A revealed
|
||||
|
||||
A was tried first. At `identityLimit = 20`, the readable budget is `mesh_` (5) + node + `_` + module
|
||||
≤ 20, i.e. **node + module ≤ 14 characters** — far tighter than it looked. The catalogue's own
|
||||
existing tests use `workstation`+`keycloak` (25), which compacts to `mesh_dbbc02f8dde34d3`; common
|
||||
mesh names (`home-server`, `the-build-node`, `workstation`) blow the budget with any module. So B's
|
||||
compact fallback would fire for the *common* case, not the rare overflow — which inverts A+B: most
|
||||
identities would be opaque hashes. A alone (hard refusal at 20) would refuse most realistic names.
|
||||
This is what moved the recommendation to E: the problem is not the limit, it is that the *readable
|
||||
name* is the wrong source when it is long, and a slug is a better source than either a hash or a ban.
|
||||
|
||||
## Recommendation
|
||||
|
||||
**E, over a per-consumer bound (start with the global minimum, 20).** Build the identity from an
|
||||
optional slug, keep it when it fits, and refuse at assignment with "declare or shorten `<module>`'s
|
||||
slug" when it does not — no hash, no lost legibility, and the fix is a first-class field. Set the
|
||||
bound to the true minimum (20) now; it needs no per-interface machinery to unblock S3, and a module
|
||||
that consumes S3 simply declares a short slug. Graduate to **C** (per-interface bounds) later if it
|
||||
turns out that non-S3 consumers are paying for S3's limit often enough to mind — E and C compose:
|
||||
slugs are the mechanism, per-interface bounds refine where the ceiling sits. **B is dropped**: a
|
||||
declared slug is a strictly better escape hatch than an opaque hash. **D stays rejected.**
|
||||
|
||||
## Consequences (of E)
|
||||
|
||||
- A module manifest gains an optional `slug`; a node may carry one too. `ConsumerIdentity` prefers
|
||||
the slug over the cleaned name for each half. `identityLimit` becomes 20 (the true minimum), and
|
||||
`CheckIdentity` refuses at `module add` / assignment — now with a message naming the slug to set.
|
||||
- The common case stays legible; only names that overflow the budget need a slug, and what they get
|
||||
is a name a person chose, not a hash.
|
||||
- Existing modules/nodes whose names overflow declare a slug once — a migration cost paid as a clear
|
||||
refusal with an obvious remedy, not a silent hash or a silent truncation.
|
||||
- minio (04-ISSUES/034) is unblocked: an S3 consumer declares a short slug and its access key fits.
|
||||
- **How it is checked:** the minio grant e2e — a consumer whose (slugged) identity fits reaches its
|
||||
bucket with the credential the mesh delivered — plus unit tests that a slug is preferred, that an
|
||||
un-sluggable over-long identity is refused (naming the slug), and that two consumers never collide.
|
||||
|
||||
## References
|
||||
|
||||
- [04-ISSUES/034](../04-ISSUES/034-mesh-login-exceeds-s3-access-key-limit/00-report.md) — the
|
||||
observation.
|
||||
- ADR 0048 — 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.
|
||||
Reference in New Issue
Block a user