Name the vault's credential under vault-name so the state guard accepts the bound entry (issue 945)
This commit is contained in:
@@ -105,8 +105,10 @@ type Entry struct {
|
|||||||
User string `json:"user,omitempty"`
|
User string `json:"user,omitempty"`
|
||||||
// Values are the served values, composed for this consumer: what `${bound:<provision>:<key>}` reads.
|
// Values are the served values, composed for this consumer: what `${bound:<provision>:<key>}` reads.
|
||||||
Values map[string]any `json:"values,omitempty"`
|
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.
|
// VaultName is the name under which the vault keeps this consumer's credential, which the consumer's client asks
|
||||||
Credential string `json:"credential,omitempty"`
|
// 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, when set, is why the consumer is not served, naming both ends.
|
||||||
Refused string `json:"refused,omitempty"`
|
Refused string `json:"refused,omitempty"`
|
||||||
// Contributes is what the consumer's module tells its provider about this provision (a database's name, its
|
// 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 {
|
switch offer.Credential {
|
||||||
case Shared:
|
case Shared:
|
||||||
e.User = offer.User
|
e.User = offer.User
|
||||||
e.Credential = SharedName(e.Provider, e.Provision)
|
e.VaultName = SharedName(e.Provider, e.Provision)
|
||||||
case PerConsumer:
|
case PerConsumer:
|
||||||
e.User = e.As
|
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 != "" {
|
if e.VaultName != "" {
|
||||||
credential, err = p.opts.Vault.Value(ctx, e.Credential)
|
credential, err = p.opts.Vault.Value(ctx, e.VaultName)
|
||||||
if err != nil {
|
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 {
|
if p.opts.Make != nil {
|
||||||
|
|||||||
@@ -224,7 +224,7 @@ func TestEachConsumerGetsAnEntryOfItsOwnAndTheVaultsValue(t *testing.T) {
|
|||||||
e := entryOf(t, bus, "ace.letta.postgres-database")
|
e := entryOf(t, bus, "ace.letta.postgres-database")
|
||||||
if e.Provider != "novox" || e.ProviderModule != "postgres" || e.Consumer != "ace" || e.Module != "letta" ||
|
if e.Provider != "novox" || e.ProviderModule != "postgres" || e.Consumer != "ace" || e.Module != "letta" ||
|
||||||
e.As != "mesh_ace_letta" || e.User != "mesh_ace_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)
|
t.Fatalf("entry %+v", e)
|
||||||
}
|
}
|
||||||
if e.Values["database"] != "mesh_ace_letta" || e.Values["host"] != "postgres.novox.mesh" ||
|
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 {
|
if err := json.Unmarshal(bus.get(BucketOf("sonarr"), key), &e); err != nil {
|
||||||
t.Fatalf("%s: %v", key, err)
|
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)
|
t.Fatalf("%s: %+v", key, e)
|
||||||
}
|
}
|
||||||
if m.made[key] != "one-value" {
|
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)
|
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)
|
t.Fatalf("a refused entry carries values: %+v", e)
|
||||||
}
|
}
|
||||||
if m.calls != 0 {
|
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 {
|
if err := json.Unmarshal(bus.get(BucketOf("redis"), "ace.letta.redis-cache.queue"), &e); err != nil {
|
||||||
t.Fatal(err)
|
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)
|
t.Fatalf("entry %+v", e)
|
||||||
}
|
}
|
||||||
if m.made["ace.letta.redis-cache.queue"] != "q-secret" {
|
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)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user