Files
traefikoidc/config/validator.go
T
lukaszraczylo e64fc7f730 Add redis support for distributed caching (#83)
* Add redis support for distributed caching

* Move towards the self-provided Redis connection pool and RESP protocol implementation.
Official redis client library won't work with yaegi.

* fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi.

* fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi.

* fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi.

* fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi.

* fixup! fixup! fixup! fixup! fixup! Move towards the self-provided Redis connection pool and RESP protocol implementation. Official redis client library won't work with yaegi.

* ... and another all nighter.

* fixup! ... and another all nighter.

* fixup! fixup! ... and another all nighter.

* fixup! fixup! fixup! ... and another all nighter.

* Resolve issue #85 by adding ability to set custom claims in JWT tokens

* Remove redundant validation in auth middleware ( issue #89 )

* Add ability to set cookie prefix for session cookies ( #87 )

* fixup! Add ability to set cookie prefix for session cookies ( #87 )

* Add ability to set cookie max age - issue #91

* Potential fix for code scanning alert no. 10: Size computation for allocation may overflow

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* fixup! Merge main into 0.8.0-redis: resolve conflicts

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-11-30 02:18:46 +00:00

653 lines
17 KiB
Go

// Package config provides validation for unified configuration
package config
import (
"fmt"
"net/url"
"regexp"
"strings"
"time"
)
// ValidationError represents a configuration validation error
type ValidationError struct {
Field string
Message string
Value interface{}
}
// Error implements the error interface
func (e *ValidationError) Error() string {
if e.Value != nil {
return fmt.Sprintf("config validation error: %s: %s (value: %v)", e.Field, e.Message, e.Value)
}
return fmt.Sprintf("config validation error: %s: %s", e.Field, e.Message)
}
// ValidationErrors represents multiple validation errors
type ValidationErrors []ValidationError
// Error implements the error interface
func (e ValidationErrors) Error() string {
if len(e) == 0 {
return ""
}
var messages []string
for _, err := range e {
messages = append(messages, err.Error())
}
return strings.Join(messages, "; ")
}
// Validate performs comprehensive validation on the unified configuration
func (c *UnifiedConfig) Validate() error {
var errors ValidationErrors
// Validate Provider configuration
if err := c.validateProvider(); err != nil {
errors = append(errors, err...)
}
// Validate Session configuration
if err := c.validateSession(); err != nil {
errors = append(errors, err...)
}
// Validate Token configuration
if err := c.validateToken(); err != nil {
errors = append(errors, err...)
}
// Validate Redis configuration (uses existing validation)
if err := c.Redis.Validate(); err != nil {
errors = append(errors, ValidationError{
Field: "Redis",
Message: err.Error(),
})
}
// Validate Security configuration
if err := c.validateSecurity(); err != nil {
errors = append(errors, err...)
}
// Validate Middleware configuration
if err := c.validateMiddleware(); err != nil {
errors = append(errors, err...)
}
// Validate Cache configuration
if err := c.validateCache(); err != nil {
errors = append(errors, err...)
}
// Validate RateLimit configuration
if err := c.validateRateLimit(); err != nil {
errors = append(errors, err...)
}
// Validate Logging configuration
if err := c.validateLogging(); err != nil {
errors = append(errors, err...)
}
// Validate Metrics configuration
if err := c.validateMetrics(); err != nil {
errors = append(errors, err...)
}
// Validate Transport configuration
if err := c.validateTransport(); err != nil {
errors = append(errors, err...)
}
// Validate Circuit configuration
if err := c.validateCircuit(); err != nil {
errors = append(errors, err...)
}
if len(errors) > 0 {
return errors
}
return nil
}
// validateProvider validates provider configuration
func (c *UnifiedConfig) validateProvider() ValidationErrors {
var errors ValidationErrors
// IssuerURL is required and must be a valid URL
if c.Provider.IssuerURL == "" {
errors = append(errors, ValidationError{
Field: "Provider.IssuerURL",
Message: "issuer URL is required",
})
} else if _, err := url.Parse(c.Provider.IssuerURL); err != nil {
errors = append(errors, ValidationError{
Field: "Provider.IssuerURL",
Message: "invalid issuer URL",
Value: c.Provider.IssuerURL,
})
}
// ClientID is required
if c.Provider.ClientID == "" {
errors = append(errors, ValidationError{
Field: "Provider.ClientID",
Message: "client ID is required",
})
}
// ClientSecret is required (except for public clients with PKCE)
if c.Provider.ClientSecret == "" && !c.Security.EnablePKCE {
errors = append(errors, ValidationError{
Field: "Provider.ClientSecret",
Message: "client secret is required (or enable PKCE for public clients)",
})
}
// RedirectURL must be valid if provided
if c.Provider.RedirectURL != "" {
if _, err := url.Parse(c.Provider.RedirectURL); err != nil {
errors = append(errors, ValidationError{
Field: "Provider.RedirectURL",
Message: "invalid redirect URL",
Value: c.Provider.RedirectURL,
})
}
}
// Scopes must include 'openid' for OIDC
hasOpenID := false
for _, scope := range c.Provider.Scopes {
if scope == "openid" {
hasOpenID = true
break
}
}
if !hasOpenID && !c.Provider.OverrideScopes {
errors = append(errors, ValidationError{
Field: "Provider.Scopes",
Message: "scopes must include 'openid' for OIDC",
Value: c.Provider.Scopes,
})
}
// JWK cache period must be positive
if c.Provider.JWKCachePeriod < 0 {
errors = append(errors, ValidationError{
Field: "Provider.JWKCachePeriod",
Message: "JWK cache period must be positive",
Value: c.Provider.JWKCachePeriod,
})
}
return errors
}
// validateSession validates session configuration
func (c *UnifiedConfig) validateSession() ValidationErrors {
var errors ValidationErrors
// Session name must not be empty
if c.Session.Name == "" {
errors = append(errors, ValidationError{
Field: "Session.Name",
Message: "session name is required",
})
}
// Session secret or encryption key is required
if c.Session.Secret == "" && c.Session.EncryptionKey == "" {
errors = append(errors, ValidationError{
Field: "Session",
Message: "either session secret or encryption key is required",
})
}
// Encryption key must be at least 32 bytes for security
if c.Session.EncryptionKey != "" && len(c.Session.EncryptionKey) < 32 {
errors = append(errors, ValidationError{
Field: "Session.EncryptionKey",
Message: "encryption key must be at least 32 characters for proper security",
Value: len(c.Session.EncryptionKey),
})
}
// ChunkSize must be reasonable (between 1KB and 10KB)
if c.Session.ChunkSize < 1000 || c.Session.ChunkSize > 10000 {
errors = append(errors, ValidationError{
Field: "Session.ChunkSize",
Message: "chunk size must be between 1000 and 10000 bytes",
Value: c.Session.ChunkSize,
})
}
// MaxChunks must be reasonable (between 1 and 100)
if c.Session.MaxChunks < 1 || c.Session.MaxChunks > 100 {
errors = append(errors, ValidationError{
Field: "Session.MaxChunks",
Message: "max chunks must be between 1 and 100",
Value: c.Session.MaxChunks,
})
}
// SameSite must be valid
validSameSite := map[string]bool{
"": true,
"Lax": true,
"Strict": true,
"None": true,
}
if !validSameSite[c.Session.SameSite] {
errors = append(errors, ValidationError{
Field: "Session.SameSite",
Message: "invalid SameSite value (must be Lax, Strict, or None)",
Value: c.Session.SameSite,
})
}
// StorageType must be valid
validStorage := map[string]bool{
"memory": true,
"redis": true,
"cookie": true,
}
if !validStorage[c.Session.StorageType] {
errors = append(errors, ValidationError{
Field: "Session.StorageType",
Message: "invalid storage type (must be memory, redis, or cookie)",
Value: c.Session.StorageType,
})
}
return errors
}
// validateToken validates token configuration
func (c *UnifiedConfig) validateToken() ValidationErrors {
var errors ValidationErrors
// Token TTLs must be positive
if c.Token.AccessTokenTTL <= 0 {
errors = append(errors, ValidationError{
Field: "Token.AccessTokenTTL",
Message: "access token TTL must be positive",
Value: c.Token.AccessTokenTTL,
})
}
if c.Token.RefreshTokenTTL <= 0 {
errors = append(errors, ValidationError{
Field: "Token.RefreshTokenTTL",
Message: "refresh token TTL must be positive",
Value: c.Token.RefreshTokenTTL,
})
}
// Validation mode must be valid
validModes := map[string]bool{
"jwt": true,
"introspect": true,
"hybrid": true,
}
if !validModes[c.Token.ValidationMode] {
errors = append(errors, ValidationError{
Field: "Token.ValidationMode",
Message: "invalid validation mode (must be jwt, introspect, or hybrid)",
Value: c.Token.ValidationMode,
})
}
// Introspect URL required for introspect or hybrid mode
if (c.Token.ValidationMode == "introspect" || c.Token.ValidationMode == "hybrid") && c.Token.IntrospectURL == "" {
errors = append(errors, ValidationError{
Field: "Token.IntrospectURL",
Message: "introspect URL is required for introspect or hybrid validation mode",
})
}
// Clock skew must be reasonable (0 to 10 minutes)
if c.Token.ClockSkew < 0 || c.Token.ClockSkew > 10*time.Minute {
errors = append(errors, ValidationError{
Field: "Token.ClockSkew",
Message: "clock skew must be between 0 and 10 minutes",
Value: c.Token.ClockSkew,
})
}
return errors
}
// validateSecurity validates security configuration
func (c *UnifiedConfig) validateSecurity() ValidationErrors {
var errors ValidationErrors
// Validate allowed user domains are valid domains
domainRegex := regexp.MustCompile(`^([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.[a-zA-Z]{2,}$`)
for _, domain := range c.Security.AllowedUserDomains {
if !domainRegex.MatchString(domain) {
errors = append(errors, ValidationError{
Field: "Security.AllowedUserDomains",
Message: "invalid domain format",
Value: domain,
})
}
}
// Max login attempts must be reasonable
if c.Security.MaxLoginAttempts < 0 || c.Security.MaxLoginAttempts > 100 {
errors = append(errors, ValidationError{
Field: "Security.MaxLoginAttempts",
Message: "max login attempts must be between 0 and 100",
Value: c.Security.MaxLoginAttempts,
})
}
// Lockout duration must be reasonable
if c.Security.LockoutDuration < 0 || c.Security.LockoutDuration > 24*time.Hour {
errors = append(errors, ValidationError{
Field: "Security.LockoutDuration",
Message: "lockout duration must be between 0 and 24 hours",
Value: c.Security.LockoutDuration,
})
}
return errors
}
// validateMiddleware validates middleware configuration
func (c *UnifiedConfig) validateMiddleware() ValidationErrors {
var errors ValidationErrors
// Max request size must be reasonable (1KB to 100MB)
if c.Middleware.MaxRequestSize < 1024 || c.Middleware.MaxRequestSize > 100*1024*1024 {
errors = append(errors, ValidationError{
Field: "Middleware.MaxRequestSize",
Message: "max request size must be between 1KB and 100MB",
Value: c.Middleware.MaxRequestSize,
})
}
// Request timeout must be reasonable
if c.Middleware.RequestTimeout < time.Second || c.Middleware.RequestTimeout > 5*time.Minute {
errors = append(errors, ValidationError{
Field: "Middleware.RequestTimeout",
Message: "request timeout must be between 1 second and 5 minutes",
Value: c.Middleware.RequestTimeout,
})
}
return errors
}
// validateCache validates cache configuration
func (c *UnifiedConfig) validateCache() ValidationErrors {
var errors ValidationErrors
if !c.Cache.Enabled {
return errors
}
// Cache type must be valid
validTypes := map[string]bool{
"memory": true,
"redis": true,
"hybrid": true,
}
if !validTypes[c.Cache.Type] {
errors = append(errors, ValidationError{
Field: "Cache.Type",
Message: "invalid cache type (must be memory, redis, or hybrid)",
Value: c.Cache.Type,
})
}
// Max entries must be reasonable
if c.Cache.MaxEntries < 10 || c.Cache.MaxEntries > 1000000 {
errors = append(errors, ValidationError{
Field: "Cache.MaxEntries",
Message: "max entries must be between 10 and 1000000",
Value: c.Cache.MaxEntries,
})
}
// Eviction policy must be valid
validEviction := map[string]bool{
"lru": true,
"lfu": true,
"fifo": true,
}
if !validEviction[c.Cache.EvictionPolicy] {
errors = append(errors, ValidationError{
Field: "Cache.EvictionPolicy",
Message: "invalid eviction policy (must be lru, lfu, or fifo)",
Value: c.Cache.EvictionPolicy,
})
}
return errors
}
// validateRateLimit validates rate limiting configuration
func (c *UnifiedConfig) validateRateLimit() ValidationErrors {
var errors ValidationErrors
if !c.RateLimit.Enabled {
return errors
}
// Requests per second must be reasonable
if c.RateLimit.RequestsPerSecond < 1 || c.RateLimit.RequestsPerSecond > 10000 {
errors = append(errors, ValidationError{
Field: "RateLimit.RequestsPerSecond",
Message: "requests per second must be between 1 and 10000",
Value: c.RateLimit.RequestsPerSecond,
})
}
// Burst must be at least as large as requests per second
if c.RateLimit.Burst < c.RateLimit.RequestsPerSecond {
errors = append(errors, ValidationError{
Field: "RateLimit.Burst",
Message: "burst must be at least as large as requests per second",
Value: c.RateLimit.Burst,
})
}
// Key type must be valid
validKeyTypes := map[string]bool{
"ip": true,
"user": true,
"token": true,
"custom": true,
}
if !validKeyTypes[c.RateLimit.KeyType] {
errors = append(errors, ValidationError{
Field: "RateLimit.KeyType",
Message: "invalid key type (must be ip, user, token, or custom)",
Value: c.RateLimit.KeyType,
})
}
return errors
}
// validateLogging validates logging configuration
func (c *UnifiedConfig) validateLogging() ValidationErrors {
var errors ValidationErrors
// Log level must be valid
validLevels := map[string]bool{
"debug": true,
"info": true,
"warn": true,
"error": true,
}
if !validLevels[c.Logging.Level] {
errors = append(errors, ValidationError{
Field: "Logging.Level",
Message: "invalid log level (must be debug, info, warn, or error)",
Value: c.Logging.Level,
})
}
// Format must be valid
validFormats := map[string]bool{
"json": true,
"text": true,
"structured": true,
}
if !validFormats[c.Logging.Format] {
errors = append(errors, ValidationError{
Field: "Logging.Format",
Message: "invalid log format (must be json, text, or structured)",
Value: c.Logging.Format,
})
}
// Output must be valid
validOutputs := map[string]bool{
"stdout": true,
"stderr": true,
"file": true,
}
if !validOutputs[c.Logging.Output] {
errors = append(errors, ValidationError{
Field: "Logging.Output",
Message: "invalid log output (must be stdout, stderr, or file)",
Value: c.Logging.Output,
})
}
// File path required if output is file
if c.Logging.Output == "file" && c.Logging.FilePath == "" {
errors = append(errors, ValidationError{
Field: "Logging.FilePath",
Message: "file path is required when output is 'file'",
})
}
return errors
}
// validateMetrics validates metrics configuration
func (c *UnifiedConfig) validateMetrics() ValidationErrors {
var errors ValidationErrors
if !c.Metrics.Enabled {
return errors
}
// Provider must be valid
validProviders := map[string]bool{
"prometheus": true,
"statsd": true,
"otlp": true,
}
if !validProviders[c.Metrics.Provider] {
errors = append(errors, ValidationError{
Field: "Metrics.Provider",
Message: "invalid metrics provider (must be prometheus, statsd, or otlp)",
Value: c.Metrics.Provider,
})
}
// Endpoint required for some providers
if (c.Metrics.Provider == "statsd" || c.Metrics.Provider == "otlp") && c.Metrics.Endpoint == "" {
errors = append(errors, ValidationError{
Field: "Metrics.Endpoint",
Message: fmt.Sprintf("endpoint is required for %s provider", c.Metrics.Provider),
})
}
return errors
}
// validateTransport validates transport configuration
func (c *UnifiedConfig) validateTransport() ValidationErrors {
var errors ValidationErrors
// Max connections must be reasonable
if c.Transport.MaxIdleConns < 0 || c.Transport.MaxIdleConns > 10000 {
errors = append(errors, ValidationError{
Field: "Transport.MaxIdleConns",
Message: "max idle connections must be between 0 and 10000",
Value: c.Transport.MaxIdleConns,
})
}
// TLS min version must be valid
validTLSVersions := map[string]bool{
"TLS1.0": true,
"TLS1.1": true,
"TLS1.2": true,
"TLS1.3": true,
}
if c.Transport.TLSMinVersion != "" && !validTLSVersions[c.Transport.TLSMinVersion] {
errors = append(errors, ValidationError{
Field: "Transport.TLSMinVersion",
Message: "invalid TLS min version (must be TLS1.0, TLS1.1, TLS1.2, or TLS1.3)",
Value: c.Transport.TLSMinVersion,
})
}
// Proxy URL must be valid if provided
if c.Transport.ProxyURL != "" {
if _, err := url.Parse(c.Transport.ProxyURL); err != nil {
errors = append(errors, ValidationError{
Field: "Transport.ProxyURL",
Message: "invalid proxy URL",
Value: c.Transport.ProxyURL,
})
}
}
return errors
}
// validateCircuit validates circuit breaker configuration
func (c *UnifiedConfig) validateCircuit() ValidationErrors {
var errors ValidationErrors
if !c.Circuit.Enabled {
return errors
}
// Consecutive failures must be reasonable
if c.Circuit.ConsecutiveFailures < 1 || c.Circuit.ConsecutiveFailures > 100 {
errors = append(errors, ValidationError{
Field: "Circuit.ConsecutiveFailures",
Message: "consecutive failures must be between 1 and 100",
Value: c.Circuit.ConsecutiveFailures,
})
}
// Failure ratio must be between 0 and 1
if c.Circuit.FailureRatio < 0 || c.Circuit.FailureRatio > 1 {
errors = append(errors, ValidationError{
Field: "Circuit.FailureRatio",
Message: "failure ratio must be between 0 and 1",
Value: c.Circuit.FailureRatio,
})
}
// OnOpen action must be valid
validActions := map[string]bool{
"reject": true,
"fallback": true,
"passthrough": true,
}
if !validActions[c.Circuit.OnOpen] {
errors = append(errors, ValidationError{
Field: "Circuit.OnOpen",
Message: "invalid OnOpen action (must be reject, fallback, or passthrough)",
Value: c.Circuit.OnOpen,
})
}
return errors
}