// codeberg.org/mapleopentech/monorepo/cloud/maplefile-backend/config/config.go package config import ( "fmt" "os" "strconv" "strings" "time" ) type Config struct { App AppConfig Server ServerConfig Database DatabaseConfig Cache CacheConfig S3 S3Config JWT JWTConfig Mailgun MailgunConfig Observability ObservabilityConfig Logging LoggingConfig Security SecurityConfig LeaderElection LeaderElectionConfig InviteEmail InviteEmailConfig LoginRateLimit LoginRateLimitConfig } // Configuration is an alias for Config for backward compatibility type Configuration = Config type AppConfig struct { Environment string Version string DataDir string } type ServerConfig struct { Host string Port int ReadTimeout time.Duration WriteTimeout time.Duration IdleTimeout time.Duration ShutdownTimeout time.Duration } type DatabaseConfig struct { Hosts []string Keyspace string Consistency string Username string Password string MigrationsPath string AutoMigrate bool // Run migrations automatically on startup ConnectTimeout time.Duration RequestTimeout time.Duration ReplicationFactor int MaxRetryAttempts int RetryDelay time.Duration } type CacheConfig struct { Host string Port int Password string DB int } type S3Config struct { Endpoint string PublicEndpoint string // Public-facing endpoint for presigned URLs (e.g., http://localhost:8334) AccessKey string SecretKey string BucketName string Region string UseSSL bool UsePathStyle bool // Use path-style URLs (true for MinIO/SeaweedFS, false for AWS S3/DigitalOcean Spaces) } type JWTConfig struct { Secret string AccessTokenDuration time.Duration RefreshTokenDuration time.Duration SessionDuration time.Duration SessionCleanupInterval time.Duration } type MailgunConfig struct { APIKey string Domain string APIBase string SenderEmail string SenderName string FrontendURL string } type ObservabilityConfig struct { Enabled bool Port int HealthCheckTimeout time.Duration MetricsEnabled bool HealthChecksEnabled bool DetailedHealthChecks bool } type LoggingConfig struct { Level string Format string EnableStacktrace bool EnableCaller bool } type SecurityConfig struct { GeoLiteDBPath string BannedCountries []string RateLimitEnabled bool IPBlockEnabled bool AllowedOrigins []string // CORS allowed origins TrustedProxies []string IPAnonymizationEnabled bool IPAnonymizationRetentionDays int IPAnonymizationSchedule string } type LeaderElectionConfig struct { Enabled bool LockTTL time.Duration HeartbeatInterval time.Duration RetryInterval time.Duration InstanceID string Hostname string } // InviteEmailConfig holds configuration for invitation emails to non-registered users type InviteEmailConfig struct { MaxEmailsPerDay int // Maximum invitation emails a user can send per day } // LoginRateLimitConfig holds configuration for login rate limiting type LoginRateLimitConfig struct { MaxAttemptsPerIP int // Maximum login attempts per IP in the window IPWindow time.Duration // Time window for IP-based rate limiting MaxFailedAttemptsPerAccount int // Maximum failed attempts before account lockout AccountLockoutDuration time.Duration // How long to lock an account after too many failures } func Load() (*Config, error) { cfg := &Config{ // App App: AppConfig{ Environment: getEnvString("APP_ENVIRONMENT", "development"), Version: getEnvString("APP_VERSION", "0.1.0"), DataDir: getEnvString("APP_DATA_DIRECTORY", "./data"), }, // Server Server: ServerConfig{ Host: getEnvString("SERVER_HOST", "0.0.0.0"), Port: getEnvInt("SERVER_PORT", 8000), ReadTimeout: getEnvDuration("SERVER_READ_TIMEOUT", 30*time.Second), WriteTimeout: getEnvDuration("SERVER_WRITE_TIMEOUT", 30*time.Second), IdleTimeout: getEnvDuration("SERVER_IDLE_TIMEOUT", 60*time.Second), ShutdownTimeout: getEnvDuration("SERVER_SHUTDOWN_TIMEOUT", 10*time.Second), }, // Database Database: DatabaseConfig{ Hosts: strings.Split(getEnvString("DATABASE_HOSTS", "localhost:9042"), ","), Keyspace: getEnvString("DATABASE_KEYSPACE", "maplefile"), Consistency: getEnvString("DATABASE_CONSISTENCY", "QUORUM"), Username: getEnvString("DATABASE_USERNAME", ""), Password: getEnvString("DATABASE_PASSWORD", ""), MigrationsPath: getEnvString("DATABASE_MIGRATIONS_PATH", "./migrations"), AutoMigrate: getEnvBool("DATABASE_AUTO_MIGRATE", true), ConnectTimeout: getEnvDuration("DATABASE_CONNECT_TIMEOUT", 10*time.Second), RequestTimeout: getEnvDuration("DATABASE_REQUEST_TIMEOUT", 5*time.Second), ReplicationFactor: getEnvInt("DATABASE_REPLICATION", 3), MaxRetryAttempts: getEnvInt("DATABASE_MAX_RETRIES", 3), RetryDelay: getEnvDuration("DATABASE_RETRY_DELAY", 1*time.Second), }, // Cache Cache: CacheConfig{ Host: getEnvString("CACHE_HOST", "localhost"), Port: getEnvInt("CACHE_PORT", 6379), Password: getEnvString("CACHE_PASSWORD", ""), DB: getEnvInt("CACHE_DB", 0), }, // S3 S3: S3Config{ Endpoint: getEnvString("S3_ENDPOINT", "http://localhost:9000"), PublicEndpoint: getEnvString("S3_PUBLIC_ENDPOINT", ""), // Falls back to Endpoint if not set // CWE-798: Remove default credentials - require explicit configuration // SECURITY: Default 'minioadmin' credentials removed for production safety AccessKey: getEnvString("S3_ACCESS_KEY", ""), SecretKey: getEnvString("S3_SECRET_KEY", ""), BucketName: getEnvString("S3_BUCKET", "maplefile"), Region: getEnvString("S3_REGION", "us-east-1"), UseSSL: getEnvBool("S3_USE_SSL", false), UsePathStyle: getEnvBool("S3_USE_PATH_STYLE", true), // Default true for dev (SeaweedFS), false for prod (DO Spaces) }, // JWT JWT: JWTConfig{ // CWE-798: Remove default weak secret - require explicit configuration // SECURITY: Default 'change-me-in-production' removed to force proper JWT secret setup Secret: getEnvString("JWT_SECRET", ""), AccessTokenDuration: getEnvDuration("JWT_ACCESS_TOKEN_DURATION", 15*time.Minute), RefreshTokenDuration: getEnvDuration("JWT_REFRESH_TOKEN_DURATION", 7*24*time.Hour), SessionDuration: getEnvDuration("JWT_SESSION_DURATION", 24*time.Hour), SessionCleanupInterval: getEnvDuration("JWT_SESSION_CLEANUP_INTERVAL", 1*time.Hour), }, // Mailgun Mailgun: MailgunConfig{ APIKey: getEnvString("MAILGUN_API_KEY", ""), Domain: getEnvString("MAILGUN_DOMAIN", ""), APIBase: getEnvString("MAILGUN_API_BASE", "https://api.mailgun.net/v3"), SenderEmail: getEnvString("MAILGUN_FROM_EMAIL", "noreply@maplefile.app"), SenderName: getEnvString("MAILGUN_FROM_NAME", "MapleFile"), FrontendURL: getEnvString("MAILGUN_FRONTEND_URL", "http://localhost:3000"), }, // Observability Observability: ObservabilityConfig{ Enabled: getEnvBool("OBSERVABILITY_ENABLED", true), Port: getEnvInt("OBSERVABILITY_PORT", 9090), HealthCheckTimeout: getEnvDuration("OBSERVABILITY_HEALTH_TIMEOUT", 5*time.Second), MetricsEnabled: getEnvBool("OBSERVABILITY_METRICS_ENABLED", true), HealthChecksEnabled: getEnvBool("OBSERVABILITY_HEALTH_ENABLED", true), DetailedHealthChecks: getEnvBool("OBSERVABILITY_DETAILED_HEALTH", false), }, // Logging Logging: LoggingConfig{ Level: getEnvString("LOG_LEVEL", "info"), Format: getEnvString("LOG_FORMAT", "json"), EnableStacktrace: getEnvBool("LOG_STACKTRACE", false), EnableCaller: getEnvBool("LOG_CALLER", true), }, // Security Security: SecurityConfig{ GeoLiteDBPath: getEnvString("SECURITY_GEOLITE_DB_PATH", "./data/GeoLite2-Country.mmdb"), BannedCountries: strings.Split(getEnvString("SECURITY_BANNED_COUNTRIES", ""), ","), RateLimitEnabled: getEnvBool("SECURITY_RATE_LIMIT_ENABLED", true), IPBlockEnabled: getEnvBool("SECURITY_IP_BLOCK_ENABLED", true), AllowedOrigins: strings.Split(getEnvString("SECURITY_ALLOWED_ORIGINS", ""), ","), TrustedProxies: strings.Split(getEnvString("SECURITY_TRUSTED_PROXIES", ""), ","), IPAnonymizationEnabled: getEnvBool("SECURITY_IP_ANONYMIZATION_ENABLED", true), IPAnonymizationRetentionDays: getEnvInt("SECURITY_IP_ANONYMIZATION_RETENTION_DAYS", 90), IPAnonymizationSchedule: getEnvString("SECURITY_IP_ANONYMIZATION_SCHEDULE", "0 2 * * *"), // Daily at 2 AM }, // Leader Election LeaderElection: LeaderElectionConfig{ Enabled: getEnvBool("LEADER_ELECTION_ENABLED", true), LockTTL: getEnvDuration("LEADER_ELECTION_LOCK_TTL", 10*time.Second), HeartbeatInterval: getEnvDuration("LEADER_ELECTION_HEARTBEAT_INTERVAL", 3*time.Second), RetryInterval: getEnvDuration("LEADER_ELECTION_RETRY_INTERVAL", 2*time.Second), InstanceID: getEnvString("LEADER_ELECTION_INSTANCE_ID", ""), Hostname: getEnvString("LEADER_ELECTION_HOSTNAME", ""), }, // Invite Email InviteEmail: InviteEmailConfig{ MaxEmailsPerDay: getEnvInt("MAPLEFILE_INVITE_MAX_EMAILS_PER_DAY", 3), }, // Login Rate Limiting LoginRateLimit: LoginRateLimitConfig{ MaxAttemptsPerIP: getEnvInt("LOGIN_RATE_LIMIT_MAX_ATTEMPTS_PER_IP", 50), IPWindow: getEnvDuration("LOGIN_RATE_LIMIT_IP_WINDOW", 15*time.Minute), MaxFailedAttemptsPerAccount: getEnvInt("LOGIN_RATE_LIMIT_MAX_FAILED_PER_ACCOUNT", 10), AccountLockoutDuration: getEnvDuration("LOGIN_RATE_LIMIT_LOCKOUT_DURATION", 30*time.Minute), }, } return cfg, nil } // Helper functions func getEnvString(key, defaultValue string) string { if value := os.Getenv(key); value != "" { return value } return defaultValue } func getEnvInt(key string, defaultValue int) int { if value := os.Getenv(key); value != "" { if intValue, err := strconv.Atoi(value); err == nil { return intValue } } return defaultValue } func getEnvBool(key string, defaultValue bool) bool { if value := os.Getenv(key); value != "" { if boolValue, err := strconv.ParseBool(value); err == nil { return boolValue } } return defaultValue } func getEnvDuration(key string, defaultValue time.Duration) time.Duration { if value := os.Getenv(key); value != "" { if duration, err := time.ParseDuration(value); err == nil { return duration } } return defaultValue } func (c *Config) Validate() error { // For backward compatibility, call ValidateProduction for production environments if c.App.Environment == "production" { return c.ValidateProduction() } return nil } // ValidateProduction performs comprehensive validation of all critical configuration // parameters for production environments to prevent security misconfigurations. // CWE-798: Use of Hard-coded Credentials // OWASP A05:2021: Security Misconfiguration func (c *Config) ValidateProduction() error { var errors []string // JWT Secret Validation if c.JWT.Secret == "" { errors = append(errors, "JWT_SECRET is required in production") } else if len(c.JWT.Secret) < 32 { errors = append(errors, "JWT_SECRET must be at least 32 characters for production security") } // Database Credentials Validation if len(c.Database.Hosts) == 0 { errors = append(errors, "DATABASE_HOSTS is required in production") } if c.Database.Keyspace == "" { errors = append(errors, "DATABASE_KEYSPACE is required in production") } // Password is optional for some Cassandra setups, but username requires password if c.Database.Username != "" && c.Database.Password == "" { errors = append(errors, "DATABASE_PASSWORD is required when DATABASE_USERNAME is set") } // S3/Object Storage Credentials Validation if c.S3.AccessKey == "" { errors = append(errors, "S3_ACCESS_KEY is required in production") } if c.S3.SecretKey == "" { errors = append(errors, "S3_SECRET_KEY is required in production") } if c.S3.BucketName == "" { errors = append(errors, "S3_BUCKET is required in production") } if c.S3.Endpoint == "" { errors = append(errors, "S3_ENDPOINT is required in production") } // Mailgun/Email Service Validation if c.Mailgun.APIKey == "" { errors = append(errors, "MAILGUN_API_KEY is required in production (email service needed)") } if c.Mailgun.Domain == "" { errors = append(errors, "MAILGUN_DOMAIN is required in production") } if c.Mailgun.SenderEmail == "" { errors = append(errors, "MAILGUN_FROM_EMAIL is required in production") } // Redis/Cache Configuration Validation if c.Cache.Host == "" { errors = append(errors, "CACHE_HOST is required in production") } // Note: Cache password is optional for some Redis setups // Security Configuration Validation if c.App.Environment != "production" { errors = append(errors, "APP_ENVIRONMENT must be set to 'production' for production deployments") } // CORS Security - Warn if allowing all origins in production for _, origin := range c.Security.AllowedOrigins { if origin == "*" { errors = append(errors, "SECURITY_ALLOWED_ORIGINS='*' is not recommended in production (security risk)") } } // SSL/TLS Validation if c.S3.UseSSL == false { // This is a warning, not a hard error, as some internal networks don't use SSL // errors = append(errors, "S3_USE_SSL should be 'true' in production for security") } // Return all validation errors if len(errors) > 0 { return fmt.Errorf("production configuration validation failed:\n - %s", strings.Join(errors, "\n - ")) } return nil } // ValidateDevelopment validates configuration for development environments // This is less strict but still checks for basic configuration issues func (c *Config) ValidateDevelopment() error { var errors []string // Basic validations that apply to all environments if c.JWT.Secret == "" { errors = append(errors, "JWT_SECRET is required") } if c.Database.Keyspace == "" { errors = append(errors, "DATABASE_KEYSPACE is required") } if c.S3.BucketName == "" { errors = append(errors, "S3_BUCKET is required") } if len(errors) > 0 { return fmt.Errorf("development configuration validation failed:\n - %s", strings.Join(errors, "\n - ")) } return nil }