// Package agentregistry verwaltet die am Control Server angemeldeten Agents. // // Leitgedanke aus PROMPT.md §59: Ein kompromittierter Agent darf nicht // automatisch vollständigen Zugriff auf die Backup-Infrastruktur erhalten. // Daraus folgt die Trennung in zwei Tokenarten: // // - Ein Aufnahme-Token gilt einmalig und nur zur Registrierung. // - Ein Betriebs-Token gilt dauerhaft, trägt aber ausschließlich // agentspezifische Rechte und ist einzeln widerrufbar. package agentregistry import ( "context" "errors" "time" "github.com/google/uuid" ) // AgentStatus ist der Betriebszustand eines Agents. type AgentStatus string const ( // AgentStatusActive bedeutet: der Agent darf arbeiten. AgentStatusActive AgentStatus = "active" // AgentStatusDisabled bedeutet: der Agent ist vorübergehend abgeschaltet. AgentStatusDisabled AgentStatus = "disabled" // AgentStatusRevoked bedeutet: der Agent ist dauerhaft gesperrt. AgentStatusRevoked AgentStatus = "revoked" ) // Platform benennt das Betriebssystem eines Agents. type Platform string const ( // PlatformWindows ist ein Windows-System. PlatformWindows Platform = "windows" // PlatformLinux ist ein Linux-System. PlatformLinux Platform = "linux" // PlatformDarwin ist ein macOS-System. PlatformDarwin Platform = "darwin" ) // IsSupported meldet, ob eine Plattform bekannt ist. func (platform Platform) IsSupported() bool { switch platform { case PlatformWindows, PlatformLinux, PlatformDarwin: return true default: return false } } // Agent beschreibt einen registrierten Agent. type Agent struct { // ID ist der öffentliche Bezeichner. ID uuid.UUID `json:"id"` // Name ist die sprechende Bezeichnung. Name string `json:"name"` // Hostname ist der gemeldete Rechnername. Hostname string `json:"hostname,omitempty"` // Platform ist das Betriebssystem. Platform Platform `json:"platform"` // Architecture ist die Rechnerarchitektur. Architecture string `json:"architecture,omitempty"` // Version ist die Programmversion des Agents. Version string `json:"version,omitempty"` // Status ist der Betriebszustand. Status AgentStatus `json:"status"` // LastHeartbeatAt ist der Zeitpunkt der letzten Lebendmeldung in UTC. LastHeartbeatAt *time.Time `json:"last_heartbeat_at,omitempty"` // LastIPAddress ist die zuletzt gesehene Absenderadresse. LastIPAddress string `json:"last_ip_address,omitempty"` // RegisteredAt ist der Zeitpunkt der Aufnahme in UTC. RegisteredAt time.Time `json:"registered_at"` } // HeartbeatAge liefert die Zeit seit der letzten Lebendmeldung. // // Der zweite Rückgabewert meldet, ob überhaupt je eine einging. Ein Agent ohne // Lebendmeldung ist nicht dasselbe wie einer mit alter Meldung. func (agent Agent) HeartbeatAge(referenceTime time.Time) (time.Duration, bool) { if agent.LastHeartbeatAt == nil { return 0, false } return referenceTime.Sub(*agent.LastHeartbeatAt), true } // IsOffline meldet, ob ein Agent länger als die erlaubte Frist stumm ist. // // Ein stummer Agent bedeutet: von diesem System kommen keine Backups mehr. // Das ist eine Warnung wert (PROMPT.md §38). func (agent Agent) IsOffline(referenceTime time.Time, allowedSilence time.Duration) bool { heartbeatAge, hasHeartbeat := agent.HeartbeatAge(referenceTime) if !hasHeartbeat { // Ein Agent, der sich nie gemeldet hat, gilt ab der Frist nach seiner // Aufnahme als offline. return referenceTime.Sub(agent.RegisteredAt) > allowedSilence } return heartbeatAge > allowedSilence } // EnrollmentToken ist ein einmaliges Aufnahme-Token. type EnrollmentToken struct { // ID ist der Bezeichner des Tokens. ID uuid.UUID `json:"id"` // Token ist der Klartext. // // Er verlässt den Server genau einmal bei der Ausstellung; danach liegt // nur noch sein Hash vor. Token string `json:"token"` // AgentName ist der vorgesehene Name des aufzunehmenden Agents. AgentName string `json:"agent_name"` // ExpiresAt ist die Ablaufzeit in UTC. ExpiresAt time.Time `json:"expires_at"` } // RegistrationResult ist das Ergebnis einer Agent-Registrierung. type RegistrationResult struct { // Agent ist der angelegte Agent. Agent Agent `json:"agent"` // AgentToken ist das Betriebstoken im Klartext. // // Es verlässt den Server genau einmal. Geht es verloren, muss der Agent // neu aufgenommen oder sein Token gewechselt werden. AgentToken string `json:"agent_token"` } // RegistrationRequest beschreibt einen sich anmeldenden Agent. type RegistrationRequest struct { // EnrollmentToken ist das Aufnahme-Token. EnrollmentToken string // Hostname ist der Rechnername des Systems. Hostname string // Platform ist das Betriebssystem. Platform Platform // Architecture ist die Rechnerarchitektur. Architecture string // Version ist die Programmversion des Agents. Version string // IPAddress ist die Absenderadresse. IPAddress string } // HeartbeatRequest ist eine Lebendmeldung eines Agents. type HeartbeatRequest struct { // Version ist die aktuelle Programmversion des Agents. // // Sie wird bei jeder Meldung übernommen, damit ein Update sofort sichtbar // ist und veraltete Agents erkennbar bleiben (PROMPT.md §90). Version string // IPAddress ist die Absenderadresse. IPAddress string } // Fehler der Agent-Verwaltung. var ( // ErrEnrollmentTokenInvalid meldet ein unbrauchbares Aufnahme-Token. // // Der Fehler unterscheidet bewusst nicht zwischen unbekannt, abgelaufen und // bereits eingelöst: eine Unterscheidung erlaubte es, gültige Tokens zu erraten. ErrEnrollmentTokenInvalid = errors.New("das aufnahme-token ist ungültig, abgelaufen oder bereits verwendet") // ErrAgentTokenInvalid meldet ein unbrauchbares Betriebstoken. ErrAgentTokenInvalid = errors.New("das agent-token ist ungültig oder widerrufen") // ErrAgentNotFound meldet einen nicht vorhandenen Agent. ErrAgentNotFound = errors.New("der agent existiert nicht") // ErrAgentRevoked meldet einen gesperrten Agent. ErrAgentRevoked = errors.New("der agent ist gesperrt") // ErrUnsupportedPlatform meldet eine unbekannte Plattform. ErrUnsupportedPlatform = errors.New("die plattform wird nicht unterstützt") ) // defaultEnrollmentValidity ist die Gültigkeitsdauer eines Aufnahme-Tokens. // // Eine Stunde reicht für eine geplante Installation und begrenzt zugleich das // Zeitfenster, in dem ein abgefangenes Token nützt. const defaultEnrollmentValidity = time.Hour // AgentFilter schränkt eine Agent-Abfrage ein. type AgentFilter struct { // Status beschränkt auf einen Betriebszustand. Status string // Platform beschränkt auf ein Betriebssystem. Platform string // Page ist die gewünschte Seite, beginnend bei 1. Page int // PageSize ist die Anzahl der Einträge je Seite. PageSize int } // Store kapselt die Ablage der Agent-Daten. // // Die Schnittstelle trennt die Domänenlogik von der Datenbank und macht sie // einzeln prüfbar. type Store interface { // CreateEnrollmentToken legt ein Aufnahme-Token an. CreateEnrollmentToken(createContext context.Context, tokenHash string, agentName string, expiresAt time.Time, createdBy *uuid.UUID) (uuid.UUID, error) // ConsumeEnrollmentToken löst ein Aufnahme-Token ein und liefert den vorgesehenen Namen. ConsumeEnrollmentToken(consumeContext context.Context, tokenHash string, agentID uuid.UUID) (agentName string, consumeError error) // PeekEnrollmentToken liest den vorgesehenen Namen, ohne einzulösen. PeekEnrollmentToken(peekContext context.Context, tokenHash string) (agentName string, peekError error) // CreateAgent legt einen Agent an. CreateAgent(createContext context.Context, agentToCreate Agent) (uuid.UUID, error) // CreateAgentToken legt ein Betriebstoken an. CreateAgentToken(createContext context.Context, agentID uuid.UUID, tokenHash string) (uuid.UUID, error) // FindAgentByTokenHash sucht einen Agent anhand seines Betriebstokens. FindAgentByTokenHash(queryContext context.Context, tokenHash string) (Agent, error) // FindAgentByID sucht einen Agent anhand seiner Kennung. FindAgentByID(queryContext context.Context, agentID uuid.UUID) (Agent, error) // ListAgents liefert eine Seite von Agents. ListAgents(queryContext context.Context, agentFilter AgentFilter) ([]Agent, int64, error) // RecordHeartbeat vermerkt eine Lebendmeldung. RecordHeartbeat(updateContext context.Context, agentID uuid.UUID, agentVersion string, ipAddress string) error // RevokeAgent sperrt einen Agent und alle seine Tokens. RevokeAgent(updateContext context.Context, agentID uuid.UUID) error // RotateAgentToken widerruft alle Tokens eines Agents und legt ein neues an. RotateAgentToken(updateContext context.Context, agentID uuid.UUID, newTokenHash string) error }