diff --git a/apps/carrier-connector/internal/tenant/middleware.go b/apps/carrier-connector/internal/tenant/middleware.go new file mode 100644 index 0000000..6a5ec9c --- /dev/null +++ b/apps/carrier-connector/internal/tenant/middleware.go @@ -0,0 +1,331 @@ +package tenant + +import ( + "context" + "errors" + "net/http" + "strings" + "time" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" +) + +// TenantMiddleware provides tenant isolation middleware +type TenantMiddleware struct { + tenantService Service + logger *logrus.Logger +} + +// NewTenantMiddleware creates a new tenant middleware +func NewTenantMiddleware(tenantService Service, logger *logrus.Logger) *TenantMiddleware { + return &TenantMiddleware{ + tenantService: tenantService, + logger: logger, + } +} + +// TenantContextKey is the key used to store tenant context in the request context +type TenantContextKey string + +const ( + TenantCtxKey TenantContextKey = "tenant_context" + UserIDKey TenantContextKey = "user_id" +) + +// ExtractTenantFromHeader extracts tenant information from HTTP headers +func (tm *TenantMiddleware) ExtractTenantFromHeader(c *gin.Context) (*TenantContext, error) { + // Try to get tenant from X-Tenant-ID header + tenantID := c.GetHeader("X-Tenant-ID") + if tenantID == "" { + // Try to get tenant from subdomain + host := c.Request.Host + parts := strings.Split(host, ".") + if len(parts) > 2 { + tenantID = parts[0] + } + } + + if tenantID == "" { + return nil, errors.New("tenant ID not found in request") + } + + // Get tenant context + tenantCtx, err := tm.tenantService.GetTenantContext(c.Request.Context(), tenantID) + if err != nil { + return nil, err + } + + return tenantCtx, nil +} + +// ExtractTenantFromAPIKey extracts tenant information from API key +func (tm *TenantMiddleware) ExtractTenantFromAPIKey(c *gin.Context) (*TenantContext, error) { + apiKey := c.GetHeader("X-API-Key") + if apiKey == "" { + // Try Authorization header with Bearer token + authHeader := c.GetHeader("Authorization") + if strings.HasPrefix(authHeader, "Bearer ") { + apiKey = strings.TrimPrefix(authHeader, "Bearer ") + } + } + + if apiKey == "" { + return nil, errors.New("API key not found in request") + } + + // Validate API key + apiKeyData, err := tm.tenantService.ValidateAPIKey(c.Request.Context(), apiKey) + if err != nil { + return nil, err + } + + // Get tenant context + tenantCtx, err := tm.tenantService.GetTenantContext(c.Request.Context(), apiKeyData.TenantID) + if err != nil { + return nil, err + } + + return tenantCtx, nil +} + +// RequireTenant middleware ensures a valid tenant is present +func (tm *TenantMiddleware) RequireTenant() gin.HandlerFunc { + return func(c *gin.Context) { + tenantCtx, err := tm.ExtractTenantFromHeader(c) + if err != nil { + // Try API key extraction + tenantCtx, err = tm.ExtractTenantFromAPIKey(c) + if err != nil { + tm.logger.WithError(err).Error("Failed to extract tenant context") + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid tenant or API key"}) + c.Abort() + return + } + } + + // Validate tenant access + _, err = tm.tenantService.ValidateTenantAccess(c.Request.Context(), tenantCtx.TenantID, "") + if err != nil { + tm.logger.WithError(err).Error("Tenant access validation failed") + c.JSON(http.StatusForbidden, gin.H{"error": "Tenant access denied"}) + c.Abort() + return + } + + // Inject tenant context + ctx := context.WithValue(c.Request.Context(), TenantCtxKey, tenantCtx) + c.Request = c.Request.WithContext(ctx) + + c.Next() + } +} + +// RequireTenantRole middleware ensures user has required role +func (tm *TenantMiddleware) RequireTenantRole(requiredRoles ...TenantRole) gin.HandlerFunc { + return func(c *gin.Context) { + tenantCtx, err := tm.GetTenantContext(c) + if err != nil { + tm.logger.WithError(err).Error("Failed to get tenant context") + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid tenant context"}) + c.Abort() + return + } + + // Check if user has required role + hasRole := false + for _, requiredRole := range requiredRoles { + if tenantCtx.UserRole == requiredRole { + hasRole = true + break + } + } + + if !hasRole { + tm.logger.WithFields(logrus.Fields{ + "tenant_id": tenantCtx.TenantID, + "user_role": tenantCtx.UserRole, + "required_roles": requiredRoles, + }).Error("Insufficient tenant role") + c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"}) + c.Abort() + return + } + + c.Next() + } +} + +// RequirePermission middleware ensures user has specific permission +func (tm *TenantMiddleware) RequirePermission(permission string) gin.HandlerFunc { + return func(c *gin.Context) { + tenantCtx, err := tm.GetTenantContext(c) + if err != nil { + tm.logger.WithError(err).Error("Failed to get tenant context") + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid tenant context"}) + c.Abort() + return + } + + userID := tm.GetUserID(c) + if userID == "" { + c.JSON(http.StatusUnauthorized, gin.H{"error": "User ID not found"}) + c.Abort() + return + } + + // Check permission + hasPermission, err := tm.tenantService.HasPermission(c.Request.Context(), tenantCtx.TenantID, userID, permission) + if err != nil { + tm.logger.WithError(err).Error("Permission check failed") + c.JSON(http.StatusInternalServerError, gin.H{"error": "Permission check failed"}) + c.Abort() + return + } + + if !hasPermission { + tm.logger.WithFields(logrus.Fields{ + "tenant_id": tenantCtx.TenantID, + "user_id": userID, + "permission": permission, + }).Error("Permission denied") + c.JSON(http.StatusForbidden, gin.H{"error": "Permission denied"}) + c.Abort() + return + } + + c.Next() + } +} + +// RateLimit middleware applies rate limiting per tenant +func (tm *TenantMiddleware) RateLimit(endpoint string) gin.HandlerFunc { + return func(c *gin.Context) { + // TODO: Implement rate limiting per tenant + c.Next() + } +} + +// GetTenantContext retrieves tenant context from gin context +func (tm *TenantMiddleware) GetTenantContext(c *gin.Context) (*TenantContext, error) { + tenantCtx, exists := c.Get(string(TenantCtxKey)) + if !exists { + return nil, errors.New("tenant context not found") + } + + ctx, ok := tenantCtx.(*TenantContext) + if !ok { + return nil, errors.New("invalid tenant context type") + } + + return ctx, nil +} + +// GetUserID retrieves user ID from gin context +func (tm *TenantMiddleware) GetUserID(c *gin.Context) string { + userID, exists := c.Get(string(UserIDKey)) + if !exists { + return "" + } + + id, ok := userID.(string) + if !ok { + return "" + } + + return id +} + +// InjectTenantContext injects tenant context into gin context +func (tm *TenantMiddleware) InjectTenantContext(c *gin.Context, tenantCtx *TenantContext) { + c.Set(string(TenantCtxKey), tenantCtx) +} + +// TenantIsolation middleware ensures data isolation between tenants +func (tm *TenantMiddleware) TenantIsolation() gin.HandlerFunc { + return func(c *gin.Context) { + tenantCtx, err := tm.GetTenantContext(c) + if err != nil { + tm.logger.WithError(err).Error("Failed to get tenant context for isolation") + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid tenant context"}) + c.Abort() + return + } + + // Inject tenant ID into request context for repository layer + ctx := context.WithValue(c.Request.Context(), "tenant_id", tenantCtx.TenantID) + c.Request = c.Request.WithContext(ctx) + + c.Next() + } +} + +// ValidateResourceAccess validates that tenant can access specific resource +func (tm *TenantMiddleware) ValidateResourceAccess(resource string) gin.HandlerFunc { + return func(c *gin.Context) { + // Get resource ID from URL parameters + resourceID := c.Param("id") + if resourceID == "" { + resourceID = c.Param("resource_id") + } + + // Validate resource access - TODO: Implement resource access validation + // For now, allow all resource access within tenant + c.Next() + } +} + +// LogTenantActivity logs tenant activity for audit purposes +func (tm *TenantMiddleware) LogTenantActivity(activity string) gin.HandlerFunc { + return func(c *gin.Context) { + tenantCtx, err := tm.GetTenantContext(c) + if err != nil { + // If we can't get tenant context, skip logging + c.Next() + return + } + + userID := tm.GetUserID(c) + + // Log tenant event + event := &TenantEvent{ + ID: generateEventID(), + TenantID: tenantCtx.TenantID, + UserID: userID, + EventType: TenantEventType(activity), + EventData: map[string]interface{}{ + "method": c.Request.Method, + "path": c.Request.URL.Path, + "user_agent": c.Request.UserAgent(), + "ip_address": c.ClientIP(), + }, + Timestamp: getCurrentTimestamp(), + } + + if err := tm.tenantService.LogTenantEvent(c.Request.Context(), event); err != nil { + tm.logger.WithError(err).Error("Failed to log tenant event") + } + + c.Next() + } +} + +// Helper functions +func generateEventID() string { + // Generate unique event ID (implementation depends on your ID generation strategy) + return "evt_" + generateRandomString(16) +} + +func generateRandomString(length int) string { + // Generate random string (implementation depends on your random string generation) + const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + b := make([]byte, length) + for i := range b { + b[i] = charset[i%len(charset)] + } + return string(b) +} + +func getCurrentTimestamp() time.Time { + return time.Now() +}