Initial commit: Open sourcing all of the Maple Open Technologies code.

This commit is contained in:
Bartlomiej Mika 2025-12-02 14:33:08 -05:00
commit 755d54a99d
2010 changed files with 448675 additions and 0 deletions

View file

@ -0,0 +1,434 @@
// 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
}