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.
82 lines
2.2 KiB
Go
82 lines
2.2 KiB
Go
package database
|
|
|
|
import (
|
|
"context"
|
|
"io"
|
|
"log/slog"
|
|
"os"
|
|
"time"
|
|
|
|
"go.uber.org/fx"
|
|
)
|
|
|
|
// NewTestRetentionReaper builds a RetentionReaper backed by the given
|
|
// main database and per-webhook database manager, without the fx
|
|
// lifecycle. Intended for tests.
|
|
func NewTestRetentionReaper(
|
|
db *Database,
|
|
mgr *WebhookDBManager,
|
|
) *RetentionReaper {
|
|
return &RetentionReaper{
|
|
db: db,
|
|
dbManager: mgr,
|
|
log: slog.New(slog.NewTextHandler(
|
|
os.Stderr,
|
|
&slog.HandlerOptions{Level: slog.LevelDebug},
|
|
)),
|
|
interval: time.Hour,
|
|
}
|
|
}
|
|
|
|
// ExportSweep runs a single retention sweep synchronously for tests.
|
|
func (r *RetentionReaper) ExportSweep(ctx context.Context) {
|
|
r.sweep(ctx)
|
|
}
|
|
|
|
// ExportRegisterHooks registers the reaper's real fx lifecycle hooks
|
|
// on a lifecycle supplied by a test, so a test can drive the exact
|
|
// OnStart/OnStop functions the application runs and hand OnStart the
|
|
// kind of context fx actually supplies.
|
|
func (r *RetentionReaper) ExportRegisterHooks(lc fx.Lifecycle) {
|
|
r.registerHooks(lc)
|
|
}
|
|
|
|
// ExportStart starts the reaper's background loop for tests.
|
|
func (r *RetentionReaper) ExportStart() {
|
|
r.start()
|
|
}
|
|
|
|
// ExportStop stops the reaper's background loop for tests.
|
|
func (r *RetentionReaper) ExportStop(ctx context.Context) error {
|
|
return r.stop(ctx)
|
|
}
|
|
|
|
// ExportWedgeLoop adds a goroutine to the reaper's WaitGroup that
|
|
// never observes cancellation and returns only when release is
|
|
// closed. It stands in for a sweep stuck on a locked database.
|
|
func (r *RetentionReaper) ExportWedgeLoop(
|
|
release <-chan struct{},
|
|
) {
|
|
r.wg.Go(func() {
|
|
<-release
|
|
})
|
|
}
|
|
|
|
// ExportSetInterval overrides the sweep interval for tests.
|
|
func (r *RetentionReaper) ExportSetInterval(d time.Duration) {
|
|
r.interval = d
|
|
}
|
|
|
|
// ExportSetBannerOut redirects the first-boot credentials banner, so a
|
|
// test can read what the operator would have seen. It must be called
|
|
// before the fx start hook runs, which is where the account is seeded.
|
|
func (d *Database) ExportSetBannerOut(w io.Writer) {
|
|
d.bannerOut = w
|
|
}
|
|
|
|
// DummyPasswordHashForTest exposes the encoded hash that unknown
|
|
// usernames are verified against.
|
|
func DummyPasswordHashForTest() string {
|
|
return dummyPasswordHash()
|
|
}
|