Add comprehensive authentication and authorization enhancements: - tokens.go: New token management system for public task access and cloning * SHA-256 hashed token storage for security * Token generation, validation, and automatic cleanup * Support for public access and clone permissions - api_key.go: Extend User struct with Groups field * Lab group membership (ml-lab, nlp-group) * Integration with permission system for group-based access - flags.go: Security hardening - migrate to structured logging * Replace log.Printf with log/slog to prevent log injection attacks * Consistent structured output for all auth warnings * Safe handling of file paths and errors in logs - permissions.go: Add task sharing permission constants * PermissionTasksReadOwn: Access own tasks * PermissionTasksReadLab: Access lab group tasks * PermissionTasksReadAll: Admin/institution-wide access * PermissionTasksShare: Grant access to other users * PermissionTasksClone: Create copies of shared tasks * CanAccessTask() method with visibility checks - database.go: Improve error handling * Add structured error logging on row close failures
64 lines
2.1 KiB
Go
64 lines
2.1 KiB
Go
package auth
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/jfraeys/fetch_ml/internal/storage"
|
|
)
|
|
|
|
// ShareTokenOptions provides options for creating share tokens
|
|
type ShareTokenOptions struct {
|
|
ExpiresAt *time.Time // nil = never expires
|
|
MaxAccesses *int // nil = unlimited
|
|
}
|
|
|
|
// GenerateShareToken creates a new cryptographically random share token.
|
|
// Returns the token string that can be shared with others.
|
|
func GenerateShareToken(db *storage.DB, taskID, experimentID *string, createdBy string, opts ShareTokenOptions) (string, error) {
|
|
// Generate 32 bytes of random data
|
|
raw := make([]byte, 32)
|
|
if _, err := rand.Read(raw); err != nil {
|
|
return "", fmt.Errorf("failed to generate random token: %w", err)
|
|
}
|
|
|
|
// Encode as base64url (URL-safe, no padding)
|
|
token := base64.RawURLEncoding.EncodeToString(raw)
|
|
|
|
// Store in database
|
|
if err := db.CreateShareToken(token, taskID, experimentID, createdBy, opts.ExpiresAt, opts.MaxAccesses); err != nil {
|
|
return "", fmt.Errorf("failed to store share token: %w", err)
|
|
}
|
|
|
|
return token, nil
|
|
}
|
|
|
|
// CanAccessWithToken checks if a token grants access to a task or experiment.
|
|
// This is called for unauthenticated access via signed share links.
|
|
// Returns true if the token is valid, not expired, and within access limits.
|
|
func CanAccessWithToken(db *storage.DB, token string, taskID, experimentID *string) bool {
|
|
t, err := db.ValidateShareToken(token, taskID, experimentID)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if t == nil {
|
|
return false
|
|
}
|
|
|
|
// Increment access count
|
|
if err := db.IncrementTokenAccessCount(token); err != nil {
|
|
// Log error but don't fail the request - access was already validated
|
|
// This is a best-effort operation for tracking purposes
|
|
fmt.Printf("WARNING: failed to increment token access count: %v\n", err)
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// BuildShareLink constructs a shareable URL from a token.
|
|
// The path should be the base API path (e.g., "/api/tasks/123" or "/api/experiments/456").
|
|
func BuildShareLink(baseURL, path, token string) string {
|
|
return fmt.Sprintf("%s%s?token=%s", baseURL, path, token)
|
|
}
|