Feature: Audit-Log - tracks all user actions with timestamps
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// auditLog writes an audit log entry, extracting user from JWT context
|
||||
func (h *Handler) auditLog(r *http.Request, action, targetType, targetID, targetName, details string) {
|
||||
userName := "api-key"
|
||||
if claims, ok := r.Context().Value(UserContextKey).(*JWTClaims); ok {
|
||||
userName = claims.Username
|
||||
}
|
||||
sourceIP := r.RemoteAddr
|
||||
|
||||
go func() {
|
||||
if err := h.db.AddAuditLog(userName, action, targetType, targetID, targetName, details, sourceIP); err != nil {
|
||||
log.Printf("Audit-Log Fehler: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// auditLogDirect writes an audit log entry with explicit userName (for login handler)
|
||||
func auditLogDirect(h *Handler, r *http.Request, userName, action, targetType, targetID, targetName, details string) {
|
||||
sourceIP := r.RemoteAddr
|
||||
go func() {
|
||||
if err := h.db.AddAuditLog(userName, action, targetType, targetID, targetName, details, sourceIP); err != nil {
|
||||
log.Printf("Audit-Log Fehler: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// GET /api/v1/audit
|
||||
func (h *Handler) getAuditLogs(w http.ResponseWriter, r *http.Request) {
|
||||
// JWT only - check that user context exists
|
||||
if _, ok := r.Context().Value(UserContextKey).(*JWTClaims); !ok {
|
||||
writeError(w, http.StatusForbidden, "JWT-Authentifizierung erforderlich")
|
||||
return
|
||||
}
|
||||
|
||||
limit := 100
|
||||
offset := 0
|
||||
action := r.URL.Query().Get("action")
|
||||
agentID := r.URL.Query().Get("agent_id")
|
||||
|
||||
if l := r.URL.Query().Get("limit"); l != "" {
|
||||
if n, err := strconv.Atoi(l); err == nil && n > 0 {
|
||||
limit = n
|
||||
}
|
||||
}
|
||||
if o := r.URL.Query().Get("offset"); o != "" {
|
||||
if n, err := strconv.Atoi(o); err == nil && n >= 0 {
|
||||
offset = n
|
||||
}
|
||||
}
|
||||
|
||||
entries, total, err := h.db.GetAuditLogs(limit, offset, action, agentID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, "Fehler beim Laden")
|
||||
return
|
||||
}
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"data": entries,
|
||||
"total": total,
|
||||
})
|
||||
}
|
||||
@@ -114,6 +114,9 @@ func (a *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
a.db.UpdateUserLastLogin(user.ID)
|
||||
|
||||
// Audit log
|
||||
go a.db.AddAuditLog(user.Username, "login", "user", "", user.Username, "", r.RemoteAddr)
|
||||
|
||||
writeJSON(w, http.StatusOK, models.LoginResponse{
|
||||
Token: token,
|
||||
ExpiresAt: expiresAt.Format(time.RFC3339),
|
||||
|
||||
@@ -86,6 +86,8 @@ func (h *Handler) triggerBackup(hub *ws.Hub) http.HandlerFunc {
|
||||
msg = "Config unveraendert (Hash identisch)"
|
||||
}
|
||||
|
||||
h.auditLog(r, "backup.create", "agent", agentID, "", fmt.Sprintf("backup_id=%d", backupID))
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"backup_id": backupID,
|
||||
"hash": hash,
|
||||
@@ -167,6 +169,8 @@ func (h *Handler) getBackup() http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
h.auditLog(r, "backup.download", "agent", agentID, "", fmt.Sprintf("backup_id=%s", backupIDStr))
|
||||
|
||||
// Raw XML Download
|
||||
if r.URL.Query().Get("format") == "xml" {
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
|
||||
@@ -31,6 +31,7 @@ func (h *Handler) createCustomer(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusInternalServerError, "Kunde anlegen fehlgeschlagen")
|
||||
return
|
||||
}
|
||||
h.auditLog(r, "customer.create", "customer", req.Number, req.Name, "")
|
||||
writeJSON(w, http.StatusCreated, c)
|
||||
}
|
||||
|
||||
@@ -91,6 +92,7 @@ func (h *Handler) deleteCustomer(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusNotFound, "Kunde nicht gefunden")
|
||||
return
|
||||
}
|
||||
h.auditLog(r, "customer.delete", "customer", strconv.Itoa(id), "", "")
|
||||
writeJSON(w, http.StatusOK, map[string]string{"message": "Kunde geloescht"})
|
||||
}
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ func (h *Handler) uploadFirmware(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
h.auditLog(r, "firmware.upload", "firmware", version, platform, fmt.Sprintf("%d bytes", len(binary)))
|
||||
log.Printf("Agent-Firmware v%s (%s) hochgeladen (%d bytes, Hash: %s)", version, platform, len(binary), hashStr[:16])
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
@@ -133,6 +134,7 @@ func (h *Handler) requestUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusInternalServerError, "Fehler beim Setzen des Update-Flags")
|
||||
return
|
||||
}
|
||||
h.auditLog(r, "firmware.update_request", "agent", agentID, "", "")
|
||||
log.Printf("Update angefordert fuer Agent %s", agentID)
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"message": "Update angefordert", "agent_id": agentID})
|
||||
}
|
||||
@@ -151,6 +153,7 @@ func (h *Handler) requestUpdateAll(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusInternalServerError, "Fehler")
|
||||
return
|
||||
}
|
||||
h.auditLog(r, "firmware.update_all", "", "", "", fmt.Sprintf("%d Agents", count))
|
||||
log.Printf("Update angefordert fuer %d Agents", count)
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"message": fmt.Sprintf("Update fuer %d Agents angefordert", count), "count": count})
|
||||
}
|
||||
|
||||
@@ -59,6 +59,9 @@ func (h *Handler) SetupRoutes(mux *http.ServeMux, hub *ws.Hub) {
|
||||
mux.HandleFunc("POST /api/v1/apikeys", h.createAPIKey)
|
||||
mux.HandleFunc("DELETE /api/v1/apikeys/{id}", h.deleteAPIKey)
|
||||
|
||||
// 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)
|
||||
@@ -210,6 +213,7 @@ func (h *Handler) preRegisterAgent(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -354,6 +358,7 @@ func (h *Handler) deleteAgent(w http.ResponseWriter, r *http.Request) {
|
||||
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"})
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
@@ -99,6 +100,13 @@ func (h *Handler) executeCommand(hub *ws.Hub) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
// Audit
|
||||
cmdTrunc := req.Command
|
||||
if len(cmdTrunc) > 100 {
|
||||
cmdTrunc = cmdTrunc[:100]
|
||||
}
|
||||
h.auditLog(r, "exec", "agent", agentID, "", cmdTrunc)
|
||||
|
||||
// Response zurückgeben
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"message": "Command gesendet",
|
||||
@@ -135,6 +143,8 @@ func (h *Handler) openTunnel(hub *ws.Hub) http.HandlerFunc {
|
||||
return
|
||||
}
|
||||
|
||||
h.auditLog(r, "tunnel.open", "agent", agentID, "", fmt.Sprintf("%s:%d", req.TargetHost, req.TargetPort))
|
||||
|
||||
// Response
|
||||
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"tunnel_id": tunnel.ID,
|
||||
@@ -155,6 +165,9 @@ func (h *Handler) closeTunnel(hub *ws.Hub) http.HandlerFunc {
|
||||
writeError(w, http.StatusNotFound, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
agentID := r.PathValue("id")
|
||||
h.auditLog(r, "tunnel.close", "agent", agentID, "", tunnelID)
|
||||
|
||||
writeJSON(w, http.StatusOK, map[string]string{
|
||||
"message": "Tunnel geschlossen",
|
||||
@@ -201,7 +214,11 @@ func (h *Handler) httpProxy(hub *ws.Hub) http.HandlerFunc {
|
||||
// --- Update Commands ---
|
||||
|
||||
func (h *Handler) agentUpdateCheck(hub *ws.Hub) http.HandlerFunc {
|
||||
return h.sendAgentCommand(hub, "update_check", nil, 120)
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
agentID := r.PathValue("id")
|
||||
h.auditLog(r, "update.check", "agent", agentID, "", "")
|
||||
h.sendCommandAndWait(hub, agentID, "update_check", nil, 120, w)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) agentUpdate(hub *ws.Hub) http.HandlerFunc {
|
||||
@@ -217,6 +234,11 @@ func (h *Handler) agentUpdate(hub *ws.Hub) http.HandlerFunc {
|
||||
"reboot": req.Reboot,
|
||||
}
|
||||
|
||||
rebootStr := "ohne Reboot"
|
||||
if req.Reboot {
|
||||
rebootStr = "mit Reboot"
|
||||
}
|
||||
h.auditLog(r, "update.run", "agent", agentID, "", rebootStr)
|
||||
h.sendCommandAndWait(hub, agentID, "update", params, 900, w)
|
||||
}
|
||||
}
|
||||
@@ -243,6 +265,7 @@ func (h *Handler) agentMajorUpdate(hub *ws.Hub) http.HandlerFunc {
|
||||
params["phase"] = req.Phase
|
||||
}
|
||||
|
||||
h.auditLog(r, "update.major", "agent", agentID, "", fmt.Sprintf("Version: %s, Phase: %s", req.Version, req.Phase))
|
||||
h.sendCommandAndWait(hub, agentID, "major_update", params, 1200, w)
|
||||
}
|
||||
}
|
||||
@@ -261,6 +284,7 @@ func (h *Handler) agentReboot(hub *ws.Hub) http.HandlerFunc {
|
||||
params["delay"] = req.Delay
|
||||
}
|
||||
|
||||
h.auditLog(r, "reboot", "agent", agentID, "", "")
|
||||
h.sendCommandAndWait(hub, agentID, "reboot", params, 30, w)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ func (h *Handler) createUser(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusInternalServerError, "User anlegen fehlgeschlagen")
|
||||
return
|
||||
}
|
||||
h.auditLog(r, "user.create", "user", "", req.Username, "")
|
||||
writeJSON(w, http.StatusCreated, u)
|
||||
}
|
||||
|
||||
@@ -61,6 +62,7 @@ func (h *Handler) changePassword(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusInternalServerError, "Passwort aendern fehlgeschlagen")
|
||||
return
|
||||
}
|
||||
h.auditLog(r, "password.change", "user", strconv.Itoa(id), "", "")
|
||||
writeJSON(w, http.StatusOK, map[string]string{"message": "Passwort geaendert"})
|
||||
}
|
||||
|
||||
@@ -80,5 +82,6 @@ func (h *Handler) deleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusNotFound, "User nicht gefunden")
|
||||
return
|
||||
}
|
||||
h.auditLog(r, "user.delete", "user", strconv.Itoa(id), "", "")
|
||||
writeJSON(w, http.StatusOK, map[string]string{"message": "User geloescht"})
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ func (h *Handler) wgAddPeer(hub *ws.Hub) http.HandlerFunc {
|
||||
params["server_name"] = req.ServerName
|
||||
}
|
||||
|
||||
h.auditLog(r, "wireguard.add", "agent", agentID, "", req.Name)
|
||||
h.sendCommandAndWait(hub, agentID, "wg_add_peer", params, 30, w)
|
||||
}
|
||||
}
|
||||
@@ -52,6 +53,7 @@ func (h *Handler) wgDeletePeer(hub *ws.Hub) http.HandlerFunc {
|
||||
"peer_uuid": peerUUID,
|
||||
}
|
||||
|
||||
h.auditLog(r, "wireguard.delete", "agent", agentID, "", peerUUID)
|
||||
h.sendCommandAndWait(hub, agentID, "wg_delete_peer", params, 30, w)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user