155 lines
6.4 KiB
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)
|
|
}
|
|
}
|