config: add OIDC/session env vars for the login gate
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -29,6 +30,21 @@ type Config struct {
|
||||
|
||||
MinConfidence float64
|
||||
IncrementalSyncEvery time.Duration
|
||||
|
||||
// OIDC login gate (Keycloak). PublicBaseURL is this app's own externally
|
||||
// reachable origin (e.g. "https://geniusrun.example.com") -- it derives
|
||||
// OIDCRedirectURL and whether session cookies can be marked Secure,
|
||||
// instead of requiring both to be configured separately and risking them
|
||||
// drifting out of sync.
|
||||
PublicBaseURL string
|
||||
OIDCIssuerURL string
|
||||
OIDCClientID string
|
||||
OIDCClientSecret string
|
||||
OIDCRedirectURL string
|
||||
OIDCRequiredRole string
|
||||
SessionSecret []byte
|
||||
SessionDuration time.Duration
|
||||
SessionSecure bool
|
||||
}
|
||||
|
||||
// Load reads configuration from environment variables, applying defaults
|
||||
@@ -42,6 +58,12 @@ func Load() (Config, error) {
|
||||
GarminTokenStore: os.Getenv("GARMIN_TOKENSTORE"),
|
||||
MinConfidence: getEnvFloat("GENIUSRUN_MIN_CONFIDENCE", 0.6),
|
||||
IncrementalSyncEvery: getEnvDuration("GENIUSRUN_INCREMENTAL_SYNC_EVERY", 6*time.Hour),
|
||||
PublicBaseURL: strings.TrimRight(os.Getenv("GENIUSRUN_PUBLIC_BASE_URL"), "/"),
|
||||
OIDCIssuerURL: os.Getenv("GENIUSRUN_OIDC_ISSUER_URL"),
|
||||
OIDCClientID: os.Getenv("GENIUSRUN_OIDC_CLIENT_ID"),
|
||||
OIDCClientSecret: os.Getenv("GENIUSRUN_OIDC_CLIENT_SECRET"),
|
||||
OIDCRequiredRole: getEnvDefault("GENIUSRUN_OIDC_REQUIRED_ROLE", "geniusrun-user"),
|
||||
SessionDuration: getEnvDuration("GENIUSRUN_SESSION_DURATION", 720*time.Hour),
|
||||
}
|
||||
|
||||
if cfg.GarminPythonPath == "" {
|
||||
@@ -50,6 +72,26 @@ func Load() (Config, error) {
|
||||
if cfg.GarminServerPath == "" {
|
||||
return cfg, fmt.Errorf("MCP_GARMIN_SERVER is required (path to mcp-garmin's server.py)")
|
||||
}
|
||||
if cfg.PublicBaseURL == "" {
|
||||
return cfg, fmt.Errorf("GENIUSRUN_PUBLIC_BASE_URL is required (e.g. https://geniusrun.example.com)")
|
||||
}
|
||||
if cfg.OIDCIssuerURL == "" {
|
||||
return cfg, fmt.Errorf("GENIUSRUN_OIDC_ISSUER_URL is required")
|
||||
}
|
||||
if cfg.OIDCClientID == "" {
|
||||
return cfg, fmt.Errorf("GENIUSRUN_OIDC_CLIENT_ID is required")
|
||||
}
|
||||
if cfg.OIDCClientSecret == "" {
|
||||
return cfg, fmt.Errorf("GENIUSRUN_OIDC_CLIENT_SECRET is required")
|
||||
}
|
||||
sessionSecret := os.Getenv("GENIUSRUN_SESSION_SECRET")
|
||||
if len(sessionSecret) < 32 {
|
||||
return cfg, fmt.Errorf("GENIUSRUN_SESSION_SECRET is required and must be at least 32 characters")
|
||||
}
|
||||
cfg.SessionSecret = []byte(sessionSecret)
|
||||
cfg.OIDCRedirectURL = cfg.PublicBaseURL + "/api/session/callback"
|
||||
cfg.SessionSecure = strings.HasPrefix(cfg.PublicBaseURL, "https://")
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
|
||||
98
backend/internal/config/config_test.go
Normal file
98
backend/internal/config/config_test.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func setRequiredEnv(t *testing.T) {
|
||||
t.Helper()
|
||||
t.Setenv("MCP_GARMIN_PYTHON", "/usr/bin/python3")
|
||||
t.Setenv("MCP_GARMIN_SERVER", "/opt/mcp-garmin/server.py")
|
||||
t.Setenv("GENIUSRUN_PUBLIC_BASE_URL", "https://geniusrun.example.com")
|
||||
t.Setenv("GENIUSRUN_OIDC_ISSUER_URL", "https://keycloak.example.com/realms/myrealm")
|
||||
t.Setenv("GENIUSRUN_OIDC_CLIENT_ID", "geniusrun")
|
||||
t.Setenv("GENIUSRUN_OIDC_CLIENT_SECRET", "client-secret")
|
||||
t.Setenv("GENIUSRUN_SESSION_SECRET", "a-session-secret-that-is-at-least-32-bytes-long")
|
||||
}
|
||||
|
||||
func TestLoad_DerivesOIDCSettingsFromPublicBaseURL(t *testing.T) {
|
||||
setRequiredEnv(t)
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.OIDCRedirectURL != "https://geniusrun.example.com/api/session/callback" {
|
||||
t.Errorf("OIDCRedirectURL = %q", cfg.OIDCRedirectURL)
|
||||
}
|
||||
if !cfg.SessionSecure {
|
||||
t.Error("SessionSecure = false, want true for an https base URL")
|
||||
}
|
||||
if cfg.OIDCRequiredRole != "geniusrun-user" {
|
||||
t.Errorf("OIDCRequiredRole = %q, want default", cfg.OIDCRequiredRole)
|
||||
}
|
||||
if cfg.SessionDuration != 720*time.Hour {
|
||||
t.Errorf("SessionDuration = %v, want default 720h", cfg.SessionDuration)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_HTTPBaseURLYieldsInsecureCookies(t *testing.T) {
|
||||
setRequiredEnv(t)
|
||||
t.Setenv("GENIUSRUN_PUBLIC_BASE_URL", "http://localhost:8080")
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.SessionSecure {
|
||||
t.Error("SessionSecure = true, want false for an http base URL")
|
||||
}
|
||||
if cfg.OIDCRedirectURL != "http://localhost:8080/api/session/callback" {
|
||||
t.Errorf("OIDCRedirectURL = %q", cfg.OIDCRedirectURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_MissingRequiredOIDCVars(t *testing.T) {
|
||||
cases := []string{
|
||||
"GENIUSRUN_PUBLIC_BASE_URL",
|
||||
"GENIUSRUN_OIDC_ISSUER_URL",
|
||||
"GENIUSRUN_OIDC_CLIENT_ID",
|
||||
"GENIUSRUN_OIDC_CLIENT_SECRET",
|
||||
}
|
||||
for _, missing := range cases {
|
||||
t.Run(missing, func(t *testing.T) {
|
||||
setRequiredEnv(t)
|
||||
t.Setenv(missing, "")
|
||||
if _, err := Load(); err == nil {
|
||||
t.Fatalf("expected error when %s is unset", missing)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_SessionSecretTooShort(t *testing.T) {
|
||||
setRequiredEnv(t)
|
||||
t.Setenv("GENIUSRUN_SESSION_SECRET", "too-short")
|
||||
|
||||
if _, err := Load(); err == nil {
|
||||
t.Fatal("expected error for a session secret under 32 characters")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoad_CustomRoleAndDuration(t *testing.T) {
|
||||
setRequiredEnv(t)
|
||||
t.Setenv("GENIUSRUN_OIDC_REQUIRED_ROLE", "admin")
|
||||
t.Setenv("GENIUSRUN_SESSION_DURATION", "24h")
|
||||
|
||||
cfg, err := Load()
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.OIDCRequiredRole != "admin" {
|
||||
t.Errorf("OIDCRequiredRole = %q", cfg.OIDCRequiredRole)
|
||||
}
|
||||
if cfg.SessionDuration != 24*time.Hour {
|
||||
t.Errorf("SessionDuration = %v", cfg.SessionDuration)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user