feat(auth): first account on a new server needs the setup token from the server log (M462 #4982)
test-go / test (push) Successful in 2m3s
test-web / test (push) Successful in 1m14s
test-go / integration (push) Successful in 4m38s
release / Build signed APK (releases and dev) (push) Successful in 5m36s
release / Build + push container image (push) Successful in 1m24s
release / Verify release artifacts (tag releases only) (push) Skipped
test-go / test (push) Successful in 2m3s
test-web / test (push) Successful in 1m14s
test-go / integration (push) Successful in 4m38s
release / Build signed APK (releases and dev) (push) Successful in 5m36s
release / Build + push container image (push) Successful in 1m24s
release / Verify release artifacts (tag releases only) (push) Skipped
While no accounts exist, the server mints a random setup token at boot and logs it. Registering the first account (which becomes admin) must carry it, so whoever reaches a freshly exposed instance first cannot claim it. The register page asks GET /api/auth/setup-status and shows a "Setup token" field in place of the invite field while setup is pending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -273,3 +273,75 @@ func TestRegister_FirstAdminRace(t *testing.T) {
|
||||
// not "exactly one" (which would require serializable isolation).
|
||||
t.Logf("admin count after race = %d (>=1 is the invariant)", adminCount)
|
||||
}
|
||||
|
||||
// With the gate on (as Mount sets it), the first account needs the setup
|
||||
// token from the log; a missing or wrong one is refused and creates nothing.
|
||||
func TestRegister_FirstUserNeedsSetupToken(t *testing.T) {
|
||||
if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
|
||||
t.Skip("MINSTREL_TEST_DATABASE_URL not set")
|
||||
}
|
||||
h, _ := testHandlers(t)
|
||||
resetUsers(t, h)
|
||||
token, err := auth.NewSetupToken()
|
||||
if err != nil {
|
||||
t.Fatalf("mint: %v", err)
|
||||
}
|
||||
h.setupToken = token
|
||||
h.requireSetupToken = true
|
||||
|
||||
register := func(body string) int {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/auth/register", bytes.NewReader([]byte(body)))
|
||||
rec := httptest.NewRecorder()
|
||||
h.handleRegister(rec, req)
|
||||
return rec.Code
|
||||
}
|
||||
|
||||
if code := register(`{"username":"squatter","password":"abcd1234"}`); code != http.StatusForbidden {
|
||||
t.Errorf("no token: status = %d, want 403", code)
|
||||
}
|
||||
if code := register(`{"username":"squatter","password":"abcd1234","setup_token":"wrong"}`); code != http.StatusForbidden {
|
||||
t.Errorf("wrong token: status = %d, want 403", code)
|
||||
}
|
||||
var n int
|
||||
if err := h.pool.QueryRow(context.Background(), `SELECT count(*) FROM users`).Scan(&n); err != nil {
|
||||
t.Fatalf("count: %v", err)
|
||||
}
|
||||
if n != 0 {
|
||||
t.Fatalf("a refused registration created %d account(s)", n)
|
||||
}
|
||||
|
||||
if code := register(`{"username":"operator","password":"abcd1234","setup_token":"` + token.Value() + `"}`); code != http.StatusOK {
|
||||
t.Fatalf("right token: status = %d, want 200", code)
|
||||
}
|
||||
|
||||
// Once an account exists the token plays no part: the second user is
|
||||
// governed by registration mode, not the setup token.
|
||||
if _, err := h.pool.Exec(context.Background(),
|
||||
`UPDATE registration_settings SET mode = 'open' WHERE id = true`); err != nil {
|
||||
t.Fatalf("open mode: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_, _ = h.pool.Exec(context.Background(),
|
||||
`UPDATE registration_settings SET mode = 'invite_only' WHERE id = true`)
|
||||
})
|
||||
if code := register(`{"username":"second","password":"abcd1234"}`); code != http.StatusOK {
|
||||
t.Errorf("second user without token: status = %d, want 200", code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetupToken_MatchesOnlyItself(t *testing.T) {
|
||||
tok, err := auth.NewSetupToken()
|
||||
if err != nil {
|
||||
t.Fatalf("mint: %v", err)
|
||||
}
|
||||
if !tok.Matches(tok.Value()) {
|
||||
t.Error("token does not match itself")
|
||||
}
|
||||
if tok.Matches("") || tok.Matches(tok.Value()+"x") {
|
||||
t.Error("token matched something else")
|
||||
}
|
||||
var none *auth.SetupToken
|
||||
if none.Matches("") || none.Matches("anything") {
|
||||
t.Error("a nil token must fail closed")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user