veza/veza-backend-api/tests/security/authorization_test.go
2026-03-05 19:22:31 +01:00

653 lines
20 KiB
Go

//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")
}