Primera tanda del hueco reportado en el commit anterior: el permiso tapaba
la página HTML pero no los datos. Cualquier usuario con sesión podía pedir
GET /app/loadusers o DELETE /app/doc/paginas/:id escribiendo la URL, sin
tener el submódulo asignado.
No alcanzaba con poner MenuMiddleware en esas rutas: compara la ruta pedida
contra los submódulos del rol, y el endpoint de datos vive en otra ruta que
la pantalla (/app/loadusers alimenta /app/users). Compararla consigo misma
nunca coincidiría y dejaría afuera hasta a quien sí tiene el permiso.
RequiereModulo("/app/users") declara en la ruta a qué pantalla pertenece el
endpoint, y valida ese submódulo. El administrador sigue entrando a todo,
igual que en MenuMiddleware.
Esta tanda cubre lo que más duele: identidad y permisos (usuarios, roles,
módulos, submódulos), donde una fuga es escalada de privilegios, y
Documentación, que es el módulo que disparó la auditoría.
Quedan las otras tandas (contabilidad, facturas, clientes, servidores...).
Se hace por partes a propósito: cerrar 400 rutas de una puede dejar gente
afuera de pantallas que hoy usa.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
184 lines
5.8 KiB
Go
Executable File
184 lines
5.8 KiB
Go
Executable File
package middlewares
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/auth"
|
|
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
|
|
)
|
|
|
|
func MenuMiddleware(c *fiber.Ctx) error {
|
|
// Verificar si el usuario está logueado
|
|
if !auth.IsLoggedIn(c) {
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
|
"error": true,
|
|
"message": "Usuario no autenticado",
|
|
})
|
|
}
|
|
|
|
// Obtener los datos del usuario autenticado
|
|
user, err := auth.User(c)
|
|
if err != nil {
|
|
// Si ocurre un error al obtener el usuario, finalizar la solicitud
|
|
fmt.Printf("Error al obtener usuario: %v\n", err)
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
|
"error": true,
|
|
"message": "Usuario no válido o sesión expirada",
|
|
})
|
|
}
|
|
|
|
// Buscar los módulos y submódulos del usuario
|
|
userModules, err := models.FindUserByID(user.ID)
|
|
if err != nil {
|
|
fmt.Printf("Error al buscar datos del usuario: %v\n", err)
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": true,
|
|
"message": "Error al obtener datos del usuario",
|
|
})
|
|
}
|
|
|
|
// Organizar los módulos y submódulos
|
|
moduleMap := make(map[string][]map[string]string)
|
|
|
|
for _, submodule := range userModules.Role.Submodules {
|
|
// Recuperar el nombre del módulo
|
|
moduleName := submodule.Module.Title
|
|
// Crear un mapa para cada submódulo que contenga el título y la URL
|
|
submoduleData := map[string]string{
|
|
"title": submodule.Title,
|
|
"url": submodule.Url, // Suponiendo que el submódulo tiene un campo `URL`
|
|
}
|
|
// Agregar el submódulo a la lista correspondiente del módulo
|
|
moduleMap[moduleName] = append(moduleMap[moduleName], submoduleData)
|
|
}
|
|
|
|
// Ordenar los módulos alfabéticamente
|
|
var sortedModules []string
|
|
for module := range moduleMap {
|
|
sortedModules = append(sortedModules, module)
|
|
}
|
|
sort.Strings(sortedModules)
|
|
|
|
// Ordenar los submódulos de cada módulo alfabéticamente
|
|
for _, moduleName := range sortedModules {
|
|
// Ordenar los submódulos alfabéticamente por título
|
|
sort.Slice(moduleMap[moduleName], func(i, j int) bool {
|
|
return moduleMap[moduleName][i]["title"] < moduleMap[moduleName][j]["title"]
|
|
})
|
|
}
|
|
c.Locals("userModules", moduleMap)
|
|
|
|
// Obtener un array solo con las URLs
|
|
var urls []string
|
|
for _, submodules := range moduleMap {
|
|
for _, submoduleData := range submodules {
|
|
urls = append(urls, submoduleData["url"])
|
|
}
|
|
}
|
|
|
|
// Opcional: adjuntar la lista de URLs al contexto
|
|
c.Locals("urls", urls)
|
|
|
|
// Los administradores tienen acceso a todas las rutas
|
|
if user.IsAdmin {
|
|
return c.Next()
|
|
}
|
|
|
|
// Se compara la ruta COMPLETA contra las URLs permitidas.
|
|
//
|
|
// Antes se comparaba solo el último segmento (lo que va después del último
|
|
// '/'), y eso rompía en las dos direcciones: dos rutas distintas que
|
|
// terminan igual quedaban indistinguibles — con permiso sobre
|
|
// /app/doc/categorias se entraba a cualquier otra ruta terminada en
|
|
// "categorias" — y a la vez el permiso parecía no aplicarse donde sí
|
|
// debía, porque el segmento coincidía por casualidad.
|
|
urlAllowed := PuedeVerRuta(c.Path(), urls)
|
|
|
|
// Si la URL no está permitida, devolver un error
|
|
if !urlAllowed {
|
|
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
|
"error": true,
|
|
"message": "Acceso no permitido a esta ruta",
|
|
})
|
|
}
|
|
|
|
// Continuar con la siguiente función de middleware o manejador
|
|
return c.Next()
|
|
}
|
|
|
|
// PuedeVerRuta decide si una ruta está cubierta por alguna de las URLs que el
|
|
// rol tiene asignadas.
|
|
//
|
|
// Coincide la ruta exacta o cualquier ruta por debajo de ella: quien tiene
|
|
// /app/doc/paginas también puede entrar a /app/doc/paginas/7, que es la misma
|
|
// pantalla con un detalle. No coincide /app/doc/paginas-privadas, porque el
|
|
// corte se hace en el separador y no en el prefijo de texto — si no, un
|
|
// permiso abriría rutas vecinas que solo comparten el comienzo del nombre.
|
|
func PuedeVerRuta(ruta string, permitidas []string) bool {
|
|
ruta = strings.TrimRight(ruta, "/")
|
|
if ruta == "" {
|
|
ruta = "/"
|
|
}
|
|
for _, u := range permitidas {
|
|
u = strings.TrimRight(strings.TrimSpace(u), "/")
|
|
if u == "" {
|
|
continue
|
|
}
|
|
if ruta == u || strings.HasPrefix(ruta, u+"/") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// RequiereModulo protege un endpoint declarando a qué pantalla pertenece.
|
|
//
|
|
// MenuMiddleware compara la ruta pedida contra los submódulos del rol, y eso
|
|
// alcanza para las páginas porque su URL ES la del submódulo. Pero los
|
|
// endpoints de datos viven en otra ruta (/app/loadusers alimenta la pantalla
|
|
// /app/users), así que compararlos contra sí mismos nunca coincidiría y
|
|
// dejaría afuera hasta a quien sí tiene el permiso.
|
|
//
|
|
// Por eso el submódulo se declara en la ruta:
|
|
//
|
|
// protected.Get("/loadusers", middlewares.RequiereModulo("/app/users"), controllers.GetUsers)
|
|
//
|
|
// Sin esto, cualquier usuario con sesión podía pedir los datos de una pantalla
|
|
// que no tiene asignada escribiendo la URL a mano: el permiso solo tapaba el
|
|
// HTML, no la información.
|
|
func RequiereModulo(urlSubmodulo string) fiber.Handler {
|
|
return func(c *fiber.Ctx) error {
|
|
user, err := auth.User(c)
|
|
if err != nil || user == nil {
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
|
"error": true, "message": "Usuario no autenticado",
|
|
})
|
|
}
|
|
// Mismo criterio que MenuMiddleware: el administrador entra a todo.
|
|
if user.IsAdmin {
|
|
return c.Next()
|
|
}
|
|
|
|
datos, err := models.FindUserByID(user.ID)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
|
|
"error": true, "message": "Error al obtener datos del usuario",
|
|
})
|
|
}
|
|
var urls []string
|
|
for _, s := range datos.Role.Submodules {
|
|
urls = append(urls, s.Url)
|
|
}
|
|
|
|
if !PuedeVerRuta(urlSubmodulo, urls) {
|
|
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
|
"error": true, "message": "No tenés permiso para acceder a esta información",
|
|
})
|
|
}
|
|
return c.Next()
|
|
}
|
|
}
|