Files
Pulse/internal/api/rbac_test.go
rcourtman 3e2824a7ff feat: remove Enterprise badges, simplify Pro upgrade prompts
- 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
2026-01-09 16:51:08 +00:00

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
}