UserSecurityAdapter: Un Enfoque Desacoplado para la Seguridad en Casos de Uso
Resumen
Este documento presenta la implementación de un UserSecurityAdapter, un componente que desacopla la seguridad del caso de uso, asegurando que la validación de permisos y la aplicación de filtros de seguridad ocurran antes de ejecutar la lógica de negocio. Esta arquitectura mantiene los principios de separación de responsabilidades, haciendo que los casos de uso sean más puros y reutilizables mientras que la seguridad se maneja de forma centralizada.
Introducción
En arquitecturas limpias, la seguridad es un cross-cutting concern que no debe estar acoplada a los casos de uso ni a la infraestructura de persistencia. La implementación de UserSecurityAdapter permite:
Inyectar seguridad antes de ejecutar el caso de uso.
Evitar lógica de permisos en el caso de uso.
Fusionar automáticamente filtros de usuario con reglas de seguridad.
Proteger la integridad de los datos sin modificar la lógica de negocio.
Este enfoque se basa en principios de arquitectura hexagonal y separación de responsabilidades, asegurando que la seguridad pueda evolucionar independientemente del resto del sistema.
Arquitectura
Diagrama de Flujo de Datos
[ Cliente (REST, gRPC, GraphQL) ]
↓
[ UserSecurityAdapter ] ← Aplica Seguridad
↓
[ UserUsecase ] ← Solo recibe filtros listos
↓
[ UserRepository ] ← Ejecuta consulta segura
↓
[ Base de Datos ]
Principales Componentes
UserSecurityAdapter
Aplica reglas de seguridad antes de ejecutar el caso de uso.
Fusiona filtros de usuario con los de seguridad.
Desacopla validación de acceso del caso de uso.
UserUsecase
Solo ejecuta la lógica de negocio con filtros ya procesados.
No maneja autenticación ni permisos.
UserRepository
- Recibe filtros limpios y ejecuta la consulta en la base de datos.
Implementación
1️⃣ UserSecurityAdapter
package adapter
import (
"context"
"myapp/security"
"myapp/usecase"
)
// UserSecurityAdapter maneja seguridad antes de llamar al caso de uso
type UserSecurityAdapter struct {
Usecase *usecase.UserUsecase
Security *security.SecurityService
}
// GetUsers aplica seguridad antes de llamar al caso de uso
func (a *UserSecurityAdapter) GetUsers(ctx context.Context, userFilters Filters) ([]User, error) {
tidyFilters := a.Security.GetFilters(ctx, "users")
finalFilters := a.Security.MergeFilters(userFilters, tidyFilters)
return a.Usecase.GetUsers(ctx, finalFilters)
}
✅ La seguridad y los filtros se manejan en el adapter, sin tocar el caso de uso.
2️⃣ Caso de Uso (UserUsecase)
package usecase
import (
"context"
"myapp/data"
)
// UserUsecase sin seguridad, solo recibe filtros listos
type UserUsecase struct {
Data *data.UserRepository
}
// Constructor del caso de uso (NO PASAMOS FILTROS NI SEGURIDAD)
func NewUserUsecase(repo *data.UserRepository) *UserUsecase {
return &UserUsecase{Data: repo}
}
// GetUsers recibe filtros YA LISTOS desde el adapter
func (uc *UserUsecase) GetUsers(ctx context.Context, filters Filters) ([]User, error) {
return uc.Data.GetUsers(ctx, filters)
}
✅ Este caso de uso es puro, solo ejecuta lógica de negocio con filtros ya aplicados.
3️⃣ Módulo de Seguridad (SecurityService)
package security
import (
"context"
)
// SecurityService maneja filtros de seguridad para cualquier modelo
type SecurityService struct{}
// GetFilters obtiene filtros de seguridad según el modelo
func (s *SecurityService) GetFilters(ctx context.Context, model string) Filters {
filters := make(Filters)
switch model {
case "users":
if companyID, ok := ctx.Value("company_id").(int); ok {
filters["company_id"] = companyID
}
case "orders":
if userID, ok := ctx.Value("user_id").(int); ok {
filters["customer_id"] = userID
}
}
return filters
}
// MergeFilters combina filtros de usuario con seguridad
func (s *SecurityService) MergeFilters(userFilters Filters, securityFilters Filters) Filters {
for k, v := range securityFilters {
userFilters[k] = v // La seguridad domina sobre los filtros del usuario
}
return userFilters
}
✅ Los filtros de seguridad se gestionan de manera centralizada sin tocar los casos de uso.
4️⃣ Repositorio (UserRepository)
package data
import (
"context"
"fmt"
)
// UserRepository ejecuta la consulta con filtros ya procesados
type UserRepository struct{}
// GetUsers ejecuta la consulta con los filtros YA PROCESADOS
func (r *UserRepository) GetUsers(ctx context.Context, filters Filters) ([]User, error) {
query := "SELECT * FROM users WHERE 1=1"
var args []interface{}
for key, value := range filters {
query += fmt.Sprintf(" AND %s = ?", key)
args = append(args, value)
}
fmt.Println("Ejecutando query:", query, args)
return nil, nil
}
✅ La consulta SQL se ejecuta con seguridad aplicada antes de llegar aquí.
5️⃣ Ejemplo de Uso en un Endpoint
package main
import (
"context"
"net/http"
"myapp/adapter"
"myapp/security"
"myapp/usecase"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
securityService := &security.SecurityService{}
userUsecase := usecase.NewUserUsecase(nil) // NO PASAMOS FILTROS NI SEGURIDAD AQUÍ
userAdapter := &adapter.UserSecurityAdapter{Usecase: userUsecase, Security: securityService}
r.GET("/users", func(c *gin.Context) {
ctx := context.WithValue(c.Request.Context(), "user_id", 42)
userFilters := Filters{"role": "admin"} // Filtros que envió el usuario
users, err := userAdapter.GetUsers(ctx, userFilters)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, users)
})
r.Run(":8080")
}
✅ El adapter maneja la seguridad, el caso de uso solo ejecuta lógica de negocio.
📌 Conclusión
El UserSecurityAdapter permite:
🔥 Desacoplar la seguridad del caso de uso.
🔥 Aplicar seguridad antes de ejecutar lógica de negocio.
🔥 Hacer que los casos de uso sean puros y reutilizables.
🔥 Ejecutar filtros de seguridad sin modificar los queries manualmente.