fetch_ml/internal/auth/api_key.go
Jeremie Fraeys 803677be57 feat: implement Go backend with comprehensive API and internal packages
- Add API server with WebSocket support and REST endpoints
- Implement authentication system with API keys and permissions
- Add task queue system with Redis backend and error handling
- Include storage layer with database migrations and schemas
- Add comprehensive logging, metrics, and telemetry
- Implement security middleware and network utilities
- Add experiment management and container orchestration
- Include configuration management with smart defaults
2025-12-04 16:53:53 -05:00

258 lines
7 KiB
Go

package auth
import (
"context"
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"fmt"
"log"
"net/http"
"os"
"strings"
"time"
)
// User represents an authenticated user
type User struct {
Name string `json:"name"`
Admin bool `json:"admin"`
Roles []string `json:"roles"`
Permissions map[string]bool `json:"permissions"`
}
// APIKeyHash represents a SHA256 hash of an API key
type APIKeyHash string
// APIKeyEntry represents an API key configuration
type APIKeyEntry struct {
Hash APIKeyHash `json:"hash"`
Admin bool `json:"admin"`
Roles []string `json:"roles,omitempty"`
Permissions map[string]bool `json:"permissions,omitempty"`
}
// Username represents a user identifier
type Username string
// AuthConfig represents the authentication configuration
type AuthConfig struct {
Enabled bool `json:"enabled"`
APIKeys map[Username]APIKeyEntry `json:"api_keys"`
}
// AuthStore interface for different authentication backends
type AuthStore interface {
ValidateAPIKey(ctx context.Context, key string) (*User, error)
CreateAPIKey(ctx context.Context, userID string, keyHash string, admin bool, roles []string, permissions map[string]bool, expiresAt *time.Time) error
RevokeAPIKey(ctx context.Context, userID string) error
ListUsers(ctx context.Context) ([]UserInfo, error)
}
// contextKey is the type for context keys
type contextKey string
const userContextKey = contextKey("user")
// ValidateAPIKey validates an API key and returns user information
func (c *AuthConfig) ValidateAPIKey(key string) (*User, error) {
if !c.Enabled {
// Auth disabled - return default admin user for development
return &User{Name: "default", Admin: true}, nil
}
keyHash := HashAPIKey(key)
for username, entry := range c.APIKeys {
if string(entry.Hash) == keyHash {
// Build user with role and permission inheritance
user := &User{
Name: string(username),
Admin: entry.Admin,
Roles: entry.Roles,
Permissions: make(map[string]bool),
}
// Copy explicit permissions
for perm, value := range entry.Permissions {
user.Permissions[perm] = value
}
// Add role-based permissions
rolePerms := getRolePermissions(entry.Roles)
for perm, value := range rolePerms {
if _, exists := user.Permissions[perm]; !exists {
user.Permissions[perm] = value
}
}
// Admin gets all permissions
if entry.Admin {
user.Permissions["*"] = true
}
return user, nil
}
}
return nil, fmt.Errorf("invalid API key")
}
// AuthMiddleware creates HTTP middleware for API key authentication
func (c *AuthConfig) AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !c.Enabled {
if os.Getenv("FETCH_ML_ALLOW_INSECURE_AUTH") != "1" || os.Getenv("FETCH_ML_DEBUG") != "1" {
http.Error(w, "Unauthorized: Authentication disabled", http.StatusUnauthorized)
return
}
log.Println("WARNING: Insecure authentication bypass enabled: FETCH_ML_ALLOW_INSECURE_AUTH=1 and FETCH_ML_DEBUG=1; do NOT use this configuration in production.")
ctx := context.WithValue(r.Context(), userContextKey, &User{Name: "default", Admin: true})
next.ServeHTTP(w, r.WithContext(ctx))
return
}
// Only accept API key from header - no query parameters for security
apiKey := r.Header.Get("X-API-Key")
if apiKey == "" {
http.Error(w, "Unauthorized: Missing API key in X-API-Key header", http.StatusUnauthorized)
return
}
user, err := c.ValidateAPIKey(apiKey)
if err != nil {
http.Error(w, "Unauthorized: Invalid API key", http.StatusUnauthorized)
return
}
// Add user to context
ctx := context.WithValue(r.Context(), userContextKey, user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// GetUserFromContext retrieves user from request context
func GetUserFromContext(ctx context.Context) *User {
if user, ok := ctx.Value(userContextKey).(*User); ok {
return user
}
return nil
}
// RequireAdmin creates middleware that requires admin privileges
func RequireAdmin(next http.Handler) http.Handler {
return RequirePermission("system:admin")(next)
}
// RequirePermission creates middleware that requires specific permission
func RequirePermission(permission string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user := GetUserFromContext(r.Context())
if user == nil {
http.Error(w, "Unauthorized: No user context", http.StatusUnauthorized)
return
}
if !user.HasPermission(permission) {
http.Error(w, "Forbidden: Insufficient permissions", http.StatusForbidden)
return
}
next.ServeHTTP(w, r)
})
}
}
// HasPermission checks if user has a specific permission
func (u *User) HasPermission(permission string) bool {
// Wildcard permission grants all access
if u.Permissions["*"] {
return true
}
// Direct permission check
if u.Permissions[permission] {
return true
}
// Hierarchical permission check (e.g., "jobs:create" matches "jobs")
parts := strings.Split(permission, ":")
for i := 1; i < len(parts); i++ {
partial := strings.Join(parts[:i], ":")
if u.Permissions[partial] {
return true
}
}
return false
}
// HasRole checks if user has a specific role
func (u *User) HasRole(role string) bool {
for _, userRole := range u.Roles {
if userRole == role {
return true
}
}
return false
}
// getRolePermissions returns permissions for given roles
func getRolePermissions(roles []string) map[string]bool {
permissions := make(map[string]bool)
// Use YAML permission manager if available
if pm := GetGlobalPermissionManager(); pm != nil && pm.loaded {
for _, role := range roles {
rolePerms := pm.GetRolePermissions(role)
for perm, value := range rolePerms {
permissions[perm] = value
}
}
return permissions
}
// Fallback to inline permissions
rolePermissions := map[string]map[string]bool{
"admin": {"*": true},
"data_scientist": {
"jobs:create": true, "jobs:read": true, "jobs:update": true,
"data:read": true, "models:read": true,
},
"data_engineer": {
"data:create": true, "data:read": true, "data:update": true, "data:delete": true,
},
"viewer": {
"jobs:read": true, "data:read": true, "models:read": true, "metrics:read": true,
},
"operator": {
"jobs:read": true, "jobs:update": true, "metrics:read": true, "system:read": true,
},
}
for _, role := range roles {
if rolePerms, exists := rolePermissions[role]; exists {
for perm, value := range rolePerms {
permissions[perm] = value
}
}
}
return permissions
}
// GenerateAPIKey generates a new random API key
func GenerateAPIKey() string {
buf := make([]byte, 32)
if _, err := rand.Read(buf); err != nil {
return fmt.Sprintf("%x", sha256.Sum256([]byte(time.Now().String())))
}
return hex.EncodeToString(buf)
}
// HashAPIKey creates a SHA256 hash of an API key
func HashAPIKey(key string) string {
hash := sha256.Sum256([]byte(key))
return hex.EncodeToString(hash[:])
}