mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-02-18 00:17:39 +01:00
- Replace barrel import in AuditLogPanel.tsx to fix ad-blocker crash - Remove all Enterprise/Pro badges from nav and feature headers - Simplify upgrade CTAs to clean 'Upgrade to Pro' links - Update docs: PULSE_PRO.md, API.md, README.md, SECURITY.md - Align terminology: single Pro tier, no separate Enterprise tier Also includes prior refactoring: - Move auth package to pkg/auth for enterprise reuse - Export server functions for testability - Stabilize CLI tests
236 lines
7.0 KiB
Go
236 lines
7.0 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/pkg/auth"
|
|
)
|
|
|
|
type mockAuthorizer struct {
|
|
allowed bool
|
|
err error
|
|
}
|
|
|
|
func (m *mockAuthorizer) Authorize(ctx context.Context, action string, resource string) (bool, error) {
|
|
return m.allowed, m.err
|
|
}
|
|
|
|
type mockAuthorizerFn struct {
|
|
fn func(ctx context.Context, action string, resource string) (bool, error)
|
|
}
|
|
|
|
func (m *mockAuthorizerFn) Authorize(ctx context.Context, action string, resource string) (bool, error) {
|
|
return m.fn(ctx, action, resource)
|
|
}
|
|
|
|
func TestRequirePermission(t *testing.T) {
|
|
cfg := &config.Config{} // Basic empty config
|
|
|
|
t.Run("Allowed", func(t *testing.T) {
|
|
var capturedSubject string
|
|
authMock := &mockAuthorizer{
|
|
allowed: true,
|
|
err: nil,
|
|
}
|
|
// Custom mock to capture subject
|
|
customAuth := func(ctx context.Context, action string, resource string) (bool, error) {
|
|
capturedSubject = auth.GetUser(ctx)
|
|
return authMock.Authorize(ctx, action, resource)
|
|
}
|
|
|
|
handler := RequirePermission(cfg, &mockAuthorizerFn{fn: customAuth}, "read", "logs", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/api/test", nil)
|
|
// Mock authentication bypass or setup
|
|
// Since CheckAuth is internal, we might need a way to mock it or set up what it expects.
|
|
// For this test, let's assume CheckAuth passes if no auth is configured.
|
|
|
|
rr := httptest.NewRecorder()
|
|
handler.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Errorf("Expected status OK, got %d", rr.Code)
|
|
}
|
|
|
|
if capturedSubject != "anonymous" {
|
|
t.Errorf("Expected subject 'anonymous', got %q", capturedSubject)
|
|
}
|
|
})
|
|
|
|
t.Run("Denied", func(t *testing.T) {
|
|
auth := &mockAuthorizer{allowed: false}
|
|
handler := RequirePermission(cfg, auth, "write", "settings", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
req := httptest.NewRequest("POST", "/api/test", nil)
|
|
rr := httptest.NewRecorder()
|
|
handler.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusForbidden {
|
|
t.Errorf("Expected status Forbidden, got %d", rr.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("EnterpriseAnonymousRefusal", func(t *testing.T) {
|
|
// Simulate Enterprise logic: deny "anonymous"
|
|
enterpriseLogic := func(ctx context.Context, action string, resource string) (bool, error) {
|
|
username := auth.GetUser(ctx)
|
|
if username == "anonymous" {
|
|
return false, nil
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
handler := RequirePermission(cfg, &mockAuthorizerFn{fn: enterpriseLogic}, "read", "nodes", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
// req with no auth -> CheckAuth sets "anonymous"
|
|
req := httptest.NewRequest("GET", "/api/test", nil)
|
|
rr := httptest.NewRecorder()
|
|
handler.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusForbidden {
|
|
t.Errorf("Expected status Forbidden for anonymous Enterprise access, got %d", rr.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("APITokenPrincipal", func(t *testing.T) {
|
|
cfg := &config.Config{}
|
|
cfg.AuthUser = "admin"
|
|
cfg.AuthPass = "password"
|
|
|
|
// Setup a token
|
|
rawToken := "valid-token"
|
|
record, _ := config.NewAPITokenRecord(rawToken, "My Token", nil)
|
|
cfg.APITokens = append(cfg.APITokens, *record)
|
|
|
|
var capturedSubject string
|
|
customAuth := func(ctx context.Context, action string, resource string) (bool, error) {
|
|
capturedSubject = auth.GetUser(ctx)
|
|
return true, nil
|
|
}
|
|
|
|
handler := RequirePermission(cfg, &mockAuthorizerFn{fn: customAuth}, "read", "logs", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/api/test", nil)
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
|
|
rr := httptest.NewRecorder()
|
|
handler.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Errorf("Expected status OK, got %d", rr.Code)
|
|
}
|
|
|
|
expectedPrincipal := "token:" + record.ID
|
|
if capturedSubject != expectedPrincipal {
|
|
t.Errorf("Expected principal %q, got %q", expectedPrincipal, capturedSubject)
|
|
}
|
|
})
|
|
|
|
t.Run("APITokenMissingIDFallback", func(t *testing.T) {
|
|
cfg := &config.Config{}
|
|
// Setup a token without an ID
|
|
rawToken := "legacy-token"
|
|
record, _ := config.NewAPITokenRecord(rawToken, "Legacy", nil)
|
|
record.ID = "" // Explicitly clear ID
|
|
cfg.APITokens = append(cfg.APITokens, *record)
|
|
|
|
var capturedSubject string
|
|
customAuth := func(ctx context.Context, action string, resource string) (bool, error) {
|
|
capturedSubject = auth.GetUser(ctx)
|
|
return true, nil
|
|
}
|
|
|
|
handler := RequirePermission(cfg, &mockAuthorizerFn{fn: customAuth}, "read", "logs", func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
req := httptest.NewRequest("GET", "/api/test", nil)
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
|
|
rr := httptest.NewRecorder()
|
|
handler.ServeHTTP(rr, req)
|
|
|
|
if !strings.HasPrefix(capturedSubject, "token:legacy-") {
|
|
t.Errorf("Expected fallback legacy principal, got %q", capturedSubject)
|
|
}
|
|
})
|
|
|
|
t.Run("APITokenAdminAccess", func(t *testing.T) {
|
|
cfg := &config.Config{}
|
|
// Setup a valid token in config
|
|
rawToken := "admin-token"
|
|
record, _ := config.NewAPITokenRecord(rawToken, "Admin Token", nil)
|
|
cfg.APITokens = append(cfg.APITokens, *record)
|
|
|
|
// Mock Enterprise authorizer that grants tokens admin access
|
|
eAuth := func(ctx context.Context, action string, resource string) (bool, error) {
|
|
user := auth.GetUser(ctx)
|
|
// Match our new stable principal format
|
|
if strings.HasPrefix(user, "token:") {
|
|
return true, nil
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
handler := RequirePermission(cfg, &mockAuthorizerFn{fn: eAuth}, auth.ActionAdmin, auth.ResourceUsers, func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusOK)
|
|
})
|
|
|
|
req := httptest.NewRequest("POST", "/api/security/tokens", nil)
|
|
req.Header.Set("X-API-Token", rawToken)
|
|
|
|
rr := httptest.NewRecorder()
|
|
handler.ServeHTTP(rr, req)
|
|
|
|
if rr.Code != http.StatusOK {
|
|
t.Errorf("Expected token to have admin access to users, got %d", rr.Code)
|
|
}
|
|
})
|
|
|
|
t.Run("AdminSync", func(t *testing.T) {
|
|
// 1. Verify sync on non-empty
|
|
e := &enterpriseMock{adminUser: "initial"}
|
|
mock := &enterpriseAuthorizerWithSync{e}
|
|
auth.SetAuthorizer(mock)
|
|
defer auth.SetAuthorizer(&auth.DefaultAuthorizer{})
|
|
|
|
auth.SetAdminUser("superadmin")
|
|
if e.adminUser != "superadmin" {
|
|
t.Errorf("Expected admin sub-system synced to 'superadmin', got %q", e.adminUser)
|
|
}
|
|
|
|
// 2. Verify skip on empty
|
|
auth.SetAdminUser("")
|
|
if e.adminUser != "superadmin" {
|
|
t.Error("Expected empty admin sync to be skipped to preserve configuration")
|
|
}
|
|
})
|
|
}
|
|
|
|
// Support types for TestRequirePermission
|
|
type enterpriseMock struct {
|
|
adminUser string
|
|
}
|
|
|
|
type enterpriseAuthorizerWithSync struct {
|
|
*enterpriseMock
|
|
}
|
|
|
|
func (e *enterpriseAuthorizerWithSync) SetAdminUser(u string) { e.adminUser = u }
|
|
func (e *enterpriseAuthorizerWithSync) Authorize(ctx context.Context, action string, resource string) (bool, error) {
|
|
return auth.GetUser(ctx) == e.adminUser, nil
|
|
}
|