veza/veza-backend-api/internal/services/rbac_service.go
okinrev b7955a680c P0: stabilisation backend/chat/stream + nouvelle base migrations v1
Backend Go:
- Remplacement complet des anciennes migrations par la base V1 alignée sur ORIGIN.
- Durcissement global du parsing JSON (BindAndValidateJSON + RespondWithAppError).
- Sécurisation de config.go, CORS, statuts de santé et monitoring.
- Implémentation des transactions P0 (RBAC, duplication de playlists, social toggles).
- Ajout d’un job worker structuré (emails, analytics, thumbnails) + tests associés.
- Nouvelle doc backend : AUDIT_CONFIG, BACKEND_CONFIG, AUTH_PASSWORD_RESET, JOB_WORKER_*.

Chat server (Rust):
- Refonte du pipeline JWT + sécurité, audit et rate limiting avancé.
- Implémentation complète du cycle de message (read receipts, delivered, edit/delete, typing).
- Nettoyage des panics, gestion d’erreurs robuste, logs structurés.
- Migrations chat alignées sur le schéma UUID et nouvelles features.

Stream server (Rust):
- Refonte du moteur de streaming (encoding pipeline + HLS) et des modules core.
- Transactions P0 pour les jobs et segments, garanties d’atomicité.
- Documentation détaillée de la pipeline (AUDIT_STREAM_*, DESIGN_STREAM_PIPELINE, TRANSACTIONS_P0_IMPLEMENTATION).

Documentation & audits:
- TRIAGE.md et AUDIT_STABILITY.md à jour avec l’état réel des 3 services.
- Cartographie complète des migrations et des transactions (DB_MIGRATIONS_*, DB_TRANSACTION_PLAN, AUDIT_DB_TRANSACTIONS, TRANSACTION_TESTS_PHASE3).
- Scripts de reset et de cleanup pour la lab DB et la V1.

Ce commit fige l’ensemble du travail de stabilisation P0 (UUID, backend, chat et stream) avant les phases suivantes (Coherence Guardian, WS hardening, etc.).
2025-12-06 11:14:38 +01:00

409 lines
No EOL
12 KiB
Go

