rmm2/backend/api/middleware.go
cynfo3000 91385b42d1 security: Terminal-Endpoint JWT-only (kein API-Key Zugriff)
- Neue JWTOnlyAuth Middleware: akzeptiert nur JWT, kein API-Key
- Terminal-Route aus allgemeinem CombinedAuth raus, eigener Mount
- API-Keys für Monitoring/Automation erlaubt, aber Terminal braucht
  echten Login mit User/Pass + 2FA
- ?token= Query-Param weiterhin erlaubt (Browser-WS kann kein Header setzen)
2026-03-09 23:44:12 +01:00

193 lines
4.9 KiB
Go

package api
import (
"context"
"log"
"net/http"
"strings"
"sync"
"time"
"github.com/cynfo/rmm-backend/db"
)
type contextKey string
const UserContextKey contextKey = "user"
// APIKeyCache cached DB-basierte API-Keys mit periodischem Refresh
type APIKeyCache struct {
db *db.Database
mu sync.RWMutex
keys map[string]bool
lastLoad time.Time
ttl time.Duration
}
func NewAPIKeyCache(database *db.Database) *APIKeyCache {
c := &APIKeyCache{
db: database,
keys: make(map[string]bool),
ttl: 30 * time.Second,
}
c.refresh()
return c
}
func (c *APIKeyCache) refresh() {
dbKeys, err := c.db.GetAllAPIKeys()
if err != nil {
log.Printf("API-Key Cache Refresh fehlgeschlagen: %v", err)
return
}
newKeys := make(map[string]bool, len(dbKeys))
for _, k := range dbKeys {
newKeys[k] = true
}
c.mu.Lock()
c.keys = newKeys
c.lastLoad = time.Now()
c.mu.Unlock()
}
func (c *APIKeyCache) IsValid(key string) bool {
c.mu.RLock()
expired := time.Since(c.lastLoad) > c.ttl
valid := c.keys[key]
c.mu.RUnlock()
if expired {
c.refresh()
c.mu.RLock()
valid = c.keys[key]
c.mu.RUnlock()
}
if valid {
go c.db.UpdateAPIKeyLastUsed(key)
}
return valid
}
// CombinedAuthWithCache - API-Key (aus DB-Cache) ODER JWT Token
func CombinedAuthWithCache(cache *APIKeyCache, auth *AuthHandler, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Try API key first (Header oder Query-Parameter)
key := r.Header.Get("X-API-Key")
if key == "" {
key = r.URL.Query().Get("api_key")
}
if key != "" && cache.IsValid(key) {
next.ServeHTTP(w, r)
return
}
// Try JWT — Authorization Header oder ?token= Query-Parameter (fuer WebSocket)
var jwtToken string
authHeader := r.Header.Get("Authorization")
if strings.HasPrefix(authHeader, "Bearer ") {
jwtToken = strings.TrimPrefix(authHeader, "Bearer ")
} else if t := r.URL.Query().Get("token"); t != "" {
jwtToken = t
}
if jwtToken != "" {
claims, err := auth.ValidateToken(jwtToken)
if err == nil {
ctx := context.WithValue(r.Context(), UserContextKey, claims)
next.ServeHTTP(w, r.WithContext(ctx))
return
}
}
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
})
}
// CombinedAuth - Abwaertskompatibel mit statischen Keys (Fallback)
func CombinedAuth(validKeys []string, auth *AuthHandler, next http.Handler) http.Handler {
keySet := make(map[string]bool, len(validKeys))
for _, k := range validKeys {
keySet[k] = true
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
key := r.Header.Get("X-API-Key")
if key == "" {
key = r.URL.Query().Get("api_key")
}
if key != "" && keySet[key] {
next.ServeHTTP(w, r)
return
}
authHeader := r.Header.Get("Authorization")
if strings.HasPrefix(authHeader, "Bearer ") {
token := strings.TrimPrefix(authHeader, "Bearer ")
claims, err := auth.ValidateToken(token)
if err == nil {
ctx := context.WithValue(r.Context(), UserContextKey, claims)
next.ServeHTTP(w, r.WithContext(ctx))
return
}
}
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
})
}
// JWTOnlyAuth - Nur JWT akzeptieren, kein API-Key (z.B. fuer Terminal-Zugriff)
// Erfordert echten Login mit User/Pass (+2FA), kein Maschinentoken reicht.
func JWTOnlyAuth(auth *AuthHandler, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var jwtToken string
// Authorization Header
authHeader := r.Header.Get("Authorization")
if strings.HasPrefix(authHeader, "Bearer ") {
jwtToken = strings.TrimPrefix(authHeader, "Bearer ")
}
// ?token= Query-Parameter (fuer WebSocket, Browser kann keinen Header setzen)
if jwtToken == "" {
jwtToken = r.URL.Query().Get("token")
}
if jwtToken == "" {
http.Error(w, `{"error":"unauthorized: JWT erforderlich"}`, http.StatusUnauthorized)
return
}
claims, err := auth.ValidateToken(jwtToken)
if err != nil {
http.Error(w, `{"error":"unauthorized: ungültiger Token"}`, http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), UserContextKey, claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// CORS Middleware
func CORS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-API-Key")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
// Logging - Request-Logging Middleware
func Logging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
})
}