Files
soft_usite/rest/middlewares/hermes_auth.go
T
Lizandro GD 40dfaf5773 Agrega API Keys scoped (token + IP obligatoria + alcance) para /api/v2
Alternativa al ADMIN_API_KEY único de entorno, que sigue funcionando
como llave maestra para no romper integraciones existentes.

- Modelo ApiKey: token hasheado, IP/CIDR obligatoria (fail-closed sin
  IP), scopes habilitados, último uso (fecha + IP).
- AdminApiAuth() acepta ahora tanto la llave maestra como una ApiKey;
  nuevo middleware RequireScope(scope) para gatear grupos de rutas.
- Fase 1: scopes aplicados a lo más sensible — oss (archivos),
  query_runner (SQL arbitrario), usuarios (usuarios/roles/módulos),
  pasarelas (credenciales de pago). El resto de /api/v2 sigue con
  la llave maestra hasta una fase 2.
- Panel /app/api-keys: crear/editar/revocar, token visible solo al
  crear, scopes por checkbox, IP obligatoria.
2026-08-10 16:33:57 +00:00

66 lines
2.0 KiB
Go

package middlewares
import (
"os"
"strings"
"github.com/gofiber/fiber/v2"
"github.com/sujit-baniya/fiber-boilerplate/pkg/models"
)
// AdminApiAuth protege /api/v2. Acepta dos tipos de credencial:
// 1. ADMIN_API_KEY de entorno — llave maestra, acceso total, la que ya usan
// las integraciones existentes (no se retira para no romperlas).
// 2. Una ApiKey emitida desde /app/api-keys — token propio, hasheado en DB,
// restringida a una IP/CIDR obligatoria y a los scopes que se le hayan
// habilitado. RequireScope() más adelante en la cadena decide si esa
// llave puede usar el grupo de endpoints puntual.
func AdminApiAuth() fiber.Handler {
return func(c *fiber.Ctx) error {
token := ""
auth := c.Get("Authorization")
if strings.HasPrefix(auth, "Bearer ") {
token = auth[7:]
}
if token == "" {
token = c.Get("X-API-Key")
}
if token == "" {
return c.Status(401).JSON(fiber.Map{"error": "unauthorized"})
}
if masterKey := os.Getenv("ADMIN_API_KEY"); masterKey != "" && token == masterKey {
c.Locals("api_key_master", true)
return c.Next()
}
key, err := models.FindApiKeyActivaByToken(token)
if err != nil {
return c.Status(401).JSON(fiber.Map{"error": "unauthorized"})
}
if !key.IPValida(c.IP()) {
return c.Status(403).JSON(fiber.Map{"error": "IP no autorizada para esta API key"})
}
go models.RegistrarUsoApiKey(key.ID, c.IP())
c.Locals("api_key", key)
return c.Next()
}
}
// RequireScope gatea un grupo de rutas a un scope puntual. La llave maestra
// (ADMIN_API_KEY de entorno) siempre pasa; una ApiKey scoped solo pasa si
// tiene ese scope habilitado.
func RequireScope(scope string) fiber.Handler {
return func(c *fiber.Ctx) error {
if master, _ := c.Locals("api_key_master").(bool); master {
return c.Next()
}
key, ok := c.Locals("api_key").(*models.ApiKey)
if !ok || key == nil || !key.TieneScope(scope) {
return c.Status(403).JSON(fiber.Map{"error": "Esta API key no tiene el scope '" + scope + "'"})
}
return c.Next()
}
}