rmm2/backend/api/handlers.go
cynfo3000 4023a14fe1 feat: Agent zeigt Warnung wenn er nicht den Kunden-Key nutzt
- last_api_key Spalte in agents, wird bei Heartbeat gesetzt
- GetAgentsByCustomer liefert last_api_key mit
- Frontend: rot + Warnsymbol wenn Key abweicht vom Kunden-Agent-Key
- Grau wenn noch kein Heartbeat mit Key-Info
2026-03-10 00:23:59 +01:00

422 lines
13 KiB
Go

package api
import (
"crypto/rand"
"encoding/hex"
"encoding/json"
"log"
"net/http"
"strconv"
"strings"
"time"
"github.com/cynfo/rmm-backend/db"
"github.com/cynfo/rmm-backend/models"
"github.com/cynfo/rmm-backend/ws"
)
type Handler struct {
db *db.Database
scheduler *TaskScheduler
}
func NewHandler(database *db.Database) *Handler {
return &Handler{db: database}
}
// SetupRoutes - Alle API-Routes registrieren
func (h *Handler) SetupRoutes(mux *http.ServeMux, hub *ws.Hub) {
// Bestehende Agent-Routes
mux.HandleFunc("POST /api/v1/agent/register", h.registerAgent)
mux.HandleFunc("POST /api/v1/agent/heartbeat", h.heartbeat)
mux.HandleFunc("POST /api/v1/agents", h.preRegisterAgent)
mux.HandleFunc("GET /api/v1/agents", h.listAgents)
mux.HandleFunc("GET /api/v1/agents/events", h.getAllAgentEvents)
mux.HandleFunc("GET /api/v1/agents/{id}", h.getAgent)
mux.HandleFunc("GET /api/v1/agents/{id}/events", h.getAgentEvents)
mux.HandleFunc("GET /api/v1/agents/{id}/system", h.getSystemData)
mux.HandleFunc("DELETE /api/v1/agents/{id}", h.deleteAgent)
// Customer-Routes
mux.HandleFunc("GET /api/v1/customers", h.listCustomers)
mux.HandleFunc("POST /api/v1/customers", h.createCustomer)
mux.HandleFunc("GET /api/v1/customers/{id}", h.getCustomer)
mux.HandleFunc("PUT /api/v1/customers/{id}", h.updateCustomer)
mux.HandleFunc("DELETE /api/v1/customers/{id}", h.deleteCustomer)
mux.HandleFunc("GET /api/v1/customers/{id}/agents", h.getCustomerAgents)
// Agent-Customer Zuordnung
mux.HandleFunc("PUT /api/v1/agents/{id}/customer", h.assignAgentCustomer)
mux.HandleFunc("DELETE /api/v1/agents/{id}/customer", h.unassignAgentCustomer)
// User-Routes
mux.HandleFunc("GET /api/v1/users", h.listUsers)
mux.HandleFunc("POST /api/v1/users", h.createUser)
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)
mux.HandleFunc("GET /api/v1/customers/{id}/apikeys", h.listCustomerAPIKeys)
// System Settings
mux.HandleFunc("GET /api/v1/settings", h.getSettings)
mux.HandleFunc("PUT /api/v1/settings", h.updateSettings)
// Audit-Log
mux.HandleFunc("GET /api/v1/audit", h.getAuditLogs)
// Firmware/Agent-Update Routes
mux.HandleFunc("GET /api/v1/firmware", h.getFirmwareInfo)
mux.HandleFunc("POST /api/v1/firmware/upload", h.uploadFirmware)
mux.HandleFunc("GET /api/v1/firmware/download", h.downloadFirmware)
mux.HandleFunc("POST /api/v1/agents/{id}/agent-update", h.pushAgentUpdate(hub))
mux.HandleFunc("POST /api/v1/agents/{id}/request-update", h.requestUpdate)
mux.HandleFunc("DELETE /api/v1/agents/{id}/request-update", h.cancelUpdate)
mux.HandleFunc("POST /api/v1/agents/request-update-all", h.requestUpdateAll)
// Installer Routes
mux.HandleFunc("GET /api/v1/installer", h.getInstallerInfo)
mux.HandleFunc("POST /api/v1/installer/upload", h.uploadInstaller)
mux.HandleFunc("GET /api/v1/installer/download", h.downloadInstaller)
// Task-Routes
mux.HandleFunc("GET /api/v1/tasks", h.listTasks)
mux.HandleFunc("GET /api/v1/tasks/{id}", h.getTask)
mux.HandleFunc("POST /api/v1/tasks", h.createTask(hub))
mux.HandleFunc("DELETE /api/v1/tasks/{id}", h.deleteTask)
mux.HandleFunc("POST /api/v1/tasks/{id}/cancel", h.cancelTask)
mux.HandleFunc("PUT /api/v1/tasks/{id}", h.updateTask)
mux.HandleFunc("PUT /api/v1/tasks/{id}/toggle", h.toggleTask)
mux.HandleFunc("POST /api/v1/tasks/{id}/run", h.runTaskNow())
// WebSocket und Tunnel-Routes
h.setupTunnelRoutes(mux, hub)
}
func generateID() string {
b := make([]byte, 16)
rand.Read(b)
return hex.EncodeToString(b)
}
func writeJSON(w http.ResponseWriter, status int, v interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, map[string]string{"error": msg})
}
// POST /api/v1/agent/register
func (h *Handler) registerAgent(w http.ResponseWriter, r *http.Request) {
var req models.RegisterRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Ungueltige Anfrage")
return
}
if req.Name == "" || req.Hostname == "" {
writeError(w, http.StatusBadRequest, "Name und Hostname sind erforderlich")
return
}
// Agent-ID bestimmen: vom Agent mitgeliefert > nach Hostname suchen > neu generieren
agentID := req.AgentID
isExisting := false
if agentID != "" {
// Pruefen ob die ID schon in der DB existiert
if existing, _, err := h.db.GetAgent(agentID); err == nil && existing != nil {
isExisting = true
}
}
if agentID == "" {
// Nach pre-registriertem Agent mit gleichem Namen suchen (hostname="(ausstehend)")
// Kein hostname-basiertes Matching mehr — gleiche Hostnamen auf verschiedenen Maschinen
// (z.B. zwei "PVE-REP" bei verschiedenen Kunden) wuerden sonst dieselbe ID bekommen
if existing, err := h.db.GetAgentByName(req.Name); err == nil && existing != nil {
if existing.Hostname == "(ausstehend)" {
agentID = existing.ID
isExisting = true
log.Printf("Pre-registrierten Agent gefunden: %s (%s)", existing.Name, existing.ID)
}
}
}
if agentID == "" {
agentID = generateID()
}
platform := req.Platform
if platform == "" {
platform = "freebsd" // Default fuer bestehende OPNsense Agents
}
agent := &models.Agent{
ID: agentID,
Name: req.Name,
Hostname: req.Hostname,
IP: req.IP,
OPNsenseVersion: req.OPNsenseVersion,
AgentVersion: req.AgentVersion,
Platform: platform,
RegisteredAt: time.Now().UTC(),
}
if err := h.db.RegisterAgent(agent); err != nil {
log.Printf("Fehler bei Registrierung: %v", err)
writeError(w, http.StatusInternalServerError, "Registrierung fehlgeschlagen")
return
}
action := "registriert"
if isExisting {
action = "re-registriert"
}
log.Printf("Agent %s: %s (%s)", action, agent.Name, agent.ID)
writeJSON(w, http.StatusCreated, models.RegisterResponse{
ID: agent.ID,
Message: "Agent erfolgreich registriert",
})
}
// POST /api/v1/agents - Pre-Registration einer Firewall
func (h *Handler) preRegisterAgent(w http.ResponseWriter, r *http.Request) {
var req struct {
Name string `json:"name"`
CustomerID *int `json:"customer_id,omitempty"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Ungueltige Anfrage")
return
}
if req.Name == "" {
writeError(w, http.StatusBadRequest, "Name ist erforderlich")
return
}
agentID := generateID()
agent := &models.Agent{
ID: agentID,
Name: req.Name,
Hostname: "(ausstehend)",
IP: "",
RegisteredAt: time.Now().UTC(),
}
if err := h.db.RegisterAgent(agent); err != nil {
log.Printf("Fehler bei Pre-Registration: %v", err)
writeError(w, http.StatusInternalServerError, "Pre-Registration fehlgeschlagen")
return
}
// Optionale Kundenzuordnung
if req.CustomerID != nil {
h.db.AssignAgentCustomer(agentID, *req.CustomerID)
}
log.Printf("Agent pre-registriert: %s (%s)", req.Name, agentID)
h.auditLog(r, "agent.create", "agent", agentID, req.Name, "")
writeJSON(w, http.StatusCreated, map[string]interface{}{
"id": agentID,
"name": req.Name,
"message": "Firewall erfolgreich pre-registriert",
})
}
// POST /api/v1/agent/heartbeat
func (h *Handler) heartbeat(w http.ResponseWriter, r *http.Request) {
var req models.HeartbeatRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Ungueltige Anfrage")
return
}
if req.AgentID == "" {
writeError(w, http.StatusBadRequest, "agent_id erforderlich")
return
}
// Agent-Version VOR Update lesen (fuer Update-Flag Logik)
prevAgentVersion := h.db.GetAgentVersionFromDB(req.AgentID)
// Agent-Version aktualisieren
if req.AgentVersion != "" {
h.db.UpdateAgentVersion(req.AgentID, req.AgentVersion)
}
// Genutzten API-Key speichern (aus Middleware-Kontext)
if keyInfo := GetAPIKeyFromContext(r); keyInfo != nil {
h.db.UpdateAgentAPIKey(req.AgentID, keyInfo.Key)
}
if err := h.db.SaveSystemData(req.AgentID, &req.SystemData); err != nil {
if strings.Contains(err.Error(), "nicht gefunden") {
writeError(w, http.StatusNotFound, "Agent nicht gefunden")
return
}
log.Printf("Fehler bei Heartbeat: %v", err)
writeError(w, http.StatusInternalServerError, "Heartbeat fehlgeschlagen")
return
}
resp := models.HeartbeatResponse{Message: "OK"}
// Pruefen ob Update ansteht — Plattform aus Heartbeat-Request verwenden
if h.db.IsUpdateRequested(req.AgentID) {
platform := req.Platform
if platform == "" {
platform = "freebsd" // Default fuer OPNsense Agents ohne Platform-Feld
}
version, hash, _, err := h.db.GetFirmwareInfo(platform)
if err != nil {
// Keine Firmware vorhanden — Flag sinnlos, loeschen
h.db.ClearUpdateRequest(req.AgentID)
} else if version == req.AgentVersion {
// Agent hat genau die Firmware-Version — fertig
h.db.ClearUpdateRequest(req.AgentID)
} else {
// Pruefen ob Agent-Version sich seit Flag-Setzung geaendert hat
// (z.B. manuelles Update auf andere Version)
if prevAgentVersion != "" && prevAgentVersion != req.AgentVersion {
// Version hat sich geaendert — Update war erfolgreich (manuell oder anderweitig)
h.db.ClearUpdateRequest(req.AgentID)
} else {
resp.UpdateAvailable = true
resp.UpdateVersion = version
resp.UpdateHash = hash
}
}
}
writeJSON(w, http.StatusOK, resp)
}
// GET /api/v1/agents
func (h *Handler) listAgents(w http.ResponseWriter, r *http.Request) {
agents, err := h.db.GetAgents()
if err != nil {
log.Printf("Fehler beim Laden der Agents: %v", err)
writeError(w, http.StatusInternalServerError, "Fehler beim Laden")
return
}
if agents == nil {
agents = []models.Agent{}
}
// Status fuer jeden Agent berechnen
var response []models.AgentResponse
for _, a := range agents {
response = append(response, models.AgentResponse{
Agent: a,
Status: models.CalculateAgentStatus(a.LastHeartbeat),
})
}
if response == nil {
response = []models.AgentResponse{}
}
writeJSON(w, http.StatusOK, response)
}
// GET /api/v1/agents/{id}
func (h *Handler) getAgent(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
agent, sysData, err := h.db.GetAgent(id)
if err != nil {
log.Printf("Fehler: %v", err)
writeError(w, http.StatusInternalServerError, "Fehler beim Laden")
return
}
if agent == nil {
writeError(w, http.StatusNotFound, "Agent nicht gefunden")
return
}
result := map[string]interface{}{
"agent": agent,
"status": models.CalculateAgentStatus(agent.LastHeartbeat),
"system_data": sysData,
}
writeJSON(w, http.StatusOK, result)
}
// GET /api/v1/agents/{id}/system
func (h *Handler) getSystemData(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
data, err := h.db.GetSystemData(id)
if err != nil {
log.Printf("Fehler: %v", err)
writeError(w, http.StatusInternalServerError, "Fehler beim Laden")
return
}
if data == nil {
writeError(w, http.StatusNotFound, "Keine Systemdaten vorhanden")
return
}
writeJSON(w, http.StatusOK, data)
}
// DELETE /api/v1/agents/{id}
func (h *Handler) deleteAgent(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
deleted, err := h.db.DeleteAgent(id)
if err != nil {
log.Printf("Fehler beim Loeschen: %v", err)
writeError(w, http.StatusInternalServerError, "Loeschen fehlgeschlagen")
return
}
if !deleted {
writeError(w, http.StatusNotFound, "Agent nicht gefunden")
return
}
log.Printf("Agent geloescht: %s", id)
h.auditLog(r, "agent.delete", "agent", id, id, "")
writeJSON(w, http.StatusOK, map[string]string{"message": "Agent geloescht"})
}
// GET /api/v1/agents/{id}/events
func (h *Handler) getAgentEvents(w http.ResponseWriter, r *http.Request) {
agentID := r.PathValue("id")
eventType := r.URL.Query().Get("type")
limit := 100
if l := r.URL.Query().Get("limit"); l != "" {
if n, err := strconv.Atoi(l); err == nil && n > 0 {
limit = n
}
}
events, err := h.db.GetAgentEvents(agentID, eventType, limit)
if err != nil {
log.Printf("Fehler beim Laden der Events: %v", err)
writeError(w, http.StatusInternalServerError, "Fehler beim Laden")
return
}
writeJSON(w, http.StatusOK, events)
}
// GET /api/v1/agents/events
func (h *Handler) getAllAgentEvents(w http.ResponseWriter, r *http.Request) {
eventType := r.URL.Query().Get("type")
limit := 100
if l := r.URL.Query().Get("limit"); l != "" {
if n, err := strconv.Atoi(l); err == nil && n > 0 {
limit = n
}
}
events, err := h.db.GetAllAgentEvents(eventType, limit)
if err != nil {
log.Printf("Fehler beim Laden der Events: %v", err)
writeError(w, http.StatusInternalServerError, "Fehler beim Laden")
return
}
writeJSON(w, http.StatusOK, events)
}