Update mit Guacamole-Extension
release-tag / release-image (push) Successful in 2m5s
release-main / release-images (push) Successful in 5m5s

This commit is contained in:
2026-08-22 15:19:17 +02:00
parent 0f4a234f52
commit 7972ed7e38
45 changed files with 6448 additions and 402 deletions
+158 -13
View File
@@ -5,17 +5,23 @@ import (
"errors"
"os"
"path/filepath"
"strings"
"github.com/example/sessionguard/internal/model"
)
type Master struct {
Listen string `json:"listen"`
PublicURL string `json:"public_url"`
DataFile string `json:"data_file"`
EnrollmentToken string `json:"enrollment_token"`
OIDC model.OIDCConfig `json:"oidc"`
OfflineAfterSeconds int `json:"offline_after_seconds"`
Listen string `json:"listen"`
PublicURL string `json:"public_url"`
DataFile string `json:"data_file,omitempty"`
DatabaseURL string `json:"database_url,omitempty"`
EnrollmentToken string `json:"enrollment_token"`
OIDC model.OIDCConfig `json:"oidc"`
RBAC model.RBACConfig `json:"rbac"`
Broker model.BrokerConfig `json:"broker"`
Alerts model.AlertConfig `json:"alerts"`
OfflineAfterSeconds int `json:"offline_after_seconds"`
HistoryLimit int `json:"history_limit"`
}
type Agent struct {
@@ -34,6 +40,7 @@ func LoadMaster(path string) (Master, error) {
if err := read(path, &c); err != nil {
return c, err
}
applyMasterEnv(&c)
if c.Listen == "" {
c.Listen = ":8080"
}
@@ -43,7 +50,55 @@ func LoadMaster(path string) (Master, error) {
if c.OfflineAfterSeconds <= 0 {
c.OfflineAfterSeconds = 30
}
return c, validateOIDC(c.OIDC)
if c.HistoryLimit <= 0 {
c.HistoryLimit = 50000
}
if c.Broker.LeaseSeconds <= 0 {
c.Broker.LeaseSeconds = 900
}
if c.Broker.MinHealthScore <= 0 {
c.Broker.MinHealthScore = 60
}
if c.Alerts.CPUPercent <= 0 {
c.Alerts.CPUPercent = 90
}
if c.Alerts.MemoryPercent <= 0 {
c.Alerts.MemoryPercent = 90
}
if c.Alerts.DiskFreeGB <= 0 {
c.Alerts.DiskFreeGB = 10
}
if c.Alerts.HealthScore <= 0 {
c.Alerts.HealthScore = 50
}
if c.Alerts.OfflineSeconds <= 0 {
c.Alerts.OfflineSeconds = 120
}
if c.Alerts.ProfileFailures <= 0 {
c.Alerts.ProfileFailures = 3
}
if c.Alerts.DisconnectedSessions <= 0 {
c.Alerts.DisconnectedSessions = 20
}
if c.Alerts.LogonDurationSeconds <= 0 {
c.Alerts.LogonDurationSeconds = 30
}
if c.Alerts.NotificationMinInterval <= 0 {
c.Alerts.NotificationMinInterval = 900
}
if c.RBAC.DefaultRole == "" {
c.RBAC.DefaultRole = "viewer"
}
if c.RBAC.Groups == nil {
c.RBAC.Groups = map[string][]string{}
}
if err := validateOIDC(c.OIDC); err != nil {
return c, err
}
if c.Broker.Enabled && strings.TrimSpace(c.Broker.APIKey) == "" {
return c, errors.New("broker.api_key is required when broker is enabled")
}
return c, nil
}
func LoadAgent(path string) (Agent, error) {
@@ -51,6 +106,7 @@ func LoadAgent(path string) (Agent, error) {
if err := read(path, &c); err != nil {
return c, err
}
applyAgentEnv(&c)
if c.Listen == "" {
c.Listen = ":9091"
}
@@ -78,6 +134,10 @@ func LoadAgent(path string) (Agent, error) {
if c.Policy.Cleanup.ExcludeSIDs == nil {
c.Policy.Cleanup.ExcludeSIDs = []string{"S-1-5-18", "S-1-5-19", "S-1-5-20"}
}
NormalizePolicy(&c.Policy)
if err := ValidatePolicy(c.Policy); err != nil {
return c, err
}
if c.OIDC.Issuer != "" {
if err := validateOIDC(c.OIDC); err != nil {
return c, err
@@ -86,17 +146,37 @@ func LoadAgent(path string) (Agent, error) {
return c, nil
}
func applyMasterEnv(c *Master) {
set := func(name string, dst *string) {
if v := strings.TrimSpace(os.Getenv(name)); v != "" {
*dst = v
}
}
set("SESSIONGUARD_DATABASE_URL", &c.DatabaseURL)
set("SESSIONGUARD_ENROLLMENT_TOKEN", &c.EnrollmentToken)
set("SESSIONGUARD_BROKER_API_KEY", &c.Broker.APIKey)
set("SESSIONGUARD_OIDC_CLIENT_SECRET", &c.OIDC.ClientSecret)
set("SESSIONGUARD_ALERT_WEBHOOK_URL", &c.Alerts.WebhookURL)
}
func applyAgentEnv(c *Agent) {
set := func(name string, dst *string) {
if v := strings.TrimSpace(os.Getenv(name)); v != "" {
*dst = v
}
}
set("SESSIONGUARD_MASTER_URL", &c.MasterURL)
set("SESSIONGUARD_ENROLLMENT_TOKEN", &c.EnrollmentToken)
set("SESSIONGUARD_OIDC_CLIENT_SECRET", &c.OIDC.ClientSecret)
}
func read(path string, out any) error {
b, err := os.ReadFile(path)
if err != nil {
return err
}
if err := json.Unmarshal(b, out); err != nil {
return err
}
return nil
return json.Unmarshal(b, out)
}
func SaveJSON(path string, v any) error {
b, err := json.MarshalIndent(v, "", " ")
if err != nil {
@@ -111,10 +191,75 @@ func SaveJSON(path string, v any) error {
}
return os.Rename(tmp, path)
}
func validateOIDC(c model.OIDCConfig) error {
if c.Issuer == "" || c.ClientID == "" || c.RedirectURL == "" {
return errors.New("oidc issuer, client_id and redirect_url are required")
}
return nil
}
func NormalizePolicy(p *model.Policy) {
if p.Cleanup.GraceSeconds <= 0 {
p.Cleanup.GraceSeconds = 600
}
if p.Cleanup.PollSeconds <= 0 {
p.Cleanup.PollSeconds = 10
}
if p.Cleanup.RetrySeconds <= 0 {
p.Cleanup.RetrySeconds = 60
}
if len(p.Cleanup.AllowedProfileRoots) == 0 {
p.Cleanup.AllowedProfileRoots = []string{`C:\Users`}
}
if p.Profiles.BackupDelaySeconds < 0 {
p.Profiles.BackupDelaySeconds = 0
}
if p.Profiles.BackupDelaySeconds == 0 {
p.Profiles.BackupDelaySeconds = 5
}
if p.Profiles.RetrySeconds <= 0 {
p.Profiles.RetrySeconds = 60
}
if p.Profiles.RestoreWindowSeconds <= 0 {
p.Profiles.RestoreWindowSeconds = 120
}
if p.Profiles.KeepVersions < 0 {
p.Profiles.KeepVersions = 0
}
if p.Sessions.DisconnectedTimeoutSeconds <= 0 {
p.Sessions.DisconnectedTimeoutSeconds = 3600
}
}
func ValidatePolicy(p model.Policy) error {
if p.Cleanup.GraceSeconds < 1 || p.Cleanup.PollSeconds < 2 || p.Cleanup.RetrySeconds < 1 {
return errors.New("invalid cleanup timing")
}
if p.Profiles.Enabled {
if strings.TrimSpace(p.Profiles.StoreRoot) == "" {
return errors.New("profiles.store_root is required when profile sync is enabled")
}
if len(p.Profiles.Folders) == 0 {
return errors.New("at least one profiles.folders entry is required when profile sync is enabled")
}
if p.Profiles.BackupDelaySeconds < 0 {
return errors.New("profiles.backup_delay_seconds must be >= 0")
}
if p.Profiles.RetrySeconds < 1 {
return errors.New("profiles.retry_seconds must be >= 1")
}
if p.Profiles.RestoreWindowSeconds < 10 {
return errors.New("profiles.restore_window_seconds must be >= 10")
}
for _, f := range p.Profiles.Folders {
v := strings.ReplaceAll(strings.TrimSpace(f.Path), `\`, "/")
if v == "" || strings.HasPrefix(v, "/") || strings.Contains(v, ":") || v == ".." || strings.HasPrefix(v, "../") || strings.Contains(v, "/../") {
return errors.New("profile folder paths must be relative and may not escape the user profile")
}
}
}
if p.Sessions.DisconnectedLogoffEnabled && p.Sessions.DisconnectedTimeoutSeconds < 60 {
return errors.New("sessions.disconnected_timeout_seconds must be >= 60")
}
return nil
}
+26
View File
@@ -4,6 +4,8 @@ import (
"os"
"path/filepath"
"testing"
"github.com/example/sessionguard/internal/model"
)
func TestAgentDefaults(t *testing.T) {
@@ -22,3 +24,27 @@ func TestAgentDefaults(t *testing.T) {
t.Fatal("missing allowed profile root")
}
}
func TestValidateProfilePolicy(t *testing.T) {
p := model.Policy{
Cleanup: model.CleanupPolicy{GraceSeconds: 600, PollSeconds: 10, RetrySeconds: 60, AllowedProfileRoots: []string{`C:\Users`}},
Profiles: model.ProfilePolicy{Enabled: true, StoreRoot: `\\server\profiles`, RetrySeconds: 60, RestoreWindowSeconds: 120, Folders: []model.ProfileFolder{{Path: `AppData\Roaming\Example`}}},
Sessions: model.SessionPolicy{DisconnectedTimeoutSeconds: 3600},
}
if err := ValidatePolicy(p); err != nil {
t.Fatal(err)
}
p.Profiles.Folders[0].Path = `..\Windows`
if err := ValidatePolicy(p); err == nil {
t.Fatal("expected profile traversal validation error")
}
}
func TestDisconnectedTimeoutMinimum(t *testing.T) {
p := model.Policy{Cleanup: model.CleanupPolicy{GraceSeconds: 600, PollSeconds: 10, RetrySeconds: 60}, Sessions: model.SessionPolicy{DisconnectedLogoffEnabled: true, DisconnectedTimeoutSeconds: 30}}
NormalizePolicy(&p)
p.Sessions.DisconnectedTimeoutSeconds = 30
if err := ValidatePolicy(p); err == nil {
t.Fatal("expected disconnected timeout validation error")
}
}