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:
47
internal/banner/banner.go
Normal file
47
internal/banner/banner.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// Package banner renders the operator-facing blocks that carry a
|
||||
// plaintext credential.
|
||||
//
|
||||
// A generated password printed as one more structured log line is lost:
|
||||
// a boot writes roughly 45 fx PROVIDE/RUN/HOOK lines around it, and
|
||||
// under `docker run -d` it is one line in a log subject to rotation. A
|
||||
// credential that is shown exactly once has to be findable by eye when
|
||||
// an operator scrolls back, so it is written as a ruled block rather
|
||||
// than as a log record.
|
||||
//
|
||||
// It is deliberately not a log line: it goes straight to the writer the
|
||||
// caller names — standard output for both the first-boot account and
|
||||
// the `resetpw` subcommand — so it is neither levelled, filtered, nor
|
||||
// rendered as JSON by whichever handler internal/logger installed.
|
||||
package banner
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ruleWidth is the length of the horizontal rules, chosen to fit an
|
||||
// 80-column terminal without wrapping.
|
||||
const ruleWidth = 72
|
||||
|
||||
// Credentials writes a ruled block naming an account and its plaintext
|
||||
// password. headline says which event produced it, and note says what
|
||||
// the operator must do about it; both are written verbatim, so a
|
||||
// multi-line note must already be wrapped.
|
||||
func Credentials(
|
||||
w io.Writer,
|
||||
headline, username, password, note string,
|
||||
) error {
|
||||
rule := strings.Repeat("=", ruleWidth)
|
||||
|
||||
_, err := fmt.Fprintf(
|
||||
w,
|
||||
"\n%s\n%s\n\n username: %s\n password: %s\n\n%s\n%s\n\n",
|
||||
rule, headline, username, password, note, rule,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("writing credentials banner: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user