1a69fcfd04
- Updated the installation script to clarify that production wrappers do not include fixed test secrets, improving user guidance for local testing. - Modified the jiggablend-manager and jiggablend-runner scripts to remove fixed test configurations, emphasizing the use of local test credentials via `make init-test`. - Enhanced error handling in the runner script to ensure that an API key is provided, improving security and user feedback. - Added new flags and options for the runner, including sandboxing capabilities and GPU ray tracing control, enhancing flexibility for users. - Improved README documentation to reflect changes in usage and configuration, ensuring users have clear instructions for setup and execution.
248 lines
7.0 KiB
Go
248 lines
7.0 KiB
Go
package auth
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"crypto/subtle"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"jiggablend/internal/config"
|
|
"jiggablend/internal/database"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Secrets handles API key management
|
|
type Secrets struct {
|
|
db *database.DB
|
|
cfg *config.Config
|
|
RegistrationMu sync.Mutex // Protects concurrent runner registrations
|
|
}
|
|
|
|
// NewSecrets creates a new secrets manager
|
|
func NewSecrets(db *database.DB, cfg *config.Config) (*Secrets, error) {
|
|
return &Secrets{db: db, cfg: cfg}, nil
|
|
}
|
|
|
|
// APIKeyInfo represents information about an API key
|
|
type APIKeyInfo struct {
|
|
ID int64 `json:"id"`
|
|
Key string `json:"key"`
|
|
Name string `json:"name"`
|
|
Description *string `json:"description,omitempty"`
|
|
Scope string `json:"scope"` // 'manager' or 'user'
|
|
IsActive bool `json:"is_active"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
CreatedBy int64 `json:"created_by"`
|
|
}
|
|
|
|
// GenerateRunnerAPIKey generates a new API key for runners
|
|
func (s *Secrets) GenerateRunnerAPIKey(createdBy int64, name, description string, scope string) (*APIKeyInfo, error) {
|
|
// Generate API key in format: jk_r1_abc123def456...
|
|
key, err := s.generateAPIKey()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to generate API key: %w", err)
|
|
}
|
|
|
|
// Extract prefix (first 5 chars after "jk_") and hash the full key
|
|
parts := strings.Split(key, "_")
|
|
if len(parts) < 3 {
|
|
return nil, fmt.Errorf("invalid API key format generated")
|
|
}
|
|
keyPrefix := fmt.Sprintf("%s_%s", parts[0], parts[1])
|
|
|
|
keyHash := sha256.Sum256([]byte(key))
|
|
keyHashStr := hex.EncodeToString(keyHash[:])
|
|
|
|
var keyInfo APIKeyInfo
|
|
err = s.db.With(func(conn *sql.DB) error {
|
|
result, err := conn.Exec(
|
|
`INSERT INTO runner_api_keys (key_prefix, key_hash, name, description, scope, is_active, created_by)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
keyPrefix, keyHashStr, name, description, scope, true, createdBy,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to store API key: %w", err)
|
|
}
|
|
|
|
keyID, err := result.LastInsertId()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get inserted key ID: %w", err)
|
|
}
|
|
|
|
// Get the inserted key info
|
|
err = conn.QueryRow(
|
|
`SELECT id, name, description, scope, is_active, created_at, created_by
|
|
FROM runner_api_keys WHERE id = ?`,
|
|
keyID,
|
|
).Scan(&keyInfo.ID, &keyInfo.Name, &keyInfo.Description, &keyInfo.Scope, &keyInfo.IsActive, &keyInfo.CreatedAt, &keyInfo.CreatedBy)
|
|
|
|
return err
|
|
})
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create API key: %w", err)
|
|
}
|
|
|
|
keyInfo.Key = key
|
|
return &keyInfo, nil
|
|
}
|
|
|
|
// generateAPIKey generates a new API key in format jk_r1_abc123def456...
|
|
func (s *Secrets) generateAPIKey() (string, error) {
|
|
// Generate random suffix
|
|
randomBytes := make([]byte, 16)
|
|
if _, err := rand.Read(randomBytes); err != nil {
|
|
return "", fmt.Errorf("failed to generate random bytes: %w", err)
|
|
}
|
|
randomStr := hex.EncodeToString(randomBytes)
|
|
|
|
// Generate a unique prefix (jk_r followed by 1 random digit)
|
|
prefixDigit := make([]byte, 1)
|
|
if _, err := rand.Read(prefixDigit); err != nil {
|
|
return "", fmt.Errorf("failed to generate prefix digit: %w", err)
|
|
}
|
|
|
|
prefix := fmt.Sprintf("jk_r%d", prefixDigit[0]%10)
|
|
key := fmt.Sprintf("%s_%s", prefix, randomStr)
|
|
|
|
// Validate generated key format
|
|
if !strings.HasPrefix(key, "jk_r") {
|
|
return "", fmt.Errorf("generated invalid API key format: %s", key)
|
|
}
|
|
|
|
return key, nil
|
|
}
|
|
|
|
// ValidateRunnerAPIKey validates an API key and returns the key ID and scope if valid
|
|
func (s *Secrets) ValidateRunnerAPIKey(apiKey string) (int64, string, error) {
|
|
if apiKey == "" {
|
|
return 0, "", fmt.Errorf("API key is required")
|
|
}
|
|
|
|
// Check fixed API key first (from database config). Constant-time compare.
|
|
// Fixed keys are refused entirely when production mode is on.
|
|
fixedKey := s.cfg.FixedAPIKey()
|
|
if fixedKey != "" && subtle.ConstantTimeCompare([]byte(apiKey), []byte(fixedKey)) == 1 {
|
|
if s.cfg.IsProductionMode() {
|
|
return 0, "", fmt.Errorf("fixed API key is not allowed in production mode")
|
|
}
|
|
// Return a special ID for fixed API key (doesn't exist in database)
|
|
return -1, "manager", nil
|
|
}
|
|
|
|
// Parse API key format: jk_rX_...
|
|
if !strings.HasPrefix(apiKey, "jk_r") {
|
|
return 0, "", fmt.Errorf("invalid API key format: expected format 'jk_rX_...' where X is a number (e.g., 'jk_r1_abc123...')")
|
|
}
|
|
|
|
parts := strings.Split(apiKey, "_")
|
|
if len(parts) < 3 {
|
|
return 0, "", fmt.Errorf("invalid API key format: expected format 'jk_rX_...' with at least 3 parts separated by underscores")
|
|
}
|
|
|
|
keyPrefix := fmt.Sprintf("%s_%s", parts[0], parts[1])
|
|
|
|
// Hash the full key for comparison
|
|
keyHash := sha256.Sum256([]byte(apiKey))
|
|
keyHashStr := hex.EncodeToString(keyHash[:])
|
|
|
|
var keyID int64
|
|
var scope string
|
|
var isActive bool
|
|
|
|
err := s.db.With(func(conn *sql.DB) error {
|
|
err := conn.QueryRow(
|
|
`SELECT id, scope, is_active FROM runner_api_keys
|
|
WHERE key_prefix = ? AND key_hash = ?`,
|
|
keyPrefix, keyHashStr,
|
|
).Scan(&keyID, &scope, &isActive)
|
|
|
|
if err == sql.ErrNoRows {
|
|
return fmt.Errorf("API key not found or invalid - please check that the key is correct and active")
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("failed to validate API key: %w", err)
|
|
}
|
|
|
|
if !isActive {
|
|
return fmt.Errorf("API key is inactive")
|
|
}
|
|
|
|
// Update last_used_at (don't fail if this update fails)
|
|
conn.Exec(`UPDATE runner_api_keys SET last_used_at = ? WHERE id = ?`, time.Now(), keyID)
|
|
return nil
|
|
})
|
|
|
|
if err != nil {
|
|
return 0, "", err
|
|
}
|
|
|
|
return keyID, scope, nil
|
|
}
|
|
|
|
// ListRunnerAPIKeys lists all runner API keys
|
|
func (s *Secrets) ListRunnerAPIKeys() ([]APIKeyInfo, error) {
|
|
var keys []APIKeyInfo
|
|
err := s.db.With(func(conn *sql.DB) error {
|
|
rows, err := conn.Query(
|
|
`SELECT id, key_prefix, name, description, scope, is_active, created_at, created_by
|
|
FROM runner_api_keys
|
|
ORDER BY created_at DESC`,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to query API keys: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
var key APIKeyInfo
|
|
var description sql.NullString
|
|
|
|
err := rows.Scan(&key.ID, &key.Key, &key.Name, &description, &key.Scope, &key.IsActive, &key.CreatedAt, &key.CreatedBy)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
if description.Valid {
|
|
key.Description = &description.String
|
|
}
|
|
|
|
keys = append(keys, key)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return keys, nil
|
|
}
|
|
|
|
// RevokeRunnerAPIKey revokes (deactivates) a runner API key
|
|
func (s *Secrets) RevokeRunnerAPIKey(keyID int64) error {
|
|
return s.db.With(func(conn *sql.DB) error {
|
|
_, err := conn.Exec("UPDATE runner_api_keys SET is_active = false WHERE id = ?", keyID)
|
|
return err
|
|
})
|
|
}
|
|
|
|
// DeleteRunnerAPIKey deletes a runner API key
|
|
func (s *Secrets) DeleteRunnerAPIKey(keyID int64) error {
|
|
return s.db.With(func(conn *sql.DB) error {
|
|
_, err := conn.Exec("DELETE FROM runner_api_keys WHERE id = ?", keyID)
|
|
return err
|
|
})
|
|
}
|
|
|
|
|
|
// generateSecret generates a random secret of the given length
|
|
func generateSecret(length int) (string, error) {
|
|
bytes := make([]byte, length)
|
|
if _, err := rand.Read(bytes); err != nil {
|
|
return "", err
|
|
}
|
|
return hex.EncodeToString(bytes), nil
|
|
}
|