package server import ( "encoding/json" "net/http" "strconv" "strings" "testing" "agent-system-backend/internal/store" ) const superAdminFixturePassword = "Super-Admin-Pass-1" // bootSuperAdmin establishes "admin" as the configured super administrator // (as startup bootstrap does) and returns a full password session cookie. func bootSuperAdmin(t *testing.T, h http.Handler, st *store.Store) string { t.Helper() hash, err := store.HashPassword(superAdminFixturePassword) if err != nil { t.Fatal(err) } if _, err := st.BootstrapSuperAdmin("admin", hash); err != nil { t.Fatal(err) } return loginFixture(t, h, "admin", superAdminFixturePassword) } // bootOrdinaryAdmin creates a legacy/ordinary administrator with a usable // password session. func bootOrdinaryAdmin(t *testing.T, h http.Handler, st *store.Store, owner string) string { t.Helper() hash, err := store.HashPassword("ordinary-admin-1") if err != nil { t.Fatal(err) } if err := st.CreateAccount(owner, "", hash, true); err != nil { t.Fatal(err) } if err := st.ClearMustChangePassword(owner); err != nil { t.Fatal(err) } return loginFixture(t, h, owner, "ordinary-admin-1") } func loginFixture(t *testing.T, h http.Handler, owner, password string) string { t.Helper() loginClear(owner) t.Cleanup(func() { loginClear(owner) }) body, _ := json.Marshal(map[string]string{"owner": owner, "password": password}) rec := do(h, "POST", "/api/auth/login", string(body), "") if rec.Code != http.StatusOK { t.Fatalf("login %s: %d %s", owner, rec.Code, rec.Body.String()) } cookie := cookieOf(rec) if cookie == "" { t.Fatalf("login %s: missing session cookie", owner) } return cookie } func decodeAccountHTTP(t *testing.T, rec interface{ Bytes() []byte }) map[string]any { t.Helper() var out map[string]any if err := json.Unmarshal(rec.Bytes(), &out); err != nil { t.Fatal(err) } return out } func stateVersionOf(t *testing.T, st *store.Store, owner string) (string, string) { t.Helper() account, err := st.GetAccountByOwner(owner) if err != nil { t.Fatal(err) } return account.ID, strconv.FormatInt(account.StateVersion, 10) } func TestSuperAdminHTTPAppointsAdministrators(t *testing.T) { srv, st := newAuthTestServer(t) h := srv.Handler() super := bootSuperAdmin(t, h, st) me := do(h, "GET", "/api/me", "", super) if me.Code != http.StatusOK || decodeAccountHTTP(t, me.Body)["isSuperAdmin"] != true { t.Fatalf("super admin /api/me: %d %s", me.Code, me.Body.String()) } if strings.Contains(me.Body.String(), superAdminFixturePassword) { t.Fatal("/api/me leaked the super admin password") } created := do(h, "POST", "/api/admin/users", `{"owner":"boss","isAdmin":true}`, super) if created.Code != http.StatusOK { t.Fatalf("super admin creates administrator: %d %s", created.Code, created.Body.String()) } out := decodeAccountHTTP(t, created.Body) if out["isAdmin"] != true || out["isSuperAdmin"] != false { t.Fatalf("created administrator projection: %v", out) } byRole := do(h, "POST", "/api/admin/users", `{"owner":"boss2","role":"admin"}`, super) if byRole.Code != http.StatusOK || decodeAccountHTTP(t, byRole.Body)["isAdmin"] != true { t.Fatalf("super admin creates administrator by role: %d %s", byRole.Code, byRole.Body.String()) } if rec := do(h, "POST", "/api/admin/users", `{"owner":"carol"}`, super); rec.Code != http.StatusOK { t.Fatalf("super admin creates user: %d %s", rec.Code, rec.Body.String()) } carolID, version := stateVersionOf(t, st, "carol") promoted := do(h, "PUT", "/api/admin/users/"+carolID+"/admin", `{"expectedStateVersion":`+version+`,"isAdmin":true}`, super) if promoted.Code != http.StatusOK || decodeAccountHTTP(t, promoted.Body)["isAdmin"] != true { t.Fatalf("super admin promotes user: %d %s", promoted.Code, promoted.Body.String()) } _, version = stateVersionOf(t, st, "carol") demoted := do(h, "PUT", "/api/admin/users/"+carolID+"/admin", `{"expectedStateVersion":`+version+`,"isAdmin":false}`, super) if demoted.Code != http.StatusOK || decodeAccountHTTP(t, demoted.Body)["isAdmin"] != false { t.Fatalf("super admin demotes administrator: %d %s", demoted.Code, demoted.Body.String()) } list := do(h, "GET", "/api/admin/users", "", super) if list.Code != http.StatusOK { t.Fatalf("list users: %d %s", list.Code, list.Body.String()) } var users []map[string]any if err := json.Unmarshal(list.Body.Bytes(), &users); err != nil { t.Fatal(err) } for _, u := range users { if u["isSuperAdmin"] != (u["owner"] == "admin") { t.Fatalf("admin listing isSuperAdmin for %v = %v", u["owner"], u["isSuperAdmin"]) } } } func TestOrdinaryAdminHTTPCannotGrantAdministrativeAuthority(t *testing.T) { srv, st := newAuthTestServer(t) h := srv.Handler() super := bootSuperAdmin(t, h, st) ops := bootOrdinaryAdmin(t, h, st, "ops") me := do(h, "GET", "/api/me", "", ops) if me.Code != http.StatusOK || decodeAccountHTTP(t, me.Body)["isSuperAdmin"] != false { t.Fatalf("ordinary admin /api/me: %d %s", me.Code, me.Body.String()) } for _, body := range []string{ `{"owner":"sneaky","isAdmin":true}`, `{"owner":"sneaky","role":"admin"}`, `{"owner":"sneaky","isAdmin":false,"role":"admin"}`, } { if rec := do(h, "POST", "/api/admin/users", body, ops); rec.Code != http.StatusForbidden { t.Fatalf("ordinary admin created administrator with %s: %d %s", body, rec.Code, rec.Body.String()) } } if _, err := st.GetAccountByOwner("sneaky"); err != store.ErrNotFound { t.Fatalf("rejected administrator creation left an account: %v", err) } created := do(h, "POST", "/api/admin/users", `{"owner":"dave","isAdmin":false}`, ops) if created.Code != http.StatusOK || decodeAccountHTTP(t, created.Body)["isAdmin"] != false { t.Fatalf("ordinary admin creates user: %d %s", created.Code, created.Body.String()) } daveID, version := stateVersionOf(t, st, "dave") if rec := do(h, "PUT", "/api/admin/users/"+daveID+"/admin", `{"expectedStateVersion":`+version+`,"isAdmin":true}`, ops); rec.Code != http.StatusForbidden { t.Fatalf("ordinary admin promoted a user: %d %s", rec.Code, rec.Body.String()) } epoch, _ := st.AuthorityEpoch() epochText := strconv.FormatInt(epoch, 10) if rec := do(h, "PUT", "/api/admin/users/"+daveID+"/system-grants", `{"expectedAuthorityEpoch":`+epochText+`,"idempotencyKey":"ops-grant-dave","actions":["system.users.create"]}`, ops); rec.Code != http.StatusForbidden { t.Fatalf("ordinary admin granted account administration: %d %s", rec.Code, rec.Body.String()) } opsID, _ := stateVersionOf(t, st, "ops") if rec := do(h, "PUT", "/api/admin/users/"+opsID+"/system-grants", `{"expectedAuthorityEpoch":`+epochText+`,"idempotencyKey":"ops-self","actions":[]}`, ops); rec.Code != http.StatusForbidden { t.Fatalf("ordinary admin changed its own administrative grants: %d %s", rec.Code, rec.Body.String()) } if dave, _ := st.GetAccountByOwner("dave"); dave.IsAdmin { t.Fatal("rejected requests elevated dave") } if actions, _, _ := st.SystemGrantsForUser(daveID); len(actions) != 0 { t.Fatalf("rejected grant left actions: %v", actions) } if rec := do(h, "PUT", "/api/admin/users/"+daveID+"/system-grants", `{"expectedAuthorityEpoch":`+epochText+`,"idempotencyKey":"super-grant-dave","actions":["system.users.view"]}`, super); rec.Code != http.StatusOK { t.Fatalf("super admin grants account administration: %d %s", rec.Code, rec.Body.String()) } } func TestOrdinaryAdminHTTPCannotMutateSuperAdmin(t *testing.T) { srv, st := newAuthTestServer(t) h := srv.Handler() super := bootSuperAdmin(t, h, st) ops := bootOrdinaryAdmin(t, h, st, "ops") root, err := st.GetAccountByOwner("admin") if err != nil { t.Fatal(err) } version := strconv.FormatInt(root.StateVersion, 10) role := do(h, "POST", "/api/admin/roles", `{"name":"普通职位"}`, ops) if role.Code != http.StatusOK { t.Fatalf("create role: %d %s", role.Code, role.Body.String()) } roleID := decodeRoleHTTP(t, role).RoleID epoch, _ := st.AuthorityEpoch() for _, route := range []struct{ method, path, body string }{ {"PATCH", "/api/admin/users/" + root.ID + "/profile", `{"expectedProfileVersion":1,"patch":{"name":"renamed"}}`}, {"PUT", "/api/admin/users/" + root.ID + "/state", `{"expectedStateVersion":` + version + `,"enabled":false}`}, {"POST", "/api/admin/users/admin/one-time-password", `{}`}, {"PUT", "/api/admin/users/" + root.ID + "/admin", `{"expectedStateVersion":` + version + `,"isAdmin":false}`}, {"PUT", "/api/admin/users/" + root.ID + "/role", `{"roleId":"` + roleID + `","expectedStateVersion":` + version + `,"idempotencyKey":"ops-role-root"}`}, {"PUT", "/api/admin/users/" + root.ID + "/system-grants", `{"expectedAuthorityEpoch":` + strconv.FormatInt(epoch, 10) + `,"idempotencyKey":"ops-grants-root","actions":[]}`}, } { if rec := do(h, route.method, route.path, route.body, ops); rec.Code != http.StatusForbidden { t.Errorf("ordinary admin %s %s: %d %s; want 403", route.method, route.path, rec.Code, rec.Body.String()) } } for _, route := range []struct{ method, path, body string }{ {"PUT", "/api/admin/users/" + root.ID + "/state", `{"expectedStateVersion":` + version + `,"enabled":false}`}, {"PUT", "/api/admin/users/" + root.ID + "/admin", `{"expectedStateVersion":` + version + `,"isAdmin":false}`}, {"POST", "/api/admin/users/admin/one-time-password", `{}`}, } { if rec := do(h, route.method, route.path, route.body, super); rec.Code != http.StatusForbidden { t.Errorf("super admin %s %s on itself: %d %s; want 403", route.method, route.path, rec.Code, rec.Body.String()) } } after, err := st.GetAccountByOwner("admin") if err != nil { t.Fatal(err) } if after != root { t.Fatalf("rejected requests changed the super admin: before=%+v after=%+v", root, after) } if rec := do(h, "GET", "/api/me", "", super); rec.Code != http.StatusOK { t.Fatalf("super admin session revoked by rejected requests: %d", rec.Code) } loginFixture(t, h, "admin", superAdminFixturePassword) }