Add a webhooker resetpw subcommand and a bootstrap banner (closes #208) (#239)
check / check (push) Successful in 3m25s
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:
+54
-1
@@ -17,6 +17,7 @@ import (
|
||||
"sneak.berlin/go/webhooker/internal/healthcheck"
|
||||
"sneak.berlin/go/webhooker/internal/logger"
|
||||
"sneak.berlin/go/webhooker/internal/middleware"
|
||||
"sneak.berlin/go/webhooker/internal/resetpw"
|
||||
"sneak.berlin/go/webhooker/internal/server"
|
||||
"sneak.berlin/go/webhooker/internal/session"
|
||||
)
|
||||
@@ -48,6 +49,11 @@ import (
|
||||
// and can still consume the whole budget on their own.
|
||||
const stopTimeout = 5 * time.Second
|
||||
|
||||
// exitUsage is the status for a command line this binary cannot make
|
||||
// sense of, kept distinct from the 1 a refusal exits with so that a
|
||||
// caller can tell "called wrong" from "declined".
|
||||
const exitUsage = 2
|
||||
|
||||
// Build-time variables set via -ldflags.
|
||||
//
|
||||
//nolint:gochecknoglobals // Build-time variables injected by the linker.
|
||||
@@ -60,7 +66,54 @@ func main() {
|
||||
globals.Appname = appname
|
||||
globals.Version = version
|
||||
|
||||
os.Exit(run(os.Stderr))
|
||||
os.Exit(dispatch(os.Args[1:], os.Stdin, os.Stdout, os.Stderr))
|
||||
}
|
||||
|
||||
// dispatch routes the command line to a subcommand.
|
||||
//
|
||||
// No arguments runs the server, which is what the image's CMD and
|
||||
// every existing deployment invoke; that path is unchanged, including
|
||||
// where the DATA_DIR lock is taken relative to building the fx graph
|
||||
// and how fx propagates a non-zero exit itself.
|
||||
func dispatch(
|
||||
args []string,
|
||||
stdin io.Reader,
|
||||
stdout, stderr io.Writer,
|
||||
) int {
|
||||
if len(args) == 0 {
|
||||
return run(stderr)
|
||||
}
|
||||
|
||||
switch args[0] {
|
||||
case resetpw.Name:
|
||||
return resetpw.Run(args[1:], stdin, stdout, stderr)
|
||||
case "help", "-h", "-help", "--help":
|
||||
usage(stdout)
|
||||
|
||||
return 0
|
||||
default:
|
||||
_, _ = fmt.Fprintf(
|
||||
stderr, "%s: unknown subcommand %q\n", appname, args[0],
|
||||
)
|
||||
usage(stderr)
|
||||
|
||||
return exitUsage
|
||||
}
|
||||
}
|
||||
|
||||
// usage lists what the binary can be asked to do.
|
||||
func usage(w io.Writer) {
|
||||
_, _ = fmt.Fprintf(w, `usage: %s [subcommand]
|
||||
|
||||
With no subcommand, runs the webhooker server.
|
||||
|
||||
Subcommands:
|
||||
%s [-generate] <username>
|
||||
Set an existing account's password on a stopped deployment.
|
||||
Recovers an admin account whose bootstrap password was lost.
|
||||
help
|
||||
Print this message.
|
||||
`, appname, resetpw.Name)
|
||||
}
|
||||
|
||||
// run takes the exclusive DATA_DIR lock, then runs the application
|
||||
|
||||
Reference in New Issue
Block a user