secret accept is refused for a name the module does not declare, and a pair delivery for a requirement or local it has not got (novox/hq issue 078)

This commit is contained in:
2026-09-21 23:31:06 +02:00
parent e4da83496f
commit 35c5c2bb9b
4 changed files with 148 additions and 11 deletions
+72
View File
@@ -4,9 +4,13 @@ import (
"context"
"errors"
"fmt"
"slices"
"sort"
"strings"
"github.com/jackc/pgx/v5"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/secrets"
)
@@ -137,6 +141,27 @@ func (i *Inventory) SecretFor(ctx context.Context, name, consumer, consumerModul
// what differs is that both ends of the pair are sealed to, and that the record says `accepted`
// so a later read never replaces it with a minted one. The plaintext is discarded here.
func (i *Inventory) AcceptSecretForPair(ctx context.Context, name, consumer, consumerModule, provider, local, value string) error {
// Refused for a requirement the module does not have, or a local it does not keep under it
// (novox/hq 04-ISSUES/078): the credential would sit in the pair unread.
m, err := i.declared(ctx, consumerModule)
if err != nil {
return err
}
if !slices.Contains(m.Requires, name) {
return fmt.Errorf("%s does not require %q; it requires: %s", consumerModule, name, orNone(m.Requires))
}
if locals := m.SecretsMany[name]; len(locals) > 0 {
if local == "" {
return fmt.Errorf("%s keeps several secrets for %q; name one with --local: %s",
consumerModule, name, orNone(sortedNames(locals)))
}
if _, kept := locals[local]; !kept {
return fmt.Errorf("%s does not keep %q for %q; it keeps: %s",
consumerModule, local, name, orNone(sortedNames(locals)))
}
} else if local != "" {
return fmt.Errorf("%s keeps one secret for %q, not several; drop --local", consumerModule, name)
}
consumerKey, err := i.SealingKeyOf(ctx, consumer)
if err != nil {
return err
@@ -362,6 +387,16 @@ func (i *Inventory) SecretForModule(ctx context.Context, node, module, name stri
// Sealed on the way in and the plaintext discarded, exactly as a generated one is — so the only
// difference between the two is where the value came from.
func (i *Inventory) AcceptSecretForModule(ctx context.Context, node, module, name, value string) error {
// Refused for a name the module does not declare. A value stored under a name nothing reads
// is a delivery that changed nothing and reported success — the shape of failure the mesh
// is built to refuse (novox/hq 04-ISSUES/078).
m, err := i.declared(ctx, module)
if err != nil {
return err
}
if _, own := m.OwnSecrets[name]; !own {
return fmt.Errorf("%s does not declare %q as an own secret; %s", module, name, declaresOwn(m))
}
key, err := i.SealingKeyOf(ctx, node)
if err != nil {
return err
@@ -448,3 +483,40 @@ func localFlag(local string) string {
}
return " --local " + local
}
// declared is the manifest the mesh holds for a module — what a delivered value is checked
// against, so a delivery for a name the module does not have is refused rather than stored.
func (i *Inventory) declared(ctx context.Context, module string) (catalogue.Manifest, error) {
known, err := i.Catalogue(ctx)
if err != nil {
return catalogue.Manifest{}, err
}
m, ok := known[module]
if !ok {
return catalogue.Manifest{}, fmt.Errorf("%s is not a module the mesh knows; `module add` it first", module)
}
return m, nil
}
func declaresOwn(m catalogue.Manifest) string {
if len(m.OwnSecrets) == 0 {
return "it declares no own secrets"
}
return "it declares: " + strings.Join(sortedNames(m.OwnSecrets), ", ")
}
func sortedNames(of map[string]string) []string {
names := make([]string, 0, len(of))
for name := range of {
names = append(names, name)
}
sort.Strings(names)
return names
}
func orNone(names []string) string {
if len(names) == 0 {
return "none"
}
return strings.Join(names, ", ")
}