package services
import (
"context"
"database/sql"
"fmt"
"github.com/google/uuid"
"veza-backend-api/internal/database"
"go.uber.org/zap"
"gorm.io/gorm"
)
// RBACService handles role-based access control
type RBACService struct {
db *database.Database
logger *zap.Logger
}
// NewRBACService creates a new RBAC service
func NewRBACService(db *database.Database, logger *zap.Logger) *RBACService {
return &RBACService{
db: db,
logger: logger,
}
}
// Role represents a user role
type Role struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Permissions []Permission `json:"permissions"`
IsSystem bool `json:"is_system"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// Permission represents a permission
type Permission struct {
ID uuid.UUID `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Resource string `json:"resource"`
Action string `json:"action"`
CreatedAt string `json:"created_at"`
}
// UserRole represents a user's role assignment
type UserRole struct {
ID uuid.UUID `json:"id"`
UserID uuid.UUID `json:"user_id"`
RoleID uuid.UUID `json:"role_id"`
Role *Role `json:"role,omitempty"`
}
// CreateRole creates a new role
func (s *RBACService) CreateRole(ctx context.Context, name, description string, permissions []uuid.UUID) (*Role, error) {
// Check if role already exists
var count int
err := s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM roles WHERE name = $1", name).Scan(&count)
if err != nil {
return nil, fmt.Errorf("failed to check role existence: %w", err)
}
if count > 0 {
return nil, fmt.Errorf("role with name '%s' already exists", name)
}
// Create role
var roleID uuid.UUID
query := `
INSERT INTO roles (id, name, description, is_system, created_at, updated_at)
VALUES (gen_random_uuid(), $1, $2, false, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
RETURNING id
`
err = s.db.QueryRowContext(ctx, query, name, description).Scan(&roleID)
if err != nil {
return nil, fmt.Errorf("failed to create role: %w", err)
}
// Assign permissions to role
if len(permissions) > 0 {
for _, permID := range permissions {
_, err = s.db.ExecContext(ctx, `
INSERT INTO role_permissions (role_id, permission_id, created_at)
VALUES ($1, $2, CURRENT_TIMESTAMP)
`, roleID, permID)
if err != nil {
s.logger.Error("Failed to assign permission to role", zap.Error(err))
// Continue with other permissions
}
}
}
// Get the created role with permissions
role, err := s.GetRoleByID(ctx, roleID)
if err != nil {
return nil, fmt.Errorf("failed to get created role: %w", err)
}
s.logger.Info("Role created successfully", zap.String("role_name", name), zap.String("role_id", roleID.String()))
return role, nil
}
// GetRoleByID gets a role by ID
func (s *RBACService) GetRoleByID(ctx context.Context, roleID uuid.UUID) (*Role, error) {
query := `
SELECT r.id, r.name, r.description, r.is_system, r.created_at, r.updated_at
FROM roles r
WHERE r.id = $1
`
var role Role
err := s.db.QueryRowContext(ctx, query, roleID).Scan(
&role.ID, &role.Name, &role.Description, &role.IsSystem, &role.CreatedAt, &role.UpdatedAt,
)
if err != nil {
if err == sql.ErrNoRows {
return nil, fmt.Errorf("role not found")
}
return nil, fmt.Errorf("failed to get role: %w", err)
}
// Get permissions for this role
permissions, err := s.GetRolePermissions(ctx, roleID)
if err != nil {
s.logger.Error("Failed to get role permissions", zap.Error(err))
} else {
role.Permissions = permissions
}
return &role, nil
}
// GetRolePermissions gets permissions for a role
func (s *RBACService) GetRolePermissions(ctx context.Context, roleID uuid.UUID) ([]Permission, error) {
query := `
SELECT p.id, p.name, p.description, p.resource, p.action, p.created_at
FROM permissions p
JOIN role_permissions rp ON p.id = rp.permission_id
WHERE rp.role_id = $1
ORDER BY p.name
`
rows, err := s.db.QueryContext(ctx, query, roleID)
if err != nil {
return nil, fmt.Errorf("failed to get role permissions: %w", err)
}
defer rows.Close()
var permissions []Permission
for rows.Next() {
var perm Permission
err := rows.Scan(&perm.ID, &perm.Name, &perm.Description, &perm.Resource, &perm.Action, &perm.CreatedAt)
if err != nil {
s.logger.Error("Failed to scan permission", zap.Error(err))
continue
}
permissions = append(permissions, perm)
}
return permissions, nil
}
// AssignRoleToUser assigns a role to a user
// MIGRATION UUID: userID migré vers uuid.UUID, roleID aussi
// Transactionnelle : Toutes les vérifications et l'INSERT sont dans une seule transaction avec FOR UPDATE
func (s *RBACService) AssignRoleToUser(ctx context.Context, userID uuid.UUID, roleID uuid.UUID) error {
return s.db.GormDB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
// 1. VALIDATION : User existe ? (SELECT avec FOR UPDATE pour éviter race condition)
var userCount int64
err := tx.Raw("SELECT COUNT(*) FROM users WHERE id = ? FOR UPDATE", userID).Scan(&userCount).Error
if err != nil {
return fmt.Errorf("AssignRoleToUser: failed to check user existence: %w", err)
}
if userCount == 0 {
return fmt.Errorf("user not found")
}
// 2. VALIDATION : Role existe ? (SELECT avec FOR UPDATE pour éviter race condition)
var roleCount int64
err = tx.Raw("SELECT COUNT(*) FROM roles WHERE id = ? FOR UPDATE", roleID).Scan(&roleCount).Error
if err != nil {
return fmt.Errorf("AssignRoleToUser: failed to check role existence: %w", err)
}
if roleCount == 0 {
return fmt.Errorf("role not found")
}
// 3. VALIDATION : Doublon ? (SELECT dans la transaction)
var assignmentCount int64
err = tx.Raw("SELECT COUNT(*) FROM user_roles WHERE user_id = ? AND role_id = ?", userID, roleID).Scan(&assignmentCount).Error
if err != nil {
return fmt.Errorf("AssignRoleToUser: failed to check role assignment: %w", err)
}
if assignmentCount > 0 {
return fmt.Errorf("role already assigned to user")
}
// 4. INSERTION : Assignation (INSERT dans la transaction)
err = tx.Exec(`
INSERT INTO user_roles (id, user_id, role_id, created_at)
VALUES (gen_random_uuid(), ?, ?, CURRENT_TIMESTAMP)
`, userID, roleID).Error
if err != nil {
// Si contrainte UNIQUE violée (race condition détectée), la contrainte DB gère cela
// La vérification du doublon avant l'INSERT devrait gérer la plupart des cas
return fmt.Errorf("AssignRoleToUser: failed to assign role to user: %w", err)
}
// 5. LOG (dans la transaction, mais ne dépend pas d'états non commit)
s.logger.Info("Role assigned to user successfully",
zap.String("user_id", userID.String()),
zap.String("role_id", roleID.String()),
)
// 6. RETOUR nil = commit automatique
return nil
})
}
// RemoveRoleFromUser removes a role from a user
// MIGRATION UUID: userID migré vers uuid.UUID, roleID aussi
func (s *RBACService) RemoveRoleFromUser(ctx context.Context, userID uuid.UUID, roleID uuid.UUID) error {
result, err := s.db.ExecContext(ctx, `
DELETE FROM user_roles
WHERE user_id = $1 AND role_id = $2
`, userID, roleID)
if err != nil {
return fmt.Errorf("failed to remove role from user: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("role not assigned to user")
}
s.logger.Info("Role removed from user successfully", zap.String("user_id", userID.String()), zap.String("role_id", roleID.String()))
return nil
}
// GetUserRoles gets all roles for a user
func (s *RBACService) GetUserRoles(ctx context.Context, userID uuid.UUID) ([]*Role, error) {
query := `
SELECT r.id, r.name, r.description, r.is_system, r.created_at, r.updated_at
FROM roles r
JOIN user_roles ur ON r.id = ur.role_id
WHERE ur.user_id = $1
ORDER BY r.name
`
rows, err := s.db.QueryContext(ctx, query, userID)
if err != nil {
return nil, fmt.Errorf("failed to get user roles: %w", err)
}
defer rows.Close()
var roles []*Role
for rows.Next() {
var role Role
err := rows.Scan(&role.ID, &role.Name, &role.Description, &role.IsSystem, &role.CreatedAt, &role.UpdatedAt)
if err != nil {
s.logger.Error("Failed to scan role", zap.Error(err))
continue
}
// Get permissions for this role
permissions, err := s.GetRolePermissions(ctx, role.ID)
if err != nil {
s.logger.Error("Failed to get role permissions", zap.Error(err))
} else {
role.Permissions = permissions
}
roles = append(roles, &role)
}
return roles, nil
}
// CheckPermission checks if a user has a specific permission
func (s *RBACService) CheckPermission(ctx context.Context, userID uuid.UUID, resource, action string) (bool, error) {
query := `
SELECT COUNT(*)
FROM permissions p
JOIN role_permissions rp ON p.id = rp.permission_id
JOIN user_roles ur ON rp.role_id = ur.role_id
WHERE ur.user_id = $1 AND p.resource = $2 AND p.action = $3
`
var count int
err := s.db.QueryRowContext(ctx, query, userID, resource, action).Scan(&count)
if err != nil {
return false, fmt.Errorf("failed to check permission: %w", err)
}
return count > 0, nil
}
// GetUserPermissions gets all permissions for a user
func (s *RBACService) GetUserPermissions(ctx context.Context, userID uuid.UUID) ([]Permission, error) {
query := `
SELECT DISTINCT p.id, p.name, p.description, p.resource, p.action, p.created_at
FROM permissions p
JOIN role_permissions rp ON p.id = rp.permission_id
JOIN user_roles ur ON rp.role_id = ur.role_id
WHERE ur.user_id = $1
ORDER BY p.resource, p.action
`
rows, err := s.db.QueryContext(ctx, query, userID)
if err != nil {
return nil, fmt.Errorf("failed to get user permissions: %w", err)
}
defer rows.Close()
var permissions []Permission
for rows.Next() {
var perm Permission
err := rows.Scan(&perm.ID, &perm.Name, &perm.Description, &perm.Resource, &perm.Action, &perm.CreatedAt)
if err != nil {
s.logger.Error("Failed to scan permission", zap.Error(err))
continue
}
permissions = append(permissions, perm)
}
return permissions, nil
}
// CreatePermission creates a new permission
func (s *RBACService) CreatePermission(ctx context.Context, name, description, resource, action string) (*Permission, error) {
// Check if permission already exists
var count int
err := s.db.QueryRowContext(ctx, "SELECT COUNT(*) FROM permissions WHERE resource = $1 AND action = $2", resource, action).Scan(&count)
if err != nil {
return nil, fmt.Errorf("failed to check permission existence: %w", err)
}
if count > 0 {
return nil, fmt.Errorf("permission with resource '%s' and action '%s' already exists", resource, action)
}
// Create permission
var permID uuid.UUID
query := `
INSERT INTO permissions (id, name, description, resource, action, created_at)
VALUES (gen_random_uuid(), $1, $2, $3, $4, CURRENT_TIMESTAMP)
RETURNING id
`
err = s.db.QueryRowContext(ctx, query, name, description, resource, action).Scan(&permID)
if err != nil {
return nil, fmt.Errorf("failed to create permission: %w", err)
}
permission := &Permission{
ID: permID,
Name: name,
Description: description,
Resource: resource,
Action: action,
}
s.logger.Info("Permission created successfully", zap.String("permission_name", name))
return permission, nil
}
// GetAllRoles gets all roles
func (s *RBACService) GetAllRoles(ctx context.Context) ([]*Role, error) {
query := `
SELECT id, name, description, is_system, created_at, updated_at
FROM roles
ORDER BY name
`
rows, err := s.db.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("failed to get roles: %w", err)
}
defer rows.Close()
var roles []*Role
for rows.Next() {
var role Role
err := rows.Scan(&role.ID, &role.Name, &role.Description, &role.IsSystem, &role.CreatedAt, &role.UpdatedAt)
if err != nil {
s.logger.Error("Failed to scan role", zap.Error(err))
continue
}
// Get permissions for this role
permissions, err := s.GetRolePermissions(ctx, role.ID)
if err != nil {
s.logger.Error("Failed to get role permissions", zap.Error(err))
} else {
role.Permissions = permissions
}
roles = append(roles, &role)
}
return roles, nil
}