//go:build security // +build security package security import ( "bytes" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "time" "github.com/gin-gonic/gin" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/zap/zaptest" "gorm.io/driver/sqlite" "gorm.io/gorm" "veza-backend-api/internal/core/track" "veza-backend-api/internal/database" "veza-backend-api/internal/handlers" "veza-backend-api/internal/middleware" "veza-backend-api/internal/models" "veza-backend-api/internal/repositories" "veza-backend-api/internal/services" "github.com/golang-jwt/jwt/v5" ) // setupAuthorizationTestRouter crée un router de test avec authentification complète func setupAuthorizationTestRouter(t *testing.T) (*gin.Engine, *gorm.DB, *services.JWTService, map[uuid.UUID]*models.User, func()) { gin.SetMode(gin.TestMode) logger := zaptest.NewLogger(t) // Setup in-memory SQLite database db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{}) require.NoError(t, err) db.Exec("PRAGMA foreign_keys = ON") // Auto-migrate models err = db.AutoMigrate( &models.User{}, &models.Track{}, &models.Playlist{}, &models.PlaylistTrack{}, &models.RefreshToken{}, &models.Session{}, &models.Role{}, &models.UserRole{}, &models.Permission{}, &models.RolePermission{}, ) require.NoError(t, err) dbWrapper := &database.Database{GormDB: db} // Create test users with different roles users := make(map[uuid.UUID]*models.User) // Regular user regularUserID := uuid.New() regularUser := &models.User{ ID: regularUserID, Email: "user@example.com", Username: "regularuser", PasswordHash: "$2a$10$abcdefghijklmnopqrstuvwxyz1234567890", IsVerified: true, TokenVersion: 1, } err = db.Create(regularUser).Error require.NoError(t, err) users[regularUserID] = regularUser // Admin user adminUserID := uuid.New() adminUser := &models.User{ ID: adminUserID, Email: "admin@example.com", Username: "adminuser", PasswordHash: "$2a$10$abcdefghijklmnopqrstuvwxyz1234567890", IsVerified: true, TokenVersion: 1, } err = db.Create(adminUser).Error require.NoError(t, err) users[adminUserID] = adminUser // Creator user creatorUserID := uuid.New() creatorUser := &models.User{ ID: creatorUserID, Email: "creator@example.com", Username: "creatoruser", PasswordHash: "$2a$10$abcdefghijklmnopqrstuvwxyz1234567890", IsVerified: true, TokenVersion: 1, } err = db.Create(creatorUser).Error require.NoError(t, err) users[creatorUserID] = creatorUser // Setup roles and permissions adminRole := &models.Role{Name: "admin", Description: "Administrator"} creatorRole := &models.Role{Name: "creator", Description: "Content Creator"} premiumRole := &models.Role{Name: "premium", Description: "Premium User"} err = db.Create(adminRole).Error require.NoError(t, err) err = db.Create(creatorRole).Error require.NoError(t, err) err = db.Create(premiumRole).Error require.NoError(t, err) // Assign roles adminUserRole := &models.UserRole{UserID: adminUserID, RoleID: adminRole.ID} creatorUserRole := &models.UserRole{UserID: creatorUserID, RoleID: creatorRole.ID} err = db.Create(adminUserRole).Error require.NoError(t, err) err = db.Create(creatorUserRole).Error require.NoError(t, err) // Setup services jwtService, err := services.NewJWTService("", "", "test-secret-key-must-be-32-chars-long", "test-issuer", "test-audience") require.NoError(t, err) sessionService := services.NewSessionService(dbWrapper, logger) permissionService := services.NewPermissionService(db) auditService := services.NewAuditService(dbWrapper, logger) // Create auth middleware authMiddleware := middleware.NewAuthMiddleware( sessionService, auditService, permissionService, jwtService, services.NewUserServiceWithDB(repositories.NewGormUserRepository(db), db), nil, nil, // TokenBlacklist logger, ) uploadDir := t.TempDir() trackService := track.NewTrackService(db, logger, uploadDir) trackUploadService := services.NewTrackUploadService(db, logger) chunkService := services.NewTrackChunkService(t.TempDir(), nil, logger) likeService := services.NewTrackLikeService(db, logger) streamService := services.NewStreamService("http://localhost:8082", logger) trackHandler := track.NewTrackHandler(trackService, trackUploadService, chunkService, likeService, streamService) playlistRepo := repositories.NewPlaylistRepository(db) playlistTrackRepo := repositories.NewPlaylistTrackRepository(db) playlistCollaboratorRepo := repositories.NewPlaylistCollaboratorRepository(db) userRepo := repositories.NewGormUserRepository(db) playlistService := services.NewPlaylistService(playlistRepo, playlistTrackRepo, playlistCollaboratorRepo, userRepo, logger) playlistHandler := handlers.NewPlaylistHandler(playlistService, db, logger) userService := services.NewUserServiceWithDB(userRepo, db) profileHandler := handlers.NewProfileHandler(userService, logger) // Create router router := gin.New() // Public routes router.GET("/health", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"status": "ok"}) }) // Protected routes protected := router.Group("/api/v1") protected.Use(authMiddleware.RequireAuth()) // Admin routes admin := protected.Group("/admin") admin.Use(authMiddleware.RequireAdmin()) { admin.GET("/users", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"message": "admin only"}) }) } // Content creator routes creator := protected.Group("/tracks") creator.POST("", authMiddleware.RequireContentCreatorRole(), trackHandler.UploadTrack) // Track routes with ownership tracks := protected.Group("/tracks") { tracks.GET("", trackHandler.ListTracks) tracks.GET("/:id", trackHandler.GetTrack) // Ownership resolver for tracks trackOwnerResolver := func(c *gin.Context) (uuid.UUID, error) { trackIDStr := c.Param("id") trackID, err := uuid.Parse(trackIDStr) if err != nil { return uuid.Nil, err } track, err := trackService.GetTrackByID(c.Request.Context(), trackID) if err != nil { return uuid.Nil, err } return track.UserID, nil } tracks.PUT("/:id", authMiddleware.RequireOwnershipOrAdmin("track", trackOwnerResolver), trackHandler.UpdateTrack) tracks.DELETE("/:id", authMiddleware.RequireOwnershipOrAdmin("track", trackOwnerResolver), trackHandler.DeleteTrack) } // Playlist routes playlists := protected.Group("/playlists") { playlists.GET("", playlistHandler.GetPlaylists) playlists.POST("", playlistHandler.CreatePlaylist) playlists.GET("/:id", playlistHandler.GetPlaylist) } // User routes usersGroup := protected.Group("/users") { usersGroup.GET("", profileHandler.ListUsers) usersGroup.GET("/:id", profileHandler.GetProfile) // Ownership resolver for users userOwnerResolver := func(c *gin.Context) (uuid.UUID, error) { userIDStr := c.Param("id") userID, err := uuid.Parse(userIDStr) if err != nil { return uuid.Nil, err } return userID, nil } usersGroup.PUT("/:id", authMiddleware.RequireOwnershipOrAdmin("user", userOwnerResolver), profileHandler.UpdateProfile) } cleanup := func() { // Cleanup handled by t.TempDir() } return router, db, jwtService, users, cleanup } // generateToken génère un token JWT pour un utilisateur func generateToken(jwtService *services.JWTService, user *models.User) (string, error) { return jwtService.GenerateAccessToken(user) } // TestAuthorization_NoToken teste que les requêtes sans token sont rejetées func TestAuthorization_NoToken(t *testing.T) { router, _, _, _, cleanup := setupAuthorizationTestRouter(t) defer cleanup() // Test various protected endpoints without token endpoints := []struct { method string path string }{ {"GET", "/api/v1/tracks"}, {"GET", "/api/v1/tracks/123e4567-e89b-12d3-a456-426614174000"}, {"POST", "/api/v1/tracks"}, {"PUT", "/api/v1/tracks/123e4567-e89b-12d3-a456-426614174000"}, {"DELETE", "/api/v1/tracks/123e4567-e89b-12d3-a456-426614174000"}, {"GET", "/api/v1/admin/users"}, {"GET", "/api/v1/users"}, {"PUT", "/api/v1/users/123e4567-e89b-12d3-a456-426614174000"}, } for _, endpoint := range endpoints { t.Run(fmt.Sprintf("%s %s", endpoint.method, endpoint.path), func(t *testing.T) { req := httptest.NewRequest(endpoint.method, endpoint.path, nil) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code, "Request without token should be rejected with 401") }) } } // TestAuthorization_InvalidToken teste que les requêtes avec token invalide sont rejetées func TestAuthorization_InvalidToken(t *testing.T) { router, _, _, _, cleanup := setupAuthorizationTestRouter(t) defer cleanup() invalidTokens := []string{ "invalid-token", "Bearer invalid-token", "not-a-bearer-token", "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.invalid", "", } for _, token := range invalidTokens { t.Run(fmt.Sprintf("Invalid token: %s", token), func(t *testing.T) { req := httptest.NewRequest(http.MethodGet, "/api/v1/tracks", nil) if token != "" { req.Header.Set("Authorization", token) } w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code, "Request with invalid token should be rejected with 401") }) } } // TestAuthorization_ExpiredToken teste que les requêtes avec token expiré sont rejetées func TestAuthorization_ExpiredToken(t *testing.T) { router, _, _, users, cleanup := setupAuthorizationTestRouter(t) defer cleanup() // Create expired token manually userID := uuid.New() for uid := range users { userID = uid break } user := users[userID] // Create expired claims manually claims := models.CustomClaims{ UserID: user.ID, Email: user.Email, Username: user.Username, Role: user.Role, TokenVersion: user.TokenVersion, TokenType: "access", RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(-1 * time.Hour)), // Expired IssuedAt: jwt.NewNumericDate(time.Now().Add(-2 * time.Hour)), Issuer: "test-issuer", Audience: jwt.ClaimStrings{"test-audience"}, ID: uuid.NewString(), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) expiredToken, err := token.SignedString([]byte("test-secret-key-must-be-32-chars-long")) require.NoError(t, err) req := httptest.NewRequest(http.MethodGet, "/api/v1/tracks", nil) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", expiredToken)) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code, "Request with expired token should be rejected with 401") } // TestAuthorization_RegularUser_AdminEndpoint teste qu'un utilisateur régulier ne peut pas accéder aux endpoints admin func TestAuthorization_RegularUser_AdminEndpoint(t *testing.T) { router, _, jwtService, users, cleanup := setupAuthorizationTestRouter(t) defer cleanup() // Find regular user (not admin) var regularUserID uuid.UUID for uid, user := range users { if user.Email == "user@example.com" { regularUserID = uid break } } regularUser := users[regularUserID] token, err := generateToken(jwtService, regularUser) require.NoError(t, err) req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/users", nil) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusForbidden, w.Code, "Regular user should not be able to access admin endpoints") } // TestAuthorization_RegularUser_CreatorEndpoint teste qu'un utilisateur régulier ne peut pas créer de contenu func TestAuthorization_RegularUser_CreatorEndpoint(t *testing.T) { router, _, jwtService, users, cleanup := setupAuthorizationTestRouter(t) defer cleanup() // Find regular user var regularUserID uuid.UUID for uid, user := range users { if user.Email == "user@example.com" { regularUserID = uid break } } regularUser := users[regularUserID] token, err := generateToken(jwtService, regularUser) require.NoError(t, err) // Try to upload a track (requires creator role) payload := map[string]interface{}{ "title": "Test Track", "artist": "Test Artist", } body, _ := json.Marshal(payload) req := httptest.NewRequest(http.MethodPost, "/api/v1/tracks", bytes.NewBuffer(body)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusForbidden, w.Code, "Regular user should not be able to create content") } // TestAuthorization_AdminUser_AdminEndpoint teste qu'un admin peut accéder aux endpoints admin func TestAuthorization_AdminUser_AdminEndpoint(t *testing.T) { router, _, jwtService, users, cleanup := setupAuthorizationTestRouter(t) defer cleanup() // Find admin user var adminUserID uuid.UUID for uid, user := range users { if user.Email == "admin@example.com" { adminUserID = uid break } } adminUser := users[adminUserID] token, err := generateToken(jwtService, adminUser) require.NoError(t, err) req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/users", nil) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusOK, w.Code, "Admin user should be able to access admin endpoints") } // TestAuthorization_CreatorUser_CreatorEndpoint teste qu'un creator peut créer du contenu func TestAuthorization_CreatorUser_CreatorEndpoint(t *testing.T) { router, _, jwtService, users, cleanup := setupAuthorizationTestRouter(t) defer cleanup() // Find creator user var creatorUserID uuid.UUID for uid, user := range users { if user.Email == "creator@example.com" { creatorUserID = uid break } } creatorUser := users[creatorUserID] token, err := generateToken(jwtService, creatorUser) require.NoError(t, err) // Try to upload a track (requires creator role) payload := map[string]interface{}{ "title": "Test Track", "artist": "Test Artist", } body, _ := json.Marshal(payload) req := httptest.NewRequest(http.MethodPost, "/api/v1/tracks", bytes.NewBuffer(body)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) // Should not be forbidden (may be 400/422 for validation, but not 403) assert.NotEqual(t, http.StatusForbidden, w.Code, "Creator user should be able to create content") } // TestAuthorization_Ownership_OtherUserResource teste qu'un utilisateur ne peut pas modifier les ressources d'un autre utilisateur func TestAuthorization_Ownership_OtherUserResource(t *testing.T) { router, db, jwtService, users, cleanup := setupAuthorizationTestRouter(t) defer cleanup() // Create a track owned by creator user var creatorUserID uuid.UUID for uid, user := range users { if user.Email == "creator@example.com" { creatorUserID = uid break } } trackID := uuid.New() track := &models.Track{ ID: trackID, UserID: creatorUserID, Title: "Creator's Track", IsPublic: true, } err := db.Create(track).Error require.NoError(t, err) // Try to update as regular user var regularUserID uuid.UUID for uid, user := range users { if user.Email == "user@example.com" { regularUserID = uid break } } regularUser := users[regularUserID] token, err := generateToken(jwtService, regularUser) require.NoError(t, err) payload := map[string]interface{}{ "title": "Hacked Title", } body, _ := json.Marshal(payload) req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/api/v1/tracks/%s", trackID), bytes.NewBuffer(body)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusForbidden, w.Code, "User should not be able to modify another user's resource") } // TestAuthorization_Ownership_OwnResource teste qu'un utilisateur peut modifier ses propres ressources func TestAuthorization_Ownership_OwnResource(t *testing.T) { router, db, jwtService, users, cleanup := setupAuthorizationTestRouter(t) defer cleanup() // Create a track owned by regular user var regularUserID uuid.UUID for uid, user := range users { if user.Email == "user@example.com" { regularUserID = uid break } } trackID := uuid.New() track := &models.Track{ ID: trackID, UserID: regularUserID, Title: "My Track", IsPublic: true, } err := db.Create(track).Error require.NoError(t, err) regularUser := users[regularUserID] token, err := generateToken(jwtService, regularUser) require.NoError(t, err) payload := map[string]interface{}{ "title": "Updated Title", } body, _ := json.Marshal(payload) req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/api/v1/tracks/%s", trackID), bytes.NewBuffer(body)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) // Should not be forbidden (may be 400/422 for validation, but not 403) assert.NotEqual(t, http.StatusForbidden, w.Code, "User should be able to modify their own resource") } // TestAuthorization_Admin_OwnershipOverride teste qu'un admin peut modifier n'importe quelle ressource func TestAuthorization_Admin_OwnershipOverride(t *testing.T) { router, db, jwtService, users, cleanup := setupAuthorizationTestRouter(t) defer cleanup() // Create a track owned by regular user var regularUserID uuid.UUID for uid, user := range users { if user.Email == "user@example.com" { regularUserID = uid break } } trackID := uuid.New() track := &models.Track{ ID: trackID, UserID: regularUserID, Title: "User's Track", IsPublic: true, } err := db.Create(track).Error require.NoError(t, err) // Try to update as admin var adminUserID uuid.UUID for uid, user := range users { if user.Email == "admin@example.com" { adminUserID = uid break } } adminUser := users[adminUserID] token, err := generateToken(jwtService, adminUser) require.NoError(t, err) payload := map[string]interface{}{ "title": "Admin Updated Title", } body, _ := json.Marshal(payload) req := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/api/v1/tracks/%s", trackID), bytes.NewBuffer(body)) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() router.ServeHTTP(w, req) // Should not be forbidden (admin can override ownership) assert.NotEqual(t, http.StatusForbidden, w.Code, "Admin should be able to modify any resource") } // TestAuthorization_TokenVersionMismatch teste que les tokens avec version incorrecte sont rejetés func TestAuthorization_TokenVersionMismatch(t *testing.T) { router, db, jwtService, users, cleanup := setupAuthorizationTestRouter(t) defer cleanup() // Get a user var userID uuid.UUID for uid := range users { userID = uid break } // Get user and generate token with old version user := users[userID] token, err := generateToken(jwtService, user) require.NoError(t, err) // Update user's token version (simulating logout or password change) err = db.Model(&models.User{}).Where("id = ?", userID).Update("token_version", 2).Error require.NoError(t, err) req := httptest.NewRequest(http.MethodGet, "/api/v1/tracks", nil) req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) w := httptest.NewRecorder() router.ServeHTTP(w, req) assert.Equal(t, http.StatusUnauthorized, w.Code, "Token with mismatched version should be rejected") }