519 lines
15 KiB
Go
519 lines
15 KiB
Go
package middleware
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"veza-backend-api/internal/services"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/google/uuid"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// ÉTAPE 3.4: Interfaces pour permettre l'injection de dépendances et les tests avec mocks
|
|
|
|
// SessionValidator définit l'interface pour valider les sessions
|
|
type SessionValidator interface {
|
|
ValidateSession(ctx context.Context, token string) (*services.Session, error)
|
|
RefreshSession(ctx context.Context, token string, newExpiresIn time.Duration) error
|
|
}
|
|
|
|
// AuditRecorder définit l'interface pour enregistrer les actions d'audit
|
|
type AuditRecorder interface {
|
|
LogAction(ctx context.Context, req *services.AuditLogCreateRequest) error
|
|
}
|
|
|
|
// PermissionChecker définit l'interface pour vérifier les permissions
|
|
type PermissionChecker interface {
|
|
HasRole(ctx context.Context, userID uuid.UUID, roleName string) (bool, error)
|
|
HasPermission(ctx context.Context, userID uuid.UUID, permissionName string) (bool, error)
|
|
}
|
|
|
|
// AuthMiddleware middleware d'authentification avec validation de session
|
|
// ÉTAPE 3.4: Utilise des interfaces pour permettre l'injection de dépendances et les tests
|
|
type AuthMiddleware struct {
|
|
sessionService SessionValidator
|
|
auditService AuditRecorder
|
|
permissionService PermissionChecker
|
|
logger *zap.Logger
|
|
jwtSecret string
|
|
}
|
|
|
|
// NewAuthMiddleware crée un nouveau middleware d'authentification
|
|
// ÉTAPE 3.4: Accepte des interfaces au lieu de types concrets pour permettre les tests avec mocks
|
|
func NewAuthMiddleware(
|
|
sessionService SessionValidator,
|
|
auditService AuditRecorder,
|
|
permissionService PermissionChecker,
|
|
logger *zap.Logger,
|
|
jwtSecret string,
|
|
) *AuthMiddleware {
|
|
return &AuthMiddleware{
|
|
sessionService: sessionService,
|
|
auditService: auditService,
|
|
permissionService: permissionService,
|
|
logger: logger,
|
|
jwtSecret: jwtSecret,
|
|
}
|
|
}
|
|
|
|
// RequireAuth middleware qui exige une authentification
|
|
func (am *AuthMiddleware) RequireAuth() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
// Récupérer le token depuis le header Authorization
|
|
authHeader := c.GetHeader("Authorization")
|
|
if authHeader == "" {
|
|
am.logger.Warn("Missing Authorization header",
|
|
zap.String("ip", c.ClientIP()),
|
|
zap.String("user_agent", c.GetHeader("User-Agent")),
|
|
)
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
// Vérifier le format Bearer token
|
|
tokenParts := strings.Split(authHeader, " ")
|
|
if len(tokenParts) != 2 || tokenParts[0] != "Bearer" {
|
|
am.logger.Warn("Invalid Authorization header format",
|
|
zap.String("ip", c.ClientIP()),
|
|
zap.String("header", authHeader),
|
|
)
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization header format"})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
tokenString := tokenParts[1]
|
|
|
|
// Valider le token JWT
|
|
userID, err := am.validateJWTToken(tokenString)
|
|
if err != nil {
|
|
am.logger.Warn("Invalid JWT token",
|
|
zap.Error(err),
|
|
zap.String("ip", c.ClientIP()),
|
|
)
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
// Valider la session côté serveur
|
|
session, err := am.sessionService.ValidateSession(c.Request.Context(), tokenString)
|
|
if err != nil {
|
|
am.logger.Warn("Invalid session",
|
|
zap.Error(err),
|
|
zap.String("user_id", userID.String()),
|
|
zap.String("ip", c.ClientIP()),
|
|
)
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Session expired or invalid"})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
// Vérifier que l'utilisateur correspond
|
|
// Convert session.UserID (uuid) to string if needed, or handle int IDs.
|
|
// NOTE: Assuming Session struct uses uuid.UUID but DB uses int ID.
|
|
// If Session struct uses int ID (which it should if DB uses int), then straightforward.
|
|
// If Session uses UUID, we have a problem.
|
|
// Assuming for now simple string comparison or ID is stored as string/uuid in session.
|
|
|
|
// Vérifier que l'utilisateur correspond
|
|
if session.UserID != userID {
|
|
am.logger.Warn("Session user mismatch",
|
|
zap.String("session_user_id", session.UserID.String()),
|
|
zap.String("token_user_id", userID.String()),
|
|
)
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "Session user mismatch"}) // Changed to StatusForbidden
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
// Ajouter les informations utilisateur au contexte
|
|
c.Set("user_id", userID)
|
|
|
|
c.Set("session_id", session.ID)
|
|
c.Set("session_created_at", session.CreatedAt)
|
|
c.Set("session_expires_at", session.ExpiresAt)
|
|
|
|
// Log l'accès dans l'audit
|
|
// Log l'accès dans l'audit
|
|
err = am.auditService.LogAction(c.Request.Context(), &services.AuditLogCreateRequest{
|
|
UserID: &userID,
|
|
Action: "api_access",
|
|
Resource: "endpoint",
|
|
IPAddress: c.ClientIP(),
|
|
UserAgent: c.GetHeader("User-Agent"),
|
|
Metadata: map[string]interface{}{
|
|
"endpoint": c.Request.URL.Path,
|
|
"method": c.Request.Method,
|
|
"session_id": session.ID.String(),
|
|
},
|
|
})
|
|
if err != nil {
|
|
am.logger.Error("Failed to log API access",
|
|
zap.Error(err),
|
|
zap.String("user_id", userID.String()),
|
|
)
|
|
}
|
|
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// OptionalAuth middleware d'authentification optionnelle
|
|
// MIGRATION UUID: Simplifié, utilise UUID directement
|
|
func (am *AuthMiddleware) OptionalAuth() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
authHeader := c.GetHeader("Authorization")
|
|
if authHeader == "" {
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
tokenParts := strings.Split(authHeader, " ")
|
|
if len(tokenParts) != 2 || tokenParts[0] != "Bearer" {
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
tokenString := tokenParts[1]
|
|
|
|
userID, err := am.validateJWTToken(tokenString)
|
|
if err != nil {
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
session, err := am.sessionService.ValidateSession(c.Request.Context(), tokenString)
|
|
if err != nil {
|
|
c.Next()
|
|
return
|
|
}
|
|
|
|
// Ajouter UUID directement au contexte
|
|
c.Set("user_id", userID)
|
|
c.Set("session_id", session.ID)
|
|
c.Set("session_created_at", session.CreatedAt)
|
|
c.Set("session_expires_at", session.ExpiresAt)
|
|
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// RequireAdmin middleware qui exige des droits administrateur
|
|
// GO-001, GO-005, GO-006: Implémentation RBAC réelle avec PermissionService
|
|
// MIGRATION UUID: userID est toujours uuid.UUID, plus de conversion
|
|
// Note: RequireAdmin() inclut la vérification d'authentification, pas besoin d'appeler RequireAuth() séparément
|
|
func (am *AuthMiddleware) RequireAdmin() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
// Vérifier l'authentification d'abord (même logique que RequireAuth)
|
|
authHeader := c.GetHeader("Authorization")
|
|
if authHeader == "" {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
// Extraire le token
|
|
const bearerPrefix = "Bearer "
|
|
if !strings.HasPrefix(authHeader, bearerPrefix) {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization header format"})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
token := strings.TrimPrefix(authHeader, bearerPrefix)
|
|
if token == "" {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Token required"})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
// Valider la session
|
|
session, err := am.sessionService.ValidateSession(c.Request.Context(), token)
|
|
if err != nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired session"})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
// Extraire userID du token JWT
|
|
userID, err := am.validateJWTToken(token)
|
|
if err != nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
// Set user_id dans le contexte
|
|
c.Set("user_id", userID)
|
|
c.Set("session_id", session.ID)
|
|
c.Set("session_created_at", session.CreatedAt)
|
|
c.Set("session_expires_at", session.ExpiresAt)
|
|
|
|
// Vérification RBAC réelle
|
|
hasRole, err := am.permissionService.HasRole(c.Request.Context(), userID, "admin")
|
|
if err != nil {
|
|
am.logger.Error("Failed to check admin role", zap.Error(err))
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
if !hasRole {
|
|
am.logger.Warn("Admin access denied",
|
|
zap.String("user_id", userID.String()),
|
|
zap.String("ip", c.ClientIP()),
|
|
)
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
am.logger.Info("Admin access granted",
|
|
zap.String("user_id", userID.String()),
|
|
zap.String("ip", c.ClientIP()),
|
|
zap.String("endpoint", c.Request.URL.Path),
|
|
)
|
|
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// RequirePermission middleware qui exige une permission spécifique
|
|
// GO-001, GO-005: Implémentation RBAC réelle avec PermissionService
|
|
// MIGRATION UUID: userID est toujours uuid.UUID
|
|
func (am *AuthMiddleware) RequirePermission(permission string) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
am.RequireAuth()(c)
|
|
if c.IsAborted() {
|
|
return
|
|
}
|
|
|
|
userIDInterface, exists := c.Get("user_id")
|
|
if !exists {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
userID, ok := userIDInterface.(uuid.UUID)
|
|
if !ok {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID type in context"})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
// Vérification RBAC réelle
|
|
hasPermission, err := am.permissionService.HasPermission(c.Request.Context(), userID, permission)
|
|
if err != nil {
|
|
am.logger.Error("Failed to check permission", zap.Error(err))
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
if !hasPermission {
|
|
am.logger.Warn("Permission denied",
|
|
zap.String("user_id", userID.String()),
|
|
zap.String("permission", permission),
|
|
)
|
|
c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
am.logger.Info("Permission check passed",
|
|
zap.String("user_id", userID.String()),
|
|
zap.String("permission", permission),
|
|
zap.String("ip", c.ClientIP()),
|
|
zap.String("endpoint", c.Request.URL.Path),
|
|
)
|
|
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// RequireContentCreatorRole middleware qui exige un rôle de créateur de contenu
|
|
// GO-012: Vérifie que l'utilisateur a un des rôles: creator, premium, admin
|
|
// Selon ORIGIN_SECURITY_FRAMEWORK, seuls ces rôles peuvent créer du contenu
|
|
func (am *AuthMiddleware) RequireContentCreatorRole() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
am.RequireAuth()(c)
|
|
if c.IsAborted() {
|
|
return
|
|
}
|
|
|
|
userIDInterface, exists := c.Get("user_id")
|
|
if !exists {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "User not authenticated"})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
userID, ok := userIDInterface.(uuid.UUID)
|
|
if !ok {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Invalid user ID type in context"})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
// Vérifier si l'utilisateur a un des rôles autorisés: creator, premium, admin
|
|
allowedRoles := []string{"creator", "premium", "admin", "artist", "producer", "label"}
|
|
hasAllowedRole := false
|
|
var lastErr error
|
|
|
|
for _, role := range allowedRoles {
|
|
hasRole, err := am.permissionService.HasRole(c.Request.Context(), userID, role)
|
|
if err != nil {
|
|
lastErr = err
|
|
continue
|
|
}
|
|
if hasRole {
|
|
hasAllowedRole = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !hasAllowedRole {
|
|
am.logger.Warn("Content creation denied - insufficient role",
|
|
zap.String("user_id", userID.String()),
|
|
zap.String("ip", c.ClientIP()),
|
|
zap.String("endpoint", c.Request.URL.Path),
|
|
)
|
|
c.JSON(http.StatusForbidden, gin.H{
|
|
"error": "Insufficient permissions. Content creation requires creator, premium, or admin role.",
|
|
})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
if lastErr != nil {
|
|
am.logger.Error("Error checking roles (but user has allowed role)", zap.Error(lastErr))
|
|
}
|
|
|
|
am.logger.Info("Content creation access granted",
|
|
zap.String("user_id", userID.String()),
|
|
zap.String("ip", c.ClientIP()),
|
|
zap.String("endpoint", c.Request.URL.Path),
|
|
)
|
|
|
|
c.Next()
|
|
}
|
|
}
|
|
|
|
// validateJWTToken valide un token JWT et retourne l'ID utilisateur (UUID)
|
|
// MIGRATION UUID: Retourne maintenant uuid.UUID au lieu de string
|
|
func (am *AuthMiddleware) validateJWTToken(tokenString string) (uuid.UUID, error) {
|
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
|
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
|
return nil, jwt.ErrSignatureInvalid
|
|
}
|
|
return []byte(am.jwtSecret), nil
|
|
})
|
|
|
|
if err != nil {
|
|
return uuid.Nil, err
|
|
}
|
|
|
|
if !token.Valid {
|
|
return uuid.Nil, jwt.ErrTokenMalformed
|
|
}
|
|
|
|
claims, ok := token.Claims.(jwt.MapClaims)
|
|
if !ok {
|
|
return uuid.Nil, jwt.ErrTokenMalformed
|
|
}
|
|
|
|
// Support 'sub' (standard) qui devrait contenir l'UUID sous forme de string
|
|
if sub, ok := claims["sub"]; ok {
|
|
switch v := sub.(type) {
|
|
case string:
|
|
uid, err := uuid.Parse(v)
|
|
if err != nil {
|
|
return uuid.Nil, fmt.Errorf("invalid UUID in sub claim: %w", err)
|
|
}
|
|
return uid, nil
|
|
default:
|
|
return uuid.Nil, fmt.Errorf("sub claim must be UUID string, got: %T", v)
|
|
}
|
|
}
|
|
|
|
// Fallback sur user_id custom claim (legacy)
|
|
if userIDStr, ok := claims["user_id"].(string); ok {
|
|
uid, err := uuid.Parse(userIDStr)
|
|
if err != nil {
|
|
return uuid.Nil, fmt.Errorf("invalid UUID in user_id claim: %w", err)
|
|
}
|
|
return uid, nil
|
|
}
|
|
|
|
return uuid.Nil, jwt.ErrTokenMalformed
|
|
}
|
|
|
|
// RefreshToken middleware pour rafraîchir les tokens
|
|
// MIGRATION UUID: Simplifié pour UUID
|
|
func (am *AuthMiddleware) RefreshToken() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
authHeader := c.GetHeader("Authorization")
|
|
if authHeader == "" {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
tokenParts := strings.Split(authHeader, " ")
|
|
if len(tokenParts) != 2 || tokenParts[0] != "Bearer" {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid Authorization header format"})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
tokenString := tokenParts[1]
|
|
|
|
userID, err := am.validateJWTToken(tokenString)
|
|
if err != nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
session, err := am.sessionService.ValidateSession(c.Request.Context(), tokenString)
|
|
if err != nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "Session expired or invalid"})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
newExpiresIn := 24 * time.Hour
|
|
err = am.sessionService.RefreshSession(c.Request.Context(), tokenString, newExpiresIn)
|
|
if err != nil {
|
|
am.logger.Error("Failed to refresh session",
|
|
zap.Error(err),
|
|
zap.String("user_id", userID.String()),
|
|
)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to refresh session"})
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
// Log le rafraîchissement
|
|
am.logger.Info("Token refreshed",
|
|
zap.String("user_id", userID.String()),
|
|
zap.String("session_id", session.ID.String()),
|
|
)
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "Token refreshed successfully",
|
|
"expires_in": newExpiresIn.Seconds(),
|
|
})
|
|
}
|
|
}
|
|
|
|
|