mirror of
https://github.com/lukaszraczylo/gohoarder.git
synced 2026-06-16 01:01:20 +00:00
fixes
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CredentialExtractor extracts authentication credentials from HTTP requests
|
||||
type CredentialExtractor struct{}
|
||||
|
||||
// NewCredentialExtractor creates a new credential extractor
|
||||
func NewCredentialExtractor() *CredentialExtractor {
|
||||
return &CredentialExtractor{}
|
||||
}
|
||||
|
||||
// Extract extracts authentication credentials from an HTTP request
|
||||
// Returns the full Authorization header value or constructed auth string
|
||||
func (e *CredentialExtractor) Extract(r *http.Request) string {
|
||||
// Try Authorization header first (most common)
|
||||
if auth := r.Header.Get("Authorization"); auth != "" {
|
||||
return auth
|
||||
}
|
||||
|
||||
// Try Basic auth from URL (for PyPI compatibility)
|
||||
if username, password, ok := r.BasicAuth(); ok {
|
||||
auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
|
||||
return "Basic " + auth
|
||||
}
|
||||
|
||||
// No credentials found
|
||||
return ""
|
||||
}
|
||||
|
||||
// ExtractScheme returns the authentication scheme (Bearer, Basic, Token)
|
||||
func (e *CredentialExtractor) ExtractScheme(r *http.Request) string {
|
||||
auth := e.Extract(r)
|
||||
if auth == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
parts := strings.SplitN(auth, " ", 2)
|
||||
if len(parts) == 2 {
|
||||
return parts[0]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// ExtractToken extracts just the token part (without scheme)
|
||||
func (e *CredentialExtractor) ExtractToken(r *http.Request) string {
|
||||
auth := e.Extract(r)
|
||||
if auth == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Remove scheme prefix
|
||||
auth = strings.TrimPrefix(auth, "Bearer ")
|
||||
auth = strings.TrimPrefix(auth, "Token ")
|
||||
auth = strings.TrimPrefix(auth, "Basic ")
|
||||
|
||||
return auth
|
||||
}
|
||||
|
||||
// HasCredentials checks if request has any credentials
|
||||
func (e *CredentialExtractor) HasCredentials(r *http.Request) bool {
|
||||
return e.Extract(r) != ""
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// CredentialHasher generates hashes of credentials for cache keys
|
||||
type CredentialHasher struct{}
|
||||
|
||||
// NewCredentialHasher creates a new credential hasher
|
||||
func NewCredentialHasher() *CredentialHasher {
|
||||
return &CredentialHasher{}
|
||||
}
|
||||
|
||||
// Hash generates a short hash of credentials for use in cache keys
|
||||
// Returns "public" if no credentials provided
|
||||
func (h *CredentialHasher) Hash(credentials string) string {
|
||||
if credentials == "" {
|
||||
return "public"
|
||||
}
|
||||
|
||||
// Use SHA256 and take first 16 characters (8 bytes)
|
||||
hash := sha256.Sum256([]byte(credentials))
|
||||
return hex.EncodeToString(hash[:8])
|
||||
}
|
||||
|
||||
// GenerateCacheKey generates a cache key that includes credential hash
|
||||
func (h *CredentialHasher) GenerateCacheKey(registry, packageName, version, credentials string) string {
|
||||
credHash := h.Hash(credentials)
|
||||
return fmt.Sprintf("%s:%s:%s:%s", registry, packageName, version, credHash)
|
||||
}
|
||||
|
||||
// IsPublicKey checks if a cache key is for public packages (no credentials)
|
||||
func (h *CredentialHasher) IsPublicKey(cacheKey string) bool {
|
||||
return len(cacheKey) > 0 && cacheKey[len(cacheKey)-6:] == "public"
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ValidationResult represents a cached credential validation result
|
||||
type ValidationResult struct {
|
||||
Allowed bool
|
||||
ExpiresAt time.Time
|
||||
Reason string
|
||||
}
|
||||
|
||||
// ValidationCache caches credential validation results to reduce upstream checks
|
||||
type ValidationCache struct {
|
||||
cache map[string]*ValidationResult
|
||||
mu sync.RWMutex
|
||||
ttl time.Duration
|
||||
}
|
||||
|
||||
// NewValidationCache creates a new validation cache
|
||||
func NewValidationCache(ttl time.Duration) *ValidationCache {
|
||||
vc := &ValidationCache{
|
||||
cache: make(map[string]*ValidationResult),
|
||||
ttl: ttl,
|
||||
}
|
||||
|
||||
// Start cleanup goroutine
|
||||
go vc.cleanupExpired()
|
||||
|
||||
return vc
|
||||
}
|
||||
|
||||
// Get retrieves a validation result from cache
|
||||
// Returns (allowed bool, cached bool, reason string)
|
||||
func (vc *ValidationCache) Get(credHash, packageURL string) (bool, bool, string) {
|
||||
vc.mu.RLock()
|
||||
defer vc.mu.RUnlock()
|
||||
|
||||
key := credHash + ":" + packageURL
|
||||
result, exists := vc.cache[key]
|
||||
|
||||
if !exists {
|
||||
return false, false, ""
|
||||
}
|
||||
|
||||
// Check if expired
|
||||
if time.Now().After(result.ExpiresAt) {
|
||||
return false, false, ""
|
||||
}
|
||||
|
||||
return result.Allowed, true, result.Reason
|
||||
}
|
||||
|
||||
// Set stores a validation result in cache
|
||||
func (vc *ValidationCache) Set(credHash, packageURL string, allowed bool, reason string) {
|
||||
vc.mu.Lock()
|
||||
defer vc.mu.Unlock()
|
||||
|
||||
key := credHash + ":" + packageURL
|
||||
vc.cache[key] = &ValidationResult{
|
||||
Allowed: allowed,
|
||||
ExpiresAt: time.Now().Add(vc.ttl),
|
||||
Reason: reason,
|
||||
}
|
||||
}
|
||||
|
||||
// Invalidate removes a specific entry from cache
|
||||
func (vc *ValidationCache) Invalidate(credHash, packageURL string) {
|
||||
vc.mu.Lock()
|
||||
defer vc.mu.Unlock()
|
||||
|
||||
key := credHash + ":" + packageURL
|
||||
delete(vc.cache, key)
|
||||
}
|
||||
|
||||
// InvalidateAll clears the entire cache
|
||||
func (vc *ValidationCache) InvalidateAll() {
|
||||
vc.mu.Lock()
|
||||
defer vc.mu.Unlock()
|
||||
|
||||
vc.cache = make(map[string]*ValidationResult)
|
||||
}
|
||||
|
||||
// Size returns the number of cached entries
|
||||
func (vc *ValidationCache) Size() int {
|
||||
vc.mu.RLock()
|
||||
defer vc.mu.RUnlock()
|
||||
|
||||
return len(vc.cache)
|
||||
}
|
||||
|
||||
// cleanupExpired removes expired entries periodically
|
||||
func (vc *ValidationCache) cleanupExpired() {
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
vc.mu.Lock()
|
||||
now := time.Now()
|
||||
for key, result := range vc.cache {
|
||||
if now.After(result.ExpiresAt) {
|
||||
delete(vc.cache, key)
|
||||
}
|
||||
}
|
||||
vc.mu.Unlock()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// CredentialValidator validates credentials with upstream registries
|
||||
type CredentialValidator interface {
|
||||
// ValidateAccess checks if credentials grant access to a package
|
||||
// Returns (allowed bool, error)
|
||||
ValidateAccess(ctx context.Context, packageURL string, credentials string) (bool, error)
|
||||
}
|
||||
|
||||
// NPMValidator validates npm registry credentials
|
||||
type NPMValidator struct {
|
||||
client *http.Client
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// NewNPMValidator creates a new npm credential validator
|
||||
func NewNPMValidator() *NPMValidator {
|
||||
return &NPMValidator{
|
||||
client: &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
},
|
||||
timeout: 5 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateAccess validates npm package access using HEAD request
|
||||
func (v *NPMValidator) ValidateAccess(ctx context.Context, packageURL string, credentials string) (bool, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "HEAD", packageURL, nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Add credentials if provided
|
||||
if credentials != "" {
|
||||
req.Header.Set("Authorization", credentials)
|
||||
}
|
||||
|
||||
resp, err := v.client.Do(req)
|
||||
if err != nil {
|
||||
// Network error - allow cache fallback with warning
|
||||
log.Warn().Err(err).Str("url", packageURL).Msg("Validation request failed, allowing cache fallback")
|
||||
return true, fmt.Errorf("validation failed: %w (allowing cache fallback)", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check status code
|
||||
switch resp.StatusCode {
|
||||
case 200, 304:
|
||||
// Access granted
|
||||
return true, nil
|
||||
case 401, 403, 404:
|
||||
// Access denied
|
||||
return false, fmt.Errorf("access denied: HTTP %d", resp.StatusCode)
|
||||
default:
|
||||
// Unexpected status - allow cache fallback with warning
|
||||
log.Warn().Int("status", resp.StatusCode).Str("url", packageURL).Msg("Unexpected validation status, allowing cache fallback")
|
||||
return true, fmt.Errorf("unexpected status %d (allowing cache fallback)", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// PyPIValidator validates PyPI registry credentials
|
||||
type PyPIValidator struct {
|
||||
client *http.Client
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// NewPyPIValidator creates a new PyPI credential validator
|
||||
func NewPyPIValidator() *PyPIValidator {
|
||||
return &PyPIValidator{
|
||||
client: &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
},
|
||||
timeout: 5 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateAccess validates PyPI package access using HEAD request
|
||||
func (v *PyPIValidator) ValidateAccess(ctx context.Context, packageURL string, credentials string) (bool, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "HEAD", packageURL, nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Add credentials if provided
|
||||
if credentials != "" {
|
||||
req.Header.Set("Authorization", credentials)
|
||||
}
|
||||
|
||||
resp, err := v.client.Do(req)
|
||||
if err != nil {
|
||||
// Network error - allow cache fallback with warning
|
||||
log.Warn().Err(err).Str("url", packageURL).Msg("Validation request failed, allowing cache fallback")
|
||||
return true, fmt.Errorf("validation failed: %w (allowing cache fallback)", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check status code
|
||||
switch resp.StatusCode {
|
||||
case 200, 304:
|
||||
// Access granted
|
||||
return true, nil
|
||||
case 401, 403, 404:
|
||||
// Access denied
|
||||
return false, fmt.Errorf("access denied: HTTP %d", resp.StatusCode)
|
||||
default:
|
||||
// Unexpected status - allow cache fallback with warning
|
||||
log.Warn().Int("status", resp.StatusCode).Str("url", packageURL).Msg("Unexpected validation status, allowing cache fallback")
|
||||
return true, fmt.Errorf("unexpected status %d (allowing cache fallback)", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// GoValidator validates Go module credentials
|
||||
type GoValidator struct {
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// NewGoValidator creates a new Go module credential validator
|
||||
func NewGoValidator() *GoValidator {
|
||||
return &GoValidator{
|
||||
timeout: 10 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateAccess validates Go module access using git ls-remote
|
||||
func (v *GoValidator) ValidateAccess(ctx context.Context, modulePath string, credentials string) (bool, error) {
|
||||
// Create context with timeout
|
||||
ctx, cancel := context.WithTimeout(ctx, v.timeout)
|
||||
defer cancel()
|
||||
|
||||
// Determine repository type and validate accordingly
|
||||
if strings.HasPrefix(modulePath, "github.com/") {
|
||||
return v.validateGitHub(ctx, modulePath, credentials)
|
||||
}
|
||||
|
||||
if strings.HasPrefix(modulePath, "gitlab.com/") {
|
||||
return v.validateGitLab(ctx, modulePath, credentials)
|
||||
}
|
||||
|
||||
// For other Git providers, use generic git validation
|
||||
return v.validateGit(ctx, modulePath, credentials)
|
||||
}
|
||||
|
||||
func (v *GoValidator) validateGitHub(ctx context.Context, modulePath, credentials string) (bool, error) {
|
||||
// Extract token from credentials
|
||||
token := strings.TrimPrefix(credentials, "Bearer ")
|
||||
token = strings.TrimPrefix(token, "Token ")
|
||||
|
||||
if token == "" || token == credentials {
|
||||
// No token provided or not in expected format
|
||||
return false, fmt.Errorf("no GitHub token provided")
|
||||
}
|
||||
|
||||
// Build git URL
|
||||
repoURL := fmt.Sprintf("https://%s.git", modulePath)
|
||||
|
||||
// Create temporary directory for .netrc
|
||||
tempDir, err := os.MkdirTemp("", "gohoarder-validate-*")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create .netrc file with credentials
|
||||
netrcPath := filepath.Join(tempDir, ".netrc")
|
||||
netrcContent := fmt.Sprintf("machine github.com\nlogin oauth2\npassword %s\n", token)
|
||||
if err := os.WriteFile(netrcPath, []byte(netrcContent), 0600); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Run git ls-remote (lightweight, just checks access)
|
||||
cmd := exec.CommandContext(ctx, "git", "ls-remote", repoURL, "HEAD")
|
||||
cmd.Env = append(os.Environ(),
|
||||
"HOME="+tempDir, // Use temp .netrc
|
||||
"GIT_TERMINAL_PROMPT=0", // Disable prompts
|
||||
)
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
// Check error message
|
||||
errMsg := string(output)
|
||||
if strings.Contains(errMsg, "could not read Username") ||
|
||||
strings.Contains(errMsg, "Authentication failed") ||
|
||||
strings.Contains(errMsg, "fatal: repository") ||
|
||||
strings.Contains(errMsg, "not found") {
|
||||
// Access denied
|
||||
return false, fmt.Errorf("access denied: %s", strings.TrimSpace(errMsg))
|
||||
}
|
||||
|
||||
// Other error (network, etc.) - allow cache fallback
|
||||
log.Warn().Err(err).Str("module", modulePath).Msg("Git validation failed, allowing cache fallback")
|
||||
return true, fmt.Errorf("validation error (allowing cache): %w", err)
|
||||
}
|
||||
|
||||
// Success - repository accessible
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (v *GoValidator) validateGitLab(ctx context.Context, modulePath, credentials string) (bool, error) {
|
||||
// Extract token from credentials
|
||||
token := strings.TrimPrefix(credentials, "Bearer ")
|
||||
token = strings.TrimPrefix(token, "Token ")
|
||||
token = strings.TrimPrefix(token, "Private-Token ")
|
||||
|
||||
if token == "" || token == credentials {
|
||||
// No token provided
|
||||
return false, fmt.Errorf("no GitLab token provided")
|
||||
}
|
||||
|
||||
// Build git URL
|
||||
repoURL := fmt.Sprintf("https://%s.git", modulePath)
|
||||
|
||||
// Create temporary directory for .netrc
|
||||
tempDir, err := os.MkdirTemp("", "gohoarder-validate-*")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create .netrc file with credentials
|
||||
netrcPath := filepath.Join(tempDir, ".netrc")
|
||||
netrcContent := fmt.Sprintf("machine gitlab.com\nlogin oauth2\npassword %s\n", token)
|
||||
if err := os.WriteFile(netrcPath, []byte(netrcContent), 0600); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Run git ls-remote
|
||||
cmd := exec.CommandContext(ctx, "git", "ls-remote", repoURL, "HEAD")
|
||||
cmd.Env = append(os.Environ(),
|
||||
"HOME="+tempDir,
|
||||
"GIT_TERMINAL_PROMPT=0",
|
||||
)
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
errMsg := string(output)
|
||||
if strings.Contains(errMsg, "could not read Username") ||
|
||||
strings.Contains(errMsg, "Authentication failed") ||
|
||||
strings.Contains(errMsg, "not found") {
|
||||
return false, fmt.Errorf("access denied: %s", strings.TrimSpace(errMsg))
|
||||
}
|
||||
|
||||
log.Warn().Err(err).Str("module", modulePath).Msg("Git validation failed, allowing cache fallback")
|
||||
return true, fmt.Errorf("validation error (allowing cache): %w", err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (v *GoValidator) validateGit(ctx context.Context, modulePath, credentials string) (bool, error) {
|
||||
// Generic git validation for other providers
|
||||
// Similar to GitHub validation but with generic host detection
|
||||
repoURL := fmt.Sprintf("https://%s.git", modulePath)
|
||||
|
||||
cmd := exec.CommandContext(ctx, "git", "ls-remote", repoURL, "HEAD")
|
||||
cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0")
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
errMsg := string(output)
|
||||
if strings.Contains(errMsg, "could not read Username") ||
|
||||
strings.Contains(errMsg, "Authentication failed") ||
|
||||
strings.Contains(errMsg, "not found") {
|
||||
return false, fmt.Errorf("access denied: %s", strings.TrimSpace(errMsg))
|
||||
}
|
||||
|
||||
log.Warn().Err(err).Str("module", modulePath).Msg("Git validation failed, allowing cache fallback")
|
||||
return true, fmt.Errorf("validation error (allowing cache): %w", err)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
Reference in New Issue
Block a user