Some checks failed
ci / go (push) Has been cancelled
ci / admin-ui (push) Has been cancelled
ci / runtime-sdk (push) Has been cancelled
ci / helm (push) Has been cancelled
ci / openapi (push) Has been cancelled
ci / container (push) Has been cancelled
ci / release-config (push) Has been cancelled
ci / shared styling (push) Has been cancelled
ci / security (push) Has been cancelled
ci / all green (push) Has been cancelled
572 lines
19 KiB
Go
572 lines
19 KiB
Go
// Package config loads and validates every runtime setting from the
|
|
// environment.
|
|
//
|
|
// Configuration is environment-only on purpose. The Helm chart is the single
|
|
// source of truth for how a deployment is configured; letting the binary also
|
|
// read a config file would create a second source that operators forget about.
|
|
// Every value has a safe default except the secrets and the OIDC issuer, which
|
|
// must be supplied explicitly when auth is enabled.
|
|
package config
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"errors"
|
|
"fmt"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// Environment prefixes, tried in order. This lets the Helm chart mount a
|
|
// ConfigMap under a distinct prefix without colliding with the pod's own
|
|
// environment, which is what makes one image usable across environments.
|
|
var envPrefixes = []string{"BANANAS_", "GO_BANANAS_"}
|
|
|
|
// Config is the fully resolved runtime configuration.
|
|
type Config struct {
|
|
// Environment is the deployment name, used in logs and the admin UI.
|
|
Environment string
|
|
// LogLevel is one of debug, info, warn, error.
|
|
LogLevel string
|
|
// LogFormat is json or text.
|
|
LogFormat string
|
|
|
|
// HTTPAddr is the listen address, host:port.
|
|
HTTPAddr string
|
|
// PublicBaseURL is the externally reachable origin, without a trailing
|
|
// slash. It is used to build absolute URLs in the runtime manifest and to
|
|
// validate OIDC redirect targets.
|
|
PublicBaseURL string
|
|
// TrustedProxies lists the CIDRs or IPs whose X-Forwarded-For headers may be
|
|
// believed. It is empty by default, which makes the service ignore
|
|
// forwarding headers entirely and log the direct peer.
|
|
TrustedProxies []string
|
|
// ReadTimeout and WriteTimeout bound a single request. WriteTimeout is
|
|
// generous because the runtime CSS endpoint is on the hot path of every page
|
|
// load in every consuming application.
|
|
ReadTimeout time.Duration
|
|
WriteTimeout time.Duration
|
|
IdleTimeout time.Duration
|
|
// ShutdownTimeout is how long in-flight requests get to finish on SIGTERM.
|
|
ShutdownTimeout time.Duration
|
|
|
|
// CORSAllowedOrigins are the browser origins allowed to call the admin API
|
|
// from a different origin. The runtime API is always public and is not
|
|
// subject to this list.
|
|
CORSAllowedOrigins []string
|
|
// RateLimitPerMinute caps requests per client for the runtime API.
|
|
RateLimitPerMinute int
|
|
|
|
// DatabasePath is the SQLite file path. Use ":memory:" for tests.
|
|
DatabasePath string
|
|
// DatabaseBusyTimeout is how long a writer waits for the database lock
|
|
// before failing.
|
|
DatabaseBusyTimeout time.Duration
|
|
// DatabaseMaxConns bounds the connection pool. SQLite serialises writers, so
|
|
// a small pool with WAL is faster than a large one.
|
|
DatabaseMaxConns int
|
|
|
|
// AuthMode is oidc, dev, or none.
|
|
AuthMode string
|
|
// OIDCIssuer is the issuer URL, e.g. https://auth-dev.urpi.be/realms/urpi.
|
|
OIDCIssuer string
|
|
// OIDCClientID is the client identifier registered with the provider.
|
|
OIDCClientID string
|
|
// OIDCClientSecret authenticates the confidential client.
|
|
OIDCClientSecret string
|
|
// OIDCScopes are requested in addition to openid.
|
|
OIDCScopes []string
|
|
// OIDCRedirectURL overrides the computed callback URL. It is only needed
|
|
// when the public URL cannot be derived, for example behind a path-based
|
|
// ingress.
|
|
OIDCRedirectURL string
|
|
// OIDCPostLogoutRedirectURL is where the provider sends the browser after
|
|
// logout. It must be an allowed post-logout target on the provider side.
|
|
OIDCPostLogoutRedirectURL string
|
|
// OIDCAdminGroups are the group names that map to the admin role.
|
|
OIDCAdminGroups []string
|
|
// OIDCEditorGroups are the group names that map to the editor role.
|
|
OIDCEditorGroups []string
|
|
// OIDCViewerGroups are the group names that map to the viewer role.
|
|
OIDCViewerGroups []string
|
|
// OIDCSkipTLSVerify disables provider certificate verification. It exists
|
|
// only so that a self-signed local provider can be used during development
|
|
// and it is refused outside the development environment.
|
|
OIDCSkipTLSVerify bool
|
|
|
|
// SessionSecret signs the session cookie. It must be at least 32 bytes and
|
|
// is required whenever auth is enabled.
|
|
SessionSecret string
|
|
// SessionCookieName is the cookie carrying the session.
|
|
SessionCookieName string
|
|
// SessionSecure marks the session cookie Secure.
|
|
SessionSecure bool
|
|
// SessionTTL is the session lifetime.
|
|
SessionTTL time.Duration
|
|
// SessionSameSite is Lax, Strict or None.
|
|
SessionSameSite string
|
|
|
|
// RuntimePublic controls whether the runtime API requires a credential. It
|
|
// is enabled by default because a theming service's whole purpose is to be
|
|
// reachable from browsers and CDN caches.
|
|
RuntimePublic bool
|
|
// RuntimeCacheControl is the Cache-Control header sent on runtime assets.
|
|
// It is tuned for aggressive CDN caching with a revalidation window that
|
|
// matches how often themes are published.
|
|
RuntimeCacheControl string
|
|
// RuntimeCDNBaseURL, when set, is used as the absolute base in the manifest
|
|
// so consumers fetch assets from a CDN rather than from this service.
|
|
RuntimeCDNBaseURL string
|
|
|
|
// SeedOnStart reconciles the built-in themes at start-up.
|
|
SeedOnStart bool
|
|
// DevFallbackRole is the role granted by the development auth bypass.
|
|
DevFallbackRole string
|
|
// DevUsers maps a development email to a role, overriding DevFallbackRole.
|
|
DevUsers map[string]string
|
|
|
|
// EnableMetrics exposes /metrics in Prometheus format.
|
|
EnableMetrics bool
|
|
// EnableProfiler exposes the net/http/pprof handlers under /debug/pprof.
|
|
EnableProfiler bool
|
|
// StaticDir is the directory holding the built admin UI. When empty the UI
|
|
// is not served.
|
|
StaticDir string
|
|
// SDKDir is the directory holding the built runtime SDK bundles.
|
|
SDKDir string
|
|
// BasePath prefixes every route, for deployments behind a path-based ingress.
|
|
BasePath string
|
|
|
|
// ephemeralSessionSecret records that the secret was generated rather than
|
|
// supplied. It is not part of the configuration surface; the application reads
|
|
// it only to decide whether to warn.
|
|
ephemeralSessionSecret bool
|
|
}
|
|
|
|
// Auth mode values.
|
|
const (
|
|
AuthModeOIDC = "oidc"
|
|
AuthModeDev = "dev"
|
|
AuthModeNone = "none"
|
|
)
|
|
|
|
// Defaults returns a Config populated with production-safe defaults. Load
|
|
// applies the environment on top of this value.
|
|
func Defaults() Config {
|
|
return Config{
|
|
Environment: "development",
|
|
LogLevel: "info",
|
|
LogFormat: "text",
|
|
|
|
HTTPAddr: ":8080",
|
|
PublicBaseURL: "http://localhost:8080",
|
|
ReadTimeout: 15 * time.Second,
|
|
WriteTimeout: 30 * time.Second,
|
|
IdleTimeout: 120 * time.Second,
|
|
ShutdownTimeout: 25 * time.Second,
|
|
|
|
CORSAllowedOrigins: []string{},
|
|
RateLimitPerMinute: 3000,
|
|
|
|
DatabasePath: "./data/go-bananas.db",
|
|
DatabaseBusyTimeout: 5 * time.Second,
|
|
DatabaseMaxConns: 8,
|
|
|
|
AuthMode: AuthModeDev,
|
|
OIDCScopes: []string{"profile", "email", "groups"},
|
|
SessionCookieName: "go-bananas-session",
|
|
SessionSecure: false,
|
|
SessionTTL: 8 * time.Hour,
|
|
SessionSameSite: "Lax",
|
|
RuntimePublic: true,
|
|
RuntimeCacheControl: "public, max-age=60, s-maxage=300, stale-while-revalidate=86400",
|
|
SeedOnStart: true,
|
|
DevFallbackRole: "admin",
|
|
DevUsers: map[string]string{},
|
|
EnableMetrics: true,
|
|
EnableProfiler: false,
|
|
StaticDir: "./web/dist",
|
|
SDKDir: "./sdk/dist",
|
|
}
|
|
}
|
|
|
|
// Load reads configuration from the environment and validates it.
|
|
func Load() (Config, error) {
|
|
return loadFrom(os.LookupEnv)
|
|
}
|
|
|
|
// loadFrom is the testable core of Load.
|
|
func loadFrom(lookup func(string) (string, bool)) (Config, error) {
|
|
cfg := Defaults()
|
|
|
|
get := func(name string) (string, bool) {
|
|
for _, prefix := range envPrefixes {
|
|
if v, ok := lookup(prefix + name); ok {
|
|
return v, true
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
str := func(name string, target *string) {
|
|
if v, ok := get(name); ok {
|
|
*target = strings.TrimSpace(v)
|
|
}
|
|
}
|
|
|
|
cfg.Environment = firstNonEmpty(cfg.Environment,
|
|
func() (string, bool) { return get("ENVIRONMENT") },
|
|
func() (string, bool) { return get("ENV") },
|
|
)
|
|
str("LOG_LEVEL", &cfg.LogLevel)
|
|
str("LOG_FORMAT", &cfg.LogFormat)
|
|
|
|
str("HTTP_ADDR", &cfg.HTTPAddr)
|
|
str("PUBLIC_BASE_URL", &cfg.PublicBaseURL)
|
|
str("BASE_PATH", &cfg.BasePath)
|
|
str("STATIC_DIR", &cfg.StaticDir)
|
|
str("SDK_DIR", &cfg.SDKDir)
|
|
|
|
dur := func(name string, target *time.Duration) error {
|
|
v, ok := get(name)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
parsed, err := time.ParseDuration(strings.TrimSpace(v))
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %w", name, err)
|
|
}
|
|
*target = parsed
|
|
return nil
|
|
}
|
|
|
|
num := func(name string, target *int) error {
|
|
v, ok := get(name)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
parsed, err := strconv.Atoi(strings.TrimSpace(v))
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %w", name, err)
|
|
}
|
|
*target = parsed
|
|
return nil
|
|
}
|
|
|
|
boolean := func(name string, target *bool) error {
|
|
v, ok := get(name)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
parsed, err := strconv.ParseBool(strings.TrimSpace(v))
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %w", name, err)
|
|
}
|
|
*target = parsed
|
|
return nil
|
|
}
|
|
|
|
list := func(name string, target *[]string) {
|
|
if v, ok := get(name); ok {
|
|
*target = splitList(v)
|
|
}
|
|
}
|
|
|
|
var err error
|
|
for _, f := range []func() error{
|
|
func() error { return dur("HTTP_READ_TIMEOUT", &cfg.ReadTimeout) },
|
|
func() error { return dur("HTTP_WRITE_TIMEOUT", &cfg.WriteTimeout) },
|
|
func() error { return dur("HTTP_IDLE_TIMEOUT", &cfg.IdleTimeout) },
|
|
func() error { return dur("SHUTDOWN_TIMEOUT", &cfg.ShutdownTimeout) },
|
|
func() error { return dur("SESSION_TTL", &cfg.SessionTTL) },
|
|
func() error { return dur("DB_BUSY_TIMEOUT", &cfg.DatabaseBusyTimeout) },
|
|
func() error { return num("RATE_LIMIT_PER_MINUTE", &cfg.RateLimitPerMinute) },
|
|
func() error { return num("DB_MAX_CONNS", &cfg.DatabaseMaxConns) },
|
|
func() error { return boolean("RUNTIME_PUBLIC", &cfg.RuntimePublic) },
|
|
func() error { return boolean("SEED_ON_START", &cfg.SeedOnStart) },
|
|
func() error { return boolean("ENABLE_METRICS", &cfg.EnableMetrics) },
|
|
func() error { return boolean("ENABLE_PROFILER", &cfg.EnableProfiler) },
|
|
func() error { return boolean("SESSION_SECURE", &cfg.SessionSecure) },
|
|
func() error { return boolean("OIDC_SKIP_TLS_VERIFY", &cfg.OIDCSkipTLSVerify) },
|
|
} {
|
|
if err = f(); err != nil {
|
|
return cfg, err
|
|
}
|
|
}
|
|
|
|
list("TRUSTED_PROXIES", &cfg.TrustedProxies)
|
|
list("CORS_ALLOWED_ORIGINS", &cfg.CORSAllowedOrigins)
|
|
list("OIDC_SCOPES", &cfg.OIDCScopes)
|
|
list("OIDC_ADMIN_GROUPS", &cfg.OIDCAdminGroups)
|
|
list("OIDC_EDITOR_GROUPS", &cfg.OIDCEditorGroups)
|
|
list("OIDC_VIEWER_GROUPS", &cfg.OIDCViewerGroups)
|
|
|
|
str("AUTH_MODE", &cfg.AuthMode)
|
|
str("OIDC_ISSUER", &cfg.OIDCIssuer)
|
|
str("OIDC_CLIENT_ID", &cfg.OIDCClientID)
|
|
str("OIDC_CLIENT_SECRET", &cfg.OIDCClientSecret)
|
|
str("OIDC_REDIRECT_URL", &cfg.OIDCRedirectURL)
|
|
str("OIDC_POST_LOGOUT_REDIRECT_URL", &cfg.OIDCPostLogoutRedirectURL)
|
|
str("SESSION_SECRET", &cfg.SessionSecret)
|
|
str("SESSION_COOKIE_NAME", &cfg.SessionCookieName)
|
|
str("SESSION_SAMESITE", &cfg.SessionSameSite)
|
|
str("DEV_FALLBACK_ROLE", &cfg.DevFallbackRole)
|
|
str("RUNTIME_CACHE_CONTROL", &cfg.RuntimeCacheControl)
|
|
str("RUNTIME_CDN_BASE_URL", &cfg.RuntimeCDNBaseURL)
|
|
str("DATABASE_PATH", &cfg.DatabasePath)
|
|
|
|
if v, ok := get("DEV_USERS"); ok {
|
|
cfg.DevUsers = parseDevUsers(v)
|
|
}
|
|
|
|
// A development run should not require the operator to invent a 32-byte
|
|
// secret before the first request. An ephemeral one is generated instead,
|
|
// which is safe because nothing durable is signed with it: sessions simply do
|
|
// not survive a restart, which a local run never notices.
|
|
if cfg.AuthMode != AuthModeNone && cfg.SessionSecret == "" && cfg.IsDevelopment() {
|
|
generated, genErr := randomSecret()
|
|
if genErr != nil {
|
|
return cfg, genErr
|
|
}
|
|
cfg.SessionSecret = generated
|
|
cfg.ephemeralSessionSecret = true
|
|
}
|
|
|
|
return cfg, cfg.Validate()
|
|
}
|
|
|
|
// EphemeralSessionSecret reports whether the session secret was generated rather
|
|
// than supplied. The caller logs it as a warning, because it means sessions
|
|
// survive only until the process restarts.
|
|
func (c Config) EphemeralSessionSecret() bool { return c.ephemeralSessionSecret }
|
|
|
|
// randomSecret returns a 48-byte cryptographically random value, base64 encoded.
|
|
func randomSecret() (string, error) {
|
|
buf := make([]byte, 48)
|
|
if _, err := rand.Read(buf); err != nil {
|
|
return "", fmt.Errorf("generate session secret: %w", err)
|
|
}
|
|
return base64.RawURLEncoding.EncodeToString(buf), nil
|
|
}
|
|
|
|
// Validate checks the configuration for internal consistency. It returns every
|
|
// problem it finds rather than only the first, because fixing configuration one
|
|
// error per deploy is a miserable loop.
|
|
func (c Config) Validate() error {
|
|
var problems []string
|
|
add := func(format string, args ...any) {
|
|
problems = append(problems, fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
switch c.AuthMode {
|
|
case AuthModeOIDC, AuthModeDev, AuthModeNone:
|
|
default:
|
|
add("AUTH_MODE %q must be one of oidc, dev or none", c.AuthMode)
|
|
}
|
|
|
|
if c.AuthMode == AuthModeOIDC {
|
|
if c.OIDCIssuer == "" {
|
|
add("OIDC_ISSUER is required when AUTH_MODE=oidc")
|
|
} else if parsed, err := url.Parse(c.OIDCIssuer); err != nil {
|
|
add("OIDC_ISSUER is not a valid URL: %v", err)
|
|
} else if parsed.Scheme != "https" && !isLocalHost(parsed.Hostname()) {
|
|
add("OIDC_ISSUER must use https outside local development, got %q", parsed.Scheme)
|
|
}
|
|
if c.OIDCClientID == "" {
|
|
add("OIDC_CLIENT_ID is required when AUTH_MODE=oidc")
|
|
}
|
|
if c.OIDCClientSecret == "" {
|
|
add("OIDC_CLIENT_SECRET is required when AUTH_MODE=oidc")
|
|
}
|
|
if c.OIDCSkipTLSVerify && !c.IsDevelopment() {
|
|
add("OIDC_SKIP_TLS_VERIFY is refused outside development; it disables certificate verification on the identity provider connection")
|
|
}
|
|
}
|
|
|
|
if c.AuthMode != AuthModeNone {
|
|
if len(c.SessionSecret) < 32 {
|
|
add("SESSION_SECRET must be at least 32 bytes when auth is enabled, got %d", len(c.SessionSecret))
|
|
}
|
|
}
|
|
|
|
if c.PublicBaseURL != "" {
|
|
parsed, err := url.Parse(c.PublicBaseURL)
|
|
switch {
|
|
case err != nil:
|
|
add("PUBLIC_BASE_URL is not a valid URL: %v", err)
|
|
case parsed.Scheme == "":
|
|
add("PUBLIC_BASE_URL must include a scheme, got %q", c.PublicBaseURL)
|
|
case parsed.Host == "":
|
|
add("PUBLIC_BASE_URL must include a host, got %q", c.PublicBaseURL)
|
|
}
|
|
}
|
|
|
|
if c.BasePath != "" && !strings.HasPrefix(c.BasePath, "/") {
|
|
add("BASE_PATH must start with a slash, got %q", c.BasePath)
|
|
}
|
|
|
|
switch strings.ToLower(c.SessionSameSite) {
|
|
case "lax", "strict", "none":
|
|
default:
|
|
add("SESSION_SAMESITE %q must be lax, strict or none", c.SessionSameSite)
|
|
}
|
|
|
|
if strings.EqualFold(c.SessionSameSite, "none") && !c.SessionSecure {
|
|
// Browsers reject SameSite=None without Secure, so this combination
|
|
// silently produces a session that never survives a redirect.
|
|
add("SESSION_SAMESITE=none requires SESSION_SECURE=true")
|
|
}
|
|
|
|
if !c.SessionSecure && !c.IsDevelopment() && c.PublicBaseURL != "" {
|
|
if strings.HasPrefix(c.PublicBaseURL, "https://") {
|
|
add("SESSION_SECURE must be true when PUBLIC_BASE_URL is https")
|
|
}
|
|
}
|
|
|
|
if c.DatabasePath == "" {
|
|
add("DATABASE_PATH must not be empty")
|
|
}
|
|
if c.DatabaseMaxConns < 1 {
|
|
add("DB_MAX_CONNS must be at least 1, got %d", c.DatabaseMaxConns)
|
|
}
|
|
if c.RateLimitPerMinute < 0 {
|
|
add("RATE_LIMIT_PER_MINUTE must not be negative")
|
|
}
|
|
if !c.IsDevelopment() && c.AuthMode == AuthModeDev {
|
|
add("AUTH_MODE=dev is refused outside development; it grants admin to anyone who can reach the service")
|
|
}
|
|
if c.AuthMode == AuthModeNone && !c.IsDevelopment() {
|
|
add("AUTH_MODE=none is refused outside development")
|
|
}
|
|
|
|
if len(problems) > 0 {
|
|
return errors.New("invalid configuration:\n - " + strings.Join(problems, "\n - "))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// IsDevelopment reports whether the service is running in development.
|
|
func (c Config) IsDevelopment() bool {
|
|
switch strings.ToLower(c.Environment) {
|
|
case "development", "dev", "local", "test", "testing", "ci":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// RedirectURL returns the OIDC callback URL, derived from the public base when
|
|
// not overridden.
|
|
func (c Config) RedirectURL() string {
|
|
if c.OIDCRedirectURL != "" {
|
|
return c.OIDCRedirectURL
|
|
}
|
|
return c.origin() + c.path("/auth/callback")
|
|
}
|
|
|
|
// PostLogoutRedirectURL returns the OIDC end-session target.
|
|
func (c Config) PostLogoutRedirectURL() string {
|
|
if c.OIDCPostLogoutRedirectURL != "" {
|
|
return c.OIDCPostLogoutRedirectURL
|
|
}
|
|
return c.origin() + c.path("/")
|
|
}
|
|
|
|
// Origin returns the public origin with no trailing slash.
|
|
func (c Config) Origin() string { return c.origin() }
|
|
|
|
// origin normalises the public base URL.
|
|
func (c Config) origin() string {
|
|
return strings.TrimRight(c.PublicBaseURL, "/")
|
|
}
|
|
|
|
// path prefixes a route with the configured base path.
|
|
func (c Config) path(route string) string {
|
|
base := strings.TrimRight(c.BasePath, "/")
|
|
if base == "" {
|
|
return route
|
|
}
|
|
return base + route
|
|
}
|
|
|
|
// Path exposes base-path prefixing to the HTTP layer.
|
|
func (c Config) Path(route string) string { return c.path(route) }
|
|
|
|
// CDNBasis returns the absolute base that runtime asset URLs are built from,
|
|
// honouring the CDN override.
|
|
func (c Config) CDNBasis() string {
|
|
if c.RuntimeCDNBaseURL != "" {
|
|
return strings.TrimRight(c.RuntimeCDNBaseURL, "/")
|
|
}
|
|
return c.origin() + c.path("/rt/v1")
|
|
}
|
|
|
|
// splitList parses a comma or whitespace separated environment list.
|
|
func splitList(raw string) []string {
|
|
fields := strings.FieldsFunc(raw, func(r rune) bool {
|
|
return r == ',' || r == '\n' || r == '\t' || r == ' '
|
|
})
|
|
out := make([]string, 0, len(fields))
|
|
for _, f := range fields {
|
|
if trimmed := strings.TrimSpace(f); trimmed != "" {
|
|
out = append(out, trimmed)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// parseDevUsers parses the development role map in "email=role,email=role" form.
|
|
func parseDevUsers(raw string) map[string]string {
|
|
out := make(map[string]string)
|
|
for _, pair := range splitList(raw) {
|
|
email, role, ok := strings.Cut(pair, "=")
|
|
if !ok {
|
|
continue
|
|
}
|
|
email = strings.ToLower(strings.TrimSpace(email))
|
|
role = strings.ToLower(strings.TrimSpace(role))
|
|
if email == "" || role == "" {
|
|
continue
|
|
}
|
|
out[email] = role
|
|
}
|
|
return out
|
|
}
|
|
|
|
// firstNonEmpty returns the first non-empty value from a default and a list of
|
|
// candidate environment lookups.
|
|
func firstNonEmpty(def string, lookups ...func() (string, bool)) string {
|
|
for _, lookup := range lookups {
|
|
if v, ok := lookup(); ok && strings.TrimSpace(v) != "" {
|
|
return strings.TrimSpace(v)
|
|
}
|
|
}
|
|
return def
|
|
}
|
|
|
|
// isLocalHost reports whether host refers to the local machine.
|
|
func isLocalHost(host string) bool {
|
|
switch strings.ToLower(host) {
|
|
case "localhost", "127.0.0.1", "::1", "keycloak", "idp", "auth":
|
|
return true
|
|
default:
|
|
return strings.HasSuffix(host, ".local") || strings.HasSuffix(host, ".localhost")
|
|
}
|
|
}
|
|
|
|
// Redacted returns a copy of the configuration with secrets masked, suitable
|
|
// for logging or for the diagnostics endpoint.
|
|
func (c Config) Redacted() Config {
|
|
mask := func(s string) string {
|
|
if s == "" {
|
|
return ""
|
|
}
|
|
return "***redacted***"
|
|
}
|
|
c.OIDCClientSecret = mask(c.OIDCClientSecret)
|
|
c.SessionSecret = mask(c.SessionSecret)
|
|
return c
|
|
}
|