Files
cairnobs/enterprise/internal/session/session_test.go
T
jcoffey-dev 3eb0f4c589 Phase 4: SSO scaffolding, RBAC enforcement, tenant-scoped dashboards, audit logging, K8s deployment
RBAC (api/internal/authz) is live on /query and /dashboards, backed by a
new enterprise/ module (session issuance, audit logging, RBAC storage,
OIDC/SAML protocol wiring) that core never imports -- only calls over
HTTP. Found and fixed a real cross-tenant vulnerability in dashboards
(no tenant_id filtering at all) while writing the threat model doc.

Two things are explicitly NOT done, documented rather than hidden:
tenant isolation for log data itself (/query still shares one ClickHouse
connection and Tantivy index across every tenant -- RBAC controls who
can query, not what a query can see), and human SSO login (protocol
wiring exists, no HTTP handler calls it yet). See
docs/security/threat-model.md and docs/phase-4-runbook.md.

Also adds deploy/ (Go Operator + Helm chart, validated offline only --
no cluster was reachable in this environment).
2026-08-13 22:16:59 -07:00

119 lines
3.2 KiB
Go

package session
import (
"strings"
"testing"
"time"
"github.com/go-jose/go-jose/v4/jwt"
)
func testKey() []byte {
return []byte("this-is-a-32-byte-test-signing-key!")
}
func TestNewManagerRejectsShortKey(t *testing.T) {
if _, err := NewManager([]byte("too-short")); err == nil {
t.Fatal("expected an error for a signing key under 32 bytes")
}
}
func TestIssueAndValidateUserSession(t *testing.T) {
m, err := NewManager(testKey())
if err != nil {
t.Fatalf("NewManager: %v", err)
}
token, err := m.IssueUserSession("acme", "u1", "editor")
if err != nil {
t.Fatalf("IssueUserSession: %v", err)
}
claims, err := m.Validate(token)
if err != nil {
t.Fatalf("Validate: %v", err)
}
if claims.TenantID != "acme" || claims.UserID != "u1" || claims.Role != "editor" {
t.Fatalf("unexpected claims: %+v", claims)
}
}
func TestIssueAndValidateServiceToken(t *testing.T) {
m, err := NewManager(testKey())
if err != nil {
t.Fatalf("NewManager: %v", err)
}
token, err := m.IssueServiceToken("alerting")
if err != nil {
t.Fatalf("IssueServiceToken: %v", err)
}
claims, err := m.Validate(token)
if err != nil {
t.Fatalf("Validate: %v", err)
}
if claims.Role != "service" || claims.Subject != "alerting" {
t.Fatalf("unexpected claims: %+v", claims)
}
if claims.TenantID != "" || claims.UserID != "" {
t.Fatalf("service token must not carry a tenant/user -- tenant is resolved server-side per request, got %+v", claims)
}
}
func TestValidateRejectsTamperedToken(t *testing.T) {
m, err := NewManager(testKey())
if err != nil {
t.Fatalf("NewManager: %v", err)
}
token, err := m.IssueUserSession("acme", "u1", "viewer")
if err != nil {
t.Fatalf("IssueUserSession: %v", err)
}
// Flip a character in the payload segment to simulate tampering.
parts := strings.Split(token, ".")
if len(parts) != 3 {
t.Fatalf("expected a 3-segment JWT, got %d segments", len(parts))
}
tampered := parts[0] + "." + parts[1] + "x" + "." + parts[2]
if _, err := m.Validate(tampered); err != ErrInvalidToken {
t.Fatalf("Validate(tampered) error = %v, want ErrInvalidToken", err)
}
}
func TestValidateRejectsWrongKey(t *testing.T) {
m1, err := NewManager(testKey())
if err != nil {
t.Fatalf("NewManager: %v", err)
}
m2, err := NewManager([]byte("a-completely-different-32-byte-key!"))
if err != nil {
t.Fatalf("NewManager: %v", err)
}
token, err := m1.IssueUserSession("acme", "u1", "viewer")
if err != nil {
t.Fatalf("IssueUserSession: %v", err)
}
if _, err := m2.Validate(token); err != ErrInvalidToken {
t.Fatalf("Validate with wrong key error = %v, want ErrInvalidToken", err)
}
}
func TestValidateRejectsExpiredToken(t *testing.T) {
m, err := NewManager(testKey())
if err != nil {
t.Fatalf("NewManager: %v", err)
}
now := time.Now()
claims := Claims{
TenantID: "acme", UserID: "u1", Role: "viewer",
Claims: jwt.Claims{
IssuedAt: jwt.NewNumericDate(now.Add(-2 * time.Hour)),
Expiry: jwt.NewNumericDate(now.Add(-1 * time.Hour)),
},
}
token, err := jwt.Signed(m.signer).Claims(claims).Serialize()
if err != nil {
t.Fatalf("building an already-expired token: %v", err)
}
if _, err := m.Validate(token); err != ErrInvalidToken {
t.Fatalf("Validate(expired) error = %v, want ErrInvalidToken", err)
}
}