Initial commit: Open sourcing all of the Maple Open Technologies code.
This commit is contained in:
commit
755d54a99d
2010 changed files with 448675 additions and 0 deletions
434
cloud/maplefile-backend/config/config.go
Normal file
434
cloud/maplefile-backend/config/config.go
Normal 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
|
||||
}
|
||||
403
cloud/maplefile-backend/config/config_test.go
Normal file
403
cloud/maplefile-backend/config/config_test.go
Normal file
|
|
@ -0,0 +1,403 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestValidateProduction_AllValid tests that a fully configured production setup passes validation
|
||||
func TestValidateProduction_AllValid(t *testing.T) {
|
||||
cfg := &Config{
|
||||
App: AppConfig{
|
||||
Environment: "production",
|
||||
},
|
||||
JWT: JWTConfig{
|
||||
Secret: "this-is-a-very-secure-secret-key-with-more-than-32-characters",
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
Hosts: []string{"cassandra1.prod.example.com:9042"},
|
||||
Keyspace: "maplefile_prod",
|
||||
Username: "admin",
|
||||
Password: "secure_password_123",
|
||||
},
|
||||
S3: S3Config{
|
||||
AccessKey: "AKIAIOSFODNN7EXAMPLE",
|
||||
SecretKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
|
||||
BucketName: "maplefile-production",
|
||||
Endpoint: "https://s3.amazonaws.com",
|
||||
},
|
||||
Mailgun: MailgunConfig{
|
||||
APIKey: "key-1234567890abcdef1234567890abcdef",
|
||||
Domain: "mg.example.com",
|
||||
SenderEmail: "noreply@example.com",
|
||||
},
|
||||
Cache: CacheConfig{
|
||||
Host: "redis.prod.example.com",
|
||||
},
|
||||
Security: SecurityConfig{
|
||||
AllowedOrigins: []string{"https://app.example.com"},
|
||||
},
|
||||
}
|
||||
|
||||
err := cfg.ValidateProduction()
|
||||
if err != nil {
|
||||
t.Errorf("Expected valid production config to pass validation, got error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateProduction_MissingJWTSecret tests JWT secret validation
|
||||
func TestValidateProduction_MissingJWTSecret(t *testing.T) {
|
||||
cfg := &Config{
|
||||
App: AppConfig{
|
||||
Environment: "production",
|
||||
},
|
||||
JWT: JWTConfig{
|
||||
Secret: "", // Missing
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
Hosts: []string{"localhost:9042"},
|
||||
Keyspace: "test",
|
||||
},
|
||||
S3: S3Config{
|
||||
AccessKey: "test",
|
||||
SecretKey: "test",
|
||||
BucketName: "test",
|
||||
Endpoint: "http://localhost:9000",
|
||||
},
|
||||
Mailgun: MailgunConfig{
|
||||
APIKey: "test",
|
||||
Domain: "test.com",
|
||||
SenderEmail: "test@test.com",
|
||||
},
|
||||
Cache: CacheConfig{
|
||||
Host: "localhost",
|
||||
},
|
||||
}
|
||||
|
||||
err := cfg.ValidateProduction()
|
||||
if err == nil {
|
||||
t.Error("Expected error for missing JWT_SECRET in production")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "JWT_SECRET is required") {
|
||||
t.Errorf("Expected JWT_SECRET error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateProduction_ShortJWTSecret tests JWT secret length validation
|
||||
func TestValidateProduction_ShortJWTSecret(t *testing.T) {
|
||||
cfg := &Config{
|
||||
App: AppConfig{
|
||||
Environment: "production",
|
||||
},
|
||||
JWT: JWTConfig{
|
||||
Secret: "short", // Too short (less than 32 chars)
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
Hosts: []string{"localhost:9042"},
|
||||
Keyspace: "test",
|
||||
},
|
||||
S3: S3Config{
|
||||
AccessKey: "test",
|
||||
SecretKey: "test",
|
||||
BucketName: "test",
|
||||
Endpoint: "http://localhost:9000",
|
||||
},
|
||||
Mailgun: MailgunConfig{
|
||||
APIKey: "test",
|
||||
Domain: "test.com",
|
||||
SenderEmail: "test@test.com",
|
||||
},
|
||||
Cache: CacheConfig{
|
||||
Host: "localhost",
|
||||
},
|
||||
}
|
||||
|
||||
err := cfg.ValidateProduction()
|
||||
if err == nil {
|
||||
t.Error("Expected error for short JWT_SECRET in production")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "at least 32 characters") {
|
||||
t.Errorf("Expected JWT_SECRET length error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateProduction_MissingS3Credentials tests S3 credential validation
|
||||
func TestValidateProduction_MissingS3Credentials(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
accessKey string
|
||||
secretKey string
|
||||
wantError string
|
||||
}{
|
||||
{
|
||||
name: "missing access key",
|
||||
accessKey: "",
|
||||
secretKey: "valid-secret",
|
||||
wantError: "S3_ACCESS_KEY is required",
|
||||
},
|
||||
{
|
||||
name: "missing secret key",
|
||||
accessKey: "valid-access",
|
||||
secretKey: "",
|
||||
wantError: "S3_SECRET_KEY is required",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
cfg := &Config{
|
||||
App: AppConfig{
|
||||
Environment: "production",
|
||||
},
|
||||
JWT: JWTConfig{
|
||||
Secret: "this-is-a-very-secure-secret-key-with-more-than-32-characters",
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
Hosts: []string{"localhost:9042"},
|
||||
Keyspace: "test",
|
||||
},
|
||||
S3: S3Config{
|
||||
AccessKey: tt.accessKey,
|
||||
SecretKey: tt.secretKey,
|
||||
BucketName: "test",
|
||||
Endpoint: "http://localhost:9000",
|
||||
},
|
||||
Mailgun: MailgunConfig{
|
||||
APIKey: "test",
|
||||
Domain: "test.com",
|
||||
SenderEmail: "test@test.com",
|
||||
},
|
||||
Cache: CacheConfig{
|
||||
Host: "localhost",
|
||||
},
|
||||
}
|
||||
|
||||
err := cfg.ValidateProduction()
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for %s in production", tt.name)
|
||||
}
|
||||
if !strings.Contains(err.Error(), tt.wantError) {
|
||||
t.Errorf("Expected error containing '%s', got: %v", tt.wantError, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateProduction_MissingMailgunCredentials tests email service validation
|
||||
func TestValidateProduction_MissingMailgunCredentials(t *testing.T) {
|
||||
cfg := &Config{
|
||||
App: AppConfig{
|
||||
Environment: "production",
|
||||
},
|
||||
JWT: JWTConfig{
|
||||
Secret: "this-is-a-very-secure-secret-key-with-more-than-32-characters",
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
Hosts: []string{"localhost:9042"},
|
||||
Keyspace: "test",
|
||||
},
|
||||
S3: S3Config{
|
||||
AccessKey: "test",
|
||||
SecretKey: "test",
|
||||
BucketName: "test",
|
||||
Endpoint: "http://localhost:9000",
|
||||
},
|
||||
Mailgun: MailgunConfig{
|
||||
APIKey: "", // Missing
|
||||
Domain: "test.com",
|
||||
SenderEmail: "test@test.com",
|
||||
},
|
||||
Cache: CacheConfig{
|
||||
Host: "localhost",
|
||||
},
|
||||
}
|
||||
|
||||
err := cfg.ValidateProduction()
|
||||
if err == nil {
|
||||
t.Error("Expected error for missing MAILGUN_API_KEY in production")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "MAILGUN_API_KEY is required") {
|
||||
t.Errorf("Expected MAILGUN_API_KEY error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateProduction_MissingDatabaseConfig tests database configuration validation
|
||||
func TestValidateProduction_MissingDatabaseConfig(t *testing.T) {
|
||||
cfg := &Config{
|
||||
App: AppConfig{
|
||||
Environment: "production",
|
||||
},
|
||||
JWT: JWTConfig{
|
||||
Secret: "this-is-a-very-secure-secret-key-with-more-than-32-characters",
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
Hosts: []string{}, // Missing
|
||||
Keyspace: "", // Missing
|
||||
},
|
||||
S3: S3Config{
|
||||
AccessKey: "test",
|
||||
SecretKey: "test",
|
||||
BucketName: "test",
|
||||
Endpoint: "http://localhost:9000",
|
||||
},
|
||||
Mailgun: MailgunConfig{
|
||||
APIKey: "test",
|
||||
Domain: "test.com",
|
||||
SenderEmail: "test@test.com",
|
||||
},
|
||||
Cache: CacheConfig{
|
||||
Host: "localhost",
|
||||
},
|
||||
}
|
||||
|
||||
err := cfg.ValidateProduction()
|
||||
if err == nil {
|
||||
t.Error("Expected error for missing database configuration in production")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "DATABASE_HOSTS is required") {
|
||||
t.Errorf("Expected DATABASE_HOSTS error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateProduction_UnsafeOrigins tests CORS wildcard detection
|
||||
func TestValidateProduction_UnsafeOrigins(t *testing.T) {
|
||||
cfg := &Config{
|
||||
App: AppConfig{
|
||||
Environment: "production",
|
||||
},
|
||||
JWT: JWTConfig{
|
||||
Secret: "this-is-a-very-secure-secret-key-with-more-than-32-characters",
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
Hosts: []string{"localhost:9042"},
|
||||
Keyspace: "test",
|
||||
},
|
||||
S3: S3Config{
|
||||
AccessKey: "test",
|
||||
SecretKey: "test",
|
||||
BucketName: "test",
|
||||
Endpoint: "http://localhost:9000",
|
||||
},
|
||||
Mailgun: MailgunConfig{
|
||||
APIKey: "test",
|
||||
Domain: "test.com",
|
||||
SenderEmail: "test@test.com",
|
||||
},
|
||||
Cache: CacheConfig{
|
||||
Host: "localhost",
|
||||
},
|
||||
Security: SecurityConfig{
|
||||
AllowedOrigins: []string{"*"}, // Unsafe wildcard
|
||||
},
|
||||
}
|
||||
|
||||
err := cfg.ValidateProduction()
|
||||
if err == nil {
|
||||
t.Error("Expected error for wildcard CORS origin in production")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "SECURITY_ALLOWED_ORIGINS='*'") {
|
||||
t.Errorf("Expected CORS wildcard warning, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateProduction_MultipleErrors tests that all validation errors are collected
|
||||
func TestValidateProduction_MultipleErrors(t *testing.T) {
|
||||
cfg := &Config{
|
||||
App: AppConfig{
|
||||
Environment: "production",
|
||||
},
|
||||
JWT: JWTConfig{
|
||||
Secret: "", // Missing
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
Hosts: []string{}, // Missing
|
||||
Keyspace: "", // Missing
|
||||
},
|
||||
S3: S3Config{
|
||||
AccessKey: "", // Missing
|
||||
SecretKey: "", // Missing
|
||||
BucketName: "",
|
||||
Endpoint: "",
|
||||
},
|
||||
Mailgun: MailgunConfig{
|
||||
APIKey: "", // Missing
|
||||
Domain: "",
|
||||
SenderEmail: "",
|
||||
},
|
||||
Cache: CacheConfig{
|
||||
Host: "",
|
||||
},
|
||||
}
|
||||
|
||||
err := cfg.ValidateProduction()
|
||||
if err == nil {
|
||||
t.Fatal("Expected multiple validation errors")
|
||||
}
|
||||
|
||||
errorMsg := err.Error()
|
||||
expectedErrors := []string{
|
||||
"JWT_SECRET is required",
|
||||
"DATABASE_HOSTS is required",
|
||||
"DATABASE_KEYSPACE is required",
|
||||
"S3_ACCESS_KEY is required",
|
||||
"S3_SECRET_KEY is required",
|
||||
"S3_BUCKET is required",
|
||||
"S3_ENDPOINT is required",
|
||||
"MAILGUN_API_KEY is required",
|
||||
"MAILGUN_DOMAIN is required",
|
||||
"CACHE_HOST is required",
|
||||
}
|
||||
|
||||
for _, expected := range expectedErrors {
|
||||
if !strings.Contains(errorMsg, expected) {
|
||||
t.Errorf("Expected error message to contain '%s', got: %v", expected, errorMsg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidate_Development tests that development environments use basic validation
|
||||
func TestValidate_Development(t *testing.T) {
|
||||
cfg := &Config{
|
||||
App: AppConfig{
|
||||
Environment: "development",
|
||||
},
|
||||
JWT: JWTConfig{
|
||||
Secret: "dev-secret", // Short secret OK in development
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
Hosts: []string{"localhost:9042"},
|
||||
Keyspace: "maplefile_dev",
|
||||
},
|
||||
S3: S3Config{
|
||||
AccessKey: "", // OK in development
|
||||
SecretKey: "", // OK in development
|
||||
BucketName: "test",
|
||||
},
|
||||
}
|
||||
|
||||
// Should not fail with lenient development validation
|
||||
err := cfg.Validate()
|
||||
if err != nil {
|
||||
t.Errorf("Development environment should not require strict validation, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidate_ProductionCallsValidateProduction tests integration
|
||||
func TestValidate_ProductionCallsValidateProduction(t *testing.T) {
|
||||
cfg := &Config{
|
||||
App: AppConfig{
|
||||
Environment: "production",
|
||||
},
|
||||
JWT: JWTConfig{
|
||||
Secret: "", // This should trigger production validation
|
||||
},
|
||||
}
|
||||
|
||||
err := cfg.Validate()
|
||||
if err == nil {
|
||||
t.Error("Expected production Validate() to call ValidateProduction() and fail")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "JWT_SECRET is required") {
|
||||
t.Errorf("Expected ValidateProduction error, got: %v", err)
|
||||
}
|
||||
}
|
||||
6
cloud/maplefile-backend/config/constants/modules.go
Normal file
6
cloud/maplefile-backend/config/constants/modules.go
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
package constants
|
||||
|
||||
const (
|
||||
MonolithModuleMapleFile key = iota + 1 // Start numbering at 1
|
||||
MonolithModulePaperCloud
|
||||
)
|
||||
23
cloud/maplefile-backend/config/constants/session.go
Normal file
23
cloud/maplefile-backend/config/constants/session.go
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
package constants
|
||||
|
||||
type key int
|
||||
|
||||
const (
|
||||
SessionIsAuthorized key = iota
|
||||
SessionSkipAuthorization
|
||||
SessionID
|
||||
SessionIPAddress
|
||||
SessionProxies
|
||||
SessionUser
|
||||
SessionUserCompanyName
|
||||
SessionUserRole
|
||||
SessionUserID
|
||||
SessionUserTimezone
|
||||
SessionUserName
|
||||
SessionUserFirstName
|
||||
SessionUserLastName
|
||||
SessionUserStoreID
|
||||
SessionUserStoreName
|
||||
SessionUserStoreLevel
|
||||
SessionUserStoreTimezone
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue