fix(summer): mask secret flag values in a delegate's error (WR-10)
This commit is contained in:
@@ -9,9 +9,9 @@ import (
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"git.golem15.com/golem15/summercms/internal/argmask"
|
||||
"git.golem15.com/golem15/summercms/internal/build"
|
||||
"git.golem15.com/golem15/summercms/internal/dev"
|
||||
"git.golem15.com/golem15/summercms/modules/bonfire"
|
||||
@@ -161,13 +161,17 @@ func appStreams(out bonfire.Output, terminal bool) (stdout, stderr io.Writer) {
|
||||
|
||||
// runAppCmd runs cmd, the app binary started with argv, forwarding SIGINT
|
||||
// and SIGTERM to it. When the binary exits with a code, the error carries
|
||||
// that code (bonfire.WithExitCode) and is marked as already reported.
|
||||
// that code (bonfire.WithExitCode) and is marked as already reported. The
|
||||
// error names the command through argmask.Join, with the values of
|
||||
// secret-looking flags masked, on every branch: a binary that fails to
|
||||
// start or is ended by a signal gets summer's error block, and a signal
|
||||
// death keeps exit code 1.
|
||||
func runAppCmd(cmd *exec.Cmd, argv []string) error {
|
||||
sigs := make(chan os.Signal, 2)
|
||||
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM)
|
||||
defer signal.Stop(sigs)
|
||||
if err := runChild(cmd, sigs); err != nil {
|
||||
err = fmt.Errorf("summer: %s: %w", strings.Join(argv, " "), err)
|
||||
err = fmt.Errorf("summer: %s: %w", argmask.Join(argv), err)
|
||||
var exit *exec.ExitError
|
||||
if errors.As(err, &exit) && exit.Exited() {
|
||||
// The binary printed its own error; summer exits with its
|
||||
|
||||
@@ -321,14 +321,15 @@ func exitHelperRunner(t *testing.T, mode string) {
|
||||
t.Cleanup(func() { appRunner = prev })
|
||||
}
|
||||
|
||||
// TestDelegateExitCodePassthrough (CR-02): summer exits with the app
|
||||
// binary's exit code and prints no error block of its own; a binary killed
|
||||
// by a signal gets summer's error block and exit 1.
|
||||
// TestDelegateExitCodePassthrough (CR-02, WR-10): summer exits with the
|
||||
// app binary's exit code and prints no error block of its own; a binary
|
||||
// killed by a signal gets summer's error block, with secret flag values
|
||||
// masked, and exit 1.
|
||||
func TestDelegateExitCodePassthrough(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("SIGKILL is POSIX-only")
|
||||
}
|
||||
run := func(t *testing.T, mode string) (string, string, int) {
|
||||
run := func(t *testing.T, mode string, args ...string) (string, string, int) {
|
||||
t.Helper()
|
||||
exitHelperRunner(t, mode)
|
||||
var out, errW syncBuffer
|
||||
@@ -336,12 +337,12 @@ func TestDelegateExitCodePassthrough(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
code := bonfire.Execute(context.Background(), root, []string{"migrate", "--bogus"})
|
||||
code := bonfire.Execute(context.Background(), root, args)
|
||||
return out.String(), errW.String(), code
|
||||
}
|
||||
|
||||
t.Run("exit code", func(t *testing.T) {
|
||||
out, errOut, code := run(t, "exit2")
|
||||
out, errOut, code := run(t, "exit2", "migrate", "--bogus")
|
||||
if code != 2 {
|
||||
t.Errorf("exit %d, want the binary's 2", code)
|
||||
}
|
||||
@@ -354,7 +355,7 @@ func TestDelegateExitCodePassthrough(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("killed by a signal", func(t *testing.T) {
|
||||
out, errOut, code := run(t, "sigkill")
|
||||
out, errOut, code := run(t, "sigkill", "migrate", "--bogus")
|
||||
if code != bonfire.ExitFailure {
|
||||
t.Errorf("exit %d, want 1", code)
|
||||
}
|
||||
@@ -362,6 +363,60 @@ func TestDelegateExitCodePassthrough(t *testing.T) {
|
||||
t.Errorf("want one error block naming the signal, got %d:\nstdout %q\nstderr %q", n, out, errOut)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("killed by a signal masks secrets", func(t *testing.T) {
|
||||
out, errOut, code := run(t, "sigkill", "migrate", "--password", "hunter2")
|
||||
if code != bonfire.ExitFailure {
|
||||
t.Errorf("exit %d, want 1", code)
|
||||
}
|
||||
if n := strings.Count(errOut, " ERROR "); n != 1 || !strings.Contains(errOut, "--password ***") {
|
||||
t.Errorf("want one error block with the password masked, got %d:\nstdout %q\nstderr %q", n, out, errOut)
|
||||
}
|
||||
if strings.Contains(out+errOut, "hunter2") {
|
||||
t.Errorf("the password reached the output:\nstdout %q\nstderr %q", out, errOut)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestRunAppCmdMasksSecrets (WR-10): the error of an app binary killed by a
|
||||
// signal or failing to start names the command with the values of
|
||||
// secret-named flags masked, as the wrong-directory Try line does.
|
||||
func TestRunAppCmdMasksSecrets(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("SIGKILL is POSIX-only")
|
||||
}
|
||||
argv := []string{"admin:create", "--email", "a@b.c", "--password", "hunter2", "--api-token=t0k3n", "--client-secret", "-Xk9q"}
|
||||
check := func(t *testing.T, err error, want ...string) {
|
||||
t.Helper()
|
||||
if err == nil {
|
||||
t.Fatal("runAppCmd returned nil")
|
||||
}
|
||||
if errors.Is(err, bonfire.ErrReported) {
|
||||
t.Errorf("error is marked as reported: %v", err)
|
||||
}
|
||||
msg := err.Error()
|
||||
for _, w := range append(want, "--password ***", "--api-token=***", "--client-secret ***") {
|
||||
if !strings.Contains(msg, w) {
|
||||
t.Errorf("error lacks %q: %s", w, msg)
|
||||
}
|
||||
}
|
||||
for _, secret := range []string{"hunter2", "t0k3n", "-Xk9q"} {
|
||||
if strings.Contains(msg, secret) {
|
||||
t.Errorf("error repeats %q: %s", secret, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("killed by a signal", func(t *testing.T) {
|
||||
cmd := newAppCmd(context.Background(), os.Args[0], ".", []string{"-test.run=^TestExitHelperProcess$"}, io.Discard, io.Discard)
|
||||
cmd.Env = append(cmd.Env, "SUMMER_EXIT_HELPER=sigkill")
|
||||
check(t, runAppCmd(cmd, argv), "signal: killed")
|
||||
})
|
||||
|
||||
t.Run("start failure", func(t *testing.T) {
|
||||
cmd := newAppCmd(context.Background(), filepath.Join(t.TempDir(), "missing"), ".", argv, io.Discard, io.Discard)
|
||||
check(t, runAppCmd(cmd, argv))
|
||||
})
|
||||
}
|
||||
|
||||
// TestDelegateStreams (CR-03): off a terminal the app binary's stdout goes
|
||||
|
||||
Reference in New Issue
Block a user