From 56018993e0d9d2c53ee598f896130a22a6f3fca9 Mon Sep 17 00:00:00 2001 From: jochen Date: Sun, 8 Nov 2026 23:56:58 +0100 Subject: [PATCH] Grants creates a one-filter consumer on the plain create subject it is granted With exactly one filter in FilterSubject, nats.go creates on $JS.API.CONSUMER.CREATE..., a subject the grants part is never granted (CreateConsumerGrants keeps filters out of grants), so the creates of ASKS.novox_mesh-delivery-indexer, ASKS.shanks_mesh-delivery-builder and FORGE.shanks_mesh-delivery-builder were refused and timed out (2026-11-08, issue 1966). Every filter now goes in FilterSubjects, which the client creates on the plain subject. Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5 --- .../internal/buscompose/buscompose_test.go | 13 +++++++++++++ modules/mesh-grants/internal/buscompose/create.go | 10 +++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/modules/mesh-grants/internal/buscompose/buscompose_test.go b/modules/mesh-grants/internal/buscompose/buscompose_test.go index 1b9cbdc8..39683774 100644 --- a/modules/mesh-grants/internal/buscompose/buscompose_test.go +++ b/modules/mesh-grants/internal/buscompose/buscompose_test.go @@ -1266,3 +1266,16 @@ func theBusModulesProcessKeepsBusState(t *testing.T) { t.Run("the names are the manifest's", namesAreTheManifests) t.Run("the bus grants are the declared", busGrantsAreTheDeclared) } + +// A consumer with one filter is created on the plain subject the grants part is granted, not the filtered form the client +// library uses for FilterSubject alone ($JS.API.CONSUMER.CREATE...), which no grant allows +// (2026-11-08, issue 1966: the ASKS and FORGE consumers of the indexer and the builder were never made). +func TestAOneFilterConsumerIsCreatedOnThePlainSubject(t *testing.T) { + cfg := ConsumerConfig(Consumer{Name: "novox_mesh-delivery-indexer", Stream: "ASKS", Filters: []string{"mesh.stream.mesh-delivery-checker.event.verdict"}}) + if cfg.FilterSubject != "" || len(cfg.FilterSubjects) != 1 || cfg.FilterSubjects[0] != "mesh.stream.mesh-delivery-checker.event.verdict" { + t.Fatalf("a one-filter consumer is created with FilterSubject %q and FilterSubjects %v: the client would ask the filtered create subject", cfg.FilterSubject, cfg.FilterSubjects) + } + if none := ConsumerConfig(Consumer{Name: "a_m", Stream: "EVENTS"}); none.FilterSubject != "" || len(none.FilterSubjects) != 0 { + t.Fatalf("a consumer with no filter is created with filters %q %v", none.FilterSubject, none.FilterSubjects) + } +} diff --git a/modules/mesh-grants/internal/buscompose/create.go b/modules/mesh-grants/internal/buscompose/create.go index bc4b5716..f4d76a68 100644 --- a/modules/mesh-grants/internal/buscompose/create.go +++ b/modules/mesh-grants/internal/buscompose/create.go @@ -101,11 +101,11 @@ func ConsumerConfig(c Consumer) jetstream.ConsumerConfig { MaxAckPending: c.MaxAckPending, Description: c.Why, } - switch len(c.Filters) { - case 0: - case 1: - cfg.FilterSubject = c.Filters[0] - default: + // Every filter goes in FilterSubjects, one alone too: with exactly one in FilterSubject the client library creates on + // `$JS.API.CONSUMER.CREATE...`, a form the part is never granted (CreateConsumerGrants: a wildcard + // in a filter would widen the grant), so such a create was refused and timed out (2026-11-08, issue 1966: ASKS and + // FORGE consumers of the indexer and the builder never made). + if len(c.Filters) > 0 { cfg.FilterSubjects = c.Filters } if c.FromNow { -- 2.54.0