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,52 @@
package gateway
import (
"context"
"fmt"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/password"
)
// CheckPasswordBreachUseCase checks if a password has been compromised in data breaches
// CWE-521: Password breach checking to prevent use of compromised passwords
type CheckPasswordBreachUseCase struct {
breachChecker password.BreachChecker
logger *zap.Logger
}
// ProvideCheckPasswordBreachUseCase creates a new CheckPasswordBreachUseCase
func ProvideCheckPasswordBreachUseCase(
breachChecker password.BreachChecker,
logger *zap.Logger,
) *CheckPasswordBreachUseCase {
return &CheckPasswordBreachUseCase{
breachChecker: breachChecker,
logger: logger.Named("check-password-breach-usecase"),
}
}
// Execute checks if a password has been found in known data breaches
// Returns an error if the password has been breached
func (uc *CheckPasswordBreachUseCase) Execute(ctx context.Context, passwordStr string) error {
uc.logger.Debug("checking password against breach database")
breachCount, err := uc.breachChecker.CheckPassword(ctx, passwordStr)
if err != nil {
// Log error but don't fail registration/login if breach check fails
// This is a defense-in-depth measure, not a critical security control
uc.logger.Warn("failed to check password breach status (non-fatal)",
zap.Error(err))
return nil // Don't block user if service is down
}
if breachCount > 0 {
uc.logger.Warn("password found in data breaches",
zap.Int("breach_count", breachCount))
return fmt.Errorf("password has been found in %d data breaches and cannot be used", breachCount)
}
uc.logger.Debug("password not found in breaches")
return nil
}

View file

@ -0,0 +1,79 @@
package gateway
import (
"context"
"crypto/rand"
"math/big"
"time"
"go.uber.org/zap"
domaintenant "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/tenant"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/logger"
)
// CheckTenantSlugAvailabilityUseCase checks if a tenant slug is available
type CheckTenantSlugAvailabilityUseCase struct {
tenantRepo domaintenant.Repository
logger *zap.Logger
}
// ProvideCheckTenantSlugAvailabilityUseCase creates a new CheckTenantSlugAvailabilityUseCase
func ProvideCheckTenantSlugAvailabilityUseCase(
tenantRepo domaintenant.Repository,
logger *zap.Logger,
) *CheckTenantSlugAvailabilityUseCase {
return &CheckTenantSlugAvailabilityUseCase{
tenantRepo: tenantRepo,
logger: logger.Named("check-tenant-slug-availability-usecase"),
}
}
// Execute checks if a tenant slug is available (not already taken)
// CWE-203: Implements timing attack mitigation to prevent tenant enumeration
func (uc *CheckTenantSlugAvailabilityUseCase) Execute(ctx context.Context, slug string) error {
// Record start time for timing attack mitigation
startTime := time.Now()
// Always perform the database lookup
existingTenant, err := uc.tenantRepo.GetBySlug(ctx, slug)
// Store the result but don't return early - prevents timing leaks
var resultError error
if err == nil && existingTenant != nil {
// CWE-532: Use redacted tenant slug for logging
uc.logger.Warn("tenant slug already exists",
logger.TenantSlugHash(slug),
logger.SafeTenantSlug("tenant_slug_redacted", slug))
resultError = domaintenant.ErrTenantExists
} else if err != nil && err != domaintenant.ErrTenantNotFound {
// Real database error (not "not found")
uc.logger.Error("failed to check tenant existence", zap.Error(err))
resultError = err
} else {
// CWE-532: Use redacted tenant slug for logging
// Slug is available (err == ErrTenantNotFound or no error with nil tenant)
uc.logger.Debug("tenant slug is available",
logger.TenantSlugHash(slug),
logger.SafeTenantSlug("tenant_slug_redacted", slug))
resultError = nil
}
// CWE-203: Add random delay to prevent timing attacks
// Ensures response time is similar whether tenant exists or not
elapsed := time.Since(startTime)
minResponseTime := 50 * time.Millisecond // Minimum response time
maxJitter := 30 * time.Millisecond // Random jitter to add unpredictability
// Generate cryptographically secure random jitter
jitterMs, _ := rand.Int(rand.Reader, big.NewInt(int64(maxJitter.Milliseconds())))
jitter := time.Duration(jitterMs.Int64()) * time.Millisecond
targetDelay := minResponseTime + jitter
if elapsed < targetDelay {
time.Sleep(targetDelay - elapsed)
}
return resultError
}

View file

@ -0,0 +1,53 @@
package gateway
import (
"context"
"errors"
"fmt"
"go.uber.org/zap"
domainuser "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/user"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/logger"
)
// GetUserByEmailUseCase retrieves a user by email for authentication
type GetUserByEmailUseCase struct {
userRepo domainuser.Repository
logger *zap.Logger
}
// ProvideGetUserByEmailUseCase creates a new GetUserByEmailUseCase
func ProvideGetUserByEmailUseCase(
userRepo domainuser.Repository,
logger *zap.Logger,
) *GetUserByEmailUseCase {
return &GetUserByEmailUseCase{
userRepo: userRepo,
logger: logger.Named("get-user-by-email-usecase"),
}
}
// Execute retrieves a user by email globally (across all tenants)
// Returns ErrInvalidCredentials instead of ErrUserNotFound for security (timing attack prevention)
func (uc *GetUserByEmailUseCase) Execute(ctx context.Context, email string) (*domainuser.User, error) {
user, err := uc.userRepo.GetByEmailGlobal(ctx, email)
if err != nil {
if errors.Is(err, domainuser.ErrUserNotFound) {
// CWE-532: Use hashed email to prevent PII in logs
uc.logger.Warn("user not found for login",
logger.EmailHash(email))
// Return generic error to prevent email enumeration
return nil, ErrInvalidCredentials
}
uc.logger.Error("failed to get user by email", zap.Error(err))
return nil, fmt.Errorf("failed to get user: %w", err)
}
// CWE-532: Use hashed email to prevent PII in logs
uc.logger.Debug("user found for login",
zap.String("user_id", user.ID),
logger.EmailHash(email))
return user, nil
}

View file

@ -0,0 +1,54 @@
package gateway
import (
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/password"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/securestring"
)
// HashPasswordUseCase handles password validation and hashing
type HashPasswordUseCase struct {
passwordProvider password.PasswordProvider
passwordValidator password.PasswordValidator
logger *zap.Logger
}
// ProvideHashPasswordUseCase creates a new HashPasswordUseCase
func ProvideHashPasswordUseCase(
passwordProvider password.PasswordProvider,
passwordValidator password.PasswordValidator,
logger *zap.Logger,
) *HashPasswordUseCase {
return &HashPasswordUseCase{
passwordProvider: passwordProvider,
passwordValidator: passwordValidator,
logger: logger.Named("hash-password-usecase"),
}
}
// Execute validates password strength and returns the hashed password
func (uc *HashPasswordUseCase) Execute(plainPassword string) (string, error) {
// Validate password strength
if err := uc.passwordValidator.ValidatePasswordStrength(plainPassword); err != nil {
uc.logger.Warn("password validation failed", zap.Error(err))
return "", err
}
// Hash the password using secure string
securePassword, err := securestring.NewSecureString(plainPassword)
if err != nil {
uc.logger.Error("failed to create secure string", zap.Error(err))
return "", err
}
defer securePassword.Wipe() // Clean up password from memory
passwordHash, err := uc.passwordProvider.GenerateHashFromPassword(securePassword)
if err != nil {
uc.logger.Error("failed to hash password", zap.Error(err))
return "", err
}
uc.logger.Debug("password hashed successfully")
return passwordHash, nil
}

View file

@ -0,0 +1,153 @@
package gateway
import (
"context"
"errors"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/logger"
)
var (
ErrInvalidCredentials = errors.New("invalid email or password")
)
// LoginInput represents the input for user login
type LoginInput struct {
Email string
Password string
}
// LoginOutput represents the output after successful login
type LoginOutput struct {
UserID string
UserEmail string
UserName string
UserRole string
TenantID string
}
// LoginUseCase handles user authentication
// Orchestrates the login workflow by coordinating focused usecases
type LoginUseCase struct {
// Focused usecases
getUserByEmailUC *GetUserByEmailUseCase
verifyPasswordUC *VerifyPasswordUseCase
logger *zap.Logger
}
// NewLoginUseCase creates a new login use case
func NewLoginUseCase(
getUserByEmailUC *GetUserByEmailUseCase,
verifyPasswordUC *VerifyPasswordUseCase,
logger *zap.Logger,
) *LoginUseCase {
return &LoginUseCase{
getUserByEmailUC: getUserByEmailUC,
verifyPasswordUC: verifyPasswordUC,
logger: logger.Named("login-usecase"),
}
}
// ProvideLoginUseCase creates a new LoginUseCase for dependency injection
func ProvideLoginUseCase(
getUserByEmailUC *GetUserByEmailUseCase,
verifyPasswordUC *VerifyPasswordUseCase,
logger *zap.Logger,
) *LoginUseCase {
return NewLoginUseCase(getUserByEmailUC, verifyPasswordUC, logger)
}
// Execute orchestrates the login workflow using focused usecases
// CWE-208: Observable Timing Discrepancy - Uses timing-safe authentication
func (uc *LoginUseCase) Execute(ctx context.Context, input *LoginInput) (*LoginOutput, error) {
// CWE-532: Use hashed email to prevent PII in logs
uc.logger.Info("authenticating user",
logger.EmailHash(input.Email))
// Step 1: Get user by email globally (no tenant_id required for login)
// Note: This returns ErrInvalidCredentials (not ErrUserNotFound) for security
user, err := uc.getUserByEmailUC.Execute(ctx, input.Email)
// CWE-208: TIMING ATTACK MITIGATION
// We must ALWAYS verify the password, even if the user doesn't exist.
// This prevents timing-based user enumeration attacks.
//
// Timing attack scenario without mitigation:
// - If user exists: database lookup (~10ms) + Argon2 hashing (~100ms) = ~110ms
// - If user doesn't exist: database lookup (~10ms) = ~10ms
// Attacker can measure response time to enumerate valid email addresses.
//
// With mitigation:
// - If user exists: database lookup + Argon2 hashing
// - If user doesn't exist: database lookup + Argon2 dummy hashing
// Both paths take approximately the same time (~110ms).
var passwordHash string
userExists := (err == nil && user != nil)
if userExists {
// User exists - use real password hash
if user.SecurityData != nil {
passwordHash = user.SecurityData.PasswordHash
}
}
// If user doesn't exist, passwordHash remains empty string
// The verifyPasswordUC will use dummy hash for timing safety
// Step 2: Verify password - ALWAYS executed regardless of user existence
if err := uc.verifyPasswordUC.ExecuteTimingSafe(input.Password, passwordHash, userExists); err != nil {
// CWE-532: Use hashed email to prevent PII in logs
if userExists {
uc.logger.Warn("login failed: password verification failed",
logger.EmailHash(input.Email),
zap.String("tenant_id", user.TenantID))
} else {
uc.logger.Warn("login failed: user not found",
logger.EmailHash(input.Email))
}
// Always return the same generic error regardless of reason
return nil, ErrInvalidCredentials
}
// Now check if user lookup failed (after timing-safe password verification)
if err != nil {
// This should never happen because ExecuteTimingSafe should have failed
// But keep for safety
uc.logger.Error("unexpected error after password verification", zap.Error(err))
return nil, ErrInvalidCredentials
}
// CWE-532: Use hashed email to prevent PII in logs
uc.logger.Info("user authenticated successfully",
zap.String("user_id", user.ID),
logger.EmailHash(user.Email),
zap.String("tenant_id", user.TenantID))
// Convert role to string (1="executive", 2="manager", 3="staff")
roleStr := getRoleString(user.Role)
// Step 3: Build output
return &LoginOutput{
UserID: user.ID,
UserEmail: user.Email,
UserName: user.Name,
UserRole: roleStr,
TenantID: user.TenantID,
}, nil
}
// getRoleString converts numeric role to string representation
func getRoleString(role int) string {
switch role {
case 1:
return "executive"
case 2:
return "manager"
case 3:
return "staff"
default:
return "unknown"
}
}

View file

@ -0,0 +1,92 @@
package gateway
import (
"go.uber.org/zap"
domaintenant "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/tenant"
domainuser "codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/internal/domain/user"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/logger"
)
// RegisterInput represents the input for user registration validation
type RegisterInput struct {
Email string
Password string
FirstName string
LastName string
TenantName string
TenantSlug string
Timezone string
// Consent fields
AgreeTermsOfService bool
AgreePromotions bool
AgreeToTrackingAcrossThirdPartyAppsAndServices bool
// Optional: IP address for audit trail
CreatedFromIPAddress string
}
// ValidateRegistrationInputUseCase validates registration input
type ValidateRegistrationInputUseCase struct {
logger *zap.Logger
}
// ProvideValidateRegistrationInputUseCase creates a new ValidateRegistrationInputUseCase
func ProvideValidateRegistrationInputUseCase(logger *zap.Logger) *ValidateRegistrationInputUseCase {
return &ValidateRegistrationInputUseCase{
logger: logger.Named("validate-registration-input-usecase"),
}
}
// Execute validates the registration input fields
func (uc *ValidateRegistrationInputUseCase) Execute(input *RegisterInput) error {
if input.Email == "" {
uc.logger.Warn("email is required")
return domainuser.ErrEmailRequired
}
if input.Password == "" {
uc.logger.Warn("password is required")
return domainuser.ErrPasswordRequired
}
if input.FirstName == "" {
uc.logger.Warn("first name is required")
return domainuser.ErrFirstNameRequired
}
if input.LastName == "" {
uc.logger.Warn("last name is required")
return domainuser.ErrLastNameRequired
}
if input.TenantName == "" {
uc.logger.Warn("tenant name is required")
return domaintenant.ErrNameRequired
}
if input.TenantSlug == "" {
uc.logger.Warn("tenant slug is required")
return domaintenant.ErrSlugRequired
}
// Validate Terms of Service agreement (REQUIRED)
if !input.AgreeTermsOfService {
uc.logger.Warn("terms of service agreement is required")
return domainuser.ErrTermsOfServiceRequired
}
// Note: AgreePromotions and AgreeToTrackingAcrossThirdPartyAppsAndServices
// are optional (defaults to false if not provided)
// CWE-532: Use hashed/redacted fields to prevent PII in logs
uc.logger.Debug("registration input validated successfully",
logger.EmailHash(input.Email),
logger.TenantSlugHash(input.TenantSlug),
zap.Bool("agree_terms", input.AgreeTermsOfService),
zap.Bool("agree_promotions", input.AgreePromotions),
zap.Bool("agree_tracking", input.AgreeToTrackingAcrossThirdPartyAppsAndServices))
return nil
}

View file

@ -0,0 +1,105 @@
package gateway
import (
"fmt"
"go.uber.org/zap"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/password"
"codeberg.org/mapleopentech/monorepo/cloud/maplepress-backend/pkg/security/securestring"
)
// VerifyPasswordUseCase verifies a password against a hash
type VerifyPasswordUseCase struct {
passwordProvider password.PasswordProvider
logger *zap.Logger
}
// ProvideVerifyPasswordUseCase creates a new VerifyPasswordUseCase
func ProvideVerifyPasswordUseCase(
passwordProvider password.PasswordProvider,
logger *zap.Logger,
) *VerifyPasswordUseCase {
return &VerifyPasswordUseCase{
passwordProvider: passwordProvider,
logger: logger.Named("verify-password-usecase"),
}
}
// Execute verifies a plain password against a hashed password
// Returns ErrInvalidCredentials if password doesn't match (for security)
func (uc *VerifyPasswordUseCase) Execute(plainPassword, passwordHash string) error {
// Create secure string from password
securePassword, err := securestring.NewSecureString(plainPassword)
if err != nil {
uc.logger.Error("failed to create secure password", zap.Error(err))
return fmt.Errorf("failed to process password: %w", err)
}
defer securePassword.Wipe() // Clean up password from memory
// Verify password
match, err := uc.passwordProvider.ComparePasswordAndHash(securePassword, passwordHash)
if err != nil {
uc.logger.Error("failed to compare password and hash", zap.Error(err))
return fmt.Errorf("failed to verify password: %w", err)
}
if !match {
uc.logger.Debug("password verification failed")
return ErrInvalidCredentials
}
uc.logger.Debug("password verified successfully")
return nil
}
// ExecuteTimingSafe verifies a password in a timing-safe manner
// CWE-208: Observable Timing Discrepancy - Prevents user enumeration via timing attacks
//
// This method ALWAYS performs password hashing, even when the user doesn't exist,
// to ensure constant-time behavior regardless of whether the email exists in the system.
//
// Parameters:
// - plainPassword: The password to verify
// - passwordHash: The hash to compare against (empty string if user doesn't exist)
// - userExists: Whether the user exists in the system
//
// Returns ErrInvalidCredentials if verification fails for any reason
func (uc *VerifyPasswordUseCase) ExecuteTimingSafe(plainPassword, passwordHash string, userExists bool) error {
// Create secure string from password
securePassword, err := securestring.NewSecureString(plainPassword)
if err != nil {
uc.logger.Error("failed to create secure password", zap.Error(err))
return fmt.Errorf("failed to process password: %w", err)
}
defer securePassword.Wipe() // Clean up password from memory
if !userExists || passwordHash == "" {
// User doesn't exist or no password hash available
// Perform dummy password hashing to maintain constant time
uc.logger.Debug("performing timing-safe dummy password verification")
match, err := uc.passwordProvider.ComparePasswordAndHash(securePassword, password.DummyPasswordHash)
if err != nil {
// Even if dummy verification fails, we don't care about the error
// The important part is that we spent the same amount of time
uc.logger.Debug("dummy password verification completed", zap.Error(err))
}
_ = match // Explicitly ignore the result
return ErrInvalidCredentials
}
// User exists - perform real password verification
match, err := uc.passwordProvider.ComparePasswordAndHash(securePassword, passwordHash)
if err != nil {
uc.logger.Error("failed to compare password and hash", zap.Error(err))
return ErrInvalidCredentials
}
if !match {
uc.logger.Debug("password verification failed")
return ErrInvalidCredentials
}
uc.logger.Debug("password verified successfully")
return nil
}