Files
llmgw/internal/web/org_admin_test.go
T
Ilker ManapandClaude Opus 5.5 102ca9556f An organization's admin, and forms only for those who may send them
C1. A membership of an organization may be admin as well as member and
manager. An admin sees /org as a manager does and changes the
organization from it: its teams and projects, who of the approved
accounts is in them, its members' roles, and the keys spent in its
projects. Every id a form names is checked against the organization;
limits, accounts, the organization's name and the cluster stay with the
cluster's staff, and an admin cannot take their own role away.

A group from OIDC, SCIM or a directory no longer rewrites a membership
it names as a plain member at every sign-in, which took a manager's role
away (EnsureMembership).

Pages draw a form only for someone who may send it: Nav.May asks the
staff rule staffMay keeps, and a page whose forms a person may not send
gets no form token. A viewer sees the people, limits and mirror pages
without forms; an operator the mirror's.

Forms are compact: labels close to their fields, fields as wide as what
goes in them, smaller buttons, and the short forms on one line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-08 11:38:06 +01:00

211 lines
7.9 KiB
Go

package web
import (
"context"
"io"
"log/slog"
"net/http"
"net/url"
"strings"
"testing"
"time"
"gitea.mynodes.xyz/ilker/llmgw/internal/store"
)
// fakeOrgEditor is fakeOrgs that also changes: u1 administers Acme, u4
// manages it, and what each change did is written down.
type fakeOrgEditor struct {
fakeOrgs
admins map[string]string // org -> admin user id
did []string
revoked []string
}
func (f *fakeOrgEditor) ManagesOrg(_ context.Context, user, org string) (bool, error) {
return f.admins[org] == user || f.managers[org] == user, nil
}
func (f *fakeOrgEditor) ManagedOrgs(ctx context.Context, user string) ([]store.Org, error) {
var out []store.Org
for _, o := range []store.Org{{ID: "org-a", Name: "Acme"}, {ID: "org-s", Name: "Scania"}} {
if ok, _ := f.ManagesOrg(ctx, user, o.ID); ok {
out = append(out, o)
}
}
return out, nil
}
func (f *fakeOrgEditor) AdministersOrg(_ context.Context, user, org string) (bool, error) {
return f.admins[org] == user, nil
}
func (f *fakeOrgEditor) UserByEmail(_ context.Context, email string) (store.User, error) {
switch email {
case "new@acme":
return store.User{ID: "u5", Email: email, State: store.UserActive}, nil
case "waiting@acme":
return store.User{ID: "u6", Email: email, State: store.UserPending}, nil
}
return store.User{}, store.ErrNotFound
}
func (f *fakeOrgEditor) CreateTeam(_ context.Context, org, name string) (string, error) {
f.did = append(f.did, "team "+org+" "+name)
return "t9", nil
}
func (f *fakeOrgEditor) CreateProject(_ context.Context, team, name string) (string, error) {
f.did = append(f.did, "project "+team+" "+name)
return "p9", nil
}
func (f *fakeOrgEditor) RenameTeam(_ context.Context, id, name string) error {
f.did = append(f.did, "rename team "+id+" "+name)
return nil
}
func (f *fakeOrgEditor) RenameProject(_ context.Context, id, name string) error {
f.did = append(f.did, "rename project "+id+" "+name)
return nil
}
func (f *fakeOrgEditor) DeleteTeam(_ context.Context, id string) error {
f.did = append(f.did, "delete team "+id)
return nil
}
func (f *fakeOrgEditor) DeleteProject(_ context.Context, id string) error {
f.did = append(f.did, "delete project "+id)
return nil
}
func (f *fakeOrgEditor) AddMembership(_ context.Context, user string, scope store.Scope, id, role, _ string, _ time.Time) error {
f.did = append(f.did, "join "+user+" "+string(scope)+":"+id+" "+role)
return nil
}
func (f *fakeOrgEditor) RemoveMembership(_ context.Context, user string, scope store.Scope, id string, _ time.Time) (int, error) {
f.did = append(f.did, "leave "+user+" "+string(scope)+":"+id)
return 0, nil
}
func (f *fakeOrgEditor) AllKeys(context.Context) ([]store.KeyRow, error) {
return []store.KeyRow{
{Key: store.Key{ID: "k-acme", Prefix: "sk-a", ProjectID: "p1"}, ProjectPath: "Acme / ml / training"},
{Key: store.Key{ID: "k-scania", Prefix: "sk-s", ProjectID: "p-s"}},
}, nil
}
func (f *fakeOrgEditor) RevokeKey(_ context.Context, id string, _ time.Time) error {
f.revoked = append(f.revoked, id)
return nil
}
func orgAdminPage(t *testing.T, user store.User) (*http.ServeMux, *fakeOrgEditor) {
t.Helper()
f := &fakeOrgEditor{
fakeOrgs: fakeOrgs{managers: map[string]string{"org-a": "u4"}},
admins: map[string]string{"org-a": "u1"},
}
p := NewAccountPages(fakeAccounts{user: user}, nil, nil, "v0", slog.New(slog.NewTextHandler(io.Discard, nil)))
mux := http.NewServeMux()
p.RegisterOrgPages(mux, f, nil)
return mux, f
}
func form(kv ...string) url.Values {
v := url.Values{"csrf": {"tok"}}
for i := 0; i+1 < len(kv); i += 2 {
v.Set(kv[i], kv[i+1])
}
return v
}
func TestAnOrganizationsAdminIsGivenTheFormsAndAManagerIsNot(t *testing.T) {
admin, _ := orgAdminPage(t, store.User{ID: "u1", Email: "boss@acme"})
body := mirrorGet(t, admin, "/org/org-a").Body.String()
for _, want := range []string{`action="/org/org-a/teams"`, `action="/org/org-a/members"`,
`action="/org/org-a/keys/k-acme/revoke"`, "sk-a"} {
if !strings.Contains(body, want) {
t.Errorf("the admin's page lacks %q", want)
}
}
if strings.Contains(body, "k-scania") {
t.Error("a key of another organization is on the page")
}
manager, f := orgAdminPage(t, store.User{ID: "u4", Email: "watch@acme"})
w := mirrorGet(t, manager, "/org/org-a")
if w.Code != http.StatusOK {
t.Fatalf("the manager lost their page: %d", w.Code)
}
if strings.Contains(w.Body.String(), "<form method=\"post\" action=\"/org/") {
t.Error("a manager is shown forms they cannot send")
}
if w := mirrorPost(t, manager, "/org/org-a/teams", form("name", "x")); w.Code != http.StatusNotFound {
t.Errorf("a manager changed the organization: %d", w.Code)
}
if len(f.did) != 0 {
t.Errorf("a manager's change reached the store: %v", f.did)
}
}
func TestAnOrganizationsAdminChangesOnlyTheirOwn(t *testing.T) {
mux, f := orgAdminPage(t, store.User{ID: "u1", Email: "boss@acme"})
if w := mirrorPost(t, mux, "/org/org-a/teams", form("name", "vision")); w.Code != http.StatusOK ||
!strings.Contains(w.Body.String(), "Team vision added") {
t.Errorf("adding a team: %d", w.Code)
}
if w := mirrorPost(t, mux, "/org/org-s/teams", form("name", "x")); w.Code != http.StatusNotFound {
t.Errorf("an admin of Acme added a team to Scania: %d", w.Code)
}
// Ids in a form are the sender's to choose: a team of another
// organization, named on Acme's page, is refused.
mirrorPost(t, mux, "/org/org-a/projects", form("team", "t-scania", "name", "x"))
mirrorPost(t, mux, "/org/org-a/members", form("email", "new@acme", "where", "org:org-s"))
mirrorPost(t, mux, "/org/org-a/keys/k-scania/revoke", form())
mirrorPost(t, mux, "/org/org-a/project/p-scania/delete", form())
if len(f.revoked) != 0 {
t.Errorf("a key of another organization was revoked: %v", f.revoked)
}
mirrorPost(t, mux, "/org/org-a/projects", form("team", "t1", "name", "eval"))
mirrorPost(t, mux, "/org/org-a/members", form("email", "new@acme", "where", "project:p1"))
mirrorPost(t, mux, "/org/org-a/members/u2/role", form("role", "manager"))
mirrorPost(t, mux, "/org/org-a/keys/k-acme/revoke", form())
want := []string{"team org-a vision", "project t1 eval", "join u5 project:p1 member", "join u2 org:org-a manager"}
if strings.Join(f.did, "|") != strings.Join(want, "|") {
t.Errorf("the store was asked\n %v\nwant\n %v", f.did, want)
}
if len(f.revoked) != 1 || f.revoked[0] != "k-acme" {
t.Errorf("revoked %v", f.revoked)
}
}
func TestAnOrganizationsAdminAddsOnlyApprovedPeople(t *testing.T) {
mux, f := orgAdminPage(t, store.User{ID: "u1", Email: "boss@acme"})
for email, says := range map[string]string{
"nobody@acme": "there is no account for nobody@acme",
"waiting@acme": "waits for the cluster",
} {
w := mirrorPost(t, mux, "/org/org-a/members", form("email", email, "where", "org:org-a"))
if !strings.Contains(w.Body.String(), says) {
t.Errorf("%s: the page does not say %q", email, says)
}
}
if len(f.did) != 0 {
t.Errorf("someone not approved was added: %v", f.did)
}
}
func TestAnOrganizationsAdminCannotLockThemselvesOut(t *testing.T) {
mux, f := orgAdminPage(t, store.User{ID: "u1", Email: "boss@acme"})
mirrorPost(t, mux, "/org/org-a/members/u1/role", form("role", "member"))
mirrorPost(t, mux, "/org/org-a/members/u1/leave", form("where", "org:org-a"))
if len(f.did) != 0 {
t.Errorf("an admin took their own role away: %v", f.did)
}
}
func TestAViewerIsShownNoFormsOnThePeoplePage(t *testing.T) {
for role, wantForms := range map[string]bool{store.RoleViewer: false, store.RoleAdmin: true} {
u := store.User{ID: "s", Email: "s@x", Role: role, IsAdmin: role == store.RoleAdmin}
p := NewAccountPages(fakeAccounts{user: u}, nil, nil, "v0", slog.New(slog.NewTextHandler(io.Discard, nil)))
mux := http.NewServeMux()
p.RegisterAdminUsers(mux, &fakeAdmin{})
body := mirrorGet(t, mux, "/admin/users").Body.String()
if got := strings.Contains(body, `method="post" action="/admin`); got != wantForms {
t.Errorf("%s: forms on the page = %v, want %v", role, got, wantForms)
}
}
}