Files
mesh-controller/internal/catalogue/identity.go
T
jschoubben 9b7ba2e20c identity: a consumer's identity fits the tightest backend, via a slug (ADR 0054)
A module may declare a short `slug`; the mesh derives mesh_<node>_<slug|name> and
refuses at assignment (naming the slug as the remedy) when it would still overflow —
identityLimit is now 20, an S3 access key's, the tightest of the backends a login
reaches (04-ISSUES/010). The slug rides the grant so the provider derives the same
login the consumer does, even across nodes. CheckIdentity is now wired, in grantsFor.

Also, the minted secret shrinks to 40 chars (30 bytes) from 43: an S3 secret key is
8-40, the same fit-the-tightest-backend rule on the credential's other half.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
2026-09-05 02:51:39 +02:00

82 lines
4.0 KiB
Go

package catalogue
import (
"fmt"
"regexp"
"strings"
)
// Who a consumer is, said once by the mesh (novox/hq 04-ISSUES/023).
//
// **The provisioner used to invent this and nothing else could derive it.** It made a role called
// `mesh_<node>_<module>`, which is a reasonable name and is knowable nowhere else: not by the
// control plane, not by the binding, and above all not by the consumer — which has to present it
// in order to authenticate. The one identifier needed to connect was the one thing no part of the
// mesh would say.
//
// So the mesh says it. It goes to the provider in the grant and to the consumer in its binding,
// from **one derivation**, which is what makes the two ends agree by construction rather than by
// two conventions that were the same on the day they were written.
//
// **It is still name-agnostic.** The mesh does not know what a role or an access key or a client
// is; it says who is asking, and each provisioner makes that true in whatever its own system
// calls an identity. What a provider does with it is the provider's business, as everything about
// a provision is.
// identityUnusable is every character that is not safe unquoted in the systems these names reach.
//
// Conservative on purpose: lower-case letters, digits and underscore reach a PostgreSQL role, a
// MinIO access key, an LDAP uid and a Keycloak client without quoting or escaping in any of them.
// A wider set would work in most and fail in one, discovered as a login that cannot be created.
var identityUnusable = regexp.MustCompile(`[^a-z0-9_]+`)
// IdentityPrefix marks what the mesh made, so a provisioner can find its own work and leave
// everything else alone. Withdrawal depends on it entirely.
const IdentityPrefix = "mesh_"
// IdentitySource is the name the mesh derives a consumer's identity from: the module's slug when it
// has declared one, otherwise its name (novox/hq ADR 0054). A module with a name short enough to fit
// the tightest backend needs no slug; one whose name would overflow declares a short legible one.
func IdentitySource(slug, name string) string {
if slug != "" {
return slug
}
return name
}
// ConsumerIdentity is what one module on one machine is called, wherever it authenticates. The
// `module` argument is the identity source — a slug or a name; see IdentitySource.
//
// A dot and a dash both become an underscore, so `home-server` and `home.server` would collide —
// which cannot happen, because a machine has one name and it is either.
func ConsumerIdentity(node, module string) string {
clean := func(s string) string {
return strings.Trim(identityUnusable.ReplaceAllString(strings.ToLower(s), "_"), "_")
}
return IdentityPrefix + clean(node) + "_" + clean(module)
}
// identityLimit is the shortest identifier limit among the systems these names reach: an S3 access
// key's 20 (novox/hq 04-ISSUES/010). PostgreSQL keeps 63 and MinIO 20, so 20 is the one that binds —
// the comment used to name PostgreSQL and was wrong. A name over it is refused, with the remedy a
// short slug (ADR 0054), not silently cut to fit.
const identityLimit = 20
// CheckIdentity refuses an identity that would not fit the tightest backend a consumer reaches.
//
// **Truncation is not an error in most of these systems** — a name past the limit is cut to fit and
// the statement succeeds, so two consumers agreeing for the first N bytes would become one login
// (04-ISSUES/022) — and S3 refuses outright. Refused here, at the mesh, because the mesh chose the
// name and is the only thing that can choose another. The remedy is a first-class one: give the
// module a short `slug` (ADR 0054), or shorten the machine's name.
func CheckIdentity(node, module string) error {
got := ConsumerIdentity(node, module)
if len(got) <= identityLimit {
return nil
}
return fmt.Errorf(
"%s on %s is identified as %q, %d characters where a backend (an S3 access key) keeps %d — "+
"give the module a shorter `slug` or shorten the machine's name",
module, node, got, len(got), identityLimit)
}