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.
This commit is contained in:
@@ -5,15 +5,18 @@ import (
|
||||
"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 {
|
||||
apiKey := os.Getenv("ADMIN_API_KEY")
|
||||
if apiKey == "" {
|
||||
return c.Status(503).JSON(fiber.Map{"error": "ADMIN_API_KEY not configured"})
|
||||
}
|
||||
|
||||
token := ""
|
||||
auth := c.Get("Authorization")
|
||||
if strings.HasPrefix(auth, "Bearer ") {
|
||||
@@ -22,11 +25,41 @@ func AdminApiAuth() fiber.Handler {
|
||||
if token == "" {
|
||||
token = c.Get("X-API-Key")
|
||||
}
|
||||
|
||||
if token == "" || token != apiKey {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user