fix(session): real getMyProfiles shape {myprofiles:[...]} + active-profile detection [#767]
https://projects.knownelement.com/issues/767#note-4191
This commit is contained in:
@@ -2,6 +2,7 @@ package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
@@ -341,6 +342,45 @@ func TestProfileSwitchAgentMode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Real GLPI exposes the active profile only via GET /getActiveProfile.
|
||||
// whoami must detect it there — and DEGRADE to all-inactive (not fail)
|
||||
// when that endpoint is missing or permission-blocked.
|
||||
func TestWhoamiActiveProfileDegradation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
brk int
|
||||
wantAct bool // [active] marker expected?
|
||||
}{
|
||||
{"working endpoint marks active", 0, true},
|
||||
{"403 degrades to all-inactive", http.StatusForbidden, false},
|
||||
{"404 degrades to all-inactive", http.StatusNotFound, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
srv := boot(t)
|
||||
srv.BreakActiveProfile = tt.brk
|
||||
out, errb, code := run(t, "whoami")
|
||||
if code != 0 {
|
||||
t.Fatalf("code = %d, stderr = %q, want 0", code, errb)
|
||||
}
|
||||
if !strings.Contains(out, "Hotliner") || !strings.Contains(out, "Super-admin") {
|
||||
t.Errorf("profiles missing from output: %q", out)
|
||||
}
|
||||
if got := strings.Contains(out, "[active]"); got != tt.wantAct {
|
||||
t.Errorf("[active] marker = %v, want %v (out %q)", got, tt.wantAct, out)
|
||||
}
|
||||
})
|
||||
}
|
||||
t.Run("500 on getActiveProfile still errors", func(t *testing.T) {
|
||||
srv := boot(t)
|
||||
srv.BreakActiveProfile = http.StatusInternalServerError
|
||||
_, _, code := run(t, "whoami")
|
||||
if code != 2 {
|
||||
t.Fatalf("code = %d, want 2 (unexpected server errors are surfaced)", code)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIErrorIsExitTwo(t *testing.T) {
|
||||
srv := boot(t)
|
||||
srv.Fail = &fakeglpi.FailSpec{Status: 404, Body: `[{"ERROR_ITEM_NOT_FOUND":"gone %s"}]`}
|
||||
|
||||
@@ -3,6 +3,7 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -19,6 +20,10 @@ func marshalIndent(v any) ([]byte, error) { return json.MarshalIndent(v, "", "
|
||||
|
||||
// cmdWhoami lists the session's profiles. It NEVER switches the active
|
||||
// profile (agent mode's MGLPI_PROFILE_ID is deliberately not applied).
|
||||
// Active-profile detection comes from GetActiveProfile; when that
|
||||
// endpoint is missing or permission-blocked (404/403), whoami degrades
|
||||
// gracefully to all-inactive rather than erroring — other failures
|
||||
// (5xx, unreachable) still surface as API errors.
|
||||
func cmdWhoami(args []string, stdout, stderr io.Writer) int {
|
||||
fs, cfgPath, out := newFlags("whoami", stderr)
|
||||
pos, err := parseArgs(fs, args)
|
||||
@@ -33,6 +38,16 @@ func cmdWhoami(args []string, stdout, stderr io.Writer) int {
|
||||
if err != nil {
|
||||
return apiErr(stderr, err)
|
||||
}
|
||||
activeID, aerr := c.GetActiveProfile(context.Background())
|
||||
degraded := aerr != nil && (errors.Is(aerr, glpi.ErrNotFound) || errors.Is(aerr, glpi.ErrAuth))
|
||||
if aerr != nil && !degraded {
|
||||
return apiErr(stderr, aerr)
|
||||
}
|
||||
if !degraded {
|
||||
for i := range profs {
|
||||
profs[i].IsActive = profs[i].ID == activeID
|
||||
}
|
||||
}
|
||||
if *out == "json" {
|
||||
emitJSON(stdout, map[string]any{"profiles": profs})
|
||||
return 0
|
||||
|
||||
@@ -116,6 +116,10 @@ type Server struct {
|
||||
// Fail, when non-nil, is returned instead of normal handling; it is
|
||||
// consumed by the first request that sees it.
|
||||
Fail *FailSpec
|
||||
// BreakActiveProfile, when non-zero, makes GET /getActiveProfile
|
||||
// answer that error status (degradation tests: 403/404 degrade,
|
||||
// 500 surfaces).
|
||||
BreakActiveProfile int
|
||||
|
||||
mu sync.Mutex
|
||||
srv *httptest.Server
|
||||
@@ -355,6 +359,8 @@ func (s *Server) handler(w http.ResponseWriter, r *http.Request) {
|
||||
respBody = `true`
|
||||
case r.Method == http.MethodGet && path == "/getMyProfiles":
|
||||
respStatus, respBody = s.getMyProfiles(tok)
|
||||
case r.Method == http.MethodGet && path == "/getActiveProfile":
|
||||
respStatus, respBody = s.getActiveProfile(tok)
|
||||
case r.Method == http.MethodPost && path == "/changeActiveProfile":
|
||||
respStatus, respBody = s.changeActiveProfile(tok, body)
|
||||
case r.Method == http.MethodGet && strings.HasPrefix(path, "/Profile/"):
|
||||
@@ -413,12 +419,37 @@ func (s *Server) initSession(r *http.Request) (int, string) {
|
||||
}
|
||||
|
||||
func (s *Server) getMyProfiles(tok string) (int, string) {
|
||||
out := make([]Profile, len(s.profiles))
|
||||
copy(out, s.profiles)
|
||||
for i := range out {
|
||||
out[i].IsActive = out[i].ID == s.activeProfile[tok]
|
||||
// GLPI's REAL wire shape (verified against prod cmdb 2026-09-03):
|
||||
// WRAPPED object {"myprofiles":[...]}; rows carry id/name/entities
|
||||
// and NO is_active — the active profile is GET /getActiveProfile.
|
||||
rows := make([]map[string]any, 0, len(s.profiles))
|
||||
for _, p := range s.profiles {
|
||||
rows = append(rows, map[string]any{"id": p.ID, "name": p.Name, "entities": []any{}})
|
||||
}
|
||||
return jsonReply(out)
|
||||
return jsonReply(map[string]any{"myprofiles": rows})
|
||||
}
|
||||
|
||||
// getActiveProfile answers GLPI's {"active_profile":{...,"id":N,...}}.
|
||||
func (s *Server) getActiveProfile(tok string) (int, string) {
|
||||
if s.BreakActiveProfile != 0 {
|
||||
switch s.BreakActiveProfile {
|
||||
case http.StatusForbidden:
|
||||
return http.StatusForbidden, `[{"ERROR_RIGHT_MISSING":true}]`
|
||||
case http.StatusNotFound:
|
||||
return http.StatusNotFound, `[{"ERROR_ITEM_NOT_FOUND":true}]`
|
||||
default:
|
||||
return s.BreakActiveProfile, `[{"ERROR_GLPI":true}]`
|
||||
}
|
||||
}
|
||||
id := s.activeProfile[tok]
|
||||
for _, p := range s.profiles {
|
||||
if p.ID == id {
|
||||
return jsonReply(map[string]any{
|
||||
"active_profile": map[string]any{"id": p.ID, "name": p.Name, "entities": []any{}},
|
||||
})
|
||||
}
|
||||
}
|
||||
return http.StatusNotFound, `[{"ERROR_ITEM_NOT_FOUND":true}]`
|
||||
}
|
||||
|
||||
func (s *Server) changeActiveProfile(tok string, body []byte) (int, string) {
|
||||
|
||||
Reference in New Issue
Block a user