Add a webhooker resetpw subcommand and a bootstrap banner (closes #208) (#239)
All checks were successful
check / check (push) Successful in 3m25s
All checks were successful
check / check (push) Successful in 3m25s
The admin bootstrap password was printed once, as one line among roughly 45 fx lines, and under docker run -d went to container logs subject to rotation. There was no reset path at all -- no subcommand, no forgot-password flow, no env override -- so recovery meant hand-deleting the users row from webhooker.db, which was documented nowhere. Adds webhooker resetpw [-generate] <username>. The password is read from stdin or generated with the existing crypto/rand helper, never taken from argv where /proc would publish it. It reuses the existing Argon2id hashing rather than reimplementing the parameters, and writes a single UPDATE only after the hash is complete, so no failure can leave an account with no usable password. An unknown username is a hard error and never creates an account. It refuses to run against a DATA_DIR held by a live instance, via the exclusive lock from #201. DATA_DIR and webhooker.db are checked to exist before the lock is acquired, so a mistyped path creates nothing -- neither a directory tree nor a stray lock file. The bootstrap password now appears exactly once, in a distinct banner written straight to a caller-named writer rather than as an fx log line.
This commit was merged in pull request #239.
This commit is contained in:
@@ -2,12 +2,14 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"sneak.berlin/go/webhooker/internal/datadir"
|
||||
"sneak.berlin/go/webhooker/internal/resetpw"
|
||||
"sneak.berlin/go/webhooker/internal/server"
|
||||
)
|
||||
|
||||
@@ -68,6 +70,65 @@ func TestRunRefusesLockedDataDir(t *testing.T) {
|
||||
assert.Contains(t, stderr.String(), "another instance")
|
||||
}
|
||||
|
||||
// TestDispatch_NoArgumentsRunsTheServer pins the routing of a bare
|
||||
// invocation, which is what the image's CMD and every deployment use.
|
||||
// Adding subcommands must not move the server off the empty argument
|
||||
// list, and must not move the DATA_DIR lock: this asserts the refusal
|
||||
// arrives with no fx graph built, exactly as run does on its own.
|
||||
func TestDispatch_NoArgumentsRunsTheServer(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("DATA_DIR", dir)
|
||||
|
||||
lock, err := datadir.Acquire(dir)
|
||||
require.NoError(t, err)
|
||||
|
||||
defer func() { _ = lock.Release() }()
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
|
||||
code := dispatch(nil, strings.NewReader(""), &stdout, &stderr)
|
||||
|
||||
require.Equal(t, 1, code)
|
||||
assert.Contains(t, stderr.String(), "another instance")
|
||||
}
|
||||
|
||||
// TestDispatch_UnknownSubcommand keeps a mistyped subcommand from
|
||||
// starting a server. Anything else would have `webhooker resetpww`
|
||||
// silently take the DATA_DIR lock and serve.
|
||||
func TestDispatch_UnknownSubcommand(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
|
||||
code := dispatch(
|
||||
[]string{"resetpww", "admin"},
|
||||
strings.NewReader(""), &stdout, &stderr,
|
||||
)
|
||||
|
||||
require.Equal(t, 2, code)
|
||||
assert.Contains(t, stderr.String(), "unknown subcommand")
|
||||
assert.Contains(
|
||||
t, stderr.String(), resetpw.Name,
|
||||
"the usage must name the subcommand that does exist",
|
||||
)
|
||||
}
|
||||
|
||||
// TestDispatch_Help answers on standard output with a zero status, so
|
||||
// `webhooker help` is usable in a pipe.
|
||||
func TestDispatch_Help(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var stdout, stderr bytes.Buffer
|
||||
|
||||
code := dispatch(
|
||||
[]string{"help"}, strings.NewReader(""), &stdout, &stderr,
|
||||
)
|
||||
|
||||
require.Equal(t, 0, code)
|
||||
assert.Empty(t, stderr.String())
|
||||
assert.Contains(t, stdout.String(), resetpw.Name)
|
||||
}
|
||||
|
||||
// tailHeadroom is the slack the fx stop budget must keep beyond the
|
||||
// server stop hook. The hooks that run after the server — the
|
||||
// delivery engine, the healthcheck, the webhook DB manager and the
|
||||
|
||||
Reference in New Issue
Block a user