Provider-Wechsel nur für Admin (403 für Nutzer); App blendet Provider-Auswahl für Nicht-Admins aus; v0.3.1

This commit is contained in:
OpenClaw Dev 2026-11-01 04:17:53 +01:00
parent 161bc02bb9
commit 70d316851a
12 changed files with 102 additions and 17 deletions

View file

@ -0,0 +1,43 @@
package server
import (
"encoding/json"
"testing"
)
func TestProviderChangesAdminOnly(t *testing.T) {
ts, _ := setupFull(t, echo{})
ut := registerUser(t, ts.URL, "alice", "correct-horse-1")
uh := bearerHdr(ut)
if r := do(t, "GET", ts.URL+"/api/providers", "", uh); r.StatusCode != 200 {
t.Fatal("users may list providers", r.StatusCode)
}
if r := do(t, "PUT", ts.URL+"/api/providers/active", `{"provider":"dahl"}`, uh); r.StatusCode != 403 {
t.Fatal("user must not change global provider", r.StatusCode)
}
var me map[string]any
json.NewDecoder(do(t, "GET", ts.URL+"/api/auth/me", "", uh).Body).Decode(&me)
if me["admin"] != false {
t.Fatalf("%v", me)
}
// own session: chat ok, provider change forbidden
r := do(t, "POST", ts.URL+"/api/chat", `{"text":"hi"}`, uh)
if r.StatusCode != 200 {
t.Fatal("user chat", r.StatusCode)
}
var cr struct {
SessionID string `json:"session_id"`
}
json.NewDecoder(r.Body).Decode(&cr)
if cr.SessionID != "" {
if r := do(t, "PUT", ts.URL+"/api/sessions/"+cr.SessionID+"/provider", `{"provider":"openai"}`, uh); r.StatusCode != 403 {
t.Fatal("session provider", r.StatusCode)
}
}
if r := do(t, "POST", ts.URL+"/api/chat", `{"text":"hi","provider":"dahl"}`, uh); r.StatusCode != 403 {
t.Fatal("chat with provider", r.StatusCode)
}
if r := do(t, "PUT", ts.URL+"/api/providers/active", `{"provider":"dahl"}`, authH); r.StatusCode != 200 {
t.Fatal("admin allowed", r.StatusCode)
}
}

View file

@ -48,6 +48,9 @@ func (s *Server) listProviders(w http.ResponseWriter, _ *http.Request) {
}
func (s *Server) setActiveProvider(w http.ResponseWriter, r *http.Request) {
if !requireAdmin(w, r) {
return
}
if !s.needProviders(w) {
return
}
@ -82,6 +85,9 @@ func (s *Server) listModels(w http.ResponseWriter, r *http.Request) {
// PUT /api/sessions/{id}/provider: per-session override; empty provider clears it.
func (s *Server) setSessionProvider(w http.ResponseWriter, r *http.Request) {
if !requireAdmin(w, r) {
return
}
if !s.needProviders(w) {
return
}
@ -148,3 +154,15 @@ func (s *Server) getSummary(w http.ResponseWriter, r *http.Request) {
}
writeJSON(w, 200, sm)
}
// errAdminOnly is returned when a non-admin tries to change provider settings.
var errAdminOnly = errors.New("admin only: provider settings can only be changed by the server admin")
// requireAdmin writes 403 unless the caller is the admin (static auth_token).
func requireAdmin(w http.ResponseWriter, r *http.Request) bool {
if principal(r.Context()).Admin {
return true
}
writeErr(w, http.StatusForbidden, errAdminOnly.Error())
return false
}

View file

@ -50,7 +50,7 @@ func setupFull(t *testing.T, p llm.Provider) (*httptest.Server, *store.Store) {
pr, _, err := pm.ForSession(ctx, sid)
return pr, err
}
s := &Server{Store: st, Token: tok, Log: slog.New(slog.NewTextHandler(io.Discard, nil)), RunTimeout: 5 * time.Second, Agent: ag, Providers: pm}
s := &Server{Store: st, Token: tok, AllowRegistration: true, Log: slog.New(slog.NewTextHandler(io.Discard, nil)), RunTimeout: 5 * time.Second, Agent: ag, Providers: pm}
ts := httptest.NewServer(s.Handler())
t.Cleanup(ts.Close)
return ts, st

View file

@ -253,6 +253,9 @@ type chatReq struct {
func (s *Server) startSession(ctx context.Context, req chatReq) (string, error) {
var a providers.Active
if req.SessionID == "" && req.Provider != "" && s.Providers != nil {
if !principal(ctx).Admin {
return "", errAdminOnly
}
var err error
if a, err = s.Providers.Validate(req.Provider, req.Model); err != nil {
return "", err
@ -288,6 +291,10 @@ func (s *Server) chat(w http.ResponseWriter, r *http.Request) {
return
}
id, err := s.startSession(r.Context(), req)
if errors.Is(err, errAdminOnly) {
writeErr(w, http.StatusForbidden, err.Error())
return
}
if err != nil {
if req.SessionID == "" && req.Provider != "" {
providerErr(w, err)