fix: a require-only consumer of a parameterless provision still asks (and is minted a credential)

A consumer that requires a provision whose serves names no consumer key (redis-cache, amqp)
contributes no payload, but it still ASKS for it. ContributionsFrom keyed 'asks' on
contributions alone, so such a consumer's grant got From='' — read as withdrawn — and the
provider never created its account. redis-cache consumers (e.g. baserow) were silently
unprovisioned, tolerated only by their embedded fallback. A module asks iff it still requires
the provision, whether or not it hands anything up. Regression test added.

Found by the lavinmq AMQP provider bed (given:[] for a require-only amqp consumer); fix
lab-proven green there.

Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
2026-09-06 23:20:58 +02:00
parent b69fbc0e86
commit d0ef659824
2 changed files with 39 additions and 0 deletions
+24
View File
@@ -333,6 +333,30 @@ func TestAConsumersOwnContributionsAreStillThere(t *testing.T) {
}
}
func TestARequireOnlyConsumerOfAParameterlessProvisionStillAsks(t *testing.T) {
// A consumer that requires a parameterless provision (one whose serves names no consumer key —
// redis-cache, amqp) contributes no payload, but it still ASKS for the provision and must be
// granted a credential. Keying "asks" on contributions alone gave its grant From="" — read as
// withdrawn — so the provider never created the account and the consumer authenticated nowhere.
r := Resolution{
Node: "laptop",
Modules: []Manifest{{
Module: "amqp-ping",
Requires: []string{"amqp"},
}},
}
values, asks, err := r.ContributionsFrom("amqp", "amqp-ping", SettingsBy{})
if err != nil {
t.Fatal(err)
}
if !asks {
t.Fatal("a require-only consumer was treated as not asking; its grant would be withdrawn and it would never be provisioned")
}
if values == nil {
t.Fatalf("asks is true but values is nil; expected an empty contribution, got %v", values)
}
}
func TestAConsumerThatStoppedAskingIsWithdrawn(t *testing.T) {
// Withdrawal is how a credential is taken away. The provisioner removes what nobody asks for,
// and it can only do that if the mesh stops asking — a consumer that was unassigned would
+15
View File
@@ -637,6 +637,21 @@ func (r Resolution) ContributionsFrom(requirement, module string, settings Setti
return g.Values, true, nil
}
}
// It contributes no payload — but a require-only consumer of a parameterless provision (one whose
// `serves` names no consumer-supplied key: `redis-cache`, `amqp`) still ASKS for it and must be
// granted a credential. Keying "asks" on contributions alone marked those grants withdrawn
// (From=""), so the provider never created the account and the consumer authenticated nowhere. A
// module asks iff it still requires the provision, whether or not it hands anything up with it.
for _, m := range r.Modules {
if m.Module != module {
continue
}
for _, req := range m.Requires {
if req == requirement {
return map[string]any{}, true, nil
}
}
}
// Nothing on that machine asks for this any more. Said as "not found" rather than as an
// error: it is how a credential is withdrawn, and the provider removes what nobody asks for.
return nil, false, nil