c.IP() detrás del proxy del hosting devuelve la IP interna del contenedor que reenvía (10.x.x.x). Contra eso, ninguna IP pública cargada en una llave podía coincidir: toda API key con restricción de IP daba 403, que es exactamente lo que le pasó a la integración de vCard. Ahora se lee X-Forwarded-For, y se toma la ÚLTIMA entrada, no la primera: con un proxy adelante esa es la que escribió el proxy. Si quien llama manda su propio X-Forwarded-For, el proxy le agrega la IP real al final — quedarse con la primera sería dejar que cada uno declare su IP y la restricción no valdría nada. El test fija ese caso. Mismo arreglo en la autenticación de Pagos Externos, que comparaba igual. El 403 ahora dice qué IP se vio y cuál está permitida: sin eso, del otro lado se prueba a ciegas. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
72 lines
2.2 KiB
Go
72 lines
2.2 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 ip := IPDelCliente(c); !key.IPValida(ip) {
|
|
// Decir qué IP se vio: sin eso, del otro lado no hay forma de saber
|
|
// qué cargar en la llave y se prueba a ciegas.
|
|
return c.Status(403).JSON(fiber.Map{
|
|
"error": "IP no autorizada para esta API key",
|
|
"ip_vista": ip,
|
|
"ip_permitida": key.IPPermitida,
|
|
})
|
|
}
|
|
|
|
go models.RegistrarUsoApiKey(key.ID, IPDelCliente(c))
|
|
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()
|
|
}
|
|
}
|