Files
Agent-System-Backend/internal/store/super_admin_fallback_test.go

155 lines
6.4 KiB
Go

package store
import (
"errors"
"testing"
"time"
)
func rootFallback(owner, password string) bool {
return owner == "root" && password == superAdminTestPassword
}
func anyOwnerFallback(_, password string) bool {
return password == superAdminTestPassword
}
func bootstrapRoot(t *testing.T, st *Store) {
t.Helper()
if _, err := st.BootstrapSuperAdmin("root", superAdminHash(t, superAdminTestPassword)); err != nil {
t.Fatal(err)
}
}
func TestSuperAdminFallbackSurvivesDatabasePasswordChanges(t *testing.T) {
st := openTestStore(t)
bootstrapRoot(t, st)
if _, _, ok := st.VerifyLoginWithFallback("root", superAdminTestPassword, rootFallback); !ok {
t.Fatal("initial environment password must authenticate")
}
if err := st.ChangeUserPasswordActiveWithFallback("root", superAdminTestPassword, superAdminHash(t, "db-password-1"), "", rootFallback); err != nil {
t.Fatal(err)
}
if err := st.ChangeUserPasswordActiveWithFallback("root", "db-password-1", superAdminHash(t, "db-password-2"), "", rootFallback); err != nil {
t.Fatal(err)
}
if _, _, ok := st.VerifyLoginWithFallback("root", "db-password-1", rootFallback); ok {
t.Fatal("replaced database password must stop working")
}
if _, _, ok := st.VerifyLoginWithFallback("root", "db-password-2", rootFallback); !ok {
t.Fatal("current database password must authenticate")
}
state, ver, ok := st.VerifyLoginWithFallback("root", superAdminTestPassword, rootFallback)
if !ok || state.Owner != "root" || state.MustChangePassword {
t.Fatalf("environment fallback must still authenticate: ok=%v %+v", ok, state)
}
account, err := st.GetAccountByOwner("root")
if err != nil {
t.Fatal(err)
}
if ver != account.AuthVersion {
t.Fatalf("fallback login must report the account auth version: %d != %d", ver, account.AuthVersion)
}
if _, _, ok := st.VerifyLoginWithVersion("root", superAdminTestPassword); ok {
t.Fatal("fallback must not be persisted: plain verifier accepted the environment password")
}
if VerifyPassword(storedPasswordHash(t, st, "root"), superAdminTestPassword) {
t.Fatal("database hash must not be overwritten with the environment password")
}
}
func TestSuperAdminFallbackAsCurrentPassword(t *testing.T) {
st := openTestStore(t)
bootstrapRoot(t, st)
if err := st.ChangeUserPasswordActiveWithFallback("root", superAdminTestPassword, superAdminHash(t, "db-password-1"), "", rootFallback); err != nil {
t.Fatal(err)
}
if err := st.ChangeUserPasswordActive("root", superAdminTestPassword, superAdminHash(t, "db-password-2"), ""); !errors.Is(err, ErrBadCredentials) {
t.Fatalf("without fallback the environment password is not current: %v", err)
}
if err := st.ChangeUserPasswordActiveWithFallback("root", superAdminTestPassword, superAdminHash(t, "db-password-2"), "", rootFallback); err != nil {
t.Fatalf("fallback must be accepted as current password: %v", err)
}
if _, _, ok := st.VerifyLoginWithFallback("root", "db-password-1", rootFallback); ok {
t.Fatal("previous database password must stop working")
}
}
func TestSuperAdminFallbackCompletesForcedPasswordChange(t *testing.T) {
st := openTestStore(t)
bootstrapRoot(t, st)
if err := st.ChangeUserPasswordActiveWithFallback("root", superAdminTestPassword, superAdminHash(t, "db-password-1"), "", rootFallback); err != nil {
t.Fatal(err)
}
if _, err := st.db.Exec(st.q(`UPDATE users SET must_change_password=? WHERE owner=?`), true, "root"); err != nil {
t.Fatal(err)
}
state, ver, ok := st.VerifyLoginWithFallback("root", superAdminTestPassword, rootFallback)
if !ok || !state.MustChangePassword {
t.Fatalf("fallback login on forced account: ok=%v %+v", ok, state)
}
tok, err := st.CreateSessionVerified("root", SessionScopeSetPasswordOnly, 10*time.Minute, ver)
if err != nil {
t.Fatal(err)
}
if _, err := st.CompleteForcedPasswordChecked(tok, superAdminTestPassword, superAdminHash(t, "db-password-2")); !errors.Is(err, ErrBadCredentials) {
t.Fatalf("without fallback the environment password is not current: %v", err)
}
if _, err := st.CompleteForcedPasswordCheckedWithFallback(tok, superAdminTestPassword, superAdminHash(t, "db-password-2"), rootFallback); err != nil {
t.Fatalf("fallback must complete forced change: %v", err)
}
if _, _, ok := st.SessionInfo(tok); ok {
t.Fatal("forced change must revoke the restricted session")
}
if _, _, ok := st.VerifyLoginWithFallback("root", "db-password-1", rootFallback); ok {
t.Fatal("previous database password must stop working")
}
for _, pw := range []string{"db-password-2", superAdminTestPassword} {
if _, _, ok := st.VerifyLoginWithFallback("root", pw, rootFallback); !ok {
t.Fatalf("credential %q must authenticate after forced change", pw)
}
}
}
func TestSuperAdminFallbackRejectedForUnmarkedAccounts(t *testing.T) {
st := openTestStore(t)
bootstrapRoot(t, st)
if err := st.CreateAccount("alice", "", superAdminHash(t, "alice-pass-1"), false); err != nil {
t.Fatal(err)
}
if err := st.ClearMustChangePassword("alice"); err != nil {
t.Fatal(err)
}
if err := st.CreateAccount("legacy-admin", "", superAdminHash(t, "legacy-pass-1"), true); err != nil {
t.Fatal(err)
}
if err := st.ClearMustChangePassword("legacy-admin"); err != nil {
t.Fatal(err)
}
for _, owner := range []string{"alice", "legacy-admin", "ghost"} {
if _, _, ok := st.VerifyLoginWithFallback(owner, superAdminTestPassword, anyOwnerFallback); ok {
t.Fatalf("fallback authenticated unmarked account %q", owner)
}
}
if err := st.ChangeUserPasswordActiveWithFallback("alice", superAdminTestPassword, superAdminHash(t, "alice-pass-2"), "", anyOwnerFallback); !errors.Is(err, ErrBadCredentials) {
t.Fatalf("fallback accepted as current password for unmarked account: %v", err)
}
if _, _, ok := st.VerifyLoginWithFallback("alice", "alice-pass-1", rootFallback); !ok {
t.Fatal("ordinary database login must be unaffected")
}
}
func TestSuperAdminFallbackRejectedWhenDisabled(t *testing.T) {
st := openTestStore(t)
bootstrapRoot(t, st)
if _, err := st.db.Exec(st.q(`UPDATE users SET enabled=? WHERE owner=?`), false, "root"); err != nil {
t.Fatal(err)
}
if _, _, ok := st.VerifyLoginWithFallback("root", superAdminTestPassword, rootFallback); ok {
t.Fatal("disabled super admin must not authenticate with the fallback")
}
if err := st.ChangeUserPasswordActiveWithFallback("root", superAdminTestPassword, superAdminHash(t, "db-password-1"), "", rootFallback); !errors.Is(err, ErrAccountForbidden) {
t.Fatalf("disabled super admin password change: %v", err)
}
}