Documentation
¶
Overview ¶
Package middleware provides HTTP middleware for the StreamSpace API. This file implements API key authentication middleware for agents.
SECURITY: Agent API Key Authentication Middleware
This middleware validates agent API keys on incoming requests. It is used to protect agent-specific endpoints:
- POST /api/v1/agents/register (agent self-registration)
- GET /api/v1/agents/connect (WebSocket upgrade)
The middleware:
- Extracts API key from X-Agent-API-Key header
- Validates key format (64 hex chars)
- Looks up agent by agent_id query/path parameter
- Compares provided key against stored bcrypt hash
- Updates api_key_last_used_at on successful auth
- Sets agent_id in Gin context for downstream handlers
Usage:
agentAuth := middleware.NewAgentAuth(database)
router.POST("/agents/register", agentAuth.RequireAPIKey(), handler.RegisterAgent)
router.GET("/agents/connect", agentAuth.RequireAPIKey(), handler.HandleAgentConnection)
Package middleware - auditlog.go ¶
This file implements comprehensive audit logging for compliance and security.
The audit logger records all API requests in a structured format to support:
- Security investigations (who did what when)
- Compliance requirements (SOC2, HIPAA, GDPR, ISO 27001)
- Usage analytics (patterns, trends)
- Incident response (forensic analysis)
Why Audit Logging is Critical ¶
**Security Requirements**:
- Detect unauthorized access attempts
- Track privilege escalation
- Identify data exfiltration
- Support incident response
**Compliance Requirements**:
- SOC2: Requires audit trail of all system changes
- HIPAA: Requires audit logs retained for 6 years
- GDPR: Requires audit trail for data access/modifications
- ISO 27001: Requires logging of user activities
**Business Requirements**:
- Usage analytics and billing
- User behavior analysis
- Performance troubleshooting
- Capacity planning
Audit Log Architecture ¶
┌─────────────────────────────────────────────────────────┐
│ HTTP Request │
└──────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Audit Middleware │
│ 1. Capture request body (if enabled) │
│ 2. Wrap response writer to capture response │
│ 3. Record start time │
└──────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Request Processing (handlers, business logic) │
└──────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ After Request Completion │
│ 1. Calculate duration │
│ 2. Extract user info from context │
│ 3. Redact sensitive data (passwords, tokens) │
│ 4. Create AuditEvent struct │
│ 5. Log asynchronously to database │
└─────────────────────────────────────────────────────────┘
What Gets Logged ¶
**Every Request**:
- Timestamp (when request started)
- User ID and username (if authenticated)
- HTTP method (GET, POST, PUT, DELETE, etc.)
- Request path (/api/sessions, /api/users, etc.)
- HTTP status code (200, 404, 500, etc.)
- Client IP address
- User agent string
- Request duration in milliseconds
- Errors (if any occurred)
**Conditionally Logged** (if enabled):
- Request body (max 10KB, sensitive fields redacted)
- Response body (disabled by default, too verbose)
Sensitive Data Redaction ¶
To prevent leaking credentials in audit logs, these fields are automatically redacted (replaced with "[REDACTED]"):
- password
- token
- secret
- apiKey
- api_key
Redaction applies recursively to nested objects:
Original: {"user": "alice", "password": "secret123", "profile": {"apiKey": "xyz"}}
Redacted: {"user": "alice", "password": "[REDACTED]", "profile": {"apiKey": "[REDACTED]"}}
Database Schema ¶
Audit logs are stored in the `audit_log` table:
CREATE TABLE audit_log (
id SERIAL PRIMARY KEY,
user_id VARCHAR(255),
action VARCHAR(100), -- HTTP method
resource_type VARCHAR(100), -- Resource path
resource_id VARCHAR(255), -- Specific resource ID (if applicable)
changes JSONB, -- Full event details (method, path, status, etc.)
timestamp TIMESTAMPTZ,
ip_address VARCHAR(45) -- IPv4 or IPv6
);
Indexes for fast queries:
- idx_audit_log_user_id: Query by user
- idx_audit_log_timestamp: Query by time range
- idx_audit_log_action: Query by action type
- idx_audit_log_resource_type: Query by resource
Performance Characteristics ¶
**Asynchronous Logging**:
- Log writing happens in a goroutine (non-blocking)
- Request completes immediately, logging happens in background
- No impact on request latency (0ms added)
**Database Impact**:
- 1 INSERT per request (~1ms write time)
- Bulk inserts possible for high-throughput (future enhancement)
- Partitioning by timestamp recommended for large datasets
**Storage Requirements**:
- ~500 bytes per event (without request/response bodies)
- ~2 KB per event (with request body)
- Example: 1 million requests/day = 500 MB/day (no bodies) or 2 GB/day (with bodies)
Retention and Compliance ¶
**Retention Policies** (configure in database):
- SOC2: 1 year minimum
- HIPAA: 6 years minimum
- GDPR: Varies by purpose
- ISO 27001: 1 year minimum
**Recommended Retention**:
- Hot storage (PostgreSQL): 90 days
- Warm storage (S3/archive): 1-7 years
- Cold storage (Glacier): 7+ years
**Cleanup Strategy**:
-- Archive old logs to S3 SELECT * FROM audit_log WHERE timestamp < NOW() - INTERVAL '90 days' -- Then delete from PostgreSQL DELETE FROM audit_log WHERE timestamp < NOW() - INTERVAL '90 days'
Querying Audit Logs ¶
**Common queries**:
-- User activity in last 24 hours SELECT * FROM audit_log WHERE user_id = 'user-123' AND timestamp > NOW() - INTERVAL '24 hours' ORDER BY timestamp DESC; -- Failed login attempts SELECT * FROM audit_log WHERE resource_type = '/api/auth/login' AND changes->>'status_code' = '401' AND timestamp > NOW() - INTERVAL '1 hour'; -- Resource deletions SELECT * FROM audit_log WHERE action = 'DELETE' AND timestamp > NOW() - INTERVAL '7 days';
Known Limitations ¶
- **No log batching**: Each request = 1 DB write - Solution: Implement batch writer (future)
- **No log rotation**: Logs grow indefinitely - Solution: Implement TTL-based cleanup (future)
- **No request correlation**: Hard to trace multi-request operations - Solution: Add request ID middleware (implemented)
- **Goroutine leak risk**: If database is slow, goroutines pile up - Solution: Use worker pool pattern (future)
See also:
- api/internal/middleware/request_id.go: Request correlation IDs
- api/internal/db/queries/audit.sql: Audit log queries
Package middleware provides HTTP middleware for the StreamSpace API. This file implements HTTP response compression using gzip.
Purpose: The compression middleware reduces bandwidth usage and improves response times by compressing HTTP responses with gzip encoding. This is especially beneficial for JSON API responses which typically compress to 60-80% smaller sizes.
Implementation Details: - Uses sync.Pool for gzip writer reuse (reduces memory allocations) - Configurable compression levels (BestSpeed, DefaultCompression, BestCompression) - Automatic skip for incompressible content (WebSocket, Server-Sent Events) - Wraps response writer transparently (handlers unaware of compression)
Performance Characteristics: - Best Speed (level 1): 2-3x faster, 70-80% compression ratio - Default (level 6): Balanced, 60-70% compression ratio - Best Compression (level 9): Slowest, 50-60% compression ratio - Memory: ~256KB per concurrent request (reused via sync.Pool) - CPU overhead: 1-5ms per response (depending on compression level and payload size)
Thread Safety: Safe for concurrent use. Each request gets its own gzip writer from the pool, uses it for the duration of the request, then returns it to the pool.
Usage:
// Use default compression (level 6)
router.Use(middleware.Gzip(middleware.DefaultCompression))
// Use best speed (level 1) for high-throughput APIs
router.Use(middleware.Gzip(middleware.BestSpeed))
// Exclude specific paths from compression
router.Use(middleware.GzipWithExclusions(
middleware.DefaultCompression,
[]string{"/api/v1/ws/", "/api/v1/upload"},
))
Configuration:
// Available compression levels middleware.NoCompression // No compression (level 0) middleware.BestSpeed // Fastest compression (level 1) middleware.DefaultCompression // Balanced (level 6) middleware.BestCompression // Maximum compression (level 9)
Package middleware defines constants for middleware components.
This file centralizes configuration values for: - Rate limiting (prevent brute force and DoS attacks) - CSRF protection (prevent cross-site request forgery) - Request size limits (prevent payload DoS attacks)
SECURITY FIX (2025-11-14): Extracted all magic numbers to improve code maintainability and security auditing.
Package middleware provides HTTP middleware for the StreamSpace API. This file implements CSRF (Cross-Site Request Forgery) protection.
SECURITY ENHANCEMENT (2025-11-14): Added CSRF protection using double-submit cookie pattern with constant-time comparison.
CSRF Attack Scenario (Without Protection): 1. User logs into StreamSpace (gets session cookie) 2. User visits malicious site evil.com 3. evil.com contains: <form action="https://streamspace.io/api/delete-account" method="POST"> 4. Browser automatically sends session cookie with the malicious request 5. StreamSpace deletes user's account (thinks it's a legitimate request)
CSRF Protection (Double-Submit Cookie Pattern): 1. GET request: Server generates random CSRF token, sends in both cookie AND header 2. Client stores header token (JavaScript can read it) 3. POST request: Client sends token in both cookie AND custom header 4. Server compares: cookie token == header token (using constant-time comparison) 5. If match: Request is from legitimate client (evil.com can't read/set custom headers) 6. If mismatch: Request is CSRF attack (blocked)
Why This Works: - Malicious sites can trigger POST requests (via forms, fetch) - Browsers automatically send cookies with requests (even cross-site) - BUT: Malicious sites CANNOT read cookies or set custom headers (Same-Origin Policy) - So attacker cannot get the token to put in the custom header
Implementation Details: - Token: 32 random bytes, base64-encoded (256 bits of entropy) - Comparison: Constant-time (prevents timing attacks) - Storage: In-memory map with automatic cleanup (24-hour expiry) - Exempt: GET, HEAD, OPTIONS requests (safe methods, no state change)
Usage:
router.Use(middleware.CSRFProtection())
Package middleware provides HTTP middleware for the StreamSpace API. This file implements comprehensive input validation and sanitization.
Purpose: The input validation middleware protects against injection attacks by validating and sanitizing all user input including query parameters, path parameters, and request bodies. This prevents SQL injection, XSS, command injection, LDAP injection, and path traversal attacks.
Implementation Details: - Path validation: Detects path traversal patterns (../, %2e%2e, etc.) - Query parameter validation: Checks for injection patterns in all query strings - JSON sanitization: Recursively sanitizes nested objects and arrays using bluemonday - Format validation: Validates usernames, emails, Kubernetes resource names, container images - Length limits: Prevents buffer overflow with 10KB max input size - Pattern detection: Regex-based detection of SQL, command, and LDAP injection attempts
Security Notes: This middleware provides defense-in-depth against common web vulnerabilities: - SQL Injection: Detects UNION, SELECT, DROP, etc. patterns - XSS (Cross-Site Scripting): Bluemonday strips all HTML tags and dangerous content - Command Injection: Blocks shell metacharacters (;, |, &, backticks, $()) - LDAP Injection: Detects LDAP special characters when used in combinations - Path Traversal: Prevents directory traversal attacks (../, ..\, null bytes) - Buffer Overflow: Enforces 10KB limit on input values
Thread Safety: Safe for concurrent use. Each request gets its own validation context. The bluemonday policy is thread-safe and shared across all requests.
Usage:
// Basic input validation on all routes
validator := middleware.NewInputValidator()
router.Use(validator.Middleware())
// JSON sanitization for API endpoints
router.Use(validator.SanitizeJSONMiddleware())
// Standalone validation functions
if err := middleware.ValidateUsername(username); err != nil {
return err
}
if err := middleware.ValidateEmail(email); err != nil {
return err
}
Configuration:
// Bluemonday uses StrictPolicy by default (strips ALL HTML) // To customize, modify the policy in NewInputValidator()
Package middleware provides HTTP middleware for the StreamSpace API. This file implements license enforcement middleware.
LICENSE ENFORCEMENT: - Check license limits before creating resources (users, sessions, nodes) - Block actions that exceed license limits - Warn when approaching limits (80%, 90%, 95%) - Cache license information for performance
LICENSE TIERS: - Community (Free): 10 users, 20 sessions, 3 nodes - Pro: 100 users, 200 sessions, 10 nodes - Enterprise: Unlimited users, sessions, nodes
USAGE:
router.Use(middleware.LicenseEnforcement(database))
router.POST("/users", handler.CreateUser) // Will check license limits
Thread Safety: - License cache is thread-safe with mutex - Database operations are thread-safe
Dependencies: - Database: PostgreSQL licenses table
Example Usage:
// Apply middleware to admin routes
admin := router.Group("/api/v1/admin")
admin.Use(middleware.LicenseEnforcement(database))
Package middleware provides HTTP middleware for the StreamSpace API. This file implements HTTP method restriction to prevent abuse through uncommon methods.
Purpose: This middleware restricts incoming requests to only commonly-used, safe HTTP methods. It prevents security issues and attacks that exploit uncommon or dangerous methods like TRACE (XSS via HTTP response splitting) and CONNECT (proxy abuse).
Implementation Details: - AllowedHTTPMethods: Whitelist approach (only allow known-safe methods) - DisallowedHTTPMethods: Blacklist approach (explicitly block dangerous methods) - Returns 405 Method Not Allowed with Allow header - Defense in depth: Use both middlewares together for maximum security
Security Notes: Dangerous HTTP methods and why they're blocked: - TRACE: Can be used in XSS attacks (cross-site tracing)
- Reflects request in response body
- Can expose authentication cookies
- Bypasses HttpOnly cookie protection
- TRACK: Microsoft proprietary variant of TRACE (same vulnerability) - CONNECT: Used for HTTP tunneling (proxy abuse)
- Only needed for proxy servers
- Can be used to bypass firewalls
- Can create unauthorized tunnels
Allowed methods (safe for web APIs): - GET: Read resources (idempotent, safe) - POST: Create resources (not idempotent) - PUT: Update resources (idempotent) - PATCH: Partial update (not idempotent) - DELETE: Remove resources (idempotent) - OPTIONS: CORS preflight (required for browser APIs) - HEAD: Metadata only (idempotent, safe)
Thread Safety: Safe for concurrent use. No shared state between requests.
Usage:
// Whitelist safe methods (recommended) router.Use(middleware.AllowedHTTPMethods()) // Blacklist dangerous methods (additional protection) router.Use(middleware.DisallowedHTTPMethods()) // Defense in depth (use both) router.Use(middleware.AllowedHTTPMethods()) router.Use(middleware.DisallowedHTTPMethods())
Package middleware provides HTTP middleware for the StreamSpace API. This file implements organization context extraction and enforcement for multi-tenancy.
SECURITY: This middleware is CRITICAL for preventing cross-tenant data access. All protected routes MUST use this middleware to ensure org_id is available in the request context for database query filtering.
Multi-Tenancy Architecture:
- Each user belongs to exactly one organization (org_id)
- org_id is embedded in JWT claims during authentication
- This middleware extracts org_id from JWT and adds to request context
- All handlers MUST use GetOrgID() to filter database queries
- Requests without valid org_id are rejected with 401 Unauthorized
Context Keys:
- "org_id": Organization ID (string)
- "org_name": Organization display name (string)
- "k8s_namespace": Kubernetes namespace for this org (string)
- "org_role": User's role within the org (string)
- "user_id": User's unique ID (string)
- "username": User's username (string)
- "role": User's system-wide role (string)
Usage:
// Apply middleware to protected routes
protected := router.Group("/api/v1")
protected.Use(middleware.OrgContextMiddleware(jwtManager))
// In handler, extract org_id for filtering
func MyHandler(c *gin.Context) {
orgID, err := middleware.GetOrgID(c)
if err != nil {
c.JSON(401, gin.H{"error": "unauthorized"})
return
}
// Use orgID to filter database queries
sessions, err := db.ListSessionsByOrg(ctx, orgID)
}
Package middleware - quota.go ¶
This file implements resource quota enforcement at the API level.
The quota middleware provides the HTTP layer integration for StreamSpace's resource quota system, preventing users from exceeding their allocated CPU, memory, GPU, and session limits.
Why Quota Enforcement is Critical ¶
Without quotas, a single user could:
- Consume all cluster resources (DoS to other users)
- Launch hundreds of sessions (resource exhaustion)
- Request unlimited CPU/memory (cluster instability)
- Exceed billing limits (cost overruns)
Multi-Layered Quota Enforcement ¶
StreamSpace enforces quotas at multiple levels for defense in depth:
┌─────────────────────────────────────────────────────────┐
│ Level 1: API Middleware (This File) │
│ - Fast rejection before DB writes │
│ - HTTP 402 (Payment Required) response │
│ - User-friendly error messages │
└──────────────────────┬──────────────────────────────────┘
│ Passed
▼
┌─────────────────────────────────────────────────────────┐
│ Level 2: API Handlers (handlers/sessions.go) │
│ - Business logic validation │
│ - Current usage calculation │
│ - Quota check with enforcer │
└──────────────────────┬──────────────────────────────────┘
│ Passed
▼
┌─────────────────────────────────────────────────────────┐
│ Level 3: Kubernetes Controller │
│ - Admission webhook validation (future) │
│ - Pod resource limits enforcement │
│ - Node resource availability check │
└─────────────────────────────────────────────────────────┘
Quota Types Enforced ¶
**Per-User Limits**:
- MaxSessions: Maximum concurrent sessions (e.g., 10)
- MaxCPU: Total CPU across all sessions (e.g., 16 cores)
- MaxMemory: Total memory across all sessions (e.g., 64 GB)
- MaxGPU: Number of GPU devices (e.g., 2)
- MaxStorage: Home directory size (e.g., 100 GB)
**Per-Session Limits**:
- MaxCPUPerSession: CPU per session (e.g., 8 cores)
- MaxMemoryPerSession: Memory per session (e.g., 32 GB)
Integration with Quota Enforcer ¶
This middleware is a thin wrapper around quota.Enforcer:
- Enforcer contains the core quota logic
- Enforcer queries database for user limits
- Enforcer calculates current resource usage
- Enforcer performs quota math and validation
This middleware just:
- Extracts username from auth context
- Injects enforcer into request context
- Provides helper functions for handlers
Error Response Format ¶
When quota is exceeded, return HTTP 402 (Payment Required):
{
"error": "quota_exceeded",
"message": "CPU quota exceeded: requested 4000m, limit 8000m, current usage 5000m",
"quota": {
"limit": "8000m",
"current": "5000m",
"requested": "4000m",
"available": "3000m"
}
}
Usage Pattern ¶
Middleware is applied globally, enforcement is selective:
// In main.go
quotaMiddleware := middleware.NewQuotaMiddleware(enforcer)
router.Use(quotaMiddleware.Middleware())
// In session creation handler
err := middleware.EnforceSessionCreation(c, cpu, memory, gpu, currentUsage)
if err != nil {
c.JSON(402, gin.H{"error": err.Error()})
return
}
Known Limitations ¶
- **Race conditions**: Two concurrent requests might both pass quota check - Solution: Database-level locking in enforcer
- **Stale usage data**: Usage is cached briefly for performance - Solution: Short cache TTL (5 seconds) in enforcer
- **No GPU accounting yet**: GPU quota exists but usage tracking incomplete - Solution: Implement GPU usage tracking in controller
See also:
- api/internal/quota/enforcer.go: Core quota enforcement logic
- api/internal/handlers/sessions.go: Session creation with quota checks
- controller/internal/controllers/session_controller.go: Resource limit enforcement
Package middleware provides HTTP middleware for the StreamSpace API. This file implements rate limiting to prevent brute force and DoS attacks.
SECURITY ENHANCEMENT (2025-11-14): Added in-memory rate limiting for MFA verification to prevent brute force attacks.
Rate limiting is critical for security: - MFA codes are only 6 digits (1 million combinations) - Without rate limiting, codes can be brute forced in minutes - With 5 attempts/minute limit, brute force takes ~160 days
Current Implementation: In-Memory (Development/Single-Server) - Fast: No network round-trips - Simple: No external dependencies - Limitations: Not distributed (doesn't work across multiple API servers)
Production Recommendation: Redis-Backed Rate Limiting - Distributed: Works across multiple API servers - Persistent: Survives API server restarts - Scalable: Handles millions of concurrent rate limit entries
Usage:
// In handler
limiter := middleware.GetRateLimiter()
if !limiter.CheckLimit("user:123:mfa", 5, 1*time.Minute) {
return errors.New("rate limit exceeded")
}
Package middleware provides HTTP middleware for the StreamSpace API. This file implements request ID generation and correlation.
Purpose: The request ID middleware provides unique identifiers for each HTTP request, enabling distributed tracing, log correlation, and debugging across multiple services and components. This is essential for troubleshooting issues in production environments.
Implementation Details: - Generates UUIDv4 for each request (or accepts existing from client) - Stores in Gin context for handlers to access - Adds to response header (X-Request-ID) so clients can reference - Enables correlation across logs, metrics, and traces - Idempotent: Preserves existing request ID from upstream services
Use Cases: 1. Distributed Tracing: Follow a request across multiple microservices
- API Gateway → StreamSpace API → Kubernetes Controller → Database
- All logs share the same request ID for easy correlation
2. Log Correlation: Find all log entries for a specific request
- User reports error at 10:35:42 AM
- Search logs for request ID from error response
- View complete request lifecycle (auth, validation, processing, error)
3. Customer Support: Users can provide request ID when reporting issues
- Error message shows: "Request ID: 550e8400-e29b-41d4-a716-446655440000"
- Support team searches logs with this ID
- Full context available for debugging
4. Performance Analysis: Track slow requests end-to-end
- Identify which service/component caused the delay
- Compare timing across different layers
Thread Safety: Safe for concurrent use. Each request gets its own unique UUID.
Usage:
// Add to middleware chain (should be first for complete tracing)
router.Use(middleware.RequestID())
// Access in handlers
func MyHandler(c *gin.Context) {
requestID := middleware.GetRequestID(c)
log.Printf("[%s] Processing request", requestID)
}
// Client can send existing request ID for distributed tracing
// curl -H "X-Request-ID: my-trace-id" https://api.streamspace.io/sessions
Package middleware - securityheaders.go ¶
This file implements comprehensive HTTP security headers.
Security headers are the first line of defense against common web attacks. They instruct browsers how to handle content, preventing XSS, clickjacking, MITM attacks, and other security vulnerabilities.
Why Security Headers are Critical ¶
**Without security headers**, StreamSpace would be vulnerable to:
- XSS (Cross-Site Scripting): Injected scripts steal user data
- Clickjacking: UI redress attacks trick users into clicking malicious links
- MITM (Man-in-the-Middle): Unencrypted connections can be intercepted
- MIME sniffing: Browser misinterprets content type, executes malicious code
- Information leakage: Server version exposed to attackers
**With security headers**, browsers enforce:
- HTTPS-only connections (HSTS)
- No inline scripts/styles (CSP with nonces)
- No framing by other sites (X-Frame-Options)
- Correct content type interpretation (X-Content-Type-Options)
- Disabled dangerous browser features (Permissions-Policy)
Security Headers Scorecard ¶
This implementation provides A+ rating on:
- Mozilla Observatory
- SecurityHeaders.com
- Qualys SSL Labs
Architecture: Defense in Depth ¶
┌─────────────────────────────────────────────────────────┐
│ Browser │
│ - Enforces all security policies │
│ - Blocks violations before execution │
└──────────────────────┬──────────────────────────────────┘
│ HTTPS (enforced by HSTS)
▼
┌─────────────────────────────────────────────────────────┐
│ Load Balancer / Ingress │
│ - TLS termination │
│ - Certificate management │
└──────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Security Headers Middleware (This File) │
│ 1. Generate nonce for this request │
│ 2. Add all security headers to response │
│ 3. Pass nonce to templates via context │
└──────────────────────┬──────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Application Handlers │
│ - Use nonce in script/style tags │
│ - <script nonce="{{.csp_nonce}}">...</script> │
└─────────────────────────────────────────────────────────┘
CSP Nonce-Based XSS Protection ¶
**Traditional CSP** (unsafe, deprecated):
Content-Security-Policy: script-src 'self' 'unsafe-inline' 'unsafe-eval' - 'unsafe-inline': Allows ALL inline scripts (attacker can inject!) - 'unsafe-eval': Allows eval() (dangerous, can execute arbitrary code) - Rating: F (no real protection)
**Modern CSP with Nonces** (secure, current implementation):
Content-Security-Policy: script-src 'self' 'nonce-xyz123' - Only scripts with matching nonce attribute can execute - Nonce changes on every request (unpredictable) - Attacker can't inject valid nonce (CSP blocks execution) - Rating: A+ (strong XSS protection)
How Nonces Work ¶
**Server-side** (this middleware):
- Generate random nonce: "abc123def456"
- Add to CSP header: script-src 'nonce-abc123def456'
- Store in context: c.Set("csp_nonce", "abc123def456")
**Template rendering**:
<script nonce="{{.csp_nonce}}">
console.log("This script is allowed");
</script>
**Browser behavior**:
- Allowed: <script nonce="abc123def456">alert('ok')</script>
- Blocked: <script>alert('injected!')</script> (no nonce)
- Blocked: <script nonce="wrong">alert('bad')</script> (wrong nonce)
Security Headers Reference ¶
**1. Strict-Transport-Security (HSTS)**:
- Forces HTTPS for 1 year
- Includes all subdomains
- Eligible for browser preload list
- Protects against: SSL stripping, MITM attacks
**2. X-Content-Type-Options**:
- Prevents MIME type sniffing
- Forces browser to respect declared content type
- Protects against: Polyglot files, content confusion
**3. X-Frame-Options**:
- Prevents clickjacking attacks
- Denies embedding in iframes
- Protects against: UI redress, iframe overlay attacks
**4. X-XSS-Protection**:
- Legacy XSS filter for old browsers
- Modern browsers use CSP instead
- Backwards compatibility only
**5. Content-Security-Policy (CSP)**:
- Whitelists allowed content sources
- Nonce-based inline script/style allowance
- Blocks all other inline content
- Protects against: XSS, code injection, data exfiltration
**6. Referrer-Policy**:
- Controls referrer information sent to other sites
- Prevents leaking sensitive URLs
- Protects against: Information disclosure
**7. Permissions-Policy**:
- Disables dangerous browser features
- Prevents unauthorized geolocation, camera, mic access
- Protects against: Feature abuse, privacy violations
**8. X-Permitted-Cross-Domain-Policies**:
- Prevents Adobe Flash/PDF content loading
- Legacy protection (Flash deprecated)
- Backwards compatibility
**9. X-Download-Options**:
- Prevents IE from executing downloads in site context
- Legacy protection for old IE versions
- Backwards compatibility
**10. Cache-Control**:
- Prevents caching of sensitive API responses
- Ensures fresh data on every request
- Protects against: Stale data, information disclosure
Production vs Development Headers ¶
**Production** (SecurityHeaders):
- Strict CSP with nonces
- No inline scripts/styles without nonces
- HSTS with preload
- Rating: A+
**Development** (SecurityHeadersRelaxed):
- Relaxed CSP (unsafe-inline, unsafe-eval allowed)
- Same-origin framing allowed
- No HSTS preload
- Rating: C (convenient for development)
Known Limitations ¶
- **CSP nonce requires template support**: Apps not using templates can't use nonces - Solution: Hash-based CSP or external JS files only
- **HSTS can lock out misconfigured sites**: Once enabled, hard to disable - Solution: Start with short max-age, increase gradually
- **Permissions-Policy may break legitimate features**: Too restrictive - Solution: Enable features selectively per route
- **No CSP reporting**: Violations not logged - Solution: Add report-uri directive (future)
See also:
- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
- https://observatory.mozilla.org/
- https://securityheaders.com/
Package middleware provides HTTP middleware for the StreamSpace API. This file implements enhanced session security with idle timeout and concurrency limits.
Purpose: The session management middleware provides security features for user authentication sessions including automatic logout after inactivity and enforcement of concurrent session limits to prevent credential sharing and session hijacking.
Implementation Details: - Idle timeout: Automatically invalidates sessions after period of inactivity - Activity tracking: Updates last activity timestamp on every authenticated request - Concurrent sessions: Limits number of simultaneous logins per user account - Memory cleanup: Periodic background cleanup prevents memory leaks - In-memory storage: Fast but not distributed (single-server only)
Security Notes: This middleware addresses critical security concerns:
1. Idle Timeout (Prevents Session Hijacking):
- User forgets to log out on public computer
- Without timeout: Session stays active indefinitely (attacker can use it)
- With timeout: Session expires after 30 minutes of inactivity
2. Concurrent Session Limits (Prevents Credential Sharing):
- User shares login credentials with colleagues
- Without limit: Unlimited concurrent sessions (credential abuse)
- With limit: Max 5 concurrent sessions (discourages sharing)
3. Session Cleanup (Prevents Memory Leaks):
- Long-running server accumulates millions of session entries
- Without cleanup: Memory usage grows unbounded (eventual crash)
- With cleanup: Stale sessions removed every 5 minutes
Thread Safety: Safe for concurrent use. Uses sync.RWMutex for thread-safe access to session maps.
Usage:
// Create session manager with 30-minute idle timeout and max 5 concurrent sessions
sessionMgr := middleware.NewSessionManager(30*time.Minute, 5)
// Apply idle timeout check to all authenticated routes
router.Use(sessionMgr.IdleTimeoutMiddleware())
// Track session activity on every request
router.Use(sessionMgr.SessionActivityMiddleware())
// In login handler, register new session
if err := sessionMgr.RegisterSession(username, sessionID); err != nil {
// Max sessions exceeded
return c.JSON(403, gin.H{"error": "Maximum concurrent sessions reached"})
}
// In logout handler, unregister session
sessionMgr.UnregisterSession(username, sessionID)
Configuration:
idleTimeout: 30*time.Minute // Session expires after 30 min inactivity maxSessions: 5 // Max 5 concurrent logins per user cleanupInterval: 5*time.Minute // Cleanup runs every 5 minutes
Package middleware provides HTTP middleware for the StreamSpace API. This file implements request size limiting to prevent DoS attacks.
SECURITY ENHANCEMENT (2025-11-14): Added request size limits to prevent denial of service via oversized payloads.
Why Request Size Limits are Critical: - Prevents memory exhaustion from giant JSON payloads - Prevents disk exhaustion from huge file uploads - Prevents slow-loris attacks with endless request bodies - Forces attackers to use many small requests (easier to detect/rate-limit)
Implementation: - Uses http.MaxBytesReader for hard limits (prevents buffer overflow) - Checks Content-Length header before processing (fail fast) - Skips for GET/HEAD/OPTIONS (no request body) - Returns 413 Payload Too Large with informative error message
Limits: - Default request body: 10MB (general API endpoints) - JSON payloads: 5MB (structured data) - File uploads: 50MB (larger files like logs, exports)
Package middleware provides HTTP middleware for the StreamSpace API. This file implements structured request logging.
Purpose: The structured logger middleware captures detailed information about every HTTP request in a consistent, machine-parseable format. This enables log analysis, alerting, debugging, and observability in production environments.
Implementation Details: - Structured format: Key-value pairs instead of unstructured text - Request correlation: Includes request ID for distributed tracing - User tracking: Logs authenticated user information when available - Performance metrics: Captures request duration in milliseconds - Error tracking: Logs Gin errors if any occurred during request processing - Configurable skipping: Can skip health check endpoints to reduce noise
Logged Fields: - request_id: Correlation ID for distributed tracing (from RequestID middleware) - method: HTTP method (GET, POST, PUT, DELETE, etc.) - path: Request path (/api/v1/sessions) - query: Query string parameters (if enabled) - status: HTTP status code (200, 404, 500, etc.) - duration: Request processing time (human-readable: "125ms") - duration_ms: Request processing time in milliseconds (for metrics: 125) - client_ip: Client IP address - user_agent: Browser/client user agent string - user_id: Authenticated user ID (if authenticated) - username: Authenticated username (if authenticated) - errors: Concatenated error messages (if any errors occurred)
Log Levels: - INFO: Successful requests (2xx status codes) - WARN: Client errors (4xx status codes) - ERROR: Server errors (5xx status codes)
Thread Safety: Safe for concurrent use. Each request logs independently.
Usage:
// Basic structured logging router.Use(middleware.StructuredLogger()) // Custom configuration config := middleware.DefaultStructuredLoggerConfig() config.SkipHealthCheck = true // Don't log /health endpoint config.LogQuery = false // Don't log query parameters (privacy) router.Use(middleware.StructuredLoggerWithConfigFunc(config))
Configuration:
SkipPaths: []string{} // Paths to skip (e.g., ["/metrics", "/health"])
SkipHealthCheck: true // Skip /health and /api/v1/health endpoints
LogQuery: true // Log query parameters
LogUserAgent: true // Log user agent string
Package middleware provides HTTP middleware for the StreamSpace API. This file implements team-based role-based access control (RBAC).
Purpose: The team RBAC middleware provides fine-grained access control for multi-tenant StreamSpace deployments where users belong to teams/groups and have different permission levels within each team. This enables enterprise features like shared sessions, team resource quotas, and delegated administration.
Implementation Details: - Database-backed permissions: Roles and permissions stored in PostgreSQL - Per-team roles: Users can have different roles in different teams - Permission-based checks: Middleware validates specific permissions (not just roles) - Session-level access: Can check if user can access sessions owned by team - Hierarchical model: Teams → Users → Roles → Permissions
Permission Model:
1. Teams (groups table):
- Organizations or departments
- Example: "Engineering", "Sales", "Data Science"
2. Team Memberships (group_memberships table):
- Links users to teams with specific roles
- Example: alice@example.com is "admin" in Engineering team
3. Roles (team_role_permissions table):
- Named permission sets
- Example: "admin", "member", "viewer"
4. Permissions:
- Fine-grained capabilities
- Example: "sessions.create", "sessions.delete", "team.manage"
Common Permissions: - sessions.view: View team sessions - sessions.create: Create sessions for team - sessions.delete: Delete team sessions - sessions.share: Share sessions with team members - team.manage: Add/remove team members - team.billing: View/manage team billing
Security Notes: This middleware enforces the principle of least privilege: - Users only have access to their own sessions OR team sessions where they have permission - Team admins can manage team resources but not other teams - Platform admins have global permissions across all teams
Thread Safety: Safe for concurrent use. Database queries are isolated per request.
Usage:
// Create team RBAC middleware
teamRBAC := middleware.NewTeamRBAC(database)
// Require specific team permission
router.POST("/api/teams/:teamId/sessions",
teamRBAC.RequireTeamPermission("sessions.create"),
handlers.CreateTeamSession,
)
// Require session access (owner OR team member with permission)
router.GET("/api/sessions/:id",
teamRBAC.RequireSessionAccess("sessions.view"),
handlers.GetSession,
)
// Check permissions manually in handler
hasPermission, err := teamRBAC.CheckTeamPermission(ctx, userID, teamID, "sessions.delete")
if !hasPermission {
return c.JSON(403, gin.H{"error": "Insufficient permissions"})
}
Package middleware provides HTTP middleware for the StreamSpace API. This file implements request timeout enforcement.
Purpose: The timeout middleware protects the API server from slow clients and long-running operations that could exhaust server resources. It enforces maximum request duration limits and returns 408 Request Timeout if the limit is exceeded.
Implementation Details: - Uses Go context.WithTimeout for cancellation propagation - Runs handler in goroutine to detect timeout vs completion - Configurable timeout duration per route (via excluded paths) - Automatic exclusions for long-running operations (WebSocket, uploads) - Graceful cleanup: context cancellation signals handlers to abort
Security Notes: This middleware prevents several attack vectors:
1. Slowloris Attack:
- Attacker sends HTTP headers very slowly (1 byte per minute)
- Without timeout: Connections stay open indefinitely (resource exhaustion)
- With timeout: Connection closed after 30 seconds
2. Denial of Service (Resource Exhaustion):
- Attacker triggers expensive database queries or computations
- Without timeout: Server CPU/memory consumed until crash
- With timeout: Operation aborted after limit, resources freed
3. Accidental Long Operations:
- Bug causes infinite loop or extremely slow query
- Without timeout: Server hangs indefinitely
- With timeout: Request fails quickly, server recovers
Performance Characteristics: - Overhead: <1ms per request (goroutine spawn + channel operations) - Memory: ~4KB per request (goroutine stack + channel) - Cleanup: Automatic via defer and context cancellation
Thread Safety: Safe for concurrent use. Each request has its own timeout context.
Usage:
// Use default 30-second timeout
config := middleware.DefaultTimeoutConfig()
router.Use(middleware.Timeout(config))
// Custom timeout duration
router.Use(middleware.TimeoutWithDuration(60*time.Second))
// Exclude specific paths (long-running operations)
config := middleware.TimeoutConfig{
Timeout: 30*time.Second,
ExcludedPaths: []string{
"/api/v1/ws/", // WebSocket connections
"/api/v1/upload", // File uploads
"/api/v1/export", // Data exports (can be slow)
},
}
router.Use(middleware.Timeout(config))
Configuration:
Timeout: 30*time.Second // Maximum request duration
ErrorMessage: "Request timeout" // Error message returned to client
ExcludedPaths: []string{...} // Paths to skip timeout enforcement
Package middleware provides HTTP middleware for the StreamSpace API. This file implements webhook authentication using HMAC-SHA256 signatures.
Purpose: The webhook authentication middleware validates that incoming webhook requests are genuinely from authorized sources by verifying cryptographic signatures. This prevents unauthorized parties from triggering webhook endpoints and injecting malicious data.
Implementation Details: - HMAC-SHA256: Industry-standard message authentication algorithm - Secret-based signing: Shared secret between sender and receiver - Hex-encoded signatures: URL-safe, easy to debug - Constant-time comparison: Prevents timing attacks - Header-based delivery: Signature sent in X-Webhook-Signature header
Security Notes: Without webhook authentication, attackers could: 1. Trigger automated actions (e.g., send notifications, create resources) 2. Inject malicious payloads (e.g., XSS in notification messages) 3. Cause denial of service (e.g., flood system with fake events) 4. Impersonate legitimate webhook sources (e.g., GitHub, Stripe, Slack)
HMAC-SHA256 prevents these attacks: - Only parties with the secret can create valid signatures - Signatures are deterministic (same payload + secret = same signature) - Cannot forge signatures without knowing the secret - Constant-time comparison prevents timing attacks
How It Works: 1. Sender (e.g., GitHub) computes HMAC-SHA256 of request body using secret 2. Sender includes signature in X-Webhook-Signature header 3. Receiver (StreamSpace) reads request body 4. Receiver computes expected signature using same secret 5. Receiver compares signatures using constant-time comparison 6. If match: Request is authentic, proceed 7. If mismatch: Request is invalid or tampered, reject with 401
Signature Format:
X-Webhook-Signature: <hex-encoded-hmac-sha256> Example: "a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890"
Thread Safety: Safe for concurrent use. HMAC computation is stateless.
Usage:
// Create webhook auth with secret
webhookAuth := middleware.NewWebhookAuth("your-secret-key-here")
// Apply to webhook endpoints
router.POST("/api/webhooks/github",
webhookAuth.Middleware(),
handlers.HandleGitHubWebhook,
)
// Generate signature for testing (sender side)
payload := []byte(`{"event": "push", "repo": "streamspace"}`)
signature := webhookAuth.Sign(payload)
// Send as: curl -H "X-Webhook-Signature: $signature" -d "$payload" /api/webhooks
Configuration:
secret: Shared secret between sender and receiver (keep confidential!) Recommended: Generate with: openssl rand -hex 32
Index ¶
- Constants
- Variables
- func AllowedHTTPMethods() gin.HandlerFunc
- func CSRFProtection() gin.HandlerFunc
- func CheckFeatureEnabled(database *db.Database, feature string) gin.HandlerFunc
- func ClearLicenseCache()
- func DefaultSizeLimiter() gin.HandlerFunc
- func DisallowedHTTPMethods() gin.HandlerFunc
- func EnforceSessionCreation(c *gin.Context, requestedCPU, requestedMemory string, requestedGPU int, ...) error
- func FileUploadLimiter() gin.HandlerFunc
- func GetCSRFToken(c *gin.Context) string
- func GetK8sNamespace(c *gin.Context) (string, error)
- func GetOrgID(c *gin.Context) (string, error)
- func GetOrgRole(c *gin.Context) (string, error)
- func GetRequestID(c *gin.Context) string
- func GetRole(c *gin.Context) (string, error)
- func GetUserID(c *gin.Context) (string, error)
- func GetUserQuota(enforcer *quota.Enforcer) gin.HandlerFunc
- func Gzip(level int) gin.HandlerFunc
- func GzipWithExclusions(level int, excludePaths []string) gin.HandlerFunc
- func JSONSizeLimiter() gin.HandlerFunc
- func LicenseEnforcement(database *db.Database) gin.HandlerFunc
- func MustGetOrgID(c *gin.Context) string
- func OrgContextMiddleware(jwtManager *auth.JWTManager) gin.HandlerFunc
- func RequestID() gin.HandlerFunc
- func RequestSizeLimiter(maxSize int64) gin.HandlerFunc
- func RequireOrgRole(allowedRoles ...string) gin.HandlerFunc
- func SecurityHeaders() gin.HandlerFunc
- func SecurityHeadersRelaxed() gin.HandlerFunc
- func StructuredLogger() gin.HandlerFunc
- func StructuredLoggerWithConfigFunc(config StructuredLoggerConfig) gin.HandlerFunc
- func Timeout(config TimeoutConfig) gin.HandlerFunc
- func TimeoutWithDuration(timeout time.Duration) gin.HandlerFunc
- func ValidateContainerImage(image string) error
- func ValidateEmail(email string) error
- func ValidateNamespace(namespace string) error
- func ValidateResourceName(name string) error
- func ValidateResourceQuantity(quantity, resourceType string) error
- func ValidateUsername(username string) error
- type AgentAuth
- type AuditEvent
- type AuditLogger
- type CSRFStore
- type InputValidator
- type LicenseInfo
- type MaxSessionsError
- type OrgContextError
- type QuotaMiddleware
- type RateLimiter
- type SessionManager
- func (sm *SessionManager) ConcurrentSessionMiddleware() gin.HandlerFunc
- func (sm *SessionManager) GetActiveSessions(username string) int
- func (sm *SessionManager) IdleTimeoutMiddleware() gin.HandlerFunc
- func (sm *SessionManager) RegisterSession(username, sessionID string) error
- func (sm *SessionManager) SessionActivityMiddleware() gin.HandlerFunc
- func (sm *SessionManager) UnregisterSession(username, sessionID string)
- type StructuredLoggerConfig
- type TeamRBAC
- func (t *TeamRBAC) CanAccessSession(ctx context.Context, userID, sessionID string, permission string) (bool, error)
- func (t *TeamRBAC) CheckTeamPermission(ctx context.Context, userID, teamID, permission string) (bool, error)
- func (t *TeamRBAC) GetUserTeamPermissions(ctx context.Context, userID, teamID string) ([]string, error)
- func (t *TeamRBAC) GetUserTeamRole(ctx context.Context, userID, teamID string) (string, error)
- func (t *TeamRBAC) ListUserTeams(ctx context.Context, userID string) ([]db.TeamMembership, error)
- func (t *TeamRBAC) RequireSessionAccess(permission string) gin.HandlerFunc
- func (t *TeamRBAC) RequireTeamPermission(permission string) gin.HandlerFunc
- type TimeoutConfig
- type WebhookAuth
Constants ¶
const ( DefaultCompression = gzip.DefaultCompression NoCompression = gzip.NoCompression BestSpeed = gzip.BestSpeed BestCompression = gzip.BestCompression )
Gzip compression levels
const ( // DefaultMaxAttempts is the default maximum number of attempts allowed DefaultMaxAttempts = 5 // DefaultRateLimitWindow is the default time window for rate limiting DefaultRateLimitWindow = 1 * time.Minute // CleanupInterval is how often the rate limiter cleans up old entries CleanupInterval = 5 * time.Minute // CleanupThreshold is the age threshold for removing old entries CleanupThreshold = 10 * time.Minute )
Rate Limiting Constants control the in-memory rate limiter.
The rate limiter prevents brute force attacks by limiting requests per time window. Cleanup runs periodically to prevent memory leaks from abandoned rate limit entries.
const ( // CSRFTokenLength is the length of CSRF tokens in bytes CSRFTokenLength = 32 // CSRFTokenHeader is the HTTP header for CSRF tokens CSRFTokenHeader = "X-CSRF-Token" // CSRFCookieName is the name of the CSRF cookie CSRFCookieName = "csrf_token" // CSRFTokenExpiry is how long CSRF tokens are valid CSRFTokenExpiry = 24 * time.Hour )
CSRF Constants define CSRF protection configuration.
const ( // ContextKeyOrgID is the key for organization ID in request context ContextKeyOrgID = "org_id" // ContextKeyOrgName is the key for organization name in request context ContextKeyOrgName = "org_name" // ContextKeyK8sNamespace is the key for Kubernetes namespace in request context ContextKeyK8sNamespace = "k8s_namespace" // ContextKeyOrgRole is the key for user's org role in request context ContextKeyOrgRole = "org_role" // ContextKeyUserID is the key for user ID in request context ContextKeyUserID = "user_id" // ContextKeyUsername is the key for username in request context ContextKeyUsername = "username" // ContextKeyRole is the key for system role in request context ContextKeyRole = "role" // ContextKeySessionID is the key for JWT session ID in request context ContextKeySessionID = "session_id" )
Context keys for org-scoped data
const ( // RequestIDHeader is the header name for request ID RequestIDHeader = "X-Request-ID" // RequestIDKey is the context key for request ID RequestIDKey = "request_id" )
const ( // MaxRequestBodySize is the maximum allowed request body size (10MB) MaxRequestBodySize int64 = 10 * 1024 * 1024 // 10 MB // MaxJSONPayloadSize is the maximum size for JSON payloads (5MB) MaxJSONPayloadSize int64 = 5 * 1024 * 1024 // 5 MB // MaxFileUploadSize is the maximum size for file uploads (50MB) MaxFileUploadSize int64 = 50 * 1024 * 1024 // 50 MB )
Request Size Limits define maximum allowed payload sizes.
These values balance security (prevent DoS) with usability (allow reasonable uploads).
Variables ¶
var ErrMissingOrgContext = &OrgContextError{message: "organization context not found in request"}
ErrMissingOrgContext indicates org_id is not in request context
var ErrMissingUserContext = &OrgContextError{message: "user context not found in request"}
ErrMissingUserContext indicates user_id is not in request context
Functions ¶
func AllowedHTTPMethods ¶
func AllowedHTTPMethods() gin.HandlerFunc
AllowedHTTPMethods restricts incoming requests to only allowed HTTP methods This prevents abuse through uncommon HTTP methods (TRACE, CONNECT, etc.)
func CSRFProtection ¶
func CSRFProtection() gin.HandlerFunc
CSRFProtection returns a Gin middleware that protects against Cross-Site Request Forgery (CSRF) attacks using the double-submit cookie pattern.
CSRF ATTACK OVERVIEW:
CSRF attacks exploit the browser's automatic cookie sending behavior to perform unauthorized actions on behalf of an authenticated user.
Attack scenario without protection:
- User logs into StreamSpace → gets session cookie
- User visits malicious site evil.com
- evil.com triggers: POST https://streamspace.io/api/delete-account
- Browser automatically sends session cookie with request
- StreamSpace sees valid session → executes action
- User's account is deleted without their knowledge
DOUBLE-SUBMIT COOKIE PATTERN:
This implementation uses the double-submit cookie pattern, which requires: 1. Server generates random token 2. Server sends token in BOTH cookie AND custom header 3. Client JavaScript reads token from header 4. Client sends token in BOTH cookie AND custom header on state-changing requests 5. Server validates: cookie token == header token
Why this works: - Malicious sites can trigger requests and browsers send cookies automatically - BUT: Same-Origin Policy prevents malicious sites from:
- Reading the token from response headers
- Setting custom headers on cross-origin requests
- Therefore, attacker cannot provide matching tokens in both places
PROTECTION FLOW:
Safe Request (GET, HEAD, OPTIONS):
- Client: GET /api/sessions
- Server: Generates CSRF token (e.g., "abc123...")
- Server: Sets X-CSRF-Token header to "abc123..."
- Server: Sets csrf_token cookie to "abc123..."
- Client: Stores token from header in memory/localStorage
- Response returned
State-Changing Request (POST, PUT, DELETE, PATCH):
- Client: POST /api/delete-account
- Client: Sets X-CSRF-Token header to "abc123..." (from previous GET)
- Client: Browser automatically sends csrf_token cookie "abc123..."
- Server: Reads token from header → "abc123..."
- Server: Reads token from cookie → "abc123..."
- Server: Compares using constant-time comparison
- Server: Validates token exists and not expired
- If all checks pass: Request processed
- If any check fails: 403 Forbidden
CSRF Attack Scenario (With Protection):
- Attacker: POST https://streamspace.io/api/delete-account
- Browser: Sends csrf_token cookie automatically → "abc123..."
- Attacker: Cannot set X-CSRF-Token header (Same-Origin Policy blocks)
- Server: headerToken = "" (missing)
- Server: cookieToken = "abc123..." (from cookie)
- Server: Comparison fails (empty ≠ "abc123...")
- Server: 403 Forbidden - Attack blocked!
SECURITY FEATURES:
1. Constant-Time Comparison:
- Uses subtle.ConstantTimeCompare instead of ==
- Prevents timing attacks
- Timing attack: measure comparison time to guess token byte-by-byte
- Constant-time: comparison always takes same time regardless of input
2. Token Expiration:
- Tokens expire after 24 hours
- Limits window for token theft
- Forces periodic token refresh
3. Cryptographically Secure Random:
- Uses crypto/rand (not math/rand)
- 256 bits of entropy
- Unpredictable and unique
4. HttpOnly Cookie:
- Cookie not accessible to JavaScript
- Prevents XSS attacks from stealing token
5. Secure Cookie (Production):
- Cookie only sent over HTTPS
- Prevents token theft via network sniffing
USAGE:
router := gin.Default()
router.Use(middleware.CSRFProtection())
// All routes now protected:
router.GET("/api/sessions", handler) // Generates token
router.POST("/api/sessions", handler) // Validates token
CLIENT IMPLEMENTATION:
JavaScript client must send token in header:
// Store token from first GET request
let csrfToken = null;
// GET request: capture token
const response = await fetch('/api/sessions');
csrfToken = response.headers.get('X-CSRF-Token');
// POST request: send token in header
await fetch('/api/delete-account', {
method: 'POST',
headers: {
'X-CSRF-Token': csrfToken, // REQUIRED
'Content-Type': 'application/json',
},
credentials: 'include', // Send cookies
});
HTML FORM IMPLEMENTATION:
<!-- Store token in hidden field -->
<form method="POST" action="/api/delete-account">
<input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
<button type="submit">Delete Account</button>
</form>
EXEMPT METHODS:
GET, HEAD, OPTIONS are exempt because they are "safe methods": - SHOULD NOT modify server state (read-only) - Idempotent (can be repeated safely) - No CSRF risk if properly implemented
IMPORTANT: If you use GET for state changes (antipattern), CSRF protection will NOT work. Always use POST/PUT/DELETE for state changes.
LIMITATIONS:
1. Subdomain Attacks:
- If attacker controls subdomain (evil.example.com)
- They can set cookies for *.example.com
- Mitigation: Validate Origin/Referer headers (not implemented)
2. XSS Attacks:
- If site has XSS vulnerability
- Attacker can read token from headers
- Mitigation: Prevent XSS (input validation, CSP)
3. Token Storage:
- Tokens stored in memory (lost on restart)
- Mitigation: Use Redis for persistent storage
COMMON ERRORS:
"CSRF token missing":
- Client didn't send csrf_token cookie
- Solution: Ensure credentials: 'include' in fetch
"CSRF token mismatch":
- Header token doesn't match cookie token
- Solution: Ensure X-CSRF-Token header is set correctly
"CSRF token invalid":
- Token expired (>24 hours old)
- Server restarted (tokens lost)
- Solution: Refresh token by making GET request
func CheckFeatureEnabled ¶
func CheckFeatureEnabled(database *db.Database, feature string) gin.HandlerFunc
CheckFeatureEnabled checks if a specific feature is enabled in the license
func ClearLicenseCache ¶
func ClearLicenseCache()
ClearLicenseCache clears the license cache (call after license activation)
func DefaultSizeLimiter ¶
func DefaultSizeLimiter() gin.HandlerFunc
DefaultSizeLimiter uses the default max request body size
func DisallowedHTTPMethods ¶
func DisallowedHTTPMethods() gin.HandlerFunc
DisallowedHTTPMethods explicitly blocks specific dangerous HTTP methods Use this in addition to AllowedHTTPMethods for defense in depth
func EnforceSessionCreation ¶
func EnforceSessionCreation(c *gin.Context, requestedCPU, requestedMemory string, requestedGPU int, currentUsage *quota.Usage) error
EnforceSessionCreation enforces quotas for session creation requests.
This helper function should be called from session creation handlers to validate that the user has sufficient quota to launch the requested session.
When to Call This ¶
Call this BEFORE creating any Kubernetes resources:
// ❌ WRONG: Creates session first, then checks quota
session := createSession(...)
if err := middleware.EnforceSessionCreation(...); err != nil {
deleteSession(session) // Wasteful
}
// ✅ CORRECT: Checks quota first, then creates session
if err := middleware.EnforceSessionCreation(...); err != nil {
return c.JSON(402, gin.H{"error": err.Error()})
}
session := createSession(...)
Parameters ¶
**requestedCPU** (string):
- CPU request in Kubernetes format (e.g., "2000m", "2", "0.5")
- Validates format and converts to millicores
- Common values: "1000m" (1 core), "2000m" (2 cores), "500m" (0.5 cores)
**requestedMemory** (string):
- Memory request in Kubernetes format (e.g., "2Gi", "512Mi", "1G")
- Validates format and converts to bytes
- Common values: "2Gi" (2 GB), "4Gi" (4 GB), "512Mi" (512 MB)
**requestedGPU** (int):
- Number of GPU devices requested (0 for none)
- Each GPU counts as 1 unit
- Example: 0 (no GPU), 1 (one GPU), 2 (two GPUs)
**currentUsage** (*quota.Usage):
- User's current resource usage across all sessions
- If nil, enforcer will query database (slower)
- If provided, uses cached value (faster, may be slightly stale)
Return Value ¶
Returns error if quota check fails:
- nil: Quota check passed, proceed with session creation
- error: Quota exceeded or validation failed, return HTTP 402
Error message format:
"CPU quota exceeded: requested 4000m, limit 8000m, current 5000m" "Invalid CPU format: must be like '1000m' or '2'" "Session limit reached: 10/10 sessions active"
Quota Check Algorithm ¶
The enforcer performs these checks in order:
- **Format validation**: Ensure CPU/memory strings are valid
- **Per-session limits**: Check if request exceeds per-session max
- **Session count**: Check if user has too many active sessions
- **Aggregate CPU**: Check if total CPU (current + requested) exceeds limit
- **Aggregate Memory**: Check if total memory (current + requested) exceeds limit
- **GPU count**: Check if GPU request exceeds limit
If any check fails, returns detailed error with quota information.
Graceful Degradation ¶
If quota enforcement is not configured, this function allows the request:
- No enforcer in context → Allow (quota enforcement disabled)
- No username in context → Allow (unauthenticated, auth layer will handle)
This prevents quota failures from breaking the platform if quota feature is not configured or temporarily unavailable.
Performance Considerations ¶
- Database query: 1 query to get user limits (~5ms) - If currentUsage provided: No additional queries - If currentUsage nil: 1 query to calculate usage (~10ms) - Total latency: 5-15ms (acceptable for session creation)
Example Usage ¶
**In session creation handler**:
func CreateSession(c *gin.Context) {
var req CreateSessionRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// Check quota BEFORE creating resources
err := middleware.EnforceSessionCreation(
c,
req.CPU, // "2000m"
req.Memory, // "4Gi"
req.GPU, // 0
nil, // Let enforcer query current usage
)
if err != nil {
c.JSON(402, gin.H{
"error": "quota_exceeded",
"message": err.Error(),
})
return
}
// Quota check passed, proceed with session creation
session := createKubernetesSession(req)
c.JSON(200, session)
}
See also:
- api/internal/quota/enforcer.go: Core quota enforcement logic
- api/internal/handlers/sessions.go: Example usage in session creation
func FileUploadLimiter ¶
func FileUploadLimiter() gin.HandlerFunc
FileUploadLimiter limits file upload size
func GetCSRFToken ¶
GetCSRFToken returns the current CSRF token for the request Useful for rendering in HTML forms or passing to frontend
func GetK8sNamespace ¶
GetK8sNamespace extracts the Kubernetes namespace from the request context. Returns the org's K8s namespace for scoping WebSocket and K8s operations.
func GetOrgID ¶
GetOrgID extracts the organization ID from the request context. Returns error if org_id is not present (middleware not applied or token invalid).
SECURITY: Always use this function to get org_id for database queries. Never trust client-provided org_id values.
func GetOrgRole ¶
GetOrgRole extracts the user's org role from the request context.
func GetRequestID ¶
GetRequestID retrieves the request ID from the Gin context
func GetUserQuota ¶
func GetUserQuota(enforcer *quota.Enforcer) gin.HandlerFunc
GetUserQuota returns a Gin handler that retrieves user quota information.
This handler is typically mounted at GET /api/quotas/me to allow users to view their resource limits and current usage.
Response Format ¶
Returns HTTP 200 with quota information:
{
"limits": {
"max_sessions": 10,
"max_cpu": "16000m",
"max_memory": "64Gi",
"max_gpu": 2,
"max_storage": "100Gi",
"max_cpu_per_session": "8000m",
"max_memory_per_session": "32Gi",
"current": {
"sessions": 3,
"cpu": "6000m",
"memory": "12Gi",
"gpu": 1,
"storage": "45Gi"
},
"available": {
"sessions": 7,
"cpu": "10000m",
"memory": "52Gi",
"gpu": 1,
"storage": "55Gi"
}
}
}
Error Responses ¶
- HTTP 401 Unauthorized: No username in context (not authenticated) - HTTP 500 Internal Server Error: Database error fetching limits
Authentication ¶
This handler requires authentication (expects "username" in context). If username is not present, returns 401 Unauthorized.
Performance ¶
- Database queries: 2 queries (user limits + current usage) - Latency: 10-20ms (typical) - Caching: Enforcer may cache limits for 5 seconds
Example Usage ¶
**Register handler**:
router.GET("/api/quotas/me", middleware.GetUserQuota(enforcer))
**Frontend usage**:
fetch('/api/quotas/me')
.then(res => res.json())
.then(data => {
console.log(`Sessions: ${data.limits.current.sessions}/${data.limits.max_sessions}`)
console.log(`CPU: ${data.limits.current.cpu}/${data.limits.max_cpu}`)
})
See also:
- api/internal/quota/enforcer.go: GetUserLimits() implementation
func Gzip ¶
func Gzip(level int) gin.HandlerFunc
Gzip returns a middleware that compresses HTTP responses using gzip
func GzipWithExclusions ¶
func GzipWithExclusions(level int, excludePaths []string) gin.HandlerFunc
GzipWithExclusions returns a middleware with path exclusions
func JSONSizeLimiter ¶
func JSONSizeLimiter() gin.HandlerFunc
JSONSizeLimiter limits JSON payload size for API endpoints
func LicenseEnforcement ¶
func LicenseEnforcement(database *db.Database) gin.HandlerFunc
LicenseEnforcement middleware checks license limits before resource creation
func MustGetOrgID ¶
MustGetOrgID extracts org_id from context, panics if not present. Use only in handlers where OrgContextMiddleware is guaranteed to run.
func OrgContextMiddleware ¶
func OrgContextMiddleware(jwtManager *auth.JWTManager) gin.HandlerFunc
OrgContextMiddleware extracts organization context from JWT claims and populates it in the request context for use by handlers.
SECURITY: This middleware is CRITICAL for multi-tenancy isolation. All protected routes MUST use this middleware.
The middleware:
- Extracts JWT from Authorization header (Bearer token)
- Validates the JWT signature and expiration
- Extracts org_id and other claims
- Populates claims in request context
- Rejects requests without valid org_id
Request Flow:
Client -> [Bearer Token] -> OrgContextMiddleware -> [org_id in context] -> Handler
Error Responses:
- 401 Unauthorized: Missing, invalid, or expired token
- 401 Unauthorized: Token missing org_id claim
func RequestID ¶
func RequestID() gin.HandlerFunc
RequestID middleware generates or extracts a correlation ID for each request This enables request tracing across distributed systems and log correlation
func RequestSizeLimiter ¶
func RequestSizeLimiter(maxSize int64) gin.HandlerFunc
RequestSizeLimiter limits the size of incoming HTTP requests to prevent DoS attacks via oversized payloads
func RequireOrgRole ¶
func RequireOrgRole(allowedRoles ...string) gin.HandlerFunc
RequireOrgRole checks if the user has one of the required org roles. Returns gin.HandlerFunc that can be used as route-level middleware.
Usage:
router.GET("/admin", RequireOrgRole("org_admin"), adminHandler)
router.GET("/manage", RequireOrgRole("org_admin", "maintainer"), manageHandler)
func SecurityHeaders ¶
func SecurityHeaders() gin.HandlerFunc
SecurityHeaders adds comprehensive security headers to all HTTP responses.
This middleware provides industry-standard security headers with modern nonce-based CSP for XSS protection. It should be applied to ALL routes.
**IMPORTANT**: Use SecurityHeaders() in production, SecurityHeadersRelaxed() only in development environments.
Headers Added ¶
See package-level documentation for detailed description of each header. Summary:
- Strict-Transport-Security: Force HTTPS
- X-Content-Type-Options: Prevent MIME sniffing
- X-Frame-Options: Prevent clickjacking
- X-XSS-Protection: Legacy XSS filter
- Content-Security-Policy: Nonce-based XSS protection
- Referrer-Policy: Limit referrer information
- Permissions-Policy: Disable dangerous features
- X-Permitted-Cross-Domain-Policies: Block Flash/PDF
- X-Download-Options: Prevent IE download execution
- Cache-Control: Prevent caching of sensitive data
- Server: Hide server version
CSP Nonce Integration ¶
Templates must use the nonce from context:
<!-- Go templates -->
<script nonce="{{.csp_nonce}}">
console.log("Allowed inline script");
</script>
<!-- React (passed as prop) -->
<script nonce={window.CSP_NONCE}>
console.log("Allowed inline script");
</script>
Graceful Degradation ¶
If nonce generation fails:
- Falls back to strict CSP without nonces
- Blocks ALL inline scripts/styles
- Still provides strong security (no XSS)
- Application may need external JS/CSS files
Performance Impact ¶
- Nonce generation: ~0.1ms (crypto/rand call) - Header setting: ~0.01ms (string operations) - Total overhead: <0.2ms per request - No database queries, no network calls
Usage Example ¶
router := gin.New()
router.Use(middleware.SecurityHeaders()) // Apply to all routes
router.GET("/", handlers.Index)
Testing CSP ¶
**View CSP in browser**:
- Open DevTools (F12)
- Go to Network tab
- Click any request
- Check Response Headers
- Look for Content-Security-Policy
**Test CSP violations**:
- Try injecting: <script>alert('xss')</script>
- Should be blocked (CSP violation in console)
- Try with nonce: <script nonce="correct-nonce">alert('ok')</script>
- Should execute (nonce matches)
Returns:
- gin.HandlerFunc: Middleware function to add to router
See also:
- SecurityHeadersRelaxed(): Development variant with relaxed CSP
- generateNonce(): Nonce generation logic
func SecurityHeadersRelaxed ¶
func SecurityHeadersRelaxed() gin.HandlerFunc
SecurityHeadersRelaxed provides relaxed security headers for development.
**WARNING**: This function provides WEAK security headers suitable ONLY for development environments. NEVER use in production.
Differences from SecurityHeaders() ¶
**Relaxed**:
- CSP allows 'unsafe-inline' and 'unsafe-eval' (NO nonce requirement)
- X-Frame-Options: SAMEORIGIN (allows framing for dev tools)
- No HSTS preload (easier to switch between HTTP/HTTPS)
- Allows WebSocket connections from any origin
**Why Relaxed for Development?**:
- Hot reload scripts need eval()
- Dev tools may inject inline scripts
- Browser extensions need relaxed CSP
- Local testing without HTTPS setup
Security Rating ¶
- SecurityHeaders(): A+ (production-ready) - SecurityHeadersRelaxed(): C (development only)
Usage ¶
if os.Getenv("ENV") == "development" {
router.Use(middleware.SecurityHeadersRelaxed())
} else {
router.Use(middleware.SecurityHeaders())
}
Returns:
- gin.HandlerFunc: Middleware function with relaxed security headers
See also:
- SecurityHeaders(): Production variant with strict CSP
func StructuredLogger ¶
func StructuredLogger() gin.HandlerFunc
StructuredLogger provides structured logging for all requests Logs include request ID, method, path, status, duration, and client IP
func StructuredLoggerWithConfigFunc ¶
func StructuredLoggerWithConfigFunc(config StructuredLoggerConfig) gin.HandlerFunc
StructuredLoggerWithConfigFunc creates a structured logger with custom config
func Timeout ¶
func Timeout(config TimeoutConfig) gin.HandlerFunc
Timeout middleware enforces a timeout on requests to prevent slow loris attacks and ensure resources are freed in a timely manner
func TimeoutWithDuration ¶
func TimeoutWithDuration(timeout time.Duration) gin.HandlerFunc
TimeoutWithDuration creates a timeout middleware with specified duration
func ValidateContainerImage ¶
ValidateContainerImage validates container image format
func ValidateNamespace ¶
ValidateNamespace validates Kubernetes namespace format
func ValidateResourceName ¶
ValidateResourceName validates Kubernetes resource names
func ValidateResourceQuantity ¶
ValidateResourceQuantity validates Kubernetes resource quantities (CPU, memory)
func ValidateUsername ¶
ValidateUsername validates username format
Types ¶
type AgentAuth ¶
type AgentAuth struct {
// contains filtered or unexported fields
}
AgentAuth provides API key authentication middleware for agents.
func NewAgentAuth ¶
NewAgentAuth creates a new agent authentication middleware.
Example:
agentAuth := middleware.NewAgentAuth(database) router.Use(agentAuth.RequireAPIKey())
func (*AgentAuth) OptionalAPIKey ¶
func (a *AgentAuth) OptionalAPIKey() gin.HandlerFunc
OptionalAPIKey returns a middleware that accepts but does not require an API key.
This is useful for endpoints that should work with or without authentication. If a valid API key is provided, the agent_id is set in the context. If no API key or invalid key, the request continues without authentication.
Example:
agentAuth := middleware.NewAgentAuth(database)
router.POST("/agents/heartbeat", agentAuth.OptionalAPIKey(), handler)
func (*AgentAuth) RequireAPIKey ¶
func (a *AgentAuth) RequireAPIKey() gin.HandlerFunc
RequireAPIKey returns a middleware that requires a valid agent API key.
The middleware:
- Extracts API key from X-Agent-API-Key header
- Extracts agent_id from query parameter or path parameter
- Validates key against database
- Updates last used timestamp
- Sets authenticated agent_id in context
Returns 401 if API key is missing or invalid. Returns 403 if API key doesn't match agent.
Example:
agentAuth := middleware.NewAgentAuth(database)
router.POST("/agents/register", agentAuth.RequireAPIKey(), handler)
func (*AgentAuth) RequireAuth ¶
func (a *AgentAuth) RequireAuth() gin.HandlerFunc
RequireAuth returns a middleware that requires agent authentication via mTLS OR API key.
This is a hybrid middleware that supports both authentication methods:
- If client certificate is present (mTLS): validates cert and extracts agent_id from CN
- If no client certificate: falls back to API key authentication
Authentication flow:
- Check for client certificate in TLS connection
- If cert present: validate against CA, extract agent_id from CN
- If no cert: require X-Agent-API-Key header and validate
- Set authenticated_agent_id in context
Example:
agentAuth := middleware.NewAgentAuth(database)
router.POST("/agents/register", agentAuth.RequireAuth(), handler)
type AuditEvent ¶
type AuditEvent struct {
Timestamp time.Time `json:"timestamp"`
UserID string `json:"user_id,omitempty"`
Username string `json:"username,omitempty"`
Action string `json:"action"`
Resource string `json:"resource"`
ResourceID string `json:"resource_id,omitempty"`
Method string `json:"method"`
Path string `json:"path"`
StatusCode int `json:"status_code"`
IPAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
Duration int64 `json:"duration_ms"`
RequestBody map[string]interface{} `json:"request_body,omitempty"`
ResponseBody map[string]interface{} `json:"response_body,omitempty"`
Error string `json:"error,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
AuditEvent represents a structured audit log event.
This struct captures all relevant information about an API request for compliance, security, and analytics purposes. Events are serialized to JSON and stored in the PostgreSQL audit_log table.
Field Descriptions ¶
**Timestamp**: When the request started (not when logged)
- Always in UTC timezone
- Microsecond precision
**UserID**: Internal user identifier (UUID or database ID)
- Empty for unauthenticated requests
- Set by auth middleware
**Username**: Human-readable username (e.g., "alice@example.com")
- Empty for unauthenticated requests
- Useful for investigations (more readable than UUID)
**Action**: HTTP method (GET, POST, PUT, DELETE, PATCH)
- Indicates intent (read vs. write)
- Used for permission auditing
**Resource**: API path (e.g., "/api/sessions")
- Identifies what was accessed
- Used for access pattern analysis
**ResourceID**: Specific resource identifier (e.g., "sess-123")
- Empty for list operations
- Extracted from URL path or request body
**Method**: HTTP method (duplicate of Action, for clarity)
**Path**: Full request path including query string
- Example: "/api/sessions?status=running&limit=10"
**StatusCode**: HTTP response status code
- 2xx: Success
- 4xx: Client error (often interesting for security)
- 5xx: Server error (often interesting for debugging)
**IPAddress**: Client IP address
- Supports IPv4 and IPv6
- May be proxied (check X-Forwarded-For header)
**UserAgent**: Browser/client identification string
- Useful for bot detection
- Useful for client debugging
**Duration**: Request processing time in milliseconds
- Time from request start to response completion
- Useful for performance analysis
**RequestBody**: Parsed JSON request body (optional)
- Only logged if enabled (disabled by default for privacy)
- Max 10KB to prevent large payloads
- Sensitive fields automatically redacted
**ResponseBody**: Parsed JSON response body (optional)
- Disabled by default (too verbose)
- Useful for debugging specific issues
**Error**: Error message if request failed
- Gin error messages concatenated
- Empty if request succeeded
**Metadata**: Additional structured data (extensible)
- Custom fields for specific handlers
- Example: {"session_duration": 3600, "template": "firefox"}
type AuditLogger ¶
type AuditLogger struct {
// contains filtered or unexported fields
}
AuditLogger handles structured audit logging.
This type manages the configuration and execution of audit logging, including what data to log, how to redact sensitive fields, and where to store the logs (database).
Configuration Options ¶
**database**: PostgreSQL connection for log storage
- If nil, audit logging is disabled (graceful degradation)
- Must have audit_log table created
**logRequestBody**: Whether to log request bodies
- true: Log bodies (max 10KB, redacted)
- false: Don't log bodies (privacy, less storage)
- Recommended: false in production, true for debugging
**logResponseBody**: Whether to log response bodies
- true: Log responses (very verbose, lots of storage)
- false: Don't log responses (recommended)
- Usually kept false due to volume
**sensitiveFields**: List of field names to redact
- Default: ["password", "token", "secret", "apiKey", "api_key"]
- Can be extended for custom sensitive fields
- Applies recursively to nested objects
Thread safety: Safe for concurrent use by multiple goroutines
func NewAuditLogger ¶
func NewAuditLogger(database *db.Database, logBodies bool) *AuditLogger
NewAuditLogger creates a new audit logger instance.
This constructor initializes the audit logger with sensible defaults for production use: request bodies optional, response bodies disabled, standard sensitive fields predefined.
Parameters:
**database** (*db.Database):
- PostgreSQL database connection (required for logging)
- If nil, audit logging will be disabled (logs to /dev/null)
- Must have audit_log table created (see schema in package docs)
**logBodies** (bool):
- true: Log request bodies (useful for debugging, uses more storage)
- false: Don't log request bodies (recommended for production)
- Response bodies are always disabled (too verbose)
Default Sensitive Fields ¶
These field names are automatically redacted in logged data:
- password: User passwords
- token: Authentication tokens
- secret: API secrets, encryption keys
- apiKey: API keys
- api_key: API keys (snake_case variant)
Usage Examples ¶
**Production configuration** (minimal logging):
logger := middleware.NewAuditLogger(database, false) router.Use(logger.Middleware())
**Development configuration** (detailed logging):
logger := middleware.NewAuditLogger(database, true) router.Use(logger.Middleware())
**Disabled configuration** (no audit logs):
logger := middleware.NewAuditLogger(nil, false) router.Use(logger.Middleware()) // No-op, no database writes
See also:
- Middleware(): Gin middleware handler
- api/internal/db/schema.sql: audit_log table definition
func (*AuditLogger) Middleware ¶
func (a *AuditLogger) Middleware() gin.HandlerFunc
Middleware returns the Gin middleware handler for audit logging.
This is the main integration point that captures all HTTP requests and logs them to the database for compliance, security, and analytics purposes.
Request Processing Flow ¶
**Before Request** (SETUP PHASE): a. Record start time (for duration calculation) b. Capture request body (if enabled, max 10KB, with redaction) c. Wrap response writer (to capture status code)
**During Request** (PASSTHROUGH): - Call c.Next() to execute handlers - Request processing happens normally - No blocking, no interference
**After Request** (LOGGING PHASE): a. Calculate request duration b. Extract user info from context (set by auth middleware) c. Build AuditEvent struct d. Launch goroutine to log event asynchronously e. Return immediately (don't wait for DB write)
Why Asynchronous Logging? ¶
**Option 1: Synchronous logging** (wait for DB write):
- Problem: Adds 1-5ms latency to EVERY request
- Problem: If database is slow/down, all requests block
- Problem: Failed audit writes break user requests
**Option 2: Asynchronous logging** (chosen):
- Benefit: Zero added latency (goroutine handles DB write)
- Benefit: Database issues don't affect user experience
- Benefit: Can batch multiple events (future optimization)
- Tradeoff: Audit log might be incomplete if server crashes
Request Body Capture ¶
Request bodies are only captured if enabled (logRequestBody = true):
- Read entire body into memory
- Restore body to c.Request.Body (so handlers can read it)
- Limit to 10KB (prevents memory exhaustion from large uploads)
- Parse as JSON
- Redact sensitive fields
- Store in event
Why 10KB limit?
- Most API requests are <1KB
- File uploads would consume too much memory
- Example: 1000 concurrent requests × 1MB each = 1GB RAM
Response Body Capture ¶
Response bodies are wrapped but NOT logged by default:
- responseWriter captures all writes
- body field stores response (not used currently)
- Future enhancement: Could log responses if needed
User Identification ¶
User info comes from Gin context (set by auth middleware):
- c.Get("userID"): Internal user ID (UUID or DB ID)
- c.Get("username"): Human-readable username
If not authenticated:
- Both fields will be empty strings
- Request is still logged (for security analysis)
Error Tracking ¶
Gin errors are automatically captured:
- c.Errors contains errors added by handlers
- Concatenated into single string for audit log
- Useful for tracking failed operations
Performance Impact ¶
**Request latency**: 0ms added (async logging)
**Memory overhead per request**:
- No body logging: ~1 KB (AuditEvent struct)
- With body logging: ~2-10 KB (body + event)
- Goroutine stack: ~2 KB
- Total: 3-12 KB per request
**CPU overhead**:
- Body capture: ~0.1ms (if enabled)
- Redaction: ~0.5ms (if body logged)
- Event creation: ~0.1ms
- Total: <1ms (runs during request, not added latency)
Example Middleware Stack ¶
Correct ordering is critical:
router := gin.New()
// 1. Request ID (for correlation)
router.Use(middleware.RequestID())
// 2. Authentication (sets userID and username)
router.Use(middleware.JWTAuth())
// 3. Audit logging (reads userID/username, logs to DB)
auditLogger := middleware.NewAuditLogger(database, false)
router.Use(auditLogger.Middleware())
// 4. Business logic handlers
router.POST("/api/sessions", handlers.CreateSession)
Security Considerations ¶
**Sensitive data protection**:
- Automatic redaction of passwords, tokens, secrets
- Custom sensitive fields configurable
- Recursive redaction for nested objects
**Audit log integrity**:
- Database constraints prevent modification
- Timestamp immutable (set once)
- Consider write-once storage for compliance
**Privacy concerns**:
- IP addresses logged (GDPR consideration)
- Request bodies may contain PII
- Response bodies disabled by default
- Retention policy must comply with regulations
Compliance Notes ¶
**SOC2 Type II**:
- Logs all system changes
- Tracks user actions
- Retention: 1 year minimum
**HIPAA**:
- Logs access to PHI
- Retention: 6 years minimum
- Must be tamper-proof
**GDPR Article 30**:
- Logs data processing activities
- User can request audit trail
- Retention: Varies by purpose
Known Limitations ¶
- **Goroutine accumulation**: If DB is very slow, goroutines pile up - Solution: Use worker pool with bounded queue (future)
- **Lost logs on crash**: In-flight goroutines lost if server crashes - Solution: Consider synchronous logging for critical operations
- **No log correlation**: Can't track multi-request workflows - Solution: Use request ID middleware (implemented separately)
- **Body size limit**: 10KB limit may truncate large requests - Solution: Configurable limit or hash-based logging
Returns:
- gin.HandlerFunc: Middleware function to add to router
See also:
- NewAuditLogger(): Configuration options
- logEvent(): Database persistence
- redactSensitiveData(): Sensitive field redaction
type CSRFStore ¶
type CSRFStore struct {
// contains filtered or unexported fields
}
CSRFStore stores CSRF tokens with expiration
type InputValidator ¶
type InputValidator struct {
// contains filtered or unexported fields
}
InputValidator handles comprehensive input validation and sanitization
func NewInputValidator ¶
func NewInputValidator() *InputValidator
NewInputValidator creates a new input validator
func (*InputValidator) Middleware ¶
func (v *InputValidator) Middleware() gin.HandlerFunc
Middleware provides input validation for all requests
func (*InputValidator) SanitizeJSONMiddleware ¶
func (v *InputValidator) SanitizeJSONMiddleware() gin.HandlerFunc
SanitizeJSONMiddleware sanitizes JSON request bodies
func (*InputValidator) SanitizeString ¶
func (v *InputValidator) SanitizeString(input string) string
SanitizeString removes HTML and dangerous characters from a string
type LicenseInfo ¶
type LicenseInfo struct {
ID int
Tier string
MaxUsers *int
MaxSessions *int
MaxNodes *int
ExpiresAt time.Time
Status string
Features map[string]interface{}
LastChecked time.Time
}
LicenseInfo holds cached license information
func GetCachedLicense ¶
func GetCachedLicense(database *db.Database) (*LicenseInfo, error)
GetCachedLicense returns the cached license (for read-only access)
type MaxSessionsError ¶
MaxSessionsError represents an error when max concurrent sessions is exceeded
func (*MaxSessionsError) Error ¶
func (e *MaxSessionsError) Error() string
type OrgContextError ¶
type OrgContextError struct {
// contains filtered or unexported fields
}
OrgContextError represents an error extracting org context
func (*OrgContextError) Error ¶
func (e *OrgContextError) Error() string
type QuotaMiddleware ¶
type QuotaMiddleware struct {
// contains filtered or unexported fields
}
QuotaMiddleware enforces resource quotas at the API level.
This middleware integrates with quota.Enforcer to provide HTTP-layer quota enforcement. It extracts user identity from the request context and makes the quota enforcer available to downstream handlers.
**Responsibilities**:
- Extract username from auth middleware (c.Get("username"))
- Inject quota enforcer into request context
- Provide helper functions for quota enforcement
**Non-Responsibilities**:
- Does NOT automatically reject requests (handlers decide what to check)
- Does NOT calculate current usage (enforcer does that)
- Does NOT store quota limits (database does that)
Thread safety: Safe for concurrent use (enforcer is thread-safe)
func NewQuotaMiddleware ¶
func NewQuotaMiddleware(enforcer *quota.Enforcer) *QuotaMiddleware
NewQuotaMiddleware creates a new quota middleware instance.
The enforcer parameter contains all the quota enforcement logic including:
- Database queries for user limits
- Current usage calculation
- Quota validation math
- Error message generation
This middleware is just a thin HTTP wrapper around the enforcer.
Parameters:
- enforcer: The quota enforcer instance (required, must not be nil)
Returns:
- QuotaMiddleware ready to be added to Gin router
Example usage:
enforcer := quota.NewEnforcer(database, k8sClient) quotaMiddleware := middleware.NewQuotaMiddleware(enforcer) router.Use(quotaMiddleware.Middleware())
func (*QuotaMiddleware) Middleware ¶
func (q *QuotaMiddleware) Middleware() gin.HandlerFunc
Middleware provides the Gin middleware handler for quota enforcement.
This middleware runs on EVERY request but does not automatically enforce quotas. It only prepares the context for downstream handlers to perform quota checks.
What This Middleware Does ¶
1. **Extract Username**: Get username from auth middleware context 2. **Inject Enforcer**: Store enforcer in request context for handlers 3. **Skip Unauthenticated**: Pass through requests without username
What This Middleware Does NOT Do ¶
- Does NOT reject requests automatically - Does NOT query database (deferred to handlers) - Does NOT calculate usage (deferred to handlers) - Does NOT apply quotas to GET requests (read-only operations)
Design Rationale: Why Not Auto-Enforce? ¶
**Option 1: Auto-enforce all requests** (rejected):
- Problem: Read operations don't consume resources
- Problem: Not all requests need quota checks
- Problem: Would slow down every request
**Option 2: Middleware just sets up context** (chosen):
- Benefit: Fast (no DB queries for reads)
- Benefit: Selective (only check when needed)
- Benefit: Flexible (handlers decide what to check)
Context Values Set ¶
The middleware stores these values in Gin context:
- "quota_enforcer": The enforcer instance
- "quota_username": The authenticated username
Handlers retrieve these with:
enforcer := c.Get("quota_enforcer").(*quota.Enforcer)
username := c.Get("quota_username").(string)
Performance Characteristics ¶
- Execution time: <0.1ms (just context operations) - No database queries - No network calls - No blocking operations
Integration with Auth Middleware ¶
This middleware must run AFTER authentication middleware:
router.Use(middleware.JWTAuth()) // Sets "username" router.Use(quotaMiddleware.Middleware()) // Reads "username"
If auth middleware doesn't set "username", this middleware does nothing (allows unauthenticated requests to pass through to auth enforcement layer).
See also:
- EnforceSessionCreation(): Helper for quota enforcement in handlers
- api/internal/quota/enforcer.go: Core quota logic
type RateLimiter ¶
type RateLimiter struct {
// contains filtered or unexported fields
}
RateLimiter implements a simple in-memory sliding window rate limiter.
Thread Safety: Uses sync.RWMutex for concurrent access protection.
Algorithm: Sliding Window - Records timestamp of each attempt - Filters out attempts outside the time window - Counts remaining attempts - Allows if count < maxAttempts
Memory Management: - Automatic cleanup runs every 5 minutes - Removes entries older than 10 minutes - Prevents memory leaks from abandoned rate limits
For production use with multiple API servers, replace with Redis-backed implementation for distributed rate limiting.
func GetRateLimiter ¶
func GetRateLimiter() *RateLimiter
GetRateLimiter returns the singleton rate limiter instance
func (*RateLimiter) CheckLimit ¶
CheckLimit checks if the rate limit has been exceeded using sliding window algorithm.
This method is the core of the rate limiting system. It implements a sliding window counter that accurately tracks requests over time, preventing both burst attacks and sustained high-rate attacks.
Algorithm: Sliding Window Counter ¶
Traditional fixed window problems:
- User makes 99 requests at 00:59
- Window resets at 01:00
- User makes 99 more requests at 01:01
- Result: 198 requests in 2 seconds (should be 100/minute max)
Sliding window solution:
- Track timestamp of each individual request
- Filter requests to only those within the time window from now
- Count filtered requests against limit
- More accurate but requires storing all timestamps
Parameters ¶
**key** (string):
- Unique identifier for the resource being rate limited
- Format: "{resource_type}:{resource_id}:{action}"
- Examples:
- "user:123:login" (login attempts for user 123)
- "user:456:mfa" (MFA verification for user 456)
- "ip:192.168.1.1:api" (API requests from IP)
- "session:sess-789:create" (session creation attempts)
**maxAttempts** (int):
- Maximum number of requests allowed within the window
- Examples:
- 5 for MFA verification (5 wrong codes/minute)
- 10 for login attempts (10 failed logins/minute)
- 100 for API requests (100 requests/minute)
- 1000 for read operations (1000 reads/minute)
**window** (time.Duration):
- Time window for counting requests
- Examples:
- 1*time.Minute for short-term protection
- 5*time.Minute for medium-term protection
- 1*time.Hour for long-term protection
Return Value ¶
Returns true if request is allowed, false if rate limit exceeded:
- true: Attempt recorded, request proceeds
- false: Limit exceeded, request rejected (attempt NOT recorded)
Thread Safety ¶
This method is thread-safe:
- Uses write lock (rl.mu.Lock()) for exclusive access
- Safe for concurrent calls from multiple goroutines
- Lock held for entire operation (atomic check-and-increment)
Performance Characteristics ¶
Time complexity:
- O(n) where n is number of attempts in window
- Typical n = 5-100 (very fast)
- Worst case: n = maxAttempts (still fast)
Memory usage:
- ~24 bytes per attempt (time.Time is 24 bytes)
- Example: 100 attempts = 2.4 KB
- Automatic cleanup prevents unbounded growth
Latency:
- Average: <1ms (in-memory operation)
- Worst case: <5ms (with many attempts to filter)
Security Considerations ¶
**Brute Force Protection**:
- Example: 6-digit MFA code (1,000,000 combinations)
- Without rate limiting: Brute force in minutes
- With 5 attempts/minute: Brute force takes ~160 days
**DoS Protection**:
- Prevents overwhelming server with requests
- Limits resource consumption per user/IP
- Ensures fair resource allocation
**Important**: Rate limit keys should include user ID or IP:
- Bad: "mfa" (global limit, one user blocks everyone)
- Good: "user:123:mfa" (per-user limit, isolated)
Edge Cases ¶
**Empty history**: First request is always allowed
- No previous attempts exist
- Request is recorded and allowed
**Exactly at limit**: If count == maxAttempts, request is rejected
- Example: maxAttempts=5, current=5, result=false
- This is correct (limit is "up to N", not "N+1")
**All attempts expired**: Old attempts don't count
- If all previous attempts are outside window, count=0
- Request is allowed (like fresh start)
**Concurrent requests**: First one to acquire lock wins
- If 2 requests race to be the "Nth" attempt
- Lock ensures only one is recorded as the Nth
- Other is rejected as "N+1th"
Example Usage ¶
**MFA verification** (strict):
limiter := middleware.GetRateLimiter()
userID := "user-123"
key := fmt.Sprintf("user:%s:mfa", userID)
if !limiter.CheckLimit(key, 5, 1*time.Minute) {
return errors.New("too many MFA attempts, please wait")
}
// Proceed with MFA verification
if !verifyMFACode(userID, code) {
return errors.New("invalid MFA code")
}
// Success - reset limit
limiter.ResetLimit(key)
**API rate limiting** (generous):
limiter := middleware.GetRateLimiter()
userID := c.GetString("user_id")
key := fmt.Sprintf("user:%s:api", userID)
if !limiter.CheckLimit(key, 1000, 1*time.Minute) {
c.JSON(429, gin.H{"error": "rate limit exceeded"})
return
}
**Progressive backoff** (escalating):
limiter := middleware.GetRateLimiter()
ip := c.ClientIP()
// Check 1-minute window (short-term protection)
if !limiter.CheckLimit(fmt.Sprintf("ip:%s:1m", ip), 10, 1*time.Minute) {
c.JSON(429, gin.H{"error": "rate limit exceeded (1 min)"})
return
}
// Check 1-hour window (long-term protection)
if !limiter.CheckLimit(fmt.Sprintf("ip:%s:1h", ip), 100, 1*time.Hour) {
c.JSON(429, gin.H{"error": "rate limit exceeded (1 hour)"})
return
}
Known Limitations ¶
**In-memory only**: Not distributed across multiple servers - Each API server has independent limits - Attackers can bypass by spreading across servers - Solution: Use Redis for distributed rate limiting
**Lost on restart**: Rate limit state lost when server restarts - Attackers could force restart to reset limits - Solution: Persist to Redis or database
**Memory growth**: Without cleanup, memory usage unbounded - Solution: Automatic cleanup runs every 5 minutes (implemented)
**No burst allowance**: Sliding window is strict - Can't "save up" unused capacity for later burst - Solution: Implement token bucket algorithm instead
See also:
- ResetLimit(): Clear rate limit for a key
- GetAttempts(): Check current attempt count
func (*RateLimiter) GetAttempts ¶
func (rl *RateLimiter) GetAttempts(key string, window time.Duration) int
GetAttempts returns the number of attempts within the window for a key
func (*RateLimiter) ResetLimit ¶
func (rl *RateLimiter) ResetLimit(key string)
ResetLimit clears all attempts for a given key
type SessionManager ¶
type SessionManager struct {
// contains filtered or unexported fields
}
SessionManager handles enhanced session security features
func NewSessionManager ¶
func NewSessionManager(idleTimeout time.Duration, maxConcurrentSessions int) *SessionManager
NewSessionManager creates a new session manager
func (*SessionManager) ConcurrentSessionMiddleware ¶
func (sm *SessionManager) ConcurrentSessionMiddleware() gin.HandlerFunc
ConcurrentSessionMiddleware enforces concurrent session limits per user
func (*SessionManager) GetActiveSessions ¶
func (sm *SessionManager) GetActiveSessions(username string) int
GetActiveSessions returns the number of active sessions for a user
func (*SessionManager) IdleTimeoutMiddleware ¶
func (sm *SessionManager) IdleTimeoutMiddleware() gin.HandlerFunc
IdleTimeoutMiddleware checks for idle sessions and invalidates them
func (*SessionManager) RegisterSession ¶
func (sm *SessionManager) RegisterSession(username, sessionID string) error
RegisterSession registers a new session for a user Returns error if max concurrent sessions exceeded
func (*SessionManager) SessionActivityMiddleware ¶
func (sm *SessionManager) SessionActivityMiddleware() gin.HandlerFunc
SessionActivityMiddleware updates session activity timestamp This should be called on every authenticated request
func (*SessionManager) UnregisterSession ¶
func (sm *SessionManager) UnregisterSession(username, sessionID string)
UnregisterSession removes a session when user logs out
type StructuredLoggerConfig ¶
type StructuredLoggerConfig struct {
// SkipPaths is a list of paths to skip logging (e.g., health checks)
SkipPaths []string
// SkipHealthCheck if true, skips logging for /health endpoint
SkipHealthCheck bool
// LogQuery if false, skips logging query parameters (for privacy)
LogQuery bool
// LogUserAgent if false, skips logging user agent
LogUserAgent bool
}
StructuredLoggerWithConfig allows customization of structured logging
func DefaultStructuredLoggerConfig ¶
func DefaultStructuredLoggerConfig() StructuredLoggerConfig
DefaultStructuredLoggerConfig returns default configuration
type TeamRBAC ¶
type TeamRBAC struct {
// contains filtered or unexported fields
}
TeamRBAC provides team-based role-based access control
func NewTeamRBAC ¶
NewTeamRBAC creates a new team RBAC middleware
func (*TeamRBAC) CanAccessSession ¶
func (t *TeamRBAC) CanAccessSession(ctx context.Context, userID, sessionID string, permission string) (bool, error)
CanAccessSession checks if a user can access a session (either owner or team member with permission)
func (*TeamRBAC) CheckTeamPermission ¶
func (t *TeamRBAC) CheckTeamPermission(ctx context.Context, userID, teamID, permission string) (bool, error)
CheckTeamPermission checks if a user has a specific permission in a team
func (*TeamRBAC) GetUserTeamPermissions ¶
func (t *TeamRBAC) GetUserTeamPermissions(ctx context.Context, userID, teamID string) ([]string, error)
GetUserTeamPermissions returns all permissions for a user in a specific team
func (*TeamRBAC) GetUserTeamRole ¶
GetUserTeamRole returns the user's role in a specific team
func (*TeamRBAC) ListUserTeams ¶
ListUserTeams returns all teams a user is a member of
func (*TeamRBAC) RequireSessionAccess ¶
func (t *TeamRBAC) RequireSessionAccess(permission string) gin.HandlerFunc
RequireSessionAccess middleware checks if user can access a specific session
func (*TeamRBAC) RequireTeamPermission ¶
func (t *TeamRBAC) RequireTeamPermission(permission string) gin.HandlerFunc
RequireTeamPermission creates middleware that checks if user has specific team permission
type TimeoutConfig ¶
type TimeoutConfig struct {
// Timeout is the maximum duration for the entire request
Timeout time.Duration
// ErrorMessage is the message returned when timeout occurs
ErrorMessage string
// ExcludedPaths are paths that should not have timeout applied
// (e.g., WebSocket endpoints, file uploads)
ExcludedPaths []string
}
TimeoutConfig holds configuration for request timeouts
func DefaultTimeoutConfig ¶
func DefaultTimeoutConfig() TimeoutConfig
DefaultTimeoutConfig returns default timeout configuration
type WebhookAuth ¶
type WebhookAuth struct {
// contains filtered or unexported fields
}
WebhookAuth validates webhook requests using HMAC-SHA256 signatures
func NewWebhookAuth ¶
func NewWebhookAuth(secret string) *WebhookAuth
NewWebhookAuth creates a new webhook authentication middleware
func (*WebhookAuth) Middleware ¶
func (w *WebhookAuth) Middleware() gin.HandlerFunc
Middleware returns a Gin middleware that validates webhook signatures Expects signature in X-Webhook-Signature header as hex-encoded HMAC-SHA256
func (*WebhookAuth) Sign ¶
func (w *WebhookAuth) Sign(payload []byte) string
Sign generates an HMAC-SHA256 signature for the given payload This is a helper function for testing or generating signatures