Add a webhooker resetpw subcommand and a bootstrap banner (closes #208) #239
Reference in New Issue
Block a user
Delete Branch "issue-208-admin-password-recovery"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #208.
The bootstrap admin password was shown exactly once, as one
INFOrecord among the ~45 fx lines a boot writes, and there was no reset
path at all. Losing that line meant deleting the
usersrow fromwebhooker.dbby hand.What landed
internal/bannerrenders the one credential shown in the clearas 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 — the old
d.log.Info("admin user created", "password", ...)field is gone, so the plaintext still appearsexactly once — and the banner names the recovery command.
webhooker resetpw [-generate] <username>sets an existingaccount's password. Hashing goes through
database.HashPassword, sothe Argon2id parameters cannot drift into a second implementation.
flockfrominternal/datadirthe server takes, detects thecontended case with
errors.Is(err, datadir.ErrLocked), and releasesit on the way out.
database.Openexposes the connect-and-migrate path without fxand without seeding; seeding moved to
ensureAdminUser, which only aserver start calls. A maintenance command must not answer "no such
user" by creating one.
maingains subcommand dispatch. No arguments still runs theserver on the unchanged path: the
DATA_DIRlock is taken insiderunbefore the fx graph is built, and fx still owns the non-zeroexit (#218). An unknown
subcommand exits 2 rather than starting a server, so
resetpwwdoesnot silently take the lock and serve. Stdlib
flag; no newdependency.
including the container invocation (the image sets
CMD, notENTRYPOINT, so the whole command has to be given) and the threethings
resetpwwill not do.Decisions the issue left open
The
MkdirAllside effect: not reached.datadir.Acquirecreatesthe directory, so
resetpwstatsDATA_DIRand{DATA_DIR}/ webhooker.dbbefore taking the lock and errors if either ismissing. A mistyped path is never built out, no lock file is left in a
wrong directory, and no empty schema is migrated into one — otherwise
the command would report success against a deployment that does not
exist while the real one stayed locked out. Two tests pin it: a missing
DATA_DIRmust still not exist afterwards, and a directory with nodatabase must still be empty (lock file included).
Password input: stdin or
-generate, never argv. On Linux argv isreadable through
/procby every account on the host for as long asthe process lives, and it lands in shell history.
-generateuses theexisting
GenerateRandomPassword, which iscrypto/randthroughout(
rand.Intovercrypto/rand.Reader). Standard input is read as oneline, minimum 8 bytes — a floor against a stray keystroke or a
truncated pipe becoming the account's only credential, not a policy.
When stdin is a terminal it prompts and says the input is echoed; no
terminal library is pulled in to turn echo off.
Unknown user: hard error.
resetpwchanges an existing account'spassword and never creates one, so a typo says so instead of quietly
adding a second user. Tested, including that the user count stays 1.
No half-written state. The account is resolved and the hash
computed in full before the single
UPDATEthat stores it, so anyfailure leaves the stored credential untouched — asserted by the
refusal, unknown-user and rejected-password tests, which all re-read
the stored hash. A supplied password is not echoed back; a generated
one falls back to stderr if stdout cannot be written, because at that
point it is already stored and losing it locks the deployment out.
Test cost. One fx application in the whole new package, shared by
the reset-then-login test, and the deployment fixture inserts its user
row directly rather than booting the graph to spend an Argon2id hash on
a password no test can use.
internal/handlersgained nothing.Gate
Load average during the runs: 44-120 across 48 cores (
uptimebefore/after each: 82.67 → 74.58 for the tests, 44.34 after lint).
make test(rebased ontoaba02bc,GOFLAGS=-count=1, zero(cached)lines):The new tests, all passing:
make checkis red, and not on this branch's account. The singlefailure is
TestGormScanIsNeverCalledOutsideTests, which fails onnextitself —internal/delivery/queue_depth.go:109and:161call(*gorm.DB).Scan. Already filed as#234; nothing here touches
that file. Because
script/checkstops at the first failing step, lintand fmt-check were run as their own targets:
docker build --no-cache-filter=lint --no-cache-filter=builder --progress=plain .(run before the rebase, on the same tree):Real durations on both stages, no
CACHEDon either, every otherpackage green inside the container —
internal/handlersat 51.3s underload 120, well inside the 90s budget. The build cannot reach the
runtime stage until issue 234 is fixed, which is not this unit's
change. No image was produced; nothing of mine is left in
docker ps -aordocker image ls. No prune was run.PASS — independent review of #239 found no blocking defects.
Disclosure: the authoritative gate re-run on
29ce4cc(docker build --no-cache-filter=lint --no-cache-filter=builder) is red, solely at#32 [builder 9/11] RUN make testonTestGormScanIsNeverCalledOutsideTests, naming onlyinternal/delivery/queue_depth.go:109:3and:161:3— #234, not attributable here. Lint ran uncached (#19 [lint 9/9], 61.4s,0 issues.),#17 make fmt-checkclean, zero(cached)go-test markers, every other packageok. Merges cleanly intonext(verified by local test-merge, not Gitea's flag).clawbot referenced this pull request2026-08-20 08:21:53 +02:00
clawbot referenced this pull request2026-08-20 08:34:07 +02:00