327 lines
9.9 KiB
Go
327 lines
9.9 KiB
Go
package config
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"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,omitempty"`
|
|
DatabaseURL string `json:"database_url,omitempty"`
|
|
EnrollmentToken string `json:"enrollment_token"`
|
|
OIDC model.OIDCConfig `json:"oidc"`
|
|
AccessAuth model.AccessAuthConfig `json:"access_auth"`
|
|
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 {
|
|
Listen string `json:"listen"`
|
|
PublicURL string `json:"public_url"`
|
|
DataDir string `json:"data_dir"`
|
|
MasterURL string `json:"master_url"`
|
|
EnrollmentToken string `json:"enrollment_token"`
|
|
HeartbeatSeconds int `json:"heartbeat_seconds"`
|
|
OIDC model.OIDCConfig `json:"oidc"`
|
|
Policy model.Policy `json:"policy"`
|
|
}
|
|
|
|
func LoadMaster(path string) (Master, error) {
|
|
var c Master
|
|
if err := read(path, &c); err != nil {
|
|
return c, err
|
|
}
|
|
applyMasterEnv(&c)
|
|
if c.Listen == "" {
|
|
c.Listen = ":8080"
|
|
}
|
|
if c.DataFile == "" {
|
|
c.DataFile = "./data/master.json"
|
|
}
|
|
if c.OfflineAfterSeconds <= 0 {
|
|
c.OfflineAfterSeconds = 30
|
|
}
|
|
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 c.AccessAuth.Enabled {
|
|
if strings.TrimSpace(c.AccessAuth.Issuer) == "" {
|
|
c.AccessAuth.Issuer = c.OIDC.Issuer
|
|
}
|
|
if strings.TrimSpace(c.AccessAuth.ClientID) == "" {
|
|
c.AccessAuth.ClientID = c.OIDC.ClientID
|
|
}
|
|
if strings.TrimSpace(c.AccessAuth.ClientSecret) == "" {
|
|
c.AccessAuth.ClientSecret = c.OIDC.ClientSecret
|
|
}
|
|
if c.AccessAuth.CookieName == "" {
|
|
c.AccessAuth.CookieName = "sg_access_session"
|
|
}
|
|
if c.AccessAuth.SessionHours <= 0 {
|
|
c.AccessAuth.SessionHours = 8
|
|
}
|
|
if c.AccessAuth.UsernameClaim == "" {
|
|
c.AccessAuth.UsernameClaim = "preferred_username"
|
|
}
|
|
if err := validateAccessAuth(c.AccessAuth); err != nil {
|
|
return c, err
|
|
}
|
|
}
|
|
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) {
|
|
var c Agent
|
|
if err := read(path, &c); err != nil {
|
|
return c, err
|
|
}
|
|
applyAgentEnv(&c)
|
|
if c.Listen == "" {
|
|
c.Listen = ":9091"
|
|
}
|
|
if c.DataDir == "" {
|
|
c.DataDir = `C:\ProgramData\SessionGuard`
|
|
}
|
|
if c.HeartbeatSeconds <= 0 {
|
|
c.HeartbeatSeconds = 10
|
|
}
|
|
if c.Policy.Cleanup.GraceSeconds <= 0 {
|
|
c.Policy.Cleanup.GraceSeconds = 600
|
|
}
|
|
if c.Policy.Cleanup.PollSeconds <= 0 {
|
|
c.Policy.Cleanup.PollSeconds = 10
|
|
}
|
|
if c.Policy.Cleanup.RetrySeconds <= 0 {
|
|
c.Policy.Cleanup.RetrySeconds = 60
|
|
}
|
|
if len(c.Policy.Cleanup.AllowedProfileRoots) == 0 {
|
|
c.Policy.Cleanup.AllowedProfileRoots = []string{`C:\Users`}
|
|
}
|
|
if c.Policy.Cleanup.ExcludeUsers == nil {
|
|
c.Policy.Cleanup.ExcludeUsers = []string{"Administrator", "DefaultAccount", "WDAGUtilityAccount"}
|
|
}
|
|
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
|
|
}
|
|
}
|
|
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_ACCESS_OIDC_CLIENT_SECRET", &c.AccessAuth.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
|
|
}
|
|
return json.Unmarshal(b, out)
|
|
}
|
|
func SaveJSON(path string, v any) error {
|
|
b, err := json.MarshalIndent(v, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
return err
|
|
}
|
|
tmp := path + ".tmp"
|
|
if err := os.WriteFile(tmp, b, 0o600); err != nil {
|
|
return err
|
|
}
|
|
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")
|
|
}
|
|
for label, raw := range map[string]string{"redirect_url": c.RedirectURL, "logout_redirect_url": c.LogoutRedirectURL} {
|
|
if strings.TrimSpace(raw) == "" {
|
|
continue
|
|
}
|
|
u, err := url.Parse(strings.TrimSpace(raw))
|
|
if err != nil || u.Hostname() == "" || u.Scheme == "" {
|
|
return fmt.Errorf("oidc.%s must be an absolute URL", label)
|
|
}
|
|
if c.SecureCookie && !strings.EqualFold(u.Scheme, "https") {
|
|
return fmt.Errorf("oidc.%s must use https when secure_cookie is enabled", label)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateAccessAuth(c model.AccessAuthConfig) error {
|
|
if strings.TrimSpace(c.Issuer) == "" || strings.TrimSpace(c.ClientID) == "" || strings.TrimSpace(c.ClientSecret) == "" || strings.TrimSpace(c.RedirectURL) == "" || strings.TrimSpace(c.LogoutRedirectURL) == "" {
|
|
return errors.New("access_auth issuer, client_id, client_secret, redirect_url and logout_redirect_url are required when access_auth is enabled")
|
|
}
|
|
for label, raw := range map[string]string{"redirect_url": c.RedirectURL, "logout_redirect_url": c.LogoutRedirectURL} {
|
|
u, err := url.Parse(strings.TrimSpace(raw))
|
|
if err != nil || u.Hostname() == "" || u.Scheme == "" {
|
|
return fmt.Errorf("access_auth.%s must be an absolute URL", label)
|
|
}
|
|
if c.SecureCookie && !strings.EqualFold(u.Scheme, "https") {
|
|
return fmt.Errorf("access_auth.%s must use https when secure_cookie is enabled", label)
|
|
}
|
|
}
|
|
if c.SessionHours < 1 || c.SessionHours > 168 {
|
|
return errors.New("access_auth.session_hours must be between 1 and 168")
|
|
}
|
|
if strings.ContainsAny(c.CookieName, " ;,\t\r\n") {
|
|
return errors.New("access_auth.cookie_name contains invalid characters")
|
|
}
|
|
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
|
|
}
|