Some checks failed
check / check (push) Failing after 2m32s
The bootstrap admin password was shown exactly once, as one INFO record among the roughly 45 fx lines a boot writes, and there was no reset path at all: no subcommand, no forgot-password flow, no override. Losing that line meant deleting the users row from webhooker.db by hand so the next start would re-seed. - internal/banner renders the one credential shown in the clear as a ruled block written straight to standard output, so it does not read as one more log line. The first boot emits the password there and nowhere else, and the banner names the recovery command. - `webhooker resetpw [-generate] <username>` sets an existing account's password. It reads the password as one line from standard input, or generates one with crypto/rand via the existing GenerateRandomPassword; it is never an argv value, which /proc would publish to every account on the host. Hashing goes through database.HashPassword, so the Argon2id parameters cannot drift. - It refuses to run against a DATA_DIR a live instance holds, by taking the same exclusive flock internal/datadir gives the server, and releases it when it finishes. - It creates nothing. A missing DATA_DIR, a directory with no webhooker.db, and an unknown username are each an error: datadir .Acquire calls os.MkdirAll, so a mistyped path would otherwise be built out and reported as a success. The existence checks therefore run before the lock is taken. - The account is resolved and the hash computed in full before the single UPDATE that stores it, so any failure leaves the stored credential untouched. - database.Open exposes the connect-and-migrate path without fx and without seeding; seeding moves to ensureAdminUser, which only a server start calls. - main gains subcommand dispatch. No arguments still runs the server on the same path, with the DATA_DIR lock taken before the fx graph is built and fx owning the non-zero exit; an unknown subcommand exits 2 rather than starting a server. Tests: reset then log in through the real form POST handler, the generated password verifying against the stored hash, the refusal against a held lock, both create-nothing cases, the unknown user, the unusable passwords, and the first-boot banner carrying a password that opens the account. README documents the bootstrap banner and the recovery command, including the container invocation and what resetpw will not do.
48 lines
1.5 KiB
Go
48 lines
1.5 KiB
Go
// 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
|
|
}
|