diff --git a/go/provider/provider.go b/go/provider/provider.go index 95bfd19..b169a49 100644 --- a/go/provider/provider.go +++ b/go/provider/provider.go @@ -105,8 +105,10 @@ type Entry struct { User string `json:"user,omitempty"` // Values are the served values, composed for this consumer: what `${bound::}` reads. Values map[string]any `json:"values,omitempty"` - // Credential is the name of the credential in the vault, which the consumer's client asks the vault for. - Credential string `json:"credential,omitempty"` + // VaultName is the name under which the vault keeps this consumer's credential, which the consumer's client asks + // the vault for: a name, never the credential. Its key is not "credential" because the runtime's state guard + // refuses any key ending in that word, whatever the value (issue 945). + VaultName string `json:"vault-name,omitempty"` // Refused, when set, is why the consumer is not served, naming both ends. Refused string `json:"refused,omitempty"` // Contributes is what the consumer's module tells its provider about this provision (a database's name, its @@ -419,15 +421,15 @@ func (p *Provider) answer(ctx context.Context, e Entry, have []byte) Outcome { switch offer.Credential { case Shared: e.User = offer.User - e.Credential = SharedName(e.Provider, e.Provision) + e.VaultName = SharedName(e.Provider, e.Provision) case PerConsumer: e.User = e.As - e.Credential = PairName(e.Provider, e.Provision, e.Consumer, e.Module, e.Local) + e.VaultName = PairName(e.Provider, e.Provision, e.Consumer, e.Module, e.Local) } - if e.Credential != "" { - credential, err = p.opts.Vault.Value(ctx, e.Credential) + if e.VaultName != "" { + credential, err = p.opts.Vault.Value(ctx, e.VaultName) if err != nil { - return Outcome{Key: key, Status: Waiting, Why: fmt.Sprintf("the vault's %s: %v", e.Credential, err)} + return Outcome{Key: key, Status: Waiting, Why: fmt.Sprintf("the vault's %s: %v", e.VaultName, err)} } } if p.opts.Make != nil { diff --git a/go/provider/provider_test.go b/go/provider/provider_test.go index 7e00b63..5c7ae2f 100644 --- a/go/provider/provider_test.go +++ b/go/provider/provider_test.go @@ -224,7 +224,7 @@ func TestEachConsumerGetsAnEntryOfItsOwnAndTheVaultsValue(t *testing.T) { e := entryOf(t, bus, "ace.letta.postgres-database") if e.Provider != "novox" || e.ProviderModule != "postgres" || e.Consumer != "ace" || e.Module != "letta" || e.As != "mesh_ace_letta" || e.User != "mesh_ace_letta" || - e.Credential != "pair/novox/postgres-database/ace/letta" || e.Refused != "" { + e.VaultName != "pair/novox/postgres-database/ace/letta" || e.Refused != "" { t.Fatalf("entry %+v", e) } if e.Values["database"] != "mesh_ace_letta" || e.Values["host"] != "postgres.novox.mesh" || @@ -263,7 +263,7 @@ func TestASharedCredentialIsTheVaultsOneValueAndTheProvidersOneUser(t *testing.T if err := json.Unmarshal(bus.get(BucketOf("sonarr"), key), &e); err != nil { t.Fatalf("%s: %v", key, err) } - if e.Credential != "provision/novox/sonarr-api" || e.User != "sonarr" || e.Values["url"] == nil { + if e.VaultName != "provision/novox/sonarr-api" || e.User != "sonarr" || e.Values["url"] == nil { t.Fatalf("%s: %+v", key, e) } if m.made[key] != "one-value" { @@ -291,7 +291,7 @@ func TestAMultiHolderConsumerIsRefusedADerivedValueNamingBothEnds(t *testing.T) t.Fatalf("the refusal does not name %q: %s", says, e.Refused) } } - if e.Values != nil || e.Credential != "" { + if e.Values != nil || e.VaultName != "" { t.Fatalf("a refused entry carries values: %+v", e) } if m.calls != 0 { @@ -317,7 +317,7 @@ func TestALocalNameIsServedWhenNothingIsDerived(t *testing.T) { if err := json.Unmarshal(bus.get(BucketOf("redis"), "ace.letta.redis-cache.queue"), &e); err != nil { t.Fatal(err) } - if e.Refused != "" || e.Credential != "pair/novox/redis-cache/ace/letta/queue" || e.Local != "queue" { + if e.Refused != "" || e.VaultName != "pair/novox/redis-cache/ace/letta/queue" || e.Local != "queue" { t.Fatalf("entry %+v", e) } if m.made["ace.letta.redis-cache.queue"] != "q-secret" { @@ -588,3 +588,62 @@ func TestAConsumersEntryCarriesTheNamesOfItsPlan(t *testing.T) { t.Fatalf("an entry of a plan without names changed: %+v", plain) } } + +// refusedByTheStateGuard mirrors the key rule of the runtime's state guard (mesh-tools node-tools internal/bus +// state.go): a JSON key whose lowercased name, with `-`, `_` and spaces taken out, ends in one of these is refused. +func refusedByTheStateGuard(key string) bool { + k := strings.NewReplacer("-", "", "_", "", " ", "").Replace(strings.ToLower(key)) + for _, end := range []string{"password", "passwd", "secret", "token", "credential", "credentials", + "authorization", "apikey", "privatekey", "accesskey", "cookie"} { + if strings.HasSuffix(k, end) { + return true + } + } + return false +} + +func keysOf(v any, into *[]string) { + switch x := v.(type) { + case map[string]any: + for k, c := range x { + *into = append(*into, k) + keysOf(c, into) + } + case []any: + for _, c := range x { + keysOf(c, into) + } + } +} + +// Every entry the helper writes passes the state guard's key rule (issue 945), and names where the vault keeps the +// credential under `vault-name`, for a login per consumer and for a shared one. +func TestAnEntryHasNoKeyTheStateGuardRefuses(t *testing.T) { + for name, form := range map[string]CredentialForm{"pair": PerConsumer, "shared": Shared} { + bus, m := newStandIn(), &maker{} + v := &vault{values: map[string]string{"pair/novox/postgres-database/ace/letta": "s1", + "provision/novox/postgres-database": "s2"}} + assign(t, bus, "ace", "letta", bind("postgres-database", "", "novox", "postgres")) + p := New(Options{Node: "novox", Module: "postgres", Bus: bus, Vault: v, Make: m.Make, Logf: func(string, ...any) {}, + Offers: map[string]Offer{"postgres-database": {Credential: form, User: "shared-user"}}}) + if _, err := p.Pass(context.Background()); err != nil { + t.Fatal(err) + } + var doc map[string]any + if err := json.Unmarshal(bus.get(BucketOf("postgres"), "ace.letta.postgres-database"), &doc); err != nil { + t.Fatal(err) + } + var keys []string + keysOf(doc, &keys) + for _, k := range keys { + if refusedByTheStateGuard(k) { + t.Fatalf("%s: the entry carries the key %q, which the state guard refuses", name, k) + } + } + want := map[string]string{"pair": "pair/novox/postgres-database/ace/letta", + "shared": "provision/novox/postgres-database"}[name] + if doc["vault-name"] != want { + t.Fatalf("%s: vault-name is %v, want %s", name, doc["vault-name"], want) + } + } +}