rmm2/backend/api/wau.go
cynfo3000 54ddc0b902 WAU Compliance: agent collector, backend endpoints, frontend badge + panel
- Agent: WAU registry config check (include/exclude URLs), pending updates count
- Backend: /wau/compliance per customer, /wau/compliance/summary all customers
- Frontend: compliance badge in customer list, compliance panel in WAU tab
- config.js: no longer gitignored (secrets-free, reads from env/window.__RMM_CONFIG__)
- vite.config.js: reads backend URL from .env instead of hardcoded value
- .env.example added for clean setup
2026-03-10 17:38:58 +01:00

204 lines
6.6 KiB
Go

package api
import (
"encoding/json"
"net/http"
"strconv"
"strings"
)
// setupWAURoutes registriert alle WAU-Endpunkte
func (h *Handler) setupWAURoutes(mux *http.ServeMux) {
// Authentifizierte Endpunkte (API-Key oder JWT)
mux.HandleFunc("GET /api/v1/customers/{id}/wau/rules", h.listWAURules)
mux.HandleFunc("POST /api/v1/customers/{id}/wau/rules", h.addWAURule)
mux.HandleFunc("DELETE /api/v1/customers/{id}/wau/rules/{rid}", h.deleteWAURule)
mux.HandleFunc("GET /api/v1/customers/{id}/wau/inventory", h.getWAUInventory)
mux.HandleFunc("GET /api/v1/customers/{id}/wau/compliance", h.getWAUCompliance)
// Compliance-Summary für alle Kunden
mux.HandleFunc("GET /api/v1/wau/compliance/summary", h.getWAUComplianceSummary)
// Plaintext-Endpunkte für WAU (kein Auth nötig — enthält nur App-IDs)
mux.HandleFunc("GET /api/v1/customers/{id}/wau/included", h.wauIncludedList)
mux.HandleFunc("GET /api/v1/customers/{id}/wau/excluded", h.wauExcludedList)
}
// GET /api/v1/customers/{id}/wau/rules
func (h *Handler) listWAURules(w http.ResponseWriter, r *http.Request) {
customerID, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
writeError(w, http.StatusBadRequest, "Ungültige Kunden-ID")
return
}
rules, err := h.db.ListWAURules(customerID)
if err != nil {
writeError(w, http.StatusInternalServerError, "Fehler beim Laden")
return
}
writeJSON(w, http.StatusOK, rules)
}
// POST /api/v1/customers/{id}/wau/rules
func (h *Handler) addWAURule(w http.ResponseWriter, r *http.Request) {
customerID, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
writeError(w, http.StatusBadRequest, "Ungültige Kunden-ID")
return
}
var req struct {
WingetID string `json:"winget_id"`
DisplayName string `json:"display_name"`
RuleType string `json:"rule_type"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Ungültige Anfrage")
return
}
req.WingetID = strings.TrimSpace(req.WingetID)
req.DisplayName = strings.TrimSpace(req.DisplayName)
if req.WingetID == "" {
writeError(w, http.StatusBadRequest, "winget_id erforderlich")
return
}
if req.RuleType != "allow" && req.RuleType != "block" {
writeError(w, http.StatusBadRequest, "rule_type muss 'allow' oder 'block' sein")
return
}
if req.DisplayName == "" {
req.DisplayName = req.WingetID
}
rule, err := h.db.AddWAURule(customerID, req.WingetID, req.DisplayName, req.RuleType)
if err != nil {
writeError(w, http.StatusInternalServerError, "Fehler beim Speichern")
return
}
h.auditLog(r, "wau.rule.add", "customer", strconv.Itoa(customerID), "",
req.RuleType+": "+req.WingetID)
writeJSON(w, http.StatusCreated, rule)
}
// DELETE /api/v1/customers/{id}/wau/rules/{rid}
func (h *Handler) deleteWAURule(w http.ResponseWriter, r *http.Request) {
customerID, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
writeError(w, http.StatusBadRequest, "Ungültige Kunden-ID")
return
}
ruleID, err := strconv.Atoi(r.PathValue("rid"))
if err != nil {
writeError(w, http.StatusBadRequest, "Ungültige Regel-ID")
return
}
ok, err := h.db.DeleteWAURule(customerID, ruleID)
if err != nil {
writeError(w, http.StatusInternalServerError, "Fehler beim Löschen")
return
}
if !ok {
writeError(w, http.StatusNotFound, "Regel nicht gefunden")
return
}
h.auditLog(r, "wau.rule.delete", "customer", strconv.Itoa(customerID), "", strconv.Itoa(ruleID))
writeJSON(w, http.StatusOK, map[string]string{"message": "Regel gelöscht"})
}
// GET /api/v1/customers/{id}/wau/inventory
func (h *Handler) getWAUInventory(w http.ResponseWriter, r *http.Request) {
customerID, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
writeError(w, http.StatusBadRequest, "Ungültige Kunden-ID")
return
}
entries, err := h.db.GetWAUSoftwareInventory(customerID)
if err != nil {
writeError(w, http.StatusInternalServerError, "Fehler beim Laden des Inventars")
return
}
writeJSON(w, http.StatusOK, entries)
}
// GET /api/v1/customers/{id}/wau/included → Plaintext für WAU
func (h *Handler) wauIncludedList(w http.ResponseWriter, r *http.Request) {
h.serveWAUList(w, r, "allow")
}
// GET /api/v1/customers/{id}/wau/excluded → Plaintext für WAU
func (h *Handler) wauExcludedList(w http.ResponseWriter, r *http.Request) {
h.serveWAUList(w, r, "block")
}
func (h *Handler) serveWAUList(w http.ResponseWriter, r *http.Request, ruleType string) {
customerID, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
http.Error(w, "Ungültige Kunden-ID", http.StatusBadRequest)
return
}
ids, err := h.db.GetWAUList(customerID, ruleType)
if err != nil {
http.Error(w, "Fehler", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write([]byte(strings.Join(ids, "\r\n")))
if len(ids) > 0 {
w.Write([]byte("\r\n"))
}
}
// wauExpectedURLs leitet die erwarteten WAU-URLs aus dem Request-Host ab
func wauExpectedURLs(r *http.Request, customerID int) (includeURL, excludeURL string) {
scheme := "https"
if r.TLS == nil && (r.Header.Get("X-Forwarded-Proto") == "" || r.Header.Get("X-Forwarded-Proto") == "http") {
scheme = "http"
}
if fwd := r.Header.Get("X-Forwarded-Proto"); fwd != "" {
scheme = fwd
}
host := r.Host
base := scheme + "://" + host
includeURL = base + "/api/v1/customers/" + strconv.Itoa(customerID) + "/wau/included"
excludeURL = base + "/api/v1/customers/" + strconv.Itoa(customerID) + "/wau/excluded"
return
}
// GET /api/v1/customers/{id}/wau/compliance
func (h *Handler) getWAUCompliance(w http.ResponseWriter, r *http.Request) {
customerID, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
writeError(w, http.StatusBadRequest, "Ungültige Kunden-ID")
return
}
includeURL, excludeURL := wauExpectedURLs(r, customerID)
agents, err := h.db.GetWAUCompliance(customerID, includeURL, excludeURL)
if err != nil {
writeError(w, http.StatusInternalServerError, "Fehler beim Laden der Compliance-Daten")
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"agents": agents,
"include_url": includeURL,
"exclude_url": excludeURL,
})
}
// GET /api/v1/wau/compliance/summary — Zusammenfassung für alle Kunden
func (h *Handler) getWAUComplianceSummary(w http.ResponseWriter, r *http.Request) {
// Ohne customer_id können wir keine URLs ableiten — wir geben Summary ohne URL-Check zurück
// oder nutzen customer_id=0 als Wildcard (kein URL-Check)
summaries, err := h.db.GetAllCustomersWAUSummary("", "")
if err != nil {
writeError(w, http.StatusInternalServerError, "Fehler beim Laden der Compliance-Daten")
return
}
writeJSON(w, http.StatusOK, summaries)
}