Files
Agent-System-Backend/internal/store/account_state_test.go
T
weichen b9c0a344b6 Implement owner-confirmed account recovery and account controls
Why: Administrator-issued password and login-key paths could take over target accounts, while old credentials and plugin handles ignored current account state.

Evidence: SELF receipts /tmp/iam-u3-backend-all-replay.json, /tmp/iam-u3-backend-final-pg-core.json, /tmp/iam-u3-backend-build.json, and focused HTTP/concurrency receipts in /tmp/iam-u3-*.json.

Impact: Adds trusted pending-account initialization and reissue, preexisting owner-session proof, proposal/rotation/grant transactions, server-side admin search, current Host guards, and legacy HTTP/CLI closure. Production cutover and downstream pins remain pending.

Refs: IAM U3 r1 stages 4-5; Dev feature/iam-u3-account-directory-recovery
2026-10-14 04:20:48 +00:00

154 lines
4.3 KiB
Go

package store
import (
"errors"
"sync"
"testing"
)
func TestAccountStateDisablesCredentialsWithoutRestoringThem(t *testing.T) {
st := openTestStore(t)
adminHash, err := HashPassword("admin-password-1")
if err != nil {
t.Fatal(err)
}
if err := st.CreateAccount("admin", "", adminHash, true); err != nil {
t.Fatal(err)
}
if err := st.ClearMustChangePassword("admin"); err != nil {
t.Fatal(err)
}
userHash, err := HashPassword("user-password-1")
if err != nil {
t.Fatal(err)
}
if err := st.CreateAccount("user", "", userHash, false); err != nil {
t.Fatal(err)
}
if err := st.ClearMustChangePassword("user"); err != nil {
t.Fatal(err)
}
key, err := st.CreateAPIKey("user", "existing")
if err != nil {
t.Fatal(err)
}
session, err := st.CreateSession("user")
if err != nil {
t.Fatal(err)
}
user, err := st.GetAccountByOwner("user")
if err != nil {
t.Fatal(err)
}
if _, err := st.SetAccountEnabled("admin", user.ID, user.StateVersion, false); err != nil {
t.Fatal(err)
}
if _, ok := st.AuthOwner(key); ok {
t.Fatal("disabled user's API key authenticated")
}
if _, _, ok := st.SessionInfo(session); ok {
t.Fatal("disabled user's session authenticated")
}
if _, _, ok := st.VerifyLoginWithVersion("user", "user-password-1"); ok {
t.Fatal("disabled user logged in")
}
if _, err := st.CreateSession("user"); !errors.Is(err, ErrAccountForbidden) {
t.Fatalf("disabled user gained a new session: %v", err)
}
if _, err := st.CreateAPIKey("user", "new"); !errors.Is(err, ErrAccountForbidden) {
t.Fatalf("disabled user gained a new key: %v", err)
}
if _, err := st.SetAccountEnabled("admin", user.ID, user.StateVersion+1, true); err != nil {
t.Fatal(err)
}
if _, ok := st.AuthOwner(key); ok {
t.Fatal("revoked key was restored by enable")
}
if _, _, ok := st.SessionInfo(session); ok {
t.Fatal("revoked session was restored by enable")
}
if _, _, ok := st.VerifyLoginWithVersion("user", "user-password-1"); !ok {
t.Fatal("retained password could not log in after enable")
}
}
func TestAccountStateConcurrentLastTwoManagersAcrossPools(t *testing.T) {
st := openTestStore(t)
other := reopenAccountModelStore(t, st)
t.Logf("last managers dialect=%s two independent pools", st.Dialect())
for _, owner := range []string{"admin-a", "admin-b"} {
if err := st.CreateAccount(owner, "", "existing-hash", true); err != nil {
t.Fatal(err)
}
if err := st.ClearMustChangePassword(owner); err != nil {
t.Fatal(err)
}
}
a, _ := st.GetAccountByOwner("admin-a")
b, _ := st.GetAccountByOwner("admin-b")
start := make(chan struct{})
results := make(chan error, 2)
var wg sync.WaitGroup
for i, item := range []struct {
owner string
account Account
}{{"admin-a", a}, {"admin-b", b}} {
conn := []*Store{st, other}[i]
wg.Add(1)
go func() {
defer wg.Done()
<-start
_, err := conn.SetAccountEnabled(item.owner, item.account.ID, item.account.StateVersion, false)
results <- err
}()
}
close(start)
wg.Wait()
close(results)
wins, conflicts := 0, 0
for err := range results {
if err == nil {
wins++
} else if errors.Is(err, ErrConflict) {
conflicts++
} else {
t.Fatal(err)
}
}
if wins != 1 || conflicts != 1 {
t.Fatalf("last-admin race wins=%d conflicts=%d", wins, conflicts)
}
a, _ = st.GetAccount(a.ID)
b, _ = st.GetAccount(b.ID)
if a.Enabled == b.Enabled {
t.Fatalf("must retain exactly one operable manager: a=%v b=%v", a.Enabled, b.Enabled)
}
}
func TestAccountStateProtectsLastOperableSystemManager(t *testing.T) {
st := openTestStore(t)
hash, err := HashPassword("admin-password-1")
if err != nil {
t.Fatal(err)
}
for _, owner := range []string{"admin-a", "admin-b"} {
if err := st.CreateAccount(owner, "", hash, true); err != nil {
t.Fatal(err)
}
if err := st.ClearMustChangePassword(owner); err != nil {
t.Fatal(err)
}
}
a, _ := st.GetAccountByOwner("admin-a")
b, _ := st.GetAccountByOwner("admin-b")
if _, err := st.SetAccountEnabled("admin-a", a.ID, a.StateVersion, false); err != nil {
t.Fatal(err)
}
if _, err := st.SetAccountEnabled("admin-b", b.ID, b.StateVersion, false); !errors.Is(err, ErrConflict) {
t.Fatalf("last operable manager disabled: %v", err)
}
if _, err := st.SetAccountEnabled("admin-a", b.ID, b.StateVersion, false); !errors.Is(err, ErrAccountForbidden) {
t.Fatalf("disabled manager changed another account: %v", err)
}
}