install.sh + frontend: use external URL dsbmueller.spdns.org:8443, configurable via RMM_BACKEND env
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// GET /api/v1/apikeys
|
||||
func (h *Handler) listAPIKeys(w http.ResponseWriter, r *http.Request) {
|
||||
keys, err := h.db.ListAPIKeys()
|
||||
if err != nil {
|
||||
log.Printf("API-Keys laden fehlgeschlagen: %v", err)
|
||||
writeError(w, http.StatusInternalServerError, "Laden fehlgeschlagen")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, keys)
|
||||
}
|
||||
|
||||
// POST /api/v1/apikeys
|
||||
func (h *Handler) createAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil || req.Name == "" {
|
||||
writeError(w, http.StatusBadRequest, "Parameter 'name' erforderlich")
|
||||
return
|
||||
}
|
||||
|
||||
// Zufaelligen 32-Byte Key generieren
|
||||
b := make([]byte, 16)
|
||||
rand.Read(b)
|
||||
key := hex.EncodeToString(b)
|
||||
|
||||
apiKey, err := h.db.CreateAPIKey(req.Name, key)
|
||||
if err != nil {
|
||||
log.Printf("API-Key erstellen fehlgeschlagen: %v", err)
|
||||
writeError(w, http.StatusInternalServerError, "Erstellen fehlgeschlagen")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Neuer API-Key erstellt: %s (%s...)", apiKey.Name, key[:8])
|
||||
writeJSON(w, http.StatusCreated, apiKey)
|
||||
}
|
||||
|
||||
// DELETE /api/v1/apikeys/{id}
|
||||
func (h *Handler) deleteAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||
idStr := r.PathValue("id")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungueltige ID")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.DeleteAPIKey(id); err != nil {
|
||||
writeError(w, http.StatusNotFound, "API-Key nicht gefunden")
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("API-Key %d geloescht", id)
|
||||
writeJSON(w, http.StatusOK, map[string]string{"message": "API-Key geloescht"})
|
||||
}
|
||||
@@ -54,6 +54,11 @@ func (h *Handler) SetupRoutes(mux *http.ServeMux, hub *ws.Hub) {
|
||||
mux.HandleFunc("PUT /api/v1/users/{id}/password", h.changePassword)
|
||||
mux.HandleFunc("DELETE /api/v1/users/{id}", h.deleteUser)
|
||||
|
||||
// API-Key Routes
|
||||
mux.HandleFunc("GET /api/v1/apikeys", h.listAPIKeys)
|
||||
mux.HandleFunc("POST /api/v1/apikeys", h.createAPIKey)
|
||||
mux.HandleFunc("DELETE /api/v1/apikeys/{id}", h.deleteAPIKey)
|
||||
|
||||
// Firmware/Agent-Update Routes
|
||||
mux.HandleFunc("GET /api/v1/firmware", h.getFirmwareInfo)
|
||||
mux.HandleFunc("POST /api/v1/firmware/upload", h.uploadFirmware)
|
||||
|
||||
+76
-11
@@ -5,34 +5,101 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/cynfo/rmm-backend/db"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
const UserContextKey contextKey = "user"
|
||||
|
||||
// APIKeyAuth - Middleware fuer API-Key Authentifizierung
|
||||
func APIKeyAuth(validKeys []string, next http.Handler) http.Handler {
|
||||
keySet := make(map[string]bool, len(validKeys))
|
||||
for _, k := range validKeys {
|
||||
keySet[k] = true
|
||||
// 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
|
||||
key := r.Header.Get("X-API-Key")
|
||||
if key == "" {
|
||||
key = r.URL.Query().Get("api_key")
|
||||
}
|
||||
if key == "" || !keySet[key] {
|
||||
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
|
||||
if key != "" && cache.IsValid(key) {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
|
||||
// Try JWT
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
// CombinedAuth - API-Key ODER JWT Token
|
||||
// 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 {
|
||||
@@ -40,7 +107,6 @@ func CombinedAuth(validKeys []string, auth *AuthHandler, next http.Handler) http
|
||||
}
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Try API key first
|
||||
key := r.Header.Get("X-API-Key")
|
||||
if key == "" {
|
||||
key = r.URL.Query().Get("api_key")
|
||||
@@ -50,7 +116,6 @@ func CombinedAuth(validKeys []string, auth *AuthHandler, next http.Handler) http
|
||||
return
|
||||
}
|
||||
|
||||
// Try JWT
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if strings.HasPrefix(authHeader, "Bearer ") {
|
||||
token := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
|
||||
@@ -182,6 +182,15 @@ func (d *Database) migrate() error {
|
||||
// Indices fuer Metrics
|
||||
d.db.Exec(`CREATE INDEX IF NOT EXISTS idx_metrics_agent_metric ON metrics(agent_id, metric, time DESC)`)
|
||||
|
||||
// API-Keys Tabelle
|
||||
d.db.Exec(`CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
key TEXT NOT NULL UNIQUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
last_used TIMESTAMPTZ
|
||||
)`)
|
||||
|
||||
// Retention Policy (90 Tage)
|
||||
d.setupRetention()
|
||||
|
||||
@@ -954,3 +963,96 @@ func (d *Database) GetAgentStatus(agentID string) string {
|
||||
}
|
||||
return models.CalculateAgentStatus(&lastHB.Time)
|
||||
}
|
||||
|
||||
// ==================== API-Keys ====================
|
||||
|
||||
type APIKey struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Key string `json:"key"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
LastUsed *time.Time `json:"last_used,omitempty"`
|
||||
}
|
||||
|
||||
func (d *Database) ListAPIKeys() ([]APIKey, error) {
|
||||
rows, err := d.db.Query("SELECT id, name, key, created_at, last_used FROM api_keys ORDER BY created_at")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var keys []APIKey
|
||||
for rows.Next() {
|
||||
var k APIKey
|
||||
var lastUsed sql.NullTime
|
||||
if err := rows.Scan(&k.ID, &k.Name, &k.Key, &k.CreatedAt, &lastUsed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if lastUsed.Valid {
|
||||
k.LastUsed = &lastUsed.Time
|
||||
}
|
||||
keys = append(keys, k)
|
||||
}
|
||||
if keys == nil {
|
||||
keys = []APIKey{}
|
||||
}
|
||||
return keys, rows.Err()
|
||||
}
|
||||
|
||||
func (d *Database) CreateAPIKey(name, key string) (*APIKey, error) {
|
||||
var k APIKey
|
||||
err := d.db.QueryRow(
|
||||
"INSERT INTO api_keys (name, key) VALUES ($1, $2) RETURNING id, name, key, created_at",
|
||||
name, key,
|
||||
).Scan(&k.ID, &k.Name, &k.Key, &k.CreatedAt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &k, nil
|
||||
}
|
||||
|
||||
func (d *Database) DeleteAPIKey(id int) error {
|
||||
result, err := d.db.Exec("DELETE FROM api_keys WHERE id = $1", id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("API-Key nicht gefunden")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Database) GetAllAPIKeys() ([]string, error) {
|
||||
rows, err := d.db.Query("SELECT key FROM api_keys")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var keys []string
|
||||
for rows.Next() {
|
||||
var k string
|
||||
if err := rows.Scan(&k); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys, rows.Err()
|
||||
}
|
||||
|
||||
func (d *Database) UpdateAPIKeyLastUsed(key string) {
|
||||
d.db.Exec("UPDATE api_keys SET last_used = NOW() WHERE key = $1", key)
|
||||
}
|
||||
|
||||
// MigrateConfigAPIKeys migriert API-Keys aus der Config in die DB (einmalig)
|
||||
func (d *Database) MigrateConfigAPIKeys(configKeys []string) {
|
||||
for _, key := range configKeys {
|
||||
var exists bool
|
||||
d.db.QueryRow("SELECT EXISTS(SELECT 1 FROM api_keys WHERE key = $1)", key).Scan(&exists)
|
||||
if !exists {
|
||||
d.db.Exec("INSERT INTO api_keys (name, key) VALUES ($1, $2)", "Default (migriert)", key)
|
||||
log.Printf("API-Key aus Config migriert: %s...", key[:8])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-3
@@ -57,6 +57,12 @@ func main() {
|
||||
mon := monitor.New(database)
|
||||
go mon.Run()
|
||||
|
||||
// Config API-Keys in DB migrieren (einmalig)
|
||||
database.MigrateConfigAPIKeys(cfg.APIKeys)
|
||||
|
||||
// API-Key Cache (laedt Keys aus DB, refresht alle 30s)
|
||||
apiKeyCache := api.NewAPIKeyCache(database)
|
||||
|
||||
// Auth-Handler initialisieren
|
||||
authHandler := api.NewAuthHandler(database, cfg.JWTSecret)
|
||||
authHandler.EnsureDefaultAdmin()
|
||||
@@ -72,10 +78,10 @@ func main() {
|
||||
// GET /api/v1/auth/me braucht JWT
|
||||
protectedMux.HandleFunc("GET /api/v1/auth/me", authHandler.Me)
|
||||
|
||||
// Combined router: auth routes ungeschuetzt, rest mit CombinedAuth
|
||||
// Combined router: auth routes ungeschuetzt, rest mit DB-basierter Auth
|
||||
mux := http.NewServeMux()
|
||||
mux.Handle("POST /api/v1/auth/login", authMux)
|
||||
mux.Handle("/", api.CombinedAuth(cfg.APIKeys, authHandler, protectedMux))
|
||||
mux.Handle("/", api.CombinedAuthWithCache(apiKeyCache, authHandler, protectedMux))
|
||||
|
||||
// Middleware-Chain: CORS -> Logging -> Handler
|
||||
var chain http.Handler = mux
|
||||
@@ -83,7 +89,7 @@ func main() {
|
||||
chain = api.CORS(chain)
|
||||
|
||||
log.Printf("RMM Backend startet auf %s (TLS)", cfg.ListenAddr)
|
||||
log.Printf("API-Keys konfiguriert: %d", len(cfg.APIKeys))
|
||||
log.Printf("API-Keys (Config): %d, DB-basierte Validierung aktiv", len(cfg.APIKeys))
|
||||
|
||||
if err := http.ListenAndServeTLS(cfg.ListenAddr, cfg.TLSCert, cfg.TLSKey, chain); err != nil {
|
||||
log.Fatalf("Server-Fehler: %v", err)
|
||||
|
||||
Reference in New Issue
Block a user