The entry point for adopting something already running, and the half that was missing. The store has carried the distinction since the beginning — a module secret records whether it was `made` or `accepted`, and refuses to invent a replacement for the second — and AcceptSecretForModule existed, with exactly one caller: the broker account issued to a build machine. Nothing else could write one. Without it every module secret is generated, which against a database that already exists puts 32 random bytes where a working credential was. The machine applies it, reports success, and whatever reads it fails to authenticate somewhere else entirely, with the mesh insisting the secret was delivered — which it was. The value is read from a file or from standard input, never from an argument: a value on the command line is in the shell's history and in the process list. Same path a model-access key already takes, and no new dependency — the first version reached for x/term and the existing one needed nothing. Sealed on the way in, plaintext discarded, and not printed back. The only difference from a generated secret is where the value came from. Two rules with a test each, and the second is the one that would have been got wrong: only the line ending is removed, never surrounding space. Trimming both ends is the obvious thing and would deliver a password chosen with a leading space as a different password, silently. Both were briefly untested for different reasons — the trimming lived where no test could reach it, and then a -run filter matched neither test. Extracted, and injected against the whole suite.
70 lines
2.0 KiB
Go
70 lines
2.0 KiB
Go
package main
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// A file has a trailing newline and a password does not.
|
|
//
|
|
// The failure this prevents is the worst kind to diagnose: the credential is delivered, the
|
|
// machine applies it, everything reports success, and authentication fails one byte from correct
|
|
// somewhere else entirely.
|
|
func TestATrailingNewlineIsNotPartOfTheSecret(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "password")
|
|
if err := os.WriteFile(path, []byte("the-database-password\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := valueFor("anchor", "umami", "database", path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if asSupplied(got) != "the-database-password" {
|
|
t.Fatalf("read %q", got)
|
|
}
|
|
}
|
|
|
|
// A password may contain spaces, and they are the operator's.
|
|
//
|
|
// Trimming both ends is the obvious thing and it is wrong: a value chosen with a leading space is
|
|
// a value the mesh would silently deliver as a different one.
|
|
func TestOnlyLineEndingsAreRemoved(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "password")
|
|
if err := os.WriteFile(path, []byte(" spaces matter \n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got, err := valueFor("anchor", "umami", "database", path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if asSupplied(got) != " spaces matter " {
|
|
t.Fatalf("the value was altered beyond its line ending: %q", got)
|
|
}
|
|
}
|
|
|
|
// A file that is not there is said plainly, rather than becoming an empty secret.
|
|
func TestAMissingFileIsRefused(t *testing.T) {
|
|
if _, err := valueFor("anchor", "umami", "database",
|
|
filepath.Join(t.TempDir(), "absent")); err == nil {
|
|
t.Fatal("a missing file produced a value")
|
|
}
|
|
}
|
|
|
|
// The command refuses what it cannot act on, rather than acting on part of it.
|
|
func TestTheArgumentsAreRequired(t *testing.T) {
|
|
for _, args := range [][]string{
|
|
{},
|
|
{"accept"},
|
|
{"accept", "anchor"},
|
|
{"accept", "anchor", "umami"},
|
|
{"give", "anchor", "umami", "database"},
|
|
} {
|
|
if err := secretCommand(t.Context(), args); err == nil {
|
|
t.Errorf("%v was accepted", args)
|
|
}
|
|
}
|
|
}
|