its a bit broken

This commit is contained in:
2025-11-25 03:48:28 -06:00
parent a53ea4dce7
commit 690e6b13f8
16 changed files with 1542 additions and 861 deletions

View File

@@ -1,292 +1,209 @@
package auth
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"database/sql"
"encoding/hex"
"fmt"
"io"
"log"
"net/http"
"os"
"strings"
"sync"
"time"
)
// Secrets handles secret and token management
// Secrets handles API key management
type Secrets struct {
db *sql.DB
fixedRegistrationToken string // Fixed token from environment variable (reusable, never expires)
db *sql.DB
RegistrationMu sync.Mutex // Protects concurrent runner registrations
fixedAPIKey string // Fixed API key from environment variable (optional)
}
// NewSecrets creates a new secrets manager
func NewSecrets(db *sql.DB) (*Secrets, error) {
s := &Secrets{db: db}
// Check for fixed registration token from environment
fixedToken := os.Getenv("FIXED_REGISTRATION_TOKEN")
if fixedToken != "" {
s.fixedRegistrationToken = fixedToken
log.Printf("Fixed registration token enabled (from FIXED_REGISTRATION_TOKEN env var)")
log.Printf("WARNING: Fixed registration token is reusable and never expires - use only for testing/development!")
}
// Ensure manager secret exists
if err := s.ensureManagerSecret(); err != nil {
return nil, fmt.Errorf("failed to ensure manager secret: %w", err)
// Check for fixed API key from environment
if fixedKey := os.Getenv("FIXED_API_KEY"); fixedKey != "" {
s.fixedAPIKey = fixedKey
}
return s, nil
}
// ensureManagerSecret ensures a manager secret exists in the database
func (s *Secrets) ensureManagerSecret() error {
var count int
err := s.db.QueryRow("SELECT COUNT(*) FROM manager_secrets").Scan(&count)
if err != nil {
return fmt.Errorf("failed to check manager secrets: %w", err)
}
if count == 0 {
// Generate new manager secret
secret, err := generateSecret(32)
if err != nil {
return fmt.Errorf("failed to generate manager secret: %w", err)
}
_, err = s.db.Exec("INSERT INTO manager_secrets (secret) VALUES (?)", secret)
if err != nil {
return fmt.Errorf("failed to store manager secret: %w", err)
}
}
return 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"`
}
// GetManagerSecret retrieves the current manager secret
func (s *Secrets) GetManagerSecret() (string, error) {
var secret string
err := s.db.QueryRow("SELECT secret FROM manager_secrets ORDER BY created_at DESC LIMIT 1").Scan(&secret)
// 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 "", fmt.Errorf("failed to get manager secret: %w", err)
}
return secret, nil
}
// GenerateRegistrationToken generates a new registration token
// If expiresIn is 0, the token will never expire (uses far future date)
// Note: Token expiration only affects whether the token can be used for registration.
// Once a runner registers, it operates independently using its own secrets.
func (s *Secrets) GenerateRegistrationToken(createdBy int64, expiresIn time.Duration) (string, error) {
token, err := generateSecret(32)
if err != nil {
return "", fmt.Errorf("failed to generate token: %w", err)
return nil, fmt.Errorf("failed to generate API key: %w", err)
}
var expiresAt time.Time
if expiresIn == 0 {
// Use far future date (year 9999) to represent infinite expiration
expiresAt = time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC)
} else {
expiresAt = time.Now().Add(expiresIn)
// 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[:])
_, err = s.db.Exec(
"INSERT INTO registration_tokens (token, expires_at, created_by) VALUES (?, ?, ?)",
token, expiresAt, createdBy,
`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 registration token: %w", err)
return nil, fmt.Errorf("failed to store API key: %w", err)
}
return token, nil
}
// Get the inserted key info
var keyInfo APIKeyInfo
err = s.db.QueryRow(
`SELECT id, name, description, scope, is_active, created_at, created_by
FROM runner_api_keys WHERE key_prefix = ?`,
keyPrefix,
).Scan(&keyInfo.ID, &keyInfo.Name, &keyInfo.Description, &keyInfo.Scope, &keyInfo.IsActive, &keyInfo.CreatedAt, &keyInfo.CreatedBy)
// TokenValidationResult represents the result of token validation
type TokenValidationResult struct {
Valid bool
Reason string // "valid", "not_found", "already_used", "expired"
Error error
}
// ValidateRegistrationToken validates a registration token
func (s *Secrets) ValidateRegistrationToken(token string) (bool, error) {
result, err := s.ValidateRegistrationTokenDetailed(token)
if err != nil {
return false, err
return nil, fmt.Errorf("failed to retrieve created API key: %w", err)
}
// For backward compatibility, return just the valid boolean
return result.Valid, nil
keyInfo.Key = key
return &keyInfo, nil
}
// ValidateRegistrationTokenDetailed validates a registration token and returns detailed result
func (s *Secrets) ValidateRegistrationTokenDetailed(token string) (*TokenValidationResult, error) {
// Check fixed token first (if set) - it's reusable and never expires
if s.fixedRegistrationToken != "" && token == s.fixedRegistrationToken {
log.Printf("Fixed registration token used (from FIXED_REGISTRATION_TOKEN env var)")
return &TokenValidationResult{Valid: true, Reason: "valid"}, 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 "", 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 "", err
}
// Check database tokens
var used bool
var expiresAt time.Time
var id int64
prefix := fmt.Sprintf("jk_r%d", prefixDigit[0]%10)
return fmt.Sprintf("%s_%s", prefix, randomStr), 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 (for testing/development)
if s.fixedAPIKey != "" && apiKey == s.fixedAPIKey {
// 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.QueryRow(
"SELECT id, expires_at, used FROM registration_tokens WHERE token = ?",
token,
).Scan(&id, &expiresAt, &used)
`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 &TokenValidationResult{Valid: false, Reason: "not_found"}, nil
return 0, "", fmt.Errorf("API key not found or invalid - please check that the key is correct and active")
}
if err != nil {
return nil, fmt.Errorf("failed to query token: %w", err)
return 0, "", fmt.Errorf("failed to validate API key: %w", err)
}
if used {
return &TokenValidationResult{Valid: false, Reason: "already_used"}, nil
if !isActive {
return 0, "", fmt.Errorf("API key is inactive")
}
// Check if token has infinite expiration (year 9999 or later)
// Tokens with infinite expiration never expire
infiniteExpirationThreshold := time.Date(3000, 1, 1, 0, 0, 0, 0, time.UTC)
if expiresAt.Before(infiniteExpirationThreshold) {
// Normal expiration check for tokens with finite expiration
if time.Now().After(expiresAt) {
return &TokenValidationResult{Valid: false, Reason: "expired"}, nil
}
}
// If expiresAt is after the threshold, treat it as infinite (never expires)
// Update last_used_at (don't fail if this update fails)
s.db.Exec(`UPDATE runner_api_keys SET last_used_at = ? WHERE id = ?`, time.Now(), keyID)
// Mark token as used
_, err = s.db.Exec("UPDATE registration_tokens SET used = 1 WHERE id = ?", id)
if err != nil {
return nil, fmt.Errorf("failed to mark token as used: %w", err)
}
return &TokenValidationResult{Valid: true, Reason: "valid"}, nil
return keyID, scope, nil
}
// ListRegistrationTokens lists all registration tokens
func (s *Secrets) ListRegistrationTokens() ([]map[string]interface{}, error) {
// ListRunnerAPIKeys lists all runner API keys
func (s *Secrets) ListRunnerAPIKeys() ([]APIKeyInfo, error) {
rows, err := s.db.Query(
`SELECT id, token, expires_at, used, created_at, created_by
FROM registration_tokens
`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 nil, fmt.Errorf("failed to query tokens: %w", err)
return nil, fmt.Errorf("failed to query API keys: %w", err)
}
defer rows.Close()
var tokens []map[string]interface{}
var keys []APIKeyInfo
for rows.Next() {
var id, createdBy sql.NullInt64
var token string
var expiresAt, createdAt time.Time
var used bool
var key APIKeyInfo
var description sql.NullString
err := rows.Scan(&id, &token, &expiresAt, &used, &createdAt, &createdBy)
err := rows.Scan(&key.ID, &key.Key, &key.Name, &description, &key.Scope, &key.IsActive, &key.CreatedAt, &key.CreatedBy)
if err != nil {
continue
}
tokens = append(tokens, map[string]interface{}{
"id": id.Int64,
"token": token,
"expires_at": expiresAt,
"used": used,
"created_at": createdAt,
"created_by": createdBy.Int64,
})
if description.Valid {
key.Description = &description.String
}
keys = append(keys, key)
}
return tokens, nil
return keys, nil
}
// RevokeRegistrationToken revokes a registration token
func (s *Secrets) RevokeRegistrationToken(tokenID int64) error {
_, err := s.db.Exec("UPDATE registration_tokens SET used = 1 WHERE id = ?", tokenID)
// RevokeRunnerAPIKey revokes (deactivates) a runner API key
func (s *Secrets) RevokeRunnerAPIKey(keyID int64) error {
_, err := s.db.Exec("UPDATE runner_api_keys SET is_active = false WHERE id = ?", keyID)
return err
}
// GenerateRunnerSecret generates a unique secret for a runner
func (s *Secrets) GenerateRunnerSecret() (string, error) {
return generateSecret(32)
// DeleteRunnerAPIKey deletes a runner API key
func (s *Secrets) DeleteRunnerAPIKey(keyID int64) error {
_, err := s.db.Exec("DELETE FROM runner_api_keys WHERE id = ?", keyID)
return err
}
// SignRequest signs a request with the given secret
func SignRequest(method, path, body, secret string, timestamp time.Time) string {
message := fmt.Sprintf("%s\n%s\n%s\n%d", method, path, body, timestamp.Unix())
h := hmac.New(sha256.New, []byte(secret))
h.Write([]byte(message))
return hex.EncodeToString(h.Sum(nil))
}
// VerifyRequest verifies a signed request
func VerifyRequest(r *http.Request, secret string, maxAge time.Duration) (bool, error) {
signature := r.Header.Get("X-Runner-Signature")
if signature == "" {
return false, fmt.Errorf("missing signature")
}
timestampStr := r.Header.Get("X-Runner-Timestamp")
if timestampStr == "" {
return false, fmt.Errorf("missing timestamp")
}
var timestampUnix int64
_, err := fmt.Sscanf(timestampStr, "%d", &timestampUnix)
if err != nil {
return false, fmt.Errorf("invalid timestamp: %w", err)
}
timestamp := time.Unix(timestampUnix, 0)
// Check timestamp is not too old
if time.Since(timestamp) > maxAge {
return false, fmt.Errorf("request too old")
}
// Check timestamp is not in the future (allow 1 minute clock skew)
if timestamp.After(time.Now().Add(1 * time.Minute)) {
return false, fmt.Errorf("timestamp in future")
}
// Read body
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
return false, fmt.Errorf("failed to read body: %w", err)
}
// Restore body for handler
r.Body = io.NopCloser(strings.NewReader(string(bodyBytes)))
// Verify signature - use path without query parameters (query params are not part of signature)
// The runner signs with the path including query params, but we verify with just the path
// This is intentional - query params are for identification, not part of the signature
path := r.URL.Path
expectedSig := SignRequest(r.Method, path, string(bodyBytes), secret, timestamp)
return hmac.Equal([]byte(signature), []byte(expectedSig)), nil
}
// GetRunnerSecret retrieves the runner secret for a runner ID
func (s *Secrets) GetRunnerSecret(runnerID int64) (string, error) {
var secret string
err := s.db.QueryRow("SELECT runner_secret FROM runners WHERE id = ?", runnerID).Scan(&secret)
if err == sql.ErrNoRows {
return "", fmt.Errorf("runner not found")
}
if err != nil {
return "", fmt.Errorf("failed to get runner secret: %w", err)
}
if secret == "" {
return "", fmt.Errorf("runner not verified")
}
return secret, nil
}
// generateSecret generates a random secret of the given length
func generateSecret(length int) (string, error) {