A consumer is a module on a machine, not a machine
novox/hq 04-ISSUES/022. A credential was keyed by provision, consumer node and provider node, so "who is asking" was answered by naming a host. The node this mesh exists to take over runs eight modules against one database server. The symptom had two halves and only one was loud. The provider refused, naming the modules and explaining they would share one credential, which reads as a decision rather than a limit. The consumer did not refuse: it resolved cleanly, wrote one module's credential file and left the others absent — a service that starts and cannot authenticate, with nothing saying why. That is 021 again on a different axis. Three modules wanting one database produced one need, carrying whichever module mentioned it first, because the resolution walk is a work-list over names. The fan-out now happens in one place, after the walk. The record path already did this correctly and said why: a consumer here is a module on a machine. It is the same rule. Downstream: the secret's key gains the consuming module, the grant file is named after both halves, needs are matched by provision and module rather than provision alone, and the provisioners name the role and the access key after the module. The refusal in ContributionsTo is gone because there is nothing left to refuse. Worth stating plainly: without that refusal, gitea's login would have opened keycloak's database. From the provisioner's side it created exactly what it was asked to create. Existing secrets are discarded rather than backfilled. They cannot say which module they were for, and a secret is remade and delivered to both ends on the next push — so this costs one rotation and invents nothing. Also guards the role name against PostgreSQL's 63-byte truncation, which is a notice rather than an error and would reintroduce exactly this collision at a length nobody tests. Three faults injected — the fan-out removed, needs matched by name alone, the grant file named after the machine — each caught.
This commit is contained in:
@@ -201,7 +201,12 @@ func run(ctx context.Context) error {
|
||||
return fmt.Errorf("%s's credential should be at %s and is not there", c.Node, c.Secret)
|
||||
}
|
||||
|
||||
role := mark + c.Node
|
||||
// **Named after the module and the machine, not the machine** (novox/hq 04-ISSUES/022).
|
||||
// A node routinely runs several services against one database server, and one role for
|
||||
// all of them means gitea's login opens keycloak's data — created exactly as asked, with
|
||||
// nothing anywhere to say so. It also makes withdrawal impossible: one role cannot be
|
||||
// removed for one consumer while another still holds it.
|
||||
role := mark + c.Node + "_" + c.From
|
||||
wanted[role] = true
|
||||
if err := ensureRole(ctx, db, role, strings.TrimSpace(string(password))); err != nil {
|
||||
return err
|
||||
@@ -217,7 +222,32 @@ func run(ctx context.Context) error {
|
||||
return revokeOrphans(ctx, db, wanted)
|
||||
}
|
||||
|
||||
// identifierLimit is where PostgreSQL stops reading a name: NAMEDATALEN - 1.
|
||||
const identifierLimit = 63
|
||||
|
||||
// usableRole refuses a role name PostgreSQL would silently shorten.
|
||||
//
|
||||
// **Truncation is a NOTICE, not an error.** A name past the limit is cut to fit and the statement
|
||||
// succeeds, so two consumers whose names agree for the first 63 bytes become one role — which is
|
||||
// the exact fault 022 was about, reappearing at a length nobody would think to test. Refusing is
|
||||
// the only honest answer: the provisioner cannot shorten the name itself without inventing a
|
||||
// second naming scheme that the mesh does not know about, and would then be creating a login the
|
||||
// mesh cannot name.
|
||||
func usableRole(role string) error {
|
||||
if len(role) <= identifierLimit {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf(
|
||||
"the role for this consumer would be %q, which is %d bytes and PostgreSQL keeps %d — "+
|
||||
"it would be shortened silently, and another consumer shortened to the same name "+
|
||||
"would share the login. Shorten the node or module name",
|
||||
role, len(role), identifierLimit)
|
||||
}
|
||||
|
||||
func ensureRole(ctx context.Context, db *pgx.Conn, role, password string) error {
|
||||
if err := usableRole(role); err != nil {
|
||||
return err
|
||||
}
|
||||
var exists bool
|
||||
if err := db.QueryRow(ctx,
|
||||
`select true from pg_roles where rolname = $1`, role).Scan(&exists); err != nil && err != pgx.ErrNoRows {
|
||||
@@ -300,7 +330,12 @@ func revokeOrphans(ctx context.Context, db *pgx.Conn, wanted map[string]bool) er
|
||||
// the output of one can be compared with another.
|
||||
func sorted(given []contribution) []contribution {
|
||||
out := append([]contribution{}, given...)
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Node < out[j].Node })
|
||||
sort.Slice(out, func(i, j int) bool {
|
||||
if out[i].Node != out[j].Node {
|
||||
return out[i].Node < out[j].Node
|
||||
}
|
||||
return out[i].From < out[j].From
|
||||
})
|
||||
return out
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user