Files
Agent-System-Backend/internal/server/super_admin_authority_test.go
T

237 lines
9.8 KiB
Go

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)
}