fetch_ml/internal/auth/validator.go
Jeremie Fraeys ea15af1833 Fix multi-user authentication and clean up debug code
- Fix YAML tags in auth config struct (json -> yaml)
- Update CLI configs to use pre-hashed API keys
- Remove double hashing in WebSocket client
- Fix port mapping (9102 -> 9103) in CLI commands
- Update permission keys to use jobs:read, jobs:create, etc.
- Clean up all debug logging from CLI and server
- All user roles now authenticate correctly:
  * Admin: Can queue jobs and see all jobs
  * Researcher: Can queue jobs and see own jobs
  * Analyst: Can see status (read-only access)

Multi-user authentication is now fully functional.
2025-12-06 12:35:32 -05:00

100 lines
2.5 KiB
Go

package auth
import (
"fmt"
"log"
"os"
"strings"
)
// ValidateAuthConfig enforces authentication requirements
func (c *Config) ValidateAuthConfig() error {
// Check if we're in production environment
isProduction := os.Getenv("FETCH_ML_ENV") == "prod"
if isProduction {
if !c.Enabled {
return fmt.Errorf("authentication must be enabled in production environment")
}
if len(c.APIKeys) == 0 {
return fmt.Errorf("at least one API key must be configured in production")
}
// Ensure at least one admin user exists
hasAdmin := false
for _, entry := range c.APIKeys {
if entry.Admin {
hasAdmin = true
break
}
}
if !hasAdmin {
return fmt.Errorf("at least one admin user must be configured in production")
}
// Check for insecure development override
if os.Getenv("FETCH_ML_ALLOW_INSECURE_AUTH") == "1" {
log.Printf("WARNING: FETCH_ML_ALLOW_INSECURE_AUTH is enabled in production - this is insecure")
}
}
// Validate API key format
for username, entry := range c.APIKeys {
if string(username) == "" {
return fmt.Errorf("empty username not allowed")
}
if entry.Hash == "" {
return fmt.Errorf("user %s has empty API key hash", username)
}
// Validate hash format (should be 64 hex chars for SHA256)
if len(entry.Hash) != 64 {
return fmt.Errorf("user %s has invalid API key hash format", username)
}
// Check hash contains only hex characters
for _, char := range entry.Hash {
if char < '0' || (char > '9' && char < 'a') || (char > 'f' && char < 'A') || char > 'F' {
return fmt.Errorf("user %s has invalid API key hash characters", username)
}
}
}
return nil
}
// CheckConfigFilePermissions ensures config files have secure permissions
func CheckConfigFilePermissions(configPath string) error {
info, err := os.Stat(configPath)
if err != nil {
return fmt.Errorf("cannot stat config file: %w", err)
}
// Check file permissions (should be 600)
perm := info.Mode().Perm()
if perm&0077 != 0 {
return fmt.Errorf("config file %s has insecure permissions: %o (should be 600)", configPath, perm)
}
return nil
}
// SanitizeConfig removes sensitive information for logging
func (c *Config) SanitizeConfig() map[string]interface{} {
sanitized := map[string]interface{}{
"enabled": c.Enabled,
"users": make(map[string]interface{}),
}
for username := range c.APIKeys {
sanitized["users"].(map[string]interface{})[string(username)] = map[string]interface{}{
"admin": c.APIKeys[username].Admin,
"hash": strings.Repeat("*", 8) + "...", // Show only prefix
}
}
return sanitized
}