mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-29 20:10:21 +00:00
1303 lines
49 KiB
Go
1303 lines
49 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gorilla/websocket"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/agentexec"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
pulsews "github.com/rcourtman/pulse-go-rewrite/internal/websocket"
|
|
)
|
|
|
|
type wsRawMessage struct {
|
|
Type agentexec.MessageType `json:"type"`
|
|
Payload json.RawMessage `json:"payload,omitempty"`
|
|
}
|
|
|
|
func newTestConfigWithTokens(t *testing.T, records ...config.APITokenRecord) *config.Config {
|
|
t.Helper()
|
|
tempDir := t.TempDir()
|
|
return &config.Config{
|
|
DataPath: tempDir,
|
|
ConfigPath: tempDir,
|
|
APITokens: records,
|
|
}
|
|
}
|
|
|
|
func newTokenRecord(t *testing.T, raw string, scopes []string, metadata map[string]string) config.APITokenRecord {
|
|
t.Helper()
|
|
record, err := config.NewAPITokenRecord(raw, "test-token", scopes)
|
|
if err != nil {
|
|
t.Fatalf("NewAPITokenRecord: %v", err)
|
|
}
|
|
if metadata != nil {
|
|
record.Metadata = metadata
|
|
}
|
|
return *record
|
|
}
|
|
|
|
func readRegisteredPayload(t *testing.T, conn *websocket.Conn) agentexec.RegisteredPayload {
|
|
t.Helper()
|
|
conn.SetReadDeadline(time.Now().Add(2 * time.Second))
|
|
_, data, err := conn.ReadMessage()
|
|
if err != nil {
|
|
t.Fatalf("ReadMessage: %v", err)
|
|
}
|
|
|
|
var msg wsRawMessage
|
|
if err := json.Unmarshal(data, &msg); err != nil {
|
|
t.Fatalf("unmarshal message: %v", err)
|
|
}
|
|
if msg.Type != agentexec.MsgTypeRegistered {
|
|
t.Fatalf("message type = %q, want %q", msg.Type, agentexec.MsgTypeRegistered)
|
|
}
|
|
if msg.Payload == nil {
|
|
t.Fatalf("registered payload missing")
|
|
}
|
|
|
|
var payload agentexec.RegisteredPayload
|
|
if err := json.Unmarshal(msg.Payload, &payload); err != nil {
|
|
t.Fatalf("unmarshal registered payload: %v", err)
|
|
}
|
|
return payload
|
|
}
|
|
|
|
func TestSimpleStatsRequiresAuthInAPIMode(t *testing.T) {
|
|
rawToken := "stats-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeMonitoringRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/simple-stats", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401 without token, got %d", rec.Code)
|
|
}
|
|
|
|
req = httptest.NewRequest(http.MethodGet, "/simple-stats", nil)
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec = httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 with token, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), "Simple Pulse Stats") {
|
|
t.Fatalf("expected stats page HTML, got %q", rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSocketIORequiresAuthInAPIMode(t *testing.T) {
|
|
rawToken := "socket-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeMonitoringRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/socket.io/?transport=polling", nil)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401 without token, got %d", rec.Code)
|
|
}
|
|
|
|
req = httptest.NewRequest(http.MethodGet, "/socket.io/?transport=polling", nil)
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec = httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 with token, got %d", rec.Code)
|
|
}
|
|
if ct := rec.Header().Get("Content-Type"); ct != "text/plain; charset=UTF-8" {
|
|
t.Fatalf("expected text/plain content type, got %q", ct)
|
|
}
|
|
if body := rec.Body.String(); !strings.HasPrefix(body, "0{") {
|
|
t.Fatalf("unexpected polling handshake body: %q", body)
|
|
}
|
|
}
|
|
|
|
func TestSocketIOWebSocketRequiresAuthInAPIMode(t *testing.T) {
|
|
rawToken := "socket-ws-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeMonitoringRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
|
|
hub := pulsews.NewHub(nil)
|
|
go hub.Run()
|
|
defer hub.Stop()
|
|
|
|
router := NewRouter(cfg, nil, nil, hub, nil, "1.0.0")
|
|
ts := httptest.NewServer(router.Handler())
|
|
defer ts.Close()
|
|
|
|
wsURL := "ws" + strings.TrimPrefix(ts.URL, "http") + "/socket.io/?transport=websocket"
|
|
|
|
conn, resp, err := websocket.DefaultDialer.Dial(wsURL, nil)
|
|
if err == nil {
|
|
conn.Close()
|
|
t.Fatalf("expected websocket auth failure without token")
|
|
}
|
|
if resp == nil {
|
|
t.Fatalf("expected HTTP response for failed websocket auth")
|
|
}
|
|
if resp.StatusCode != http.StatusUnauthorized {
|
|
t.Fatalf("expected 401 for missing token, got %d", resp.StatusCode)
|
|
}
|
|
|
|
headers := http.Header{}
|
|
headers.Set("X-API-Token", rawToken)
|
|
conn, resp, err = websocket.DefaultDialer.Dial(wsURL, headers)
|
|
if err != nil {
|
|
t.Fatalf("expected websocket connection with token, got %v", err)
|
|
}
|
|
if resp == nil || resp.StatusCode != http.StatusSwitchingProtocols {
|
|
t.Fatalf("expected 101 switching protocols, got %v", resp)
|
|
}
|
|
conn.Close()
|
|
}
|
|
|
|
func TestWebSocketRequiresMonitoringReadScope(t *testing.T) {
|
|
rawToken := "ws-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeHostReport}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/ws", nil)
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing monitoring:read scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeMonitoringRead) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeMonitoringRead, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestWebSocketRequiresMonitoringReadScopeForUpgrade(t *testing.T) {
|
|
rawToken := "ws-scope-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeHostReport}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
|
|
hub := pulsews.NewHub(nil)
|
|
go hub.Run()
|
|
defer hub.Stop()
|
|
|
|
router := NewRouter(cfg, nil, nil, hub, nil, "1.0.0")
|
|
ts := httptest.NewServer(router.Handler())
|
|
defer ts.Close()
|
|
|
|
wsURL := "ws" + strings.TrimPrefix(ts.URL, "http") + "/ws"
|
|
headers := http.Header{}
|
|
headers.Set("X-API-Token", rawToken)
|
|
conn, resp, err := websocket.DefaultDialer.Dial(wsURL, headers)
|
|
if err == nil {
|
|
conn.Close()
|
|
t.Fatalf("expected websocket upgrade to be rejected without monitoring:read scope")
|
|
}
|
|
if resp == nil {
|
|
t.Fatalf("expected HTTP response for failed websocket upgrade")
|
|
}
|
|
if resp.StatusCode != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing scope, got %d", resp.StatusCode)
|
|
}
|
|
}
|
|
|
|
func TestHostAgentManagementRequiresSettingsWriteScope(t *testing.T) {
|
|
rawToken := "host-manage-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeHostManage}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
cases := []struct {
|
|
name string
|
|
method string
|
|
path string
|
|
}{
|
|
{name: "link", method: http.MethodPost, path: "/api/agents/host/link"},
|
|
{name: "unlink", method: http.MethodPost, path: "/api/agents/host/unlink"},
|
|
{name: "delete", method: http.MethodDelete, path: "/api/agents/host/agent-1"},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
req := httptest.NewRequest(tc.method, tc.path, nil)
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:write scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsWrite) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsWrite, rec.Body.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTestNotificationRequiresSettingsWriteScope(t *testing.T) {
|
|
rawToken := "notify-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeSettingsRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/test-notification", nil)
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:write scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsWrite) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsWrite, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAIFindingsRequiresAIExecuteScope(t *testing.T) {
|
|
rawToken := "ai-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeAIChat}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/findings/f-1/investigation", nil)
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing ai:execute scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeAIExecute) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeAIExecute, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestNotificationsDLQRequiresSettingsReadScope(t *testing.T) {
|
|
rawToken := "dlq-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeMonitoringRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/notifications/dlq", nil)
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:read scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsRead) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsRead, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestNotificationsDLQMutationsRequireSettingsWriteScope(t *testing.T) {
|
|
rawToken := "dlq-write-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeSettingsRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
cases := []string{
|
|
"/api/notifications/dlq/retry",
|
|
"/api/notifications/dlq/delete",
|
|
}
|
|
|
|
for _, path := range cases {
|
|
req := httptest.NewRequest(http.MethodPost, path, bytes.NewReader([]byte(`{"id":"test"}`)))
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:write scope on %s, got %d", path, rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsWrite) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsWrite, rec.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAgentExecTokenBindingEnforced(t *testing.T) {
|
|
rawToken := "agent-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeAgentExec}, map[string]string{"bound_agent_id": "agent-1"})
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
ts := httptest.NewServer(router.Handler())
|
|
defer ts.Close()
|
|
|
|
wsURL := wsURLForHTTP(ts.URL) + "/api/agent/ws"
|
|
|
|
// Mismatched agent ID should be rejected
|
|
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
|
|
if err != nil {
|
|
t.Fatalf("Dial: %v", err)
|
|
}
|
|
if err := conn.WriteJSON(agentexec.Message{
|
|
Type: agentexec.MsgTypeAgentRegister,
|
|
Timestamp: time.Now(),
|
|
Payload: agentexec.AgentRegisterPayload{
|
|
AgentID: "agent-2",
|
|
Hostname: "host-2",
|
|
Version: "1.0.0",
|
|
Platform: "linux",
|
|
Token: rawToken,
|
|
},
|
|
}); err != nil {
|
|
conn.Close()
|
|
t.Fatalf("WriteJSON: %v", err)
|
|
}
|
|
reg := readRegisteredPayload(t, conn)
|
|
if reg.Success {
|
|
conn.Close()
|
|
t.Fatalf("expected registration to be rejected for mismatched bound agent")
|
|
}
|
|
conn.Close()
|
|
|
|
// Matching agent ID should succeed
|
|
conn, _, err = websocket.DefaultDialer.Dial(wsURL, nil)
|
|
if err != nil {
|
|
t.Fatalf("Dial: %v", err)
|
|
}
|
|
if err := conn.WriteJSON(agentexec.Message{
|
|
Type: agentexec.MsgTypeAgentRegister,
|
|
Timestamp: time.Now(),
|
|
Payload: agentexec.AgentRegisterPayload{
|
|
AgentID: "agent-1",
|
|
Hostname: "host-1",
|
|
Version: "1.0.0",
|
|
Platform: "linux",
|
|
Token: rawToken,
|
|
},
|
|
}); err != nil {
|
|
conn.Close()
|
|
t.Fatalf("WriteJSON: %v", err)
|
|
}
|
|
reg = readRegisteredPayload(t, conn)
|
|
if !reg.Success {
|
|
conn.Close()
|
|
t.Fatalf("expected registration to be accepted for matching bound agent, got %q", reg.Message)
|
|
}
|
|
conn.Close()
|
|
}
|
|
|
|
func TestAgentExecRequiresAgentExecScope(t *testing.T) {
|
|
rawToken := "agent-scope-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeAIChat}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
ts := httptest.NewServer(router.Handler())
|
|
defer ts.Close()
|
|
|
|
wsURL := "ws" + strings.TrimPrefix(ts.URL, "http") + "/api/agent/ws"
|
|
conn, _, err := websocket.DefaultDialer.Dial(wsURL, nil)
|
|
if err != nil {
|
|
t.Fatalf("Dial: %v", err)
|
|
}
|
|
if err := conn.WriteJSON(agentexec.Message{
|
|
Type: agentexec.MsgTypeAgentRegister,
|
|
Timestamp: time.Now(),
|
|
Payload: agentexec.AgentRegisterPayload{
|
|
AgentID: "agent-1",
|
|
Hostname: "host-1",
|
|
Version: "1.0.0",
|
|
Platform: "linux",
|
|
Token: rawToken,
|
|
},
|
|
}); err != nil {
|
|
conn.Close()
|
|
t.Fatalf("WriteJSON: %v", err)
|
|
}
|
|
reg := readRegisteredPayload(t, conn)
|
|
if reg.Success {
|
|
conn.Close()
|
|
t.Fatalf("expected registration to be rejected without agent:exec scope")
|
|
}
|
|
conn.Close()
|
|
}
|
|
|
|
func TestWebSocketAllowsMonitoringReadScope(t *testing.T) {
|
|
rawToken := "ws-allow-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeMonitoringRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
|
|
hub := pulsews.NewHub(nil)
|
|
go hub.Run()
|
|
defer hub.Stop()
|
|
|
|
router := NewRouter(cfg, nil, nil, hub, nil, "1.0.0")
|
|
ts := httptest.NewServer(router.Handler())
|
|
defer ts.Close()
|
|
|
|
wsURL := "ws" + strings.TrimPrefix(ts.URL, "http") + "/ws"
|
|
headers := http.Header{}
|
|
headers.Set("X-API-Token", rawToken)
|
|
conn, _, err := websocket.DefaultDialer.Dial(wsURL, headers)
|
|
if err != nil {
|
|
t.Fatalf("Dial: %v", err)
|
|
}
|
|
conn.Close()
|
|
}
|
|
|
|
func TestLogEndpointsRequireSettingsReadScope(t *testing.T) {
|
|
rawToken := "logs-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeMonitoringRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
paths := []string{
|
|
"/api/logs/stream",
|
|
"/api/logs/download",
|
|
}
|
|
|
|
for _, path := range paths {
|
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:read scope on %s, got %d", path, rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsRead) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsRead, rec.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLogLevelReadRequiresSettingsReadScope(t *testing.T) {
|
|
rawToken := "log-level-read-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeMonitoringRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/logs/level", nil)
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:read scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsRead) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsRead, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestLogLevelUpdateRequiresSettingsWriteScope(t *testing.T) {
|
|
rawToken := "log-level-write-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeSettingsRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/logs/level", strings.NewReader(`{"level":"info"}`))
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:write scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsWrite) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsWrite, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestUpdateReadEndpointsRequireSettingsReadScope(t *testing.T) {
|
|
rawToken := "updates-read-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeMonitoringRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
paths := []string{
|
|
"/api/updates/check",
|
|
"/api/updates/status",
|
|
"/api/updates/plan",
|
|
}
|
|
|
|
for _, path := range paths {
|
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:read scope on %s, got %d", path, rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsRead) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsRead, rec.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUpdateApplyRequiresSettingsWriteScope(t *testing.T) {
|
|
rawToken := "updates-write-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeSettingsRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/updates/apply", strings.NewReader(`{}`))
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:write scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsWrite) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsWrite, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestLicenseMutationsRequireSettingsWriteScope(t *testing.T) {
|
|
rawToken := "license-write-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeSettingsRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
paths := []string{
|
|
"/api/license/activate",
|
|
"/api/license/clear",
|
|
}
|
|
|
|
for _, path := range paths {
|
|
req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(`{}`))
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:write scope on %s, got %d", path, rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsWrite) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsWrite, rec.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSetupScriptURLRequiresSettingsWriteScope(t *testing.T) {
|
|
rawToken := "setup-script-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeSettingsRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/setup-script-url", strings.NewReader(`{}`))
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:write scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsWrite) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsWrite, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAgentInstallCommandRequiresSettingsWriteScope(t *testing.T) {
|
|
rawToken := "agent-install-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeSettingsRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/agent-install-command", strings.NewReader(`{}`))
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:write scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsWrite) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsWrite, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestDiscoverRequiresSettingsWriteScope(t *testing.T) {
|
|
rawToken := "discover-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeSettingsRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
paths := []struct {
|
|
name string
|
|
method string
|
|
body string
|
|
}{
|
|
{name: "get", method: http.MethodGet, body: ""},
|
|
{name: "post", method: http.MethodPost, body: `{}`},
|
|
}
|
|
|
|
for _, tc := range paths {
|
|
req := httptest.NewRequest(tc.method, "/api/discover", strings.NewReader(tc.body))
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:write scope on %s, got %d", tc.name, rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsWrite) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsWrite, rec.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAIOAuthEndpointsRequireSettingsWriteScope(t *testing.T) {
|
|
rawToken := "ai-oauth-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeSettingsRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
paths := []string{
|
|
"/api/ai/oauth/start",
|
|
"/api/ai/oauth/exchange",
|
|
"/api/ai/oauth/disconnect",
|
|
}
|
|
|
|
for _, path := range paths {
|
|
req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(`{}`))
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:write scope on %s, got %d", path, rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsWrite) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsWrite, rec.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAIExecuteEndpointsRequireAIExecuteScope(t *testing.T) {
|
|
rawToken := "ai-exec-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeAIChat}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
paths := []string{
|
|
"/api/ai/execute",
|
|
"/api/ai/execute/stream",
|
|
}
|
|
|
|
for _, path := range paths {
|
|
req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(`{}`))
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing ai:execute scope on %s, got %d", path, rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeAIExecute) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeAIExecute, rec.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAIRemediationMutationsRequireAIExecuteScope(t *testing.T) {
|
|
rawToken := "ai-remediate-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeAIChat}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
paths := []string{
|
|
"/api/ai/remediation/execute",
|
|
"/api/ai/remediation/rollback",
|
|
}
|
|
|
|
for _, path := range paths {
|
|
req := httptest.NewRequest(http.MethodPost, path, strings.NewReader(`{}`))
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing ai:execute scope on %s, got %d", path, rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeAIExecute) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeAIExecute, rec.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAIAgentsRequiresAIExecuteScope(t *testing.T) {
|
|
rawToken := "ai-agents-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeAIChat}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/agents", nil)
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing ai:execute scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeAIExecute) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeAIExecute, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAICostEndpointsRequireSettingsScopes(t *testing.T) {
|
|
rawToken := "ai-cost-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeMonitoringRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
// Summary requires settings:read
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/cost/summary", nil)
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:read scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsRead) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsRead, rec.Body.String())
|
|
}
|
|
|
|
// Reset requires settings:write
|
|
req = httptest.NewRequest(http.MethodPost, "/api/ai/cost/reset", strings.NewReader(`{}`))
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec = httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:write scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsWrite) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsWrite, rec.Body.String())
|
|
}
|
|
|
|
// Export requires settings:read
|
|
req = httptest.NewRequest(http.MethodGet, "/api/ai/cost/export", nil)
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec = httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:read scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsRead) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsRead, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAIDebugContextRequiresSettingsReadScope(t *testing.T) {
|
|
rawToken := "ai-debug-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeMonitoringRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/debug/context", nil)
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:read scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsRead) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsRead, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAIRunCommandRequiresAIExecuteScope(t *testing.T) {
|
|
rawToken := "ai-run-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeAIChat}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/run-command", strings.NewReader(`{}`))
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing ai:execute scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeAIExecute) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeAIExecute, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAIPatrolRunRequiresAIExecuteScope(t *testing.T) {
|
|
rawToken := "ai-patrol-run-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeAIChat}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/ai/patrol/run", strings.NewReader(`{}`))
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing ai:execute scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeAIExecute) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeAIExecute, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAIPatrolAutonomyRequiresSettingsWriteScope(t *testing.T) {
|
|
rawToken := "ai-patrol-autonomy-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeSettingsRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/ai/patrol/autonomy", nil)
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:write scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsWrite) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsWrite, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAIExecuteReadEndpointsRequireAIExecuteScope(t *testing.T) {
|
|
rawToken := "ai-exec-read-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeAIChat}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
paths := []string{
|
|
"/api/ai/patrol/status",
|
|
"/api/ai/patrol/stream",
|
|
"/api/ai/patrol/findings",
|
|
"/api/ai/patrol/history",
|
|
"/api/ai/patrol/runs",
|
|
"/api/ai/patrol/dismissed",
|
|
"/api/ai/patrol/suppressions",
|
|
"/api/ai/approvals",
|
|
"/api/ai/approvals/approval-1",
|
|
"/api/ai/intelligence",
|
|
"/api/ai/intelligence/patterns",
|
|
"/api/ai/intelligence/predictions",
|
|
"/api/ai/intelligence/correlations",
|
|
"/api/ai/intelligence/changes",
|
|
"/api/ai/intelligence/baselines",
|
|
"/api/ai/intelligence/remediations",
|
|
"/api/ai/intelligence/anomalies",
|
|
"/api/ai/intelligence/learning",
|
|
"/api/ai/unified/findings",
|
|
"/api/ai/forecast",
|
|
"/api/ai/forecasts/overview",
|
|
"/api/ai/learning/preferences",
|
|
"/api/ai/proxmox/events",
|
|
"/api/ai/proxmox/correlations",
|
|
"/api/ai/remediation/plans",
|
|
"/api/ai/remediation/plan",
|
|
"/api/ai/circuit/status",
|
|
"/api/ai/incidents",
|
|
"/api/ai/incidents/incident-1",
|
|
}
|
|
|
|
for _, path := range paths {
|
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing ai:execute scope on %s, got %d", path, rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeAIExecute) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeAIExecute, rec.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAIExecuteMutationEndpointsRequireAIExecuteScope(t *testing.T) {
|
|
rawToken := "ai-exec-mutate-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeAIChat}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
paths := []struct {
|
|
method string
|
|
path string
|
|
body string
|
|
}{
|
|
{method: http.MethodPost, path: "/api/ai/patrol/acknowledge", body: `{}`},
|
|
{method: http.MethodPost, path: "/api/ai/patrol/dismiss", body: `{}`},
|
|
{method: http.MethodPost, path: "/api/ai/patrol/findings/note", body: `{}`},
|
|
{method: http.MethodPost, path: "/api/ai/patrol/suppress", body: `{}`},
|
|
{method: http.MethodPost, path: "/api/ai/patrol/snooze", body: `{}`},
|
|
{method: http.MethodPost, path: "/api/ai/patrol/resolve", body: `{}`},
|
|
{method: http.MethodPost, path: "/api/ai/patrol/suppressions", body: `{}`},
|
|
{method: http.MethodDelete, path: "/api/ai/patrol/suppressions/rule-1", body: ""},
|
|
{method: http.MethodPost, path: "/api/ai/remediation/approve", body: `{}`},
|
|
}
|
|
|
|
for _, tc := range paths {
|
|
req := httptest.NewRequest(tc.method, tc.path, strings.NewReader(tc.body))
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing ai:execute scope on %s %s, got %d", tc.method, tc.path, rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeAIExecute) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeAIExecute, rec.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInfraUpdateReadEndpointsRequireMonitoringReadScope(t *testing.T) {
|
|
rawToken := "infra-read-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeSettingsRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
paths := []string{
|
|
"/api/infra-updates",
|
|
"/api/infra-updates/summary",
|
|
"/api/infra-updates/host/host-1",
|
|
"/api/infra-updates/docker:host-1/c1",
|
|
}
|
|
|
|
for _, path := range paths {
|
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing monitoring:read scope on %s, got %d", path, rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeMonitoringRead) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeMonitoringRead, rec.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestInfraUpdateCheckRequiresMonitoringWriteScope(t *testing.T) {
|
|
rawToken := "infra-write-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeMonitoringRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/infra-updates/check", strings.NewReader(`{}`))
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing monitoring:write scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeMonitoringWrite) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeMonitoringWrite, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAlertReadEndpointsRequireMonitoringReadScope(t *testing.T) {
|
|
rawToken := "alerts-read-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeSettingsRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
paths := []string{
|
|
"/api/alerts/config",
|
|
"/api/alerts/active",
|
|
"/api/alerts/history",
|
|
"/api/alerts/incidents",
|
|
}
|
|
|
|
for _, path := range paths {
|
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing monitoring:read scope on %s, got %d", path, rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeMonitoringRead) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeMonitoringRead, rec.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAlertMutationEndpointsRequireMonitoringWriteScope(t *testing.T) {
|
|
rawToken := "alerts-write-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeMonitoringRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
paths := []struct {
|
|
method string
|
|
path string
|
|
body string
|
|
}{
|
|
{method: http.MethodPut, path: "/api/alerts/config", body: `{}`},
|
|
{method: http.MethodPost, path: "/api/alerts/activate", body: `{}`},
|
|
{method: http.MethodDelete, path: "/api/alerts/history", body: ""},
|
|
{method: http.MethodPost, path: "/api/alerts/bulk/acknowledge", body: `{}`},
|
|
{method: http.MethodPost, path: "/api/alerts/bulk/clear", body: `{}`},
|
|
{method: http.MethodPost, path: "/api/alerts/acknowledge", body: `{}`},
|
|
{method: http.MethodPost, path: "/api/alerts/unacknowledge", body: `{}`},
|
|
{method: http.MethodPost, path: "/api/alerts/clear", body: `{}`},
|
|
{method: http.MethodPost, path: "/api/alerts/alert-1/acknowledge", body: `{}`},
|
|
{method: http.MethodPost, path: "/api/alerts/alert-1/unacknowledge", body: `{}`},
|
|
{method: http.MethodPost, path: "/api/alerts/alert-1/clear", body: `{}`},
|
|
{method: http.MethodPost, path: "/api/alerts/incidents/note", body: `{}`},
|
|
}
|
|
|
|
for _, tc := range paths {
|
|
req := httptest.NewRequest(tc.method, tc.path, strings.NewReader(tc.body))
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing monitoring:write scope on %s %s, got %d", tc.method, tc.path, rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeMonitoringWrite) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeMonitoringWrite, rec.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNotificationQueueStatsRequireSettingsReadScope(t *testing.T) {
|
|
rawToken := "queue-stats-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeMonitoringRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/notifications/queue/stats", nil)
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:read scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsRead) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsRead, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestConfigSystemRequiresSettingsReadScope(t *testing.T) {
|
|
rawToken := "config-system-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeMonitoringRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/config/system", nil)
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:read scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsRead) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsRead, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSystemSettingsRequiresSettingsReadScope(t *testing.T) {
|
|
rawToken := "system-settings-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeMonitoringRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/system/settings", nil)
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:read scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsRead) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsRead, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestSystemSettingsUpdateRequiresSettingsWriteScope(t *testing.T) {
|
|
rawToken := "system-settings-write-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeSettingsRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/system/settings/update", strings.NewReader(`{}`))
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:write scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsWrite) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsWrite, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestMockModeReadRequiresSettingsReadScope(t *testing.T) {
|
|
rawToken := "mock-mode-read-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeMonitoringRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/system/mock-mode", nil)
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:read scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsRead) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsRead, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestMockModeWriteRequiresSettingsWriteScope(t *testing.T) {
|
|
rawToken := "mock-mode-write-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeSettingsRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/system/mock-mode", strings.NewReader(`{}`))
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:write scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsWrite) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsWrite, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestConfigNodesReadRequiresSettingsReadScope(t *testing.T) {
|
|
rawToken := "config-nodes-read-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeMonitoringRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/config/nodes", nil)
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:read scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsRead) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsRead, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestConfigNodesWriteRequiresSettingsWriteScope(t *testing.T) {
|
|
rawToken := "config-nodes-write-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeSettingsRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/config/nodes", strings.NewReader(`{}`))
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:write scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsWrite) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsWrite, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestConfigNodeMutationsRequireSettingsWriteScope(t *testing.T) {
|
|
rawToken := "config-node-mutate-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeSettingsRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
paths := []struct {
|
|
method string
|
|
path string
|
|
body string
|
|
}{
|
|
{method: http.MethodPost, path: "/api/config/nodes/test-config", body: `{}`},
|
|
{method: http.MethodPost, path: "/api/config/nodes/test-connection", body: `{}`},
|
|
{method: http.MethodPut, path: "/api/config/nodes/node-1", body: `{}`},
|
|
{method: http.MethodDelete, path: "/api/config/nodes/node-1", body: ""},
|
|
{method: http.MethodPost, path: "/api/config/nodes/node-1/test", body: `{}`},
|
|
{method: http.MethodPost, path: "/api/config/nodes/node-1/refresh-cluster", body: `{}`},
|
|
}
|
|
|
|
for _, tc := range paths {
|
|
req := httptest.NewRequest(tc.method, tc.path, strings.NewReader(tc.body))
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:write scope on %s %s, got %d", tc.method, tc.path, rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsWrite) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsWrite, rec.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSecurityOIDCRequiresSettingsWriteScope(t *testing.T) {
|
|
rawToken := "security-oidc-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeSettingsRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/security/oidc", strings.NewReader(`{}`))
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:write scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsWrite) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsWrite, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestUpdateHistoryEndpointsRequireSettingsReadScope(t *testing.T) {
|
|
rawToken := "updates-history-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeMonitoringRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
paths := []string{
|
|
"/api/updates/history",
|
|
"/api/updates/history/entry",
|
|
"/api/updates/stream",
|
|
}
|
|
|
|
for _, path := range paths {
|
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:read scope on %s, got %d", path, rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsRead) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsRead, rec.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDiagnosticsRequireSettingsReadScope(t *testing.T) {
|
|
rawToken := "diag-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeMonitoringRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/diagnostics", nil)
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:read scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsRead) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsRead, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestDiagnosticsPrepareTokenRequiresSettingsWriteScope(t *testing.T) {
|
|
rawToken := "diag-write-token-123.12345678"
|
|
record := newTokenRecord(t, rawToken, []string{config.ScopeSettingsRead}, nil)
|
|
cfg := newTestConfigWithTokens(t, record)
|
|
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/api/diagnostics/docker/prepare-token", strings.NewReader(`{}`))
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
rec := httptest.NewRecorder()
|
|
router.Handler().ServeHTTP(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for missing settings:write scope, got %d", rec.Code)
|
|
}
|
|
if !strings.Contains(rec.Body.String(), config.ScopeSettingsWrite) {
|
|
t.Fatalf("expected missing scope response to mention %q, got %q", config.ScopeSettingsWrite, rec.Body.String())
|
|
}
|
|
}
|