- 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
258 lines
7 KiB
Go
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[:])
|
|
}
|