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:
59
internal/banner/banner_test.go
Normal file
59
internal/banner/banner_test.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package banner_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"sneak.berlin/go/webhooker/internal/banner"
|
||||
)
|
||||
|
||||
// TestCredentials_IsFindableByEye pins the properties that make the
|
||||
// block worth having: rules above and below it, the two fields on
|
||||
// their own lines, and blank lines separating it from whatever the
|
||||
// surrounding log wrote.
|
||||
func TestCredentials_IsFindableByEye(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var out bytes.Buffer
|
||||
|
||||
require.NoError(t, banner.Credentials(
|
||||
&out, "HEADLINE", "admin", "s3cret", "NOTE",
|
||||
))
|
||||
|
||||
got := out.String()
|
||||
lines := strings.Split(strings.Trim(got, "\n"), "\n")
|
||||
|
||||
require.GreaterOrEqual(t, len(lines), 3)
|
||||
assert.Equal(t, lines[0], lines[len(lines)-1], "rules must match")
|
||||
assert.Greater(
|
||||
t, len(lines[0]), 40, "the rule must be visible at a glance",
|
||||
)
|
||||
assert.Equal(t, strings.Repeat("=", len(lines[0])), lines[0])
|
||||
|
||||
assert.Contains(t, got, "\n username: admin\n")
|
||||
assert.Contains(t, got, "\n password: s3cret\n")
|
||||
assert.Contains(t, got, "HEADLINE")
|
||||
assert.Contains(t, got, "NOTE")
|
||||
assert.True(t, strings.HasPrefix(got, "\n"))
|
||||
}
|
||||
|
||||
// failingWriter reports the write error a banner must not swallow: it
|
||||
// is the one copy of a password that will never be shown again.
|
||||
type failingWriter struct{}
|
||||
|
||||
func (failingWriter) Write([]byte) (int, error) {
|
||||
return 0, assert.AnError
|
||||
}
|
||||
|
||||
func TestCredentials_ReportsAWriteFailure(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := banner.Credentials(
|
||||
failingWriter{}, "HEADLINE", "admin", "s3cret", "NOTE",
|
||||
)
|
||||
|
||||
require.ErrorIs(t, err, assert.AnError)
|
||||
}
|
||||
Reference in New Issue
Block a user