veza/veza-backend-api/internal/middleware/api_key_scope.go
senke b47fa21331 feat(v0.12.8): documentation & API publique — rate limiting, scopes, OpenAPI
- API key rate limiting middleware (1000 reads/h, 200 writes/h par clé)
  — tracking séparé read/write, par API key ID (pas par IP)
  — headers X-RateLimit-Limit/Remaining/Reset sur chaque réponse
- API key scope enforcement middleware (read → GET, write → POST/PUT/DELETE)
  — admin scope permet tout, CSRF skip pour API key auth
- OpenAPI spec: ajout securityDefinition ApiKeyAuth (X-API-Key header)
- Swagger annotations: ajout ApiKeyAuth dans cmd/api/main.go
- Wiring dans router.go: middlewares appliqués sur tout le groupe /api/v1
- Tests: 10 tests (5 rate limiter + 5 scope enforcement), tous PASS

Backend existant déjà en place (pré-v0.12.8):
- Swagger UI (gin-swagger + frontend SwaggerUIDoc component)
- API key CRUD (create/list/delete + X-API-Key auth dans AuthMiddleware)
- Developer Dashboard frontend (API keys, webhooks, playground)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 18:44:09 +01:00

66 lines
1.6 KiB
Go

package middleware
import (
"net/http"
"veza-backend-api/internal/models"
"veza-backend-api/internal/services"
"github.com/gin-gonic/gin"
)
// RequireAPIKeyScope returns a Gin middleware that enforces API key scopes.
// For API key authenticated requests, it verifies the key has the required scope.
// JWT-authenticated requests pass through (they already use RBAC).
//
// Scope mapping:
// - GET/HEAD/OPTIONS → "read"
// - POST/PUT/PATCH/DELETE → "write"
// - "admin" scope implies both "read" and "write"
func RequireAPIKeyScope(apiKeyService *services.APIKeyService) gin.HandlerFunc {
return func(c *gin.Context) {
apiKeyVal, exists := c.Get("api_key")
if !exists {
// Not an API key request (JWT auth) — pass through
c.Next()
return
}
key, ok := apiKeyVal.(*models.APIKey)
if !ok {
c.JSON(http.StatusInternalServerError, gin.H{
"success": false,
"error": gin.H{
"code": "INTERNAL_ERROR",
"message": "Invalid API key context",
},
})
c.Abort()
return
}
var requiredScope string
switch c.Request.Method {
case http.MethodGet, http.MethodHead, http.MethodOptions:
requiredScope = "read"
default:
requiredScope = "write"
}
if !apiKeyService.HasScope(key, requiredScope) {
c.JSON(http.StatusForbidden, gin.H{
"success": false,
"error": gin.H{
"code": "INSUFFICIENT_SCOPE",
"message": "API key does not have the required scope: " + requiredScope,
"required_scope": requiredScope,
"key_scopes": key.Scopes,
},
})
c.Abort()
return
}
c.Next()
}
}