package services import ( "context" "database/sql" "fmt" "github.com/google/uuid" "go.uber.org/zap" "gorm.io/gorm" "gorm.io/gorm/clause" "veza-backend-api/internal/database" "veza-backend-api/internal/models" ) // 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 { var err error // 1. VALIDATION : User existe ? (SELECT avec FOR UPDATE pour éviter race condition) var user models.User if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&user, userID).Error; err != nil { if err == gorm.ErrRecordNotFound { return fmt.Errorf("user not found") } return fmt.Errorf("AssignRoleToUser: failed to check user existence: %w", err) } // 2. VALIDATION : Role existe ? (SELECT avec FOR UPDATE pour éviter race condition) var role models.Role if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&role, roleID).Error; err != nil { if err == gorm.ErrRecordNotFound { return fmt.Errorf("role not found") } return fmt.Errorf("AssignRoleToUser: failed to check role existence: %w", err) } // 1. Vérifier si l'utilisateur a déjà ce rôle (avec verrou) var existingRole models.UserRole err = tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("user_id = ? AND role_id = ?", userID, roleID). First(&existingRole).Error if err == nil { return fmt.Errorf("role already assigned to user") } if err != gorm.ErrRecordNotFound { return fmt.Errorf("failed to check existing role: %w", err) } // 4. INSERTION : Assignation (INSERT dans la transaction) // Note: 'role' column is required by schema (legacy/redundant field) err = tx.Exec(` INSERT INTO user_roles (id, user_id, role_id, role, created_at) VALUES (gen_random_uuid(), ?, ?, ?, CURRENT_TIMESTAMP) `, userID, roleID, role.Name).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 }