From d0ef6598245ac669b607a803199ed5b82019d81a Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 6 Sep 2026 23:20:58 +0200 Subject: [PATCH] fix: a require-only consumer of a parameterless provision still asks (and is minted a credential) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- internal/catalogue/contributes_test.go | 24 ++++++++++++++++++++++++ internal/catalogue/declaration.go | 15 +++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/internal/catalogue/contributes_test.go b/internal/catalogue/contributes_test.go index e572736..1065295 100644 --- a/internal/catalogue/contributes_test.go +++ b/internal/catalogue/contributes_test.go @@ -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 diff --git a/internal/catalogue/declaration.go b/internal/catalogue/declaration.go index 2274829..c02669e 100644 --- a/internal/catalogue/declaration.go +++ b/internal/catalogue/declaration.go @@ -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 -- 2.54.0