Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c0098418b | ||
|
|
7f35dbf645 |
@@ -160,6 +160,11 @@ func me(s *cloud.Service[core.State], c *zip.Ctx) error {
|
||||
Email: strings.TrimSpace(c.UserEmail()),
|
||||
DisplayName: name,
|
||||
IsSuperAdmin: sc.Super,
|
||||
// Admitted (the gate already 200'd) but not fleet-super ⇒ a subtree-scoped
|
||||
// white-label tenant operator; ScopeOrgs is its visible subtree (empty for
|
||||
// a SuperAdmin, who sees the whole fleet).
|
||||
IsWhiteLabel: !sc.Super,
|
||||
ScopeOrgs: sc.Orgs,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -210,11 +215,20 @@ func users(s *cloud.Service[core.State], c *zip.Ctx) error {
|
||||
} else if owner := strings.TrimSpace(c.Query("org")); owner != "" {
|
||||
q.Set("owner", owner)
|
||||
}
|
||||
// Default pagination when the client omits it. IAM's user list returns ZERO
|
||||
// rows AND total 0 when p/pageSize are unset — which surfaced as the admin
|
||||
// directory showing "0 of 222". Default to the first page at the shared admin
|
||||
// page size so the directory populates and the REAL total is reported; an
|
||||
// explicit client p/pageSize still wins (the UI paginates from there).
|
||||
if p := strings.TrimSpace(c.Query("p")); p != "" {
|
||||
q.Set("p", p)
|
||||
} else {
|
||||
q.Set("p", "1")
|
||||
}
|
||||
if ps := strings.TrimSpace(c.Query("pageSize")); ps != "" {
|
||||
q.Set("pageSize", ps)
|
||||
} else {
|
||||
q.Set("pageSize", "200")
|
||||
}
|
||||
if term := strings.TrimSpace(c.Query("q")); term != "" {
|
||||
// IAM's list uses field/value contains-matching for the free-text filter.
|
||||
|
||||
@@ -180,6 +180,52 @@ func TestGate_AllowsSuperAdmin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestUsers_DefaultsPagination locks the "0 of 222" fix: IAM's user list returns
|
||||
// ZERO rows AND total 0 when p/pageSize are unset, so the /v1/admin/users handler
|
||||
// MUST supply a default first page + page size when the client (the operator
|
||||
// directory) omits them. Proves the handler forwards p=1 & pageSize=200 and that
|
||||
// the real total reaches the client.
|
||||
func TestUsers_DefaultsPagination(t *testing.T) {
|
||||
var gotP, gotPageSize string
|
||||
iamSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasSuffix(r.URL.Path, "/get-users") {
|
||||
gotP = r.URL.Query().Get("p")
|
||||
gotPageSize = r.URL.Query().Get("pageSize")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
io.WriteString(w, `{"status":"ok","msg":"","data":[
|
||||
{"owner":"hanzo","name":"alice","email":"alice@hanzo.ai","displayName":"Alice"}
|
||||
],"data2":222}`)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(404)
|
||||
io.WriteString(w, `{"status":"error","msg":"not found"}`)
|
||||
}))
|
||||
defer iamSrv.Close()
|
||||
|
||||
do := mount(t, iamSrv.URL, "http://127.0.0.1:0", "http://127.0.0.1:0")
|
||||
admin := map[string]string{"X-User-IsAdmin": "true", "X-Org-Id": "admin", "X-User-Id": "admin/z", "X-User-Email": "z@hanzo.ai"}
|
||||
resp, body := do("GET", "/v1/admin/users", admin) // NOTE: no ?pageSize — the bug path.
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("GET /v1/admin/users: got %d (body=%s)", resp.StatusCode, body)
|
||||
}
|
||||
if gotPageSize != "200" {
|
||||
t.Fatalf("users list must default pageSize=200 when the client omits it, got %q", gotPageSize)
|
||||
}
|
||||
if gotP != "1" {
|
||||
t.Fatalf("users list must default p=1 when the client omits it, got %q", gotP)
|
||||
}
|
||||
var env struct {
|
||||
Data []operatorUser `json:"data"`
|
||||
Data2 int `json:"data2"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &env); err != nil {
|
||||
t.Fatalf("decode users envelope: %v (body=%s)", err, body)
|
||||
}
|
||||
if env.Data2 != 222 || len(env.Data) == 0 {
|
||||
t.Fatalf("users must surface the REAL directory (got %d rows, total %d), not 0-of-222", len(env.Data), env.Data2)
|
||||
}
|
||||
}
|
||||
|
||||
// fakeIAM stands in for the IAM management surface. It records whether the
|
||||
// caller's credential was replayed and returns /v1 envelopes.
|
||||
type fakeIAM struct {
|
||||
|
||||
@@ -19,6 +19,14 @@ type adminMe struct {
|
||||
Email string `json:"email"`
|
||||
DisplayName string `json:"displayName"`
|
||||
IsSuperAdmin bool `json:"isSuperAdmin"`
|
||||
// IsWhiteLabel: admitted but not fleet-super — a subtree-scoped (white-label)
|
||||
// tenant operator. Mutually exclusive with IsSuperAdmin; drives the subtree
|
||||
// cockpit vs the fleet god-view in the operator console.
|
||||
IsWhiteLabel bool `json:"isWhiteLabel"`
|
||||
// ScopeOrgs: the operator's visible tenant window — empty/nil for a SuperAdmin
|
||||
// (all orgs), else the scoped subtree (sc.Orgs). The faceting layer threads
|
||||
// this as its scope ceiling so a tenant can never facet outside its subtree.
|
||||
ScopeOrgs []string `json:"scopeOrgs,omitempty"`
|
||||
}
|
||||
|
||||
// overviewData is the fleet overview tiles (OverviewData / GET /v1/admin/overview).
|
||||
|
||||
Reference in New Issue
Block a user