Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
// Package middleware provides HTTP middleware for the API Gateway
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
)
|
||||
|
||||
// Claims represents JWT claims
|
||||
type Claims struct {
|
||||
TenantID string `json:"tenant_id"`
|
||||
UserID string `json:"user_id"`
|
||||
Scopes []string `json:"scopes"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// Auth middleware validates JWT tokens or API keys
|
||||
func Auth(jwtSecret string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "Missing authorization header",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Check for Bearer token (JWT)
|
||||
if strings.HasPrefix(authHeader, "Bearer ") {
|
||||
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
|
||||
// Check if it's an API key (starts with pk_ or sk_)
|
||||
if strings.HasPrefix(tokenString, "pk_") || strings.HasPrefix(tokenString, "sk_") {
|
||||
// API Key authentication
|
||||
if err := validateAPIKey(c, tokenString); err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "Invalid API key",
|
||||
})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// JWT authentication
|
||||
claims, err := validateJWT(tokenString, jwtSecret)
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "Invalid token",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Store claims in context
|
||||
c.Set("tenant_id", claims.TenantID)
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("scopes", claims.Scopes)
|
||||
}
|
||||
} else {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "Invalid authorization format",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func validateJWT(tokenString, secret string) (*Claims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(secret), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, jwt.ErrTokenInvalidClaims
|
||||
}
|
||||
|
||||
func validateAPIKey(c *gin.Context, apiKey string) error {
|
||||
// In production, this would validate against a database of API keys
|
||||
// For now, we extract tenant info from the X-Tenant-ID header
|
||||
tenantID := c.GetHeader("X-Tenant-ID")
|
||||
if tenantID == "" {
|
||||
tenantID = "default"
|
||||
}
|
||||
|
||||
c.Set("tenant_id", tenantID)
|
||||
c.Set("user_id", "api-key-user")
|
||||
c.Set("scopes", []string{"read", "write"})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TenantIsolation ensures requests only access their own tenant's data
|
||||
func TenantIsolation() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
tenantID, exists := c.Get("tenant_id")
|
||||
if !exists || tenantID == "" {
|
||||
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{
|
||||
"error": "Tenant ID required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Add tenant ID to all database queries (handled by handlers)
|
||||
c.Set("tenant_filter", tenantID)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
// Package middleware provides HTTP middleware for the API Gateway
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
// Logger middleware logs requests
|
||||
func Logger(logger *zap.Logger) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
path := c.Request.URL.Path
|
||||
raw := c.Request.URL.RawQuery
|
||||
|
||||
// Process request
|
||||
c.Next()
|
||||
|
||||
// Log after request
|
||||
latency := time.Since(start)
|
||||
clientIP := c.ClientIP()
|
||||
method := c.Request.Method
|
||||
statusCode := c.Writer.Status()
|
||||
|
||||
if raw != "" {
|
||||
path = path + "?" + raw
|
||||
}
|
||||
|
||||
logger.Info("request",
|
||||
zap.String("method", method),
|
||||
zap.String("path", path),
|
||||
zap.Int("status", statusCode),
|
||||
zap.String("ip", clientIP),
|
||||
zap.Duration("latency", latency),
|
||||
zap.String("request_id", c.GetString("request_id")),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// CORS middleware handles Cross-Origin Resource Sharing
|
||||
func CORS() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
origin := c.GetHeader("Origin")
|
||||
if origin == "" {
|
||||
origin = "*"
|
||||
}
|
||||
|
||||
c.Header("Access-Control-Allow-Origin", origin)
|
||||
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
|
||||
c.Header("Access-Control-Allow-Headers", "Origin, Content-Type, Accept, Authorization, X-Tenant-ID, X-Request-ID")
|
||||
c.Header("Access-Control-Allow-Credentials", "true")
|
||||
c.Header("Access-Control-Max-Age", "86400")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequestID middleware adds a unique request ID to each request
|
||||
func RequestID() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = uuid.New().String()
|
||||
}
|
||||
|
||||
c.Set("request_id", requestID)
|
||||
c.Header("X-Request-ID", requestID)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RateLimitConfig holds rate limiting configuration
|
||||
type RateLimitConfig struct {
|
||||
RequestsPerSecond int
|
||||
Burst int
|
||||
}
|
||||
|
||||
// RateLimiter middleware limits request rate per client
|
||||
func RateLimiter(config RateLimitConfig) gin.HandlerFunc {
|
||||
// In production, use a distributed rate limiter with Redis
|
||||
// This is a simple in-memory rate limiter per IP
|
||||
limiters := make(map[string]*rate.Limiter)
|
||||
|
||||
return func(c *gin.Context) {
|
||||
clientIP := c.ClientIP()
|
||||
|
||||
limiter, exists := limiters[clientIP]
|
||||
if !exists {
|
||||
limiter = rate.NewLimiter(rate.Limit(config.RequestsPerSecond), config.Burst)
|
||||
limiters[clientIP] = limiter
|
||||
}
|
||||
|
||||
if !limiter.Allow() {
|
||||
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{
|
||||
"error": "Rate limit exceeded",
|
||||
"retry_after": "1s",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// Recovery middleware recovers from panics
|
||||
func Recovery() gin.HandlerFunc {
|
||||
return gin.Recovery()
|
||||
}
|
||||
Reference in New Issue
Block a user