115 lines
3.6 KiB
Go
115 lines
3.6 KiB
Go
// Dieses File lädt die Team-Konfiguration aus einer YAML-Datei (z.B. teams.yaml).
|
|
//
|
|
// Jedes Team bekommt einen eigenen Zugriffskey und einen eigenen Ordner unter
|
|
// dem Daten-Wurzelverzeichnis (data/<id>/content). Die teams.yaml enthält
|
|
// Klartext-Keys — genau wie ADMIN_TOKEN/MCP_TOKEN heute in .env — und gehört
|
|
// deshalb NIE ins Git-Repository (siehe .gitignore).
|
|
package handler
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"regexp"
|
|
"time"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// teamIDPattern erlaubt nur kleine Buchstaben, Ziffern, Bindestrich und Unterstrich.
|
|
// Das verhindert Path Traversal über die Team-ID (z.B. "../../etc" als ID)
|
|
// und stellt sicher, dass die ID direkt als Ordnername verwendet werden kann.
|
|
var teamIDPattern = regexp.MustCompile(`^[a-z0-9_-]+$`)
|
|
|
|
// TeamConfig ist ein einzelner Team-Eintrag aus der YAML-Datei.
|
|
type TeamConfig struct {
|
|
ID string `yaml:"id"`
|
|
Name string `yaml:"name"`
|
|
Key string `yaml:"key"`
|
|
}
|
|
|
|
// teamsFile bildet die Struktur der teams.yaml ab (Wurzelelement "teams").
|
|
type teamsFile struct {
|
|
Teams []TeamConfig `yaml:"teams"`
|
|
}
|
|
|
|
// LoadTeamsConfig liest und validiert die Team-Konfiguration aus dem angegebenen Pfad.
|
|
func LoadTeamsConfig(path string) ([]TeamConfig, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Team-Konfiguration konnte nicht gelesen werden (%s): %w", path, err)
|
|
}
|
|
|
|
var parsed teamsFile
|
|
if err := yaml.Unmarshal(data, &parsed); err != nil {
|
|
return nil, fmt.Errorf("Team-Konfiguration ist kein gültiges YAML: %w", err)
|
|
}
|
|
|
|
if len(parsed.Teams) == 0 {
|
|
return nil, fmt.Errorf("Team-Konfiguration enthält keine Teams")
|
|
}
|
|
|
|
seen := make(map[string]bool, len(parsed.Teams))
|
|
for _, team := range parsed.Teams {
|
|
if !teamIDPattern.MatchString(team.ID) {
|
|
return nil, fmt.Errorf("ungültige Team-ID %q — erlaubt sind nur a-z, 0-9, '-' und '_'", team.ID)
|
|
}
|
|
if team.ID == "admin" {
|
|
return nil, fmt.Errorf("Team-ID \"admin\" ist reserviert und darf nicht vergeben werden")
|
|
}
|
|
if team.Key == "" {
|
|
return nil, fmt.Errorf("Team %q hat keinen Key gesetzt", team.ID)
|
|
}
|
|
if seen[team.ID] {
|
|
return nil, fmt.Errorf("Team-ID %q ist mehrfach vergeben", team.ID)
|
|
}
|
|
seen[team.ID] = true
|
|
}
|
|
|
|
return parsed.Teams, nil
|
|
}
|
|
|
|
// StartTeamsConfigWatcher prüft alle interval einmal die Änderungszeit der Team-
|
|
// Konfigurationsdatei und lädt sie bei einer Änderung neu in die Registry — so
|
|
// wirken neue/geänderte/entfernte Teams ohne Serverneustart.
|
|
//
|
|
// Ein einfacher mtime-Poll reicht hier völlig aus (eine einzelne kleine Datei,
|
|
// Änderungen sind selten) und braucht keine zusätzliche Dependency wie fsnotify.
|
|
// Bei einem Fehler (Datei kurzzeitig unlesbar während des Speicherns, ungültiges
|
|
// YAML) wird die Änderung übersprungen und beim nächsten Tick erneut versucht —
|
|
// der Server läuft mit der zuletzt gültigen Konfiguration weiter.
|
|
func StartTeamsConfigWatcher(path string, registry *TeamRegistry, interval time.Duration) {
|
|
var lastModTime time.Time
|
|
if info, err := os.Stat(path); err == nil {
|
|
lastModTime = info.ModTime()
|
|
}
|
|
|
|
go func() {
|
|
ticker := time.NewTicker(interval)
|
|
defer ticker.Stop()
|
|
|
|
for range ticker.C {
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if !info.ModTime().After(lastModTime) {
|
|
continue
|
|
}
|
|
lastModTime = info.ModTime()
|
|
|
|
configs, err := LoadTeamsConfig(path)
|
|
if err != nil {
|
|
log.Printf("teams.yaml geändert, aber ungültig — Änderung wird ignoriert: %v", err)
|
|
continue
|
|
}
|
|
|
|
if err := registry.Reload(configs); err != nil {
|
|
log.Printf("teams.yaml: Reload fehlgeschlagen: %v", err)
|
|
continue
|
|
}
|
|
|
|
log.Printf("teams.yaml neu geladen (%d Teams)", len(configs))
|
|
}
|
|
}()
|
|
}
|