Files
notifier/internal/auth/rest_middleware.go
T
igodwin eda033ff9b
CI / Lint (push) Successful in 2m29s
Build and Publish Container / build-and-publish (push) Successful in 2m58s
CI / Vulnerability scan (push) Successful in 44s
CI / Test (push) Successful in 1m45s
fix: clear golangci-lint backlog and make lint job blocking
Addresses errcheck, gosec, revive, staticcheck, and unused findings
across the codebase (unchecked error returns, unsafe file inclusion
warnings on operator/test-controlled paths, missing package comments,
unused parameters, deprecated API usage). Also fixes two suppression
comments that were silently no-ops due to wrong syntax (#nosec needs
a leading '#', nolint reasons need '//' not '--').

With the backlog clear, drop continue-on-error from the CI lint job
per the plan left in b4b4806.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-18 10:32:51 -07:00

90 lines
2.5 KiB
Go

package auth
import (
"net/http"
"strings"
"github.com/igodwin/notifier/internal/logging"
)
// RESTAuthMiddleware provides authentication for REST APIs
type RESTAuthMiddleware struct {
store *APIKeyStore
logger *logging.Logger
}
// NewRESTAuthMiddleware creates a new REST auth middleware
func NewRESTAuthMiddleware(store *APIKeyStore, logger *logging.Logger) *RESTAuthMiddleware {
return &RESTAuthMiddleware{
store: store,
logger: logger,
}
}
// Middleware returns an HTTP middleware function
func (m *RESTAuthMiddleware) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Extract API key from Authorization header or X-API-Key header
apiKey := m.extractAPIKey(r)
if apiKey == "" {
m.logger.Warnf("REST: Missing API key in request from %s", r.RemoteAddr)
http.Error(w, "Missing or invalid Authorization header", http.StatusUnauthorized)
return
}
// Validate API key
key, err := m.store.ValidateKey(apiKey)
if err != nil {
m.logger.Warnf("REST: Invalid API key from %s - error=%v", r.RemoteAddr, err)
http.Error(w, "Invalid API key", http.StatusUnauthorized)
return
}
// Check rate limit
allowed, err := m.store.CheckRateLimit(apiKey)
if err != nil || !allowed {
m.logger.Warnf("REST: Rate limit exceeded for key=%s from %s", key.ClientID, r.RemoteAddr)
w.Header().Set("Retry-After", "60")
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
return
}
// Update last used timestamp
if err := m.store.UpdateLastUsed(apiKey); err != nil {
m.logger.Errorf("REST: Failed to update last used time for key=%s - error=%v", key.ClientID, err)
}
// Create auth context and attach to request
authCtx := &Context{
APIKey: key,
ClientID: key.ClientID,
Roles: key.Roles,
}
// Add auth context to request context
ctx := ContextWithAuth(r.Context(), authCtx)
m.logger.Debugf("REST: Authenticated request from client=%s with roles=%v", key.ClientID, key.Roles)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// extractAPIKey extracts API key from Authorization header or X-API-Key header
func (m *RESTAuthMiddleware) extractAPIKey(r *http.Request) string {
// Try Authorization header first (Bearer token)
authHeader := r.Header.Get("Authorization")
if authHeader != "" {
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) == 2 && strings.ToLower(parts[0]) == "bearer" {
return parts[1]
}
}
// Try X-API-Key header
if apiKey := r.Header.Get("X-API-Key"); apiKey != "" {
return apiKey
}
return ""
}