**Warum keine Wiederherstellung funktionierte.** Der Dienst laeuft mit
`ProtectSystem=strict` und `ReadWritePaths` nur auf Repository und
Sicherungsordner — alles andere ist fuer ihn schreibgeschuetzt. Jedes Ziel
ausserhalb endete mit "mkdir: permission denied", und zwar **nach** der
Vorabpruefung. `/tmp` scheiterte anders: Mit `PrivateTmp=yes` hat der Dienst ein
eigenes /tmp, und was dort landet, sieht man von aussen gar nicht.
`setup.sh` legt jetzt `/srv/syncova-restore` an und traegt es in
`ReadWritePaths` ein; `--wiederherstellungsziel` ergaenzt weitere. Der Ort liegt
unter /srv und nicht unter /var/lib — Letzteres steht auf der Sperrliste des
Zielschutzes. Beide Regeln zugleich zu erfuellen laesst genau /srv uebrig; das
ist mir erst aufgefallen, nachdem ich die Flaeche zunaechst falsch gelegt hatte
und der eigene Zielschutz sie ablehnte.
**Zwei neue Endpunkte** (Vertrag entsprechend erweitert):
- `GET /filesystem/browse` — Verzeichnisse mit der Angabe, ob der **Dienst**
dort schreiben darf. **Gemessen** durch eine Probedatei, nicht aus den
Rechtebits geraten: Unter ProtectSystem=strict sagen die Bits nichts ueber
das aus, was der Namensraum zulaesst. Gesperrte Orte werden gezeigt, nicht
versteckt — sonst bliebe offen, warum ein Pfad fehlt.
- `GET /backups/{id}/contents` — das Manifest als Ebene eines Baums. Der Baum
entsteht aus den **Pfaden**, nicht aus Verzeichniseintraegen: Ein Manifest
kann eine Datei enthalten, deren Elternverzeichnis nicht als eigener Eintrag
vorliegt, und wer nur `directory`-Eintraege auflistet, verliert ganze
Teilbaeume. Durch Mutation bestaetigt.
**Auswahl statt Textfeld.** Der Assistent hat jetzt einen Ordnerbaum fuer das
Ziel und einen Browser fuer den Backup-Inhalt. Ordner **und** einzelne Dateien
lassen sich waehlen; beides geht als `path_prefix` in die Anfrage, weil der
Server auf Gleichheit oder Praefix mit Verzeichnisgrenze vergleicht. Bewusste
Grenze: eine Auswahl je Lauf — eine Liste kennt die API nicht, und mehrere
Laeufe vorzutaeuschen ergaebe mehrere Ausgaenge, die niemand mehr erklaeren
kann.
**Integritaetslauf.** "can't access property toLocaleString, chunks_checked is
undefined" — die Ergebnisse liegen unter `details`, und die Felder heissen
`missing_chunks`/`corrupted_chunks`, nicht umgekehrt. Betrifft alle vier
Pruefendpunkte; sie tragen dieselbe Huelle. Derselbe Fehler wie bei
/retention-policies: die Antwortform angenommen statt geprueft.
**Geist Mono liegt jetzt im Paket** (drei Schnitte, 128 KB, OFL-Lizenz dabei).
Ausgeliefert vom eigenen Ursprung — das verlangt die CSP, und ein
Backup-Server, dessen Oberflaeche von einem CDN abhaengt, waere auch ohne CSP
falsch. `font-display: swap`, damit der Text sofort steht.
Nachgewiesen gegen Debian 12: Vollwiederherstellung (5 Dateien), nur ein Ordner
(2 Dateien), nur eine Datei (1 Datei) — alle drei bitgenau. Der Server nennt
/srv/syncova-restore als beschreibbar und /etc, /usr, /var als gesperrt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
719 lines
38 KiB
Go
719 lines
38 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"log/slog"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/syncova/syncova/packages/agentregistry"
|
|
"github.com/syncova/syncova/packages/agenttasks"
|
|
"github.com/syncova/syncova/packages/alerting"
|
|
"github.com/syncova/syncova/packages/audit"
|
|
"github.com/syncova/syncova/packages/auth"
|
|
"github.com/syncova/syncova/packages/hypervisor"
|
|
"github.com/syncova/syncova/packages/jobs"
|
|
"github.com/syncova/syncova/packages/metrics"
|
|
"github.com/syncova/syncova/packages/platform/config"
|
|
"github.com/syncova/syncova/packages/platform/crypto"
|
|
"github.com/syncova/syncova/packages/platform/health"
|
|
"github.com/syncova/syncova/packages/platform/logging"
|
|
"github.com/syncova/syncova/packages/platform/netguard"
|
|
"github.com/syncova/syncova/packages/ransomware"
|
|
"github.com/syncova/syncova/packages/recovery"
|
|
"github.com/syncova/syncova/packages/reports"
|
|
"github.com/syncova/syncova/packages/retention"
|
|
"github.com/syncova/syncova/packages/security"
|
|
"github.com/syncova/syncova/packages/verification"
|
|
)
|
|
|
|
// apiBasePath ist das Präfix aller fachlichen Endpunkte (SYNCOVA_API.md).
|
|
// Die Version ist Teil des Pfads, damit spätere Verträge parallel bestehen können.
|
|
const apiBasePath = "/api/v1"
|
|
|
|
// Grenzen des Anmeldeschutzes.
|
|
//
|
|
// Sie greifen je Absenderadresse und ergänzen die kontobezogene Sperre: ohne sie
|
|
// könnte ein Angreifer viele Konten mit je wenigen Versuchen durchprobieren.
|
|
const (
|
|
// loginRateLimitAttempts ist die Anzahl erlaubter Anmeldeversuche je Zeitfenster.
|
|
loginRateLimitAttempts = 10
|
|
// loginRateLimitWindow ist das betrachtete Zeitfenster.
|
|
loginRateLimitWindow = 5 * time.Minute
|
|
// rateLimiterCleanupInterval ist der Abstand der Speicherbereinigung.
|
|
rateLimiterCleanupInterval = 10 * time.Minute
|
|
)
|
|
|
|
// RouterDependencies bündelt, was der Router zur Bedienung der Endpunkte braucht.
|
|
type RouterDependencies struct {
|
|
// Config ist die geladene Dienstkonfiguration.
|
|
Config config.Config
|
|
// Logger ist der Basis-Logger des Dienstes.
|
|
Logger *slog.Logger
|
|
// HealthRegistry liefert den Zustand aller überwachten Komponenten.
|
|
HealthRegistry *health.Registry
|
|
// BuildVersion ist die ausgelieferte Version, sichtbar unter /health.
|
|
BuildVersion string
|
|
// AuthService ist die Domänenlogik der Identitätsverwaltung.
|
|
AuthService *auth.Service
|
|
// AuthRepository ist die Datenzugriffsschicht für Rollen und Berechtigungen.
|
|
AuthRepository *auth.Repository
|
|
// AuditRecorder protokolliert sicherheitsrelevante Handlungen.
|
|
AuditRecorder audit.Recorder
|
|
// AgentService ist die Domänenlogik der Agent-Verwaltung.
|
|
AgentService *agentregistry.Service
|
|
// RestoreStore ist die Datenzugriffsschicht der Wiederherstellungen.
|
|
//
|
|
// Sie darf nil sein; dann werden die Wiederherstellungsrouten nicht
|
|
// eingebunden.
|
|
RestoreStore *recovery.Store
|
|
// JobStore ist die Datenzugriffsschicht der Sicherungsaufträge.
|
|
//
|
|
// Sie darf nil sein; dann werden die Auftragsrouten nicht eingebunden. Das
|
|
// erlaubt einen Dienst ohne Auftragsverwaltung, ohne dass ein Aufruf in
|
|
// einen Nil-Zeiger läuft.
|
|
JobStore *jobs.PostgresStore
|
|
// VerificationStore ist die Datenzugriffsschicht der Prüfungen.
|
|
//
|
|
// Sie darf nil sein; dann werden die Prüfrouten nicht eingebunden.
|
|
VerificationStore *verification.Store
|
|
// RetentionStore ist die Datenzugriffsschicht der Aufbewahrung.
|
|
//
|
|
// Sie darf nil sein; dann werden die Aufbewahrungsrouten nicht eingebunden.
|
|
RetentionStore *retention.Store
|
|
// MetricsStore bildet die Zeitreihen der Diagramme.
|
|
//
|
|
// Sie darf nil sein; dann werden die Kennzahlenrouten nicht eingebunden.
|
|
MetricsStore *metrics.Store
|
|
// AlertStore ist die Datenzugriffsschicht der Meldungen.
|
|
//
|
|
// Sie darf nil sein; dann werden die Meldungsrouten nicht eingebunden.
|
|
AlertStore *alerting.Store
|
|
// SecretStore verschlüsselt die Zugangsgeheimnisse der Kanäle.
|
|
SecretStore crypto.SecretStore
|
|
// SecurityInspector beurteilt die Sicherheitslage.
|
|
//
|
|
// Er darf nil sein; dann wird das Security Center nicht eingebunden.
|
|
SecurityInspector *security.Inspector
|
|
// RansomwareDetector bewertet Sicherungslaeufe statistisch.
|
|
//
|
|
// Er darf nil sein; dann wird die Auffaelligkeitsbewertung nicht eingebunden.
|
|
RansomwareDetector *ransomware.Detector
|
|
// ReportGenerator erzeugt die Berichte.
|
|
//
|
|
// Er darf nil sein; dann werden die Berichtsrouten nicht eingebunden.
|
|
ReportGenerator *reports.Generator
|
|
// AgentTaskStore vermittelt Sicherungsaufträge an Agenten.
|
|
//
|
|
// Er darf nil sein; dann werden die Auftragsrouten nicht eingebunden.
|
|
AgentTaskStore *agenttasks.Store
|
|
// HypervisorStore verwaltet die Virtualisierungsumgebungen (Phase 7).
|
|
//
|
|
// Er darf nil sein; dann werden die Verbundrouten nicht eingebunden. Das
|
|
// ist der Normalfall ohne Schlüsselmaterial: Ohne SecretStore lassen sich
|
|
// keine Zugangsdaten ablegen, und ein Verbund ohne Zugangsdaten wäre eine
|
|
// Maske, die nichts bewirkt.
|
|
HypervisorStore *hypervisor.Store
|
|
}
|
|
|
|
// NewRouter baut den vollständigen HTTP-Handler des API-Dienstes.
|
|
//
|
|
// Die Middleware-Reihenfolge ist bewusst gewählt: Recovery und Correlation ID
|
|
// liegen außen, damit auch Fehler innerer Schichten protokolliert und einer
|
|
// Operation zugeordnet werden können.
|
|
func NewRouter(routerDependencies RouterDependencies) http.Handler {
|
|
requestMultiplexer := http.NewServeMux()
|
|
|
|
registerHealthRoutes(requestMultiplexer, routerDependencies)
|
|
registerAuthRoutes(requestMultiplexer, routerDependencies)
|
|
registerUserRoutes(requestMultiplexer, routerDependencies)
|
|
registerRoleRoutes(requestMultiplexer, routerDependencies)
|
|
registerAgentRoutes(requestMultiplexer, routerDependencies)
|
|
registerJobRoutes(requestMultiplexer, routerDependencies)
|
|
registerRestoreRoutes(requestMultiplexer, routerDependencies)
|
|
registerVerificationRoutes(requestMultiplexer, routerDependencies)
|
|
registerRetentionRoutes(requestMultiplexer, routerDependencies)
|
|
registerMetricsRoutes(requestMultiplexer, routerDependencies)
|
|
registerAlertRoutes(requestMultiplexer, routerDependencies)
|
|
registerSecurityRoutes(requestMultiplexer, routerDependencies)
|
|
registerRansomwareRoutes(requestMultiplexer, routerDependencies)
|
|
registerReportRoutes(requestMultiplexer, routerDependencies)
|
|
registerHypervisorRoutes(requestMultiplexer, routerDependencies)
|
|
|
|
// Alles Unbekannte wird in der Standard-Fehlerhülle beantwortet, damit Clients
|
|
// nie eine HTML-Fehlerseite von net/http erhalten.
|
|
requestMultiplexer.HandleFunc("/", func(responseWriter http.ResponseWriter, request *http.Request) {
|
|
requestLogger := logging.WithContext(request.Context(), routerDependencies.Logger)
|
|
WriteError(responseWriter, request, requestLogger, NewNotFoundError("Der angefragte Endpunkt existiert nicht."))
|
|
})
|
|
|
|
middlewareChain := []Middleware{
|
|
RecoveryMiddleware(routerDependencies.Logger),
|
|
CorrelationMiddleware(),
|
|
SecurityHeadersMiddleware(),
|
|
CORSMiddleware(routerDependencies.Config.HTTP.AllowedOrigins),
|
|
BodyLimitMiddleware(routerDependencies.Config.HTTP.MaxRequestBodyBytes),
|
|
}
|
|
|
|
// Der allgemeine Begrenzer schuetzt die gesamte API, nicht nur die
|
|
// Anmeldung (Phase 19).
|
|
//
|
|
// Bis dahin war ausschliesslich der Login begrenzt: Ein angemeldeter
|
|
// Benutzer — oder ein entwendetes Token — konnte die Anlage mit Anfragen
|
|
// ueberfluten. Besonders teuer sind die Endpunkte, die im Hintergrund
|
|
// arbeiten: Ein Bericht erzeugt ein PDF, eine Vorabpruefung liest jeden
|
|
// Block eines Backups, das Security Center stellt zehn Abfragen.
|
|
if routerDependencies.Config.HTTP.RequestsPerMinute > 0 {
|
|
generalRateLimiter := NewRateLimiter(
|
|
routerDependencies.Config.HTTP.RequestsPerMinute, time.Minute)
|
|
startRateLimiterCleanup(generalRateLimiter)
|
|
|
|
middlewareChain = append(middlewareChain,
|
|
RateLimitMiddleware(generalRateLimiter, routerDependencies.Logger))
|
|
}
|
|
|
|
middlewareChain = append(middlewareChain, AccessLogMiddleware(routerDependencies.Logger))
|
|
|
|
return Chain(requestMultiplexer, middlewareChain...)
|
|
}
|
|
|
|
// registerHealthRoutes bindet die Betriebs- und Zustandsendpunkte ein.
|
|
//
|
|
// Sie sind bewusst ohne Anmeldung erreichbar: ein Load Balancer kann sich nicht
|
|
// anmelden, und der Zustandsbericht enthält keine vertraulichen Angaben.
|
|
func registerHealthRoutes(requestMultiplexer *http.ServeMux, routerDependencies RouterDependencies) {
|
|
healthHandlerInstance := &healthHandler{
|
|
healthRegistry: routerDependencies.HealthRegistry,
|
|
logger: logging.WithComponent(routerDependencies.Logger, "health"),
|
|
buildVersion: routerDependencies.BuildVersion,
|
|
environment: routerDependencies.Config.Environment,
|
|
}
|
|
|
|
requestMultiplexer.HandleFunc("GET /health/live", healthHandlerInstance.handleLiveness)
|
|
requestMultiplexer.HandleFunc("GET /health/ready", healthHandlerInstance.handleReadiness)
|
|
requestMultiplexer.HandleFunc("GET "+apiBasePath+"/health", healthHandlerInstance.handleSystemHealth)
|
|
}
|
|
|
|
// registerAuthRoutes bindet Anmeldung und Selbstverwaltung ein.
|
|
func registerAuthRoutes(requestMultiplexer *http.ServeMux, routerDependencies RouterDependencies) {
|
|
authHandlerInstance := &authHandler{
|
|
authService: routerDependencies.AuthService,
|
|
logger: logging.WithComponent(routerDependencies.Logger, "auth"),
|
|
}
|
|
|
|
// Der Begrenzer schützt die unauthentifizierten Endpunkte vor dem
|
|
// Durchprobieren von Zugangsdaten.
|
|
loginRateLimiter := NewRateLimiter(loginRateLimitAttempts, loginRateLimitWindow)
|
|
startRateLimiterCleanup(loginRateLimiter)
|
|
|
|
rateLimitedChain := func(handlerFunction http.HandlerFunc) http.HandlerFunc {
|
|
limitedHandler := RateLimitMiddleware(loginRateLimiter, routerDependencies.Logger)(handlerFunction)
|
|
return limitedHandler.ServeHTTP
|
|
}
|
|
|
|
// Anmeldung: ohne Nachweis erreichbar, aber mengenmäßig begrenzt.
|
|
requestMultiplexer.HandleFunc("POST "+apiBasePath+"/auth/login", rateLimitedChain(authHandlerInstance.handleLogin))
|
|
requestMultiplexer.HandleFunc("POST "+apiBasePath+"/auth/mfa/verify", rateLimitedChain(authHandlerInstance.handleVerifyMFA))
|
|
requestMultiplexer.HandleFunc("POST "+apiBasePath+"/auth/refresh", rateLimitedChain(authHandlerInstance.handleRefresh))
|
|
|
|
// Alles Weitere setzt eine gültige Sitzung voraus.
|
|
authenticated := authenticatedHandler(routerDependencies)
|
|
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/auth/logout", authenticated(authHandlerInstance.handleLogout))
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/me", authenticated(authHandlerInstance.handleGetCurrentUser))
|
|
|
|
// Den eigenen zweiten Faktor darf jeder angemeldete Benutzer einrichten;
|
|
// dafür ist keine gesonderte Berechtigung nötig.
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/me/mfa/enroll", authenticated(authHandlerInstance.handleBeginMFAEnrollment))
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/me/mfa/confirm", authenticated(authHandlerInstance.handleConfirmMFAEnrollment))
|
|
}
|
|
|
|
// registerUserRoutes bindet die Benutzerverwaltung ein.
|
|
func registerUserRoutes(requestMultiplexer *http.ServeMux, routerDependencies RouterDependencies) {
|
|
userHandlerInstance := &userHandler{
|
|
authService: routerDependencies.AuthService,
|
|
logger: logging.WithComponent(routerDependencies.Logger, "users"),
|
|
}
|
|
|
|
protected := protectedHandler(routerDependencies)
|
|
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/users", protected("users.read", userHandlerInstance.handleListUsers))
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/users", protected("users.write", userHandlerInstance.handleCreateUser))
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/users/{id}", protected("users.read", userHandlerInstance.handleGetUser))
|
|
requestMultiplexer.Handle("PATCH "+apiBasePath+"/users/{id}", protected("users.write", userHandlerInstance.handleUpdateUser))
|
|
requestMultiplexer.Handle("DELETE "+apiBasePath+"/users/{id}", protected("users.write", userHandlerInstance.handleDeleteUser))
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/users/{id}/mfa/disable", protected("users.write", userHandlerInstance.handleDisableUserMFA))
|
|
}
|
|
|
|
// registerRoleRoutes bindet Rollen, Berechtigungen und das Auditprotokoll ein.
|
|
func registerRoleRoutes(requestMultiplexer *http.ServeMux, routerDependencies RouterDependencies) {
|
|
roleHandlerInstance := &roleHandler{
|
|
repository: routerDependencies.AuthRepository,
|
|
auditRecorder: routerDependencies.AuditRecorder,
|
|
logger: logging.WithComponent(routerDependencies.Logger, "roles"),
|
|
}
|
|
|
|
auditHandlerInstance := &auditHandler{
|
|
auditRecorder: routerDependencies.AuditRecorder,
|
|
logger: logging.WithComponent(routerDependencies.Logger, "audit"),
|
|
}
|
|
|
|
protected := protectedHandler(routerDependencies)
|
|
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/roles", protected("roles.read", roleHandlerInstance.handleListRoles))
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/roles", protected("roles.write", roleHandlerInstance.handleCreateRole))
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/roles/{id}", protected("roles.read", roleHandlerInstance.handleGetRole))
|
|
requestMultiplexer.Handle("PATCH "+apiBasePath+"/roles/{id}", protected("roles.write", roleHandlerInstance.handleUpdateRole))
|
|
requestMultiplexer.Handle("DELETE "+apiBasePath+"/roles/{id}", protected("roles.write", roleHandlerInstance.handleDeleteRole))
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/permissions", protected("roles.read", roleHandlerInstance.handleListPermissions))
|
|
|
|
// Der Zugriff auf das Auditprotokoll ist gesondert beschränkt (SYNCOVA_API.md §17).
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/audit-events", protected("audit.read", auditHandlerInstance.handleListAuditEvents))
|
|
}
|
|
|
|
// registerJobRoutes bindet die Sicherungsaufträge ein (SYNCOVA_API.md §9).
|
|
func registerJobRoutes(requestMultiplexer *http.ServeMux, routerDependencies RouterDependencies) {
|
|
if routerDependencies.JobStore == nil {
|
|
return
|
|
}
|
|
|
|
jobHandlerInstance := &jobHandler{
|
|
store: routerDependencies.JobStore,
|
|
auditRecorder: routerDependencies.AuditRecorder,
|
|
logger: logging.WithComponent(routerDependencies.Logger, "jobs"),
|
|
}
|
|
|
|
protected := protectedHandler(routerDependencies)
|
|
|
|
// Die Repositoryliste gehört fachlich zu den Zielen, wird aber vom
|
|
// Auftrags-Handler bedient: Sie hat kein eigenes Verhalten, und ein zweiter
|
|
// Handler für eine einzelne lesende Route wäre Aufwand ohne Nutzen.
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/repositories", protected("repositories.read", jobHandlerInstance.handleListRepositories))
|
|
|
|
// Eintragen, ansehen, umschalten und prüfen (SYNCOVA_API.md §8).
|
|
//
|
|
// Bis Phase 22 fehlten diese Endpunkte vollständig: Ein Repository ließ
|
|
// sich nur über SQL in die Control Plane bringen. Aufgefallen ist das erst
|
|
// beim vollständigen Durchlauf — die Anlage war über ihre eigene API nicht
|
|
// in Betrieb zu nehmen.
|
|
repositoryHandlerInstance := &repositoryHandler{
|
|
store: routerDependencies.JobStore,
|
|
auditRecorder: routerDependencies.AuditRecorder,
|
|
logger: logging.WithComponent(routerDependencies.Logger, "repositories"),
|
|
}
|
|
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/repositories",
|
|
protected("repositories.write", repositoryHandlerInstance.handleRegisterRepository))
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/repositories/{id}",
|
|
protected("repositories.read", repositoryHandlerInstance.handleGetRepository))
|
|
requestMultiplexer.Handle("PATCH "+apiBasePath+"/repositories/{id}",
|
|
protected("repositories.write", repositoryHandlerInstance.handleUpdateRepository))
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/repositories/{id}/test",
|
|
protected("repositories.read", repositoryHandlerInstance.handleTestRepository))
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/repositories/{id}/health-check",
|
|
protected("repositories.read", repositoryHandlerInstance.handleRepositoryHealth))
|
|
|
|
// Integritätslauf und Katalogaufbau hängen am Schreibrecht: Beide lesen das
|
|
// gesamte Repository und binden dessen Datenträger für die Dauer des Laufs.
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/repositories/{id}/integrity-scan",
|
|
protected("repositories.write", repositoryHandlerInstance.handleRepositoryIntegrityScan))
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/repositories/{id}/rebuild-catalog",
|
|
protected("repositories.write", repositoryHandlerInstance.handleRebuildCatalog))
|
|
|
|
// Die Wiederherstellungspunkte sind die zentrale Auskunft der Anlage. Sie
|
|
// hängen am Leserecht für Backups, nicht an dem für Aufträge: Wer beurteilen
|
|
// soll, ob etwas wiederherstellbar ist, braucht keinen Zugriff auf die
|
|
// Zeitpläne.
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/backups", protected("backups.read", jobHandlerInstance.handleListBackups))
|
|
|
|
// Die Übersicht fasst zusammen, was ohnehin lesbar ist, und braucht deshalb
|
|
// kein eigenes Recht über das Lesen von Backups hinaus.
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/dashboard", protected("backups.read", jobHandlerInstance.handleDashboard))
|
|
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/jobs", protected("jobs.read", jobHandlerInstance.handleListJobs))
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/jobs", protected("jobs.write", jobHandlerInstance.handleCreateJob))
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/jobs/{id}", protected("jobs.read", jobHandlerInstance.handleGetJob))
|
|
|
|
// Das Löschen verlangt dasselbe Recht wie das Ändern (siehe 000002: die
|
|
// Beschreibung von jobs.write nennt das Löschen ausdrücklich mit). Es wird
|
|
// immer auditiert.
|
|
requestMultiplexer.Handle("DELETE "+apiBasePath+"/jobs/{id}", protected("jobs.write", jobHandlerInstance.handleDeleteJob))
|
|
|
|
// Ausführen, Aussetzen und Fortsetzen greifen in den Betrieb ein, ändern
|
|
// aber keine Konfiguration — dafür gibt es jobs.run.
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/jobs/{id}/run", protected("jobs.run", jobHandlerInstance.handleRunJob))
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/jobs/{id}/pause", protected("jobs.run", jobHandlerInstance.handlePauseJob))
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/jobs/{id}/resume", protected("jobs.run", jobHandlerInstance.handleResumeJob))
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/jobs/{id}/runs", protected("jobs.read", jobHandlerInstance.handleListJobRuns))
|
|
|
|
// Der Abbruch eines Laufs steht unter /backup-runs (SYNCOVA_API.md §10):
|
|
// Er betrifft den Lauf, nicht den Auftrag, und der Aufrufer kennt an dieser
|
|
// Stelle oft nur die Laufkennung.
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/backup-runs/{id}/cancel", protected("jobs.run", jobHandlerInstance.handleCancelRun))
|
|
}
|
|
|
|
// registerRestoreRoutes bindet die Wiederherstellung ein (SYNCOVA_API.md §13).
|
|
func registerRestoreRoutes(requestMultiplexer *http.ServeMux, routerDependencies RouterDependencies) {
|
|
if routerDependencies.RestoreStore == nil || routerDependencies.JobStore == nil {
|
|
return
|
|
}
|
|
|
|
restoreHandlerInstance := &restoreHandler{
|
|
restoreStore: routerDependencies.RestoreStore,
|
|
jobStore: routerDependencies.JobStore,
|
|
auditRecorder: routerDependencies.AuditRecorder,
|
|
targetGuard: recovery.NewTargetGuard(routerDependencies.Config.Hardening.RestoreAllowedRoots),
|
|
logger: logging.WithComponent(routerDependencies.Logger, "restores"),
|
|
}
|
|
|
|
protected := protectedHandler(routerDependencies)
|
|
|
|
// Die Vorabprüfung schreibt nichts und braucht deshalb nur das Leserecht.
|
|
// Sie soll niedrigschwellig sein: Wer den Zustand der Backups beurteilen
|
|
// soll, muss sie ausführen können.
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/restores/validate", protected("restores.read", restoreHandlerInstance.handleValidateRestore))
|
|
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/restores", protected("restores.read", restoreHandlerInstance.handleListRestores))
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/restores", protected("restores.execute", restoreHandlerInstance.handleCreateRestore))
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/restores/{id}", protected("restores.read", restoreHandlerInstance.handleGetRestore))
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/restores/{id}/cancel", protected("restores.execute", restoreHandlerInstance.handleCancelRestore))
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/restores/{id}/resume", protected("restores.execute", restoreHandlerInstance.handleResumeRestore))
|
|
|
|
// Blättern in Dateisystem und Backup.
|
|
//
|
|
// Beide gehören zur Wiederherstellung und tragen deshalb deren Leserecht:
|
|
// Wer eine Wiederherstellung vorbereiten darf, muss sehen können, was im
|
|
// Backup steckt und wohin sich zurückschreiben lässt.
|
|
browseHandlerInstance := &browseHandler{
|
|
restoreHandlerReference: restoreHandlerInstance,
|
|
targetGuard: restoreHandlerInstance.targetGuard,
|
|
}
|
|
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/filesystem/browse", protected("restores.read", browseHandlerInstance.handleBrowseFilesystem))
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/backups/{id}/contents", protected("restores.read", browseHandlerInstance.handleBrowseBackupContents))
|
|
}
|
|
|
|
// registerVerificationRoutes bindet die Prüfung ein (SYNCOVA_API.md §14).
|
|
func registerVerificationRoutes(requestMultiplexer *http.ServeMux, routerDependencies RouterDependencies) {
|
|
if routerDependencies.VerificationStore == nil || routerDependencies.JobStore == nil {
|
|
return
|
|
}
|
|
|
|
verificationHandlerInstance := &verificationHandler{
|
|
verificationStore: routerDependencies.VerificationStore,
|
|
jobStore: routerDependencies.JobStore,
|
|
auditRecorder: routerDependencies.AuditRecorder,
|
|
logger: logging.WithComponent(routerDependencies.Logger, "verification"),
|
|
}
|
|
|
|
protected := protectedHandler(routerDependencies)
|
|
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/verification", protected("verification.read", verificationHandlerInstance.handleListVerifications))
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/verification", protected("verification.write", verificationHandlerInstance.handleCreateVerification))
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/verification/{id}", protected("verification.read", verificationHandlerInstance.handleGetVerification))
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/verification/{id}/results", protected("verification.read", verificationHandlerInstance.handleGetVerificationResults))
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/verification/{id}/cancel", protected("verification.write", verificationHandlerInstance.handleCancelVerification))
|
|
|
|
// Die Bewertung steht am Backup, nicht bei den Prüfungen: Sie beantwortet
|
|
// eine Frage über das Backup („kann ich mich darauf verlassen?"), nicht über
|
|
// einen einzelnen Prüflauf. Sie schreibt nichts und braucht deshalb nur das
|
|
// Leserecht auf Backups.
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/backups/{id}/assurance", protected("backups.read", verificationHandlerInstance.handleGetAssurance))
|
|
}
|
|
|
|
// registerRetentionRoutes bindet Aufbewahrung und Unveränderlichkeit ein (Phase 11).
|
|
func registerRetentionRoutes(requestMultiplexer *http.ServeMux, routerDependencies RouterDependencies) {
|
|
if routerDependencies.RetentionStore == nil || routerDependencies.JobStore == nil {
|
|
return
|
|
}
|
|
|
|
retentionHandlerInstance := &retentionHandler{
|
|
retentionStore: routerDependencies.RetentionStore,
|
|
jobStore: routerDependencies.JobStore,
|
|
auditRecorder: routerDependencies.AuditRecorder,
|
|
logger: logging.WithComponent(routerDependencies.Logger, "retention"),
|
|
}
|
|
|
|
protected := protectedHandler(routerDependencies)
|
|
|
|
// Aufbewahrungsregeln.
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/retention-policies", protected("repositories.read", retentionHandlerInstance.handleListPolicies))
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/retention-policies", protected("retention.write", retentionHandlerInstance.handleCreatePolicy))
|
|
requestMultiplexer.Handle("PATCH "+apiBasePath+"/retention-policies/{id}", protected("retention.write", retentionHandlerInstance.handleUpdatePolicy))
|
|
requestMultiplexer.Handle("DELETE "+apiBasePath+"/retention-policies/{id}", protected("retention.write", retentionHandlerInstance.handleDeletePolicy))
|
|
|
|
// Die Vorschau schreibt nichts und braucht deshalb nur das Leserecht. Sie
|
|
// soll niedrigschwellig sein: Wer wissen will, was eine Regel anrichtet, muss
|
|
// sie ausprobieren können, ohne sie ausführen zu dürfen.
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/repositories/{id}/retention/preview", protected("repositories.read", retentionHandlerInstance.handlePreviewRetention))
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/repositories/{id}/retention/apply", protected("retention.write", retentionHandlerInstance.handleApplyRetention))
|
|
|
|
// Die Messung fasst nur eine Probedatei an und beantwortet die wichtigste
|
|
// Frage des gehärteten Betriebs: Was verhindert dieser Speicher wirklich?
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/repositories/{id}/enforcement/measure", protected("repositories.write", retentionHandlerInstance.handleMeasureEnforcement))
|
|
|
|
// Schutzlage eines einzelnen Backups.
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/backups/{id}/protection", protected("backups.read", retentionHandlerInstance.handleGetProtection))
|
|
|
|
// Verlängern und Legal Hold hängen an immutability.manage — nicht an
|
|
// backups.delete. Wer aufräumen darf, darf deshalb noch lange keinen
|
|
// Aufbewahrungsschutz aufheben.
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/backups/{id}/retention/extend", protected("immutability.manage", retentionHandlerInstance.handleExtendRetention))
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/backups/{id}/legal-hold", protected("immutability.manage", retentionHandlerInstance.handlePlaceLegalHold))
|
|
requestMultiplexer.Handle("DELETE "+apiBasePath+"/backups/{id}/legal-hold", protected("immutability.manage", retentionHandlerInstance.handleReleaseLegalHold))
|
|
|
|
// Das Löschen eines einzelnen Backups.
|
|
requestMultiplexer.Handle("DELETE "+apiBasePath+"/backups/{id}", protected("backups.delete", retentionHandlerInstance.handleDeleteBackup))
|
|
}
|
|
|
|
// registerMetricsRoutes bindet die Kennzahlen und Diagramme ein (Phase 13).
|
|
func registerMetricsRoutes(requestMultiplexer *http.ServeMux, routerDependencies RouterDependencies) {
|
|
if routerDependencies.MetricsStore == nil {
|
|
return
|
|
}
|
|
|
|
metricsHandlerInstance := &metricsHandler{
|
|
metricsStore: routerDependencies.MetricsStore,
|
|
logger: logging.WithComponent(routerDependencies.Logger, "metrics"),
|
|
}
|
|
|
|
protected := protectedHandler(routerDependencies)
|
|
|
|
// Die Kennzahlen fassen zusammen, was ohnehin lesbar ist, und hängen deshalb
|
|
// am Überwachungsrecht statt an einem eigenen.
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/metrics", protected("monitoring.read", metricsHandlerInstance.handleListCharts))
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/metrics/{metric}", protected("monitoring.read", metricsHandlerInstance.handleGetChart))
|
|
}
|
|
|
|
// registerAlertRoutes bindet Meldungen und Benachrichtigungen ein (Phase 14).
|
|
func registerAlertRoutes(requestMultiplexer *http.ServeMux, routerDependencies RouterDependencies) {
|
|
if routerDependencies.AlertStore == nil {
|
|
return
|
|
}
|
|
|
|
alertHandlerInstance := &alertHandler{
|
|
alertStore: routerDependencies.AlertStore,
|
|
secretStore: routerDependencies.SecretStore,
|
|
auditRecorder: routerDependencies.AuditRecorder,
|
|
addressGuard: netguard.NewGuard(routerDependencies.Config.Hardening.AllowInternalNotificationTargets),
|
|
logger: logging.WithComponent(routerDependencies.Logger, "alerts"),
|
|
}
|
|
|
|
protected := protectedHandler(routerDependencies)
|
|
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/alerts", protected("alerts.read", alertHandlerInstance.handleListAlerts))
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/alerts/summary", protected("alerts.read", alertHandlerInstance.handleAlertSummary))
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/alerts/{id}", protected("alerts.read", alertHandlerInstance.handleGetAlert))
|
|
|
|
// Bestätigen und Auflösen greifen in den Betrieb ein, ändern aber keine
|
|
// Konfiguration — beides hängt an alerts.write.
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/alerts/{id}/acknowledge", protected("alerts.write", alertHandlerInstance.handleAcknowledgeAlert))
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/alerts/{id}/resolve", protected("alerts.write", alertHandlerInstance.handleResolveAlert))
|
|
|
|
// Die Kanäle sind Konfiguration und hängen deshalb am Einstellungsrecht: Wer
|
|
// Benachrichtigungen umleitet, kann erreichen, dass niemand mehr von einem
|
|
// Ausfall erfährt.
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/notification-channels", protected("settings.read", alertHandlerInstance.handleListChannels))
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/notification-channels", protected("settings.write", alertHandlerInstance.handleCreateChannel))
|
|
requestMultiplexer.Handle("DELETE "+apiBasePath+"/notification-channels/{id}", protected("settings.write", alertHandlerInstance.handleDeleteChannel))
|
|
}
|
|
|
|
// registerSecurityRoutes bindet das Security Center ein (Phase 15).
|
|
func registerSecurityRoutes(requestMultiplexer *http.ServeMux, routerDependencies RouterDependencies) {
|
|
if routerDependencies.SecurityInspector == nil {
|
|
return
|
|
}
|
|
|
|
securityHandlerInstance := &securityHandler{
|
|
inspector: routerDependencies.SecurityInspector,
|
|
logger: logging.WithComponent(routerDependencies.Logger, "security"),
|
|
}
|
|
|
|
protected := protectedHandler(routerDependencies)
|
|
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/security", protected("security.read", securityHandlerInstance.handleGetAssessment))
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/security/findings", protected("security.read", securityHandlerInstance.handleListFindings))
|
|
}
|
|
|
|
// registerAgentRoutes bindet die Agent-Endpunkte ein (SYNCOVA_API.md §6).
|
|
func registerAgentRoutes(requestMultiplexer *http.ServeMux, routerDependencies RouterDependencies) {
|
|
agentHandlerInstance := &agentHandler{
|
|
agentService: routerDependencies.AgentService,
|
|
logger: logging.WithComponent(routerDependencies.Logger, "agents"),
|
|
}
|
|
|
|
protected := protectedHandler(routerDependencies)
|
|
|
|
// Verwaltung durch Benutzer.
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/agents", protected("agents.read", agentHandlerInstance.handleListAgents))
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/agents/{id}", protected("agents.read", agentHandlerInstance.handleGetAgent))
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/agents/{id}/health", protected("agents.read", agentHandlerInstance.handleAgentHealth))
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/agents/{id}/revoke", protected("agents.write", agentHandlerInstance.handleRevokeAgent))
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/agents/{id}/rotate-credentials", protected("agents.write", agentHandlerInstance.handleRotateCredentials))
|
|
|
|
// Die Ausstellung eines Aufnahme-Tokens nimmt ein neues System in die
|
|
// Infrastruktur auf und ist deshalb an ein eigenes Recht gebunden.
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/agents/enrollment-tokens",
|
|
protected("agents.enroll", agentHandlerInstance.handleIssueEnrollmentToken))
|
|
|
|
// Die Registrierung ist ohne Benutzeranmeldung erreichbar: ein sich
|
|
// aufnehmender Agent besitzt noch kein Betriebstoken. Sein Nachweis ist
|
|
// das Aufnahme-Token, das die Domänenlogik prüft.
|
|
requestMultiplexer.HandleFunc("POST "+apiBasePath+"/agents/register", agentHandlerInstance.handleRegisterAgent)
|
|
|
|
// Die Lebendmeldung verlangt das Betriebstoken des Agents, nicht die
|
|
// Sitzung eines Benutzers.
|
|
agentAuthenticated := AgentAuthenticationMiddleware(routerDependencies.AgentService, routerDependencies.Logger)
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/agents/heartbeat",
|
|
agentAuthenticated(http.HandlerFunc(agentHandlerInstance.handleHeartbeat)))
|
|
|
|
// Die Auftragsübermittlung hängt ebenfalls am Betriebstoken des Agenten.
|
|
//
|
|
// Der Agent **holt** ab; der Server drückt nicht. Ein Agent steht hinter
|
|
// einer Firewall, oft hinter NAT — die Verbindung geht immer von ihm aus.
|
|
if routerDependencies.AgentTaskStore != nil {
|
|
agentTaskHandlerInstance := &agentTaskHandler{
|
|
taskStore: routerDependencies.AgentTaskStore,
|
|
logger: logging.WithComponent(routerDependencies.Logger, "agent-tasks"),
|
|
}
|
|
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/agents/tasks/claim",
|
|
agentAuthenticated(http.HandlerFunc(agentTaskHandlerInstance.handleClaimTask)))
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/agents/tasks/{id}/progress",
|
|
agentAuthenticated(http.HandlerFunc(agentTaskHandlerInstance.handleReportProgress)))
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/agents/tasks/{id}/result",
|
|
agentAuthenticated(http.HandlerFunc(agentTaskHandlerInstance.handleReportResult)))
|
|
}
|
|
}
|
|
|
|
// authenticatedHandler baut einen Handler, der eine gültige Sitzung verlangt.
|
|
func authenticatedHandler(routerDependencies RouterDependencies) func(http.HandlerFunc) http.Handler {
|
|
return func(handlerFunction http.HandlerFunc) http.Handler {
|
|
return AuthenticationMiddleware(routerDependencies.AuthService, routerDependencies.Logger)(handlerFunction)
|
|
}
|
|
}
|
|
|
|
// protectedHandler baut einen Handler, der Sitzung und Berechtigung verlangt.
|
|
//
|
|
// Beide Prüfungen sind hier untrennbar verbunden: ein Endpunkt lässt sich damit
|
|
// nicht versehentlich ohne Berechtigungsprüfung einbinden (PROMPT.md §42).
|
|
func protectedHandler(routerDependencies RouterDependencies) func(string, http.HandlerFunc) http.Handler {
|
|
return func(requiredPermission string, handlerFunction http.HandlerFunc) http.Handler {
|
|
permissionCheckedHandler := RequirePermission(
|
|
requiredPermission,
|
|
routerDependencies.AuthService,
|
|
routerDependencies.AuditRecorder,
|
|
routerDependencies.Logger,
|
|
handlerFunction,
|
|
)
|
|
|
|
return AuthenticationMiddleware(routerDependencies.AuthService, routerDependencies.Logger)(permissionCheckedHandler)
|
|
}
|
|
}
|
|
|
|
// startRateLimiterCleanup entfernt regelmäßig abgelaufene Einträge.
|
|
//
|
|
// Ohne Bereinigung wüchse die Zählerkarte mit jeder neuen Absenderadresse.
|
|
func startRateLimiterCleanup(limiter *RateLimiter) {
|
|
cleanupTicker := time.NewTicker(rateLimiterCleanupInterval)
|
|
|
|
go func() {
|
|
// Die Aufräumroutine läuft für die Lebensdauer des Dienstes.
|
|
for range cleanupTicker.C {
|
|
limiter.Cleanup()
|
|
}
|
|
}()
|
|
}
|
|
|
|
// registerRansomwareRoutes bindet die Auffaelligkeitsbewertung ein.
|
|
//
|
|
// Sie haengt an backups.read, nicht an einem eigenen Sicherheitsrecht: Wer die
|
|
// Backups sehen darf, muss auch erfahren duerfen, ob einer davon aus dem
|
|
// Rahmen faellt — die Bewertung ist eine Eigenschaft des Laufs, kein Geheimnis.
|
|
func registerRansomwareRoutes(requestMultiplexer *http.ServeMux, routerDependencies RouterDependencies) {
|
|
if routerDependencies.RansomwareDetector == nil {
|
|
return
|
|
}
|
|
|
|
ransomwareHandlerInstance := &ransomwareHandler{
|
|
detector: routerDependencies.RansomwareDetector,
|
|
logger: logging.WithComponent(routerDependencies.Logger, "ransomware"),
|
|
}
|
|
|
|
protected := protectedHandler(routerDependencies)
|
|
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/backups/{id}/ransomware-assessment",
|
|
protected("backups.read", ransomwareHandlerInstance.handleGetAssessment))
|
|
}
|
|
|
|
// registerReportRoutes bindet die Berichte ein.
|
|
//
|
|
// Beide Routen haengen an reports.read. Die Berechtigung gibt es seit Phase 1;
|
|
// Viewer und Auditor tragen sie bereits — der Auditor ist genau die Rolle, fuer
|
|
// die der Bericht fuer Pruefungen geschrieben wurde.
|
|
func registerReportRoutes(requestMultiplexer *http.ServeMux, routerDependencies RouterDependencies) {
|
|
if routerDependencies.ReportGenerator == nil {
|
|
return
|
|
}
|
|
|
|
reportHandlerInstance := &reportHandler{
|
|
reportGenerator: routerDependencies.ReportGenerator,
|
|
auditRecorder: routerDependencies.AuditRecorder,
|
|
logger: logging.WithComponent(routerDependencies.Logger, "reports"),
|
|
}
|
|
|
|
protected := protectedHandler(routerDependencies)
|
|
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/reports",
|
|
protected("reports.read", reportHandlerInstance.handleListReports))
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/reports/generate",
|
|
protected("reports.read", reportHandlerInstance.handleGenerateReport))
|
|
}
|
|
|
|
// registerHypervisorRoutes bindet die Endpunkte der Virtualisierungsumgebungen ein.
|
|
//
|
|
// Die Rechte sind die bereits vorhandenen providers.read und providers.write —
|
|
// ein neues Recht hätte jede bestehende Rollenzuweisung stillschweigend
|
|
// verändert.
|
|
func registerHypervisorRoutes(requestMultiplexer *http.ServeMux, routerDependencies RouterDependencies) {
|
|
if routerDependencies.HypervisorStore == nil {
|
|
return
|
|
}
|
|
|
|
hypervisorHandlerInstance := &hypervisorHandler{
|
|
clusterStore: routerDependencies.HypervisorStore,
|
|
auditRecorder: routerDependencies.AuditRecorder,
|
|
logger: logging.WithComponent(routerDependencies.Logger, "hypervisor"),
|
|
}
|
|
|
|
protected := protectedHandler(routerDependencies)
|
|
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/proxmox/clusters",
|
|
protected("providers.read", hypervisorHandlerInstance.handleListClusters))
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/proxmox/clusters/{id}",
|
|
protected("providers.read", hypervisorHandlerInstance.handleGetCluster))
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/proxmox/clusters",
|
|
protected("providers.write", hypervisorHandlerInstance.handleCreateCluster))
|
|
requestMultiplexer.Handle("DELETE "+apiBasePath+"/proxmox/clusters/{id}",
|
|
protected("providers.write", hypervisorHandlerInstance.handleDeleteCluster))
|
|
|
|
// Prüfung und Bestandsaufnahme lesen nur — aber sie bauen eine Verbindung
|
|
// nach außen auf und erzeugen Last auf dem Verbund. Deshalb hängen sie am
|
|
// Schreibrecht, nicht am Leserecht.
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/proxmox/clusters/{id}/test",
|
|
protected("providers.write", hypervisorHandlerInstance.handleTestConnection))
|
|
requestMultiplexer.Handle("POST "+apiBasePath+"/proxmox/clusters/{id}/discover",
|
|
protected("providers.write", hypervisorHandlerInstance.handleDiscover))
|
|
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/proxmox/clusters/{id}/hosts",
|
|
protected("providers.read", hypervisorHandlerInstance.handleListHosts))
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/proxmox/clusters/{id}/vms",
|
|
protected("providers.read", hypervisorHandlerInstance.handleListClusterMachines))
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/proxmox/vms/{id}",
|
|
protected("providers.read", hypervisorHandlerInstance.handleGetVirtualMachine))
|
|
|
|
// Zusätzlich zur Vertragsliste: der Bestand über alle Verbünde hinweg. Eine
|
|
// Übersicht, die zuerst nach dem Verbund fragt, ist keine Übersicht.
|
|
requestMultiplexer.Handle("GET "+apiBasePath+"/virtual-machines",
|
|
protected("providers.read", hypervisorHandlerInstance.handleListVirtualMachines))
|
|
}
|