novox/hq 04-ISSUES/023. A consumer was given its password, the address,
the port and where its credential lives, and still could not connect —
the user name was invented by the provisioner and recorded nowhere, and
the rest sat in a JSON binding that a program reading KEY=value cannot
use.
Both halves have the same cause: the mesh knew something and did not say
it.
**Who a consumer is, said once.** The provisioner used to derive
mesh_<node>_<module> and that string existed nowhere else — not in the
control plane, not in the binding, and above all not at the consumer,
which has to present it. Now the mesh derives it once and sends it to
both ends, so they agree by construction rather than by two conventions
that were the same on the day they were written. The provisioners refuse
to invent one if the mesh says nothing, because falling back to a name
of their own would create a role the consumer would never guess and
everything would report success.
**Bound values reach the file that needs them.** ${bound:provision:key}
is the symmetric twin of the sealed placeholder, and simpler: these
values are not secret, so the control plane fills them in before sending
and the host gains no field and learns no format. It stays
name-agnostic — at, as and from are true of any provision, and every
other key comes from what the provider said it serves.
The asymmetry it removes was backwards. The secret is the hard case,
because the mesh must not be able to read it, and the secret was the
part that already arrived.
Keycloak and Gitea now produce complete connections, asserted from the
manifests on disk rather than from fixtures: every part filled, no
placeholder surviving as a value, and the password still a hole only the
host can close. Three faults injected, each caught.
69 lines
3.2 KiB
Go
69 lines
3.2 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_"
|
|
|
|
// ConsumerIdentity is what one module on one machine is called, wherever it authenticates.
|
|
//
|
|
// 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:
|
|
// PostgreSQL's NAMEDATALEN - 1.
|
|
const identityLimit = 63
|
|
|
|
// CheckIdentity refuses a name a provider would silently shorten.
|
|
//
|
|
// **Truncation is not an error in PostgreSQL** — a name past the limit is cut to fit and the
|
|
// statement succeeds. Two consumers agreeing for the first 63 bytes would become one login, which
|
|
// is 022 again at a length nobody would think to test. Refused here rather than in each
|
|
// provisioner, because the mesh chose the name and is the only thing that can choose another.
|
|
func CheckIdentity(node, module string) error {
|
|
got := ConsumerIdentity(node, module)
|
|
if len(got) <= identityLimit {
|
|
return nil
|
|
}
|
|
return fmt.Errorf(
|
|
"%s on %s would be identified as %q, which is %d characters and some providers keep %d — "+
|
|
"another consumer shortened to the same name would share its login. Shorten the "+
|
|
"machine's name or the module's",
|
|
module, node, got, len(got), identityLimit)
|
|
}
|