mirror of
https://github.com/lukaszraczylo/gohoarder.git
synced 2026-06-27 02:53:49 +00:00
Initial commit
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/lukaszraczylo/gohoarder/pkg/errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
// Manager handles authentication and authorization
|
||||
type Manager struct {
|
||||
keys map[string]*APIKey
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// APIKey represents an API key
|
||||
type APIKey struct {
|
||||
ID string
|
||||
Name string
|
||||
HashedKey string
|
||||
Role Role
|
||||
CreatedAt time.Time
|
||||
ExpiresAt *time.Time
|
||||
LastUsedAt time.Time
|
||||
Permissions []Permission
|
||||
}
|
||||
|
||||
// Role represents user role
|
||||
type Role string
|
||||
|
||||
const (
|
||||
RoleReadOnly Role = "readonly"
|
||||
RoleReadWrite Role = "readwrite"
|
||||
RoleAdmin Role = "admin"
|
||||
)
|
||||
|
||||
// Permission represents a specific permission
|
||||
type Permission string
|
||||
|
||||
const (
|
||||
PermissionReadPackage Permission = "package:read"
|
||||
PermissionWritePackage Permission = "package:write"
|
||||
PermissionDeletePackage Permission = "package:delete"
|
||||
PermissionViewStats Permission = "stats:view"
|
||||
PermissionManageKeys Permission = "keys:manage"
|
||||
PermissionManageSettings Permission = "settings:manage"
|
||||
PermissionScanPackages Permission = "scan:execute"
|
||||
PermissionManageBypasses Permission = "bypasses:manage"
|
||||
)
|
||||
|
||||
// New creates a new authentication manager
|
||||
func New() *Manager {
|
||||
return &Manager{
|
||||
keys: make(map[string]*APIKey),
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateAPIKey generates a new API key
|
||||
func (m *Manager) GenerateAPIKey(name string, role Role, expiresIn *time.Duration) (*APIKey, string, error) {
|
||||
// Generate random key
|
||||
keyBytes := make([]byte, 32)
|
||||
if _, err := rand.Read(keyBytes); err != nil {
|
||||
return nil, "", errors.Wrap(err, errors.ErrCodeInternalServer, "failed to generate random key")
|
||||
}
|
||||
|
||||
rawKey := base64.URLEncoding.EncodeToString(keyBytes)
|
||||
|
||||
// Hash the key
|
||||
hashedKey, err := bcrypt.GenerateFromPassword([]byte(rawKey), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, "", errors.Wrap(err, errors.ErrCodeInternalServer, "failed to hash key")
|
||||
}
|
||||
|
||||
var expiresAt *time.Time
|
||||
if expiresIn != nil {
|
||||
t := time.Now().Add(*expiresIn)
|
||||
expiresAt = &t
|
||||
}
|
||||
|
||||
apiKey := &APIKey{
|
||||
ID: generateID(),
|
||||
Name: name,
|
||||
HashedKey: string(hashedKey),
|
||||
Role: role,
|
||||
CreatedAt: time.Now(),
|
||||
ExpiresAt: expiresAt,
|
||||
Permissions: getPermissionsForRole(role),
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
m.keys[apiKey.ID] = apiKey
|
||||
m.mu.Unlock()
|
||||
|
||||
return apiKey, rawKey, nil
|
||||
}
|
||||
|
||||
// ValidateAPIKey validates an API key and returns the associated key object
|
||||
func (m *Manager) ValidateAPIKey(ctx context.Context, rawKey string) (*APIKey, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
for _, apiKey := range m.keys {
|
||||
// Check if key is expired
|
||||
if apiKey.ExpiresAt != nil && time.Now().After(*apiKey.ExpiresAt) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Compare hashed key
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(apiKey.HashedKey), []byte(rawKey)); err == nil {
|
||||
// Update last used
|
||||
apiKey.LastUsedAt = time.Now()
|
||||
return apiKey, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.New(errors.ErrCodeUnauthorized, "invalid API key")
|
||||
}
|
||||
|
||||
// RevokeAPIKey revokes an API key
|
||||
func (m *Manager) RevokeAPIKey(keyID string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if _, exists := m.keys[keyID]; !exists {
|
||||
return errors.NotFound("API key not found")
|
||||
}
|
||||
|
||||
delete(m.keys, keyID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListAPIKeys lists all API keys
|
||||
func (m *Manager) ListAPIKeys() []*APIKey {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
keys := make([]*APIKey, 0, len(m.keys))
|
||||
for _, key := range m.keys {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// HasPermission checks if an API key has a specific permission
|
||||
func (k *APIKey) HasPermission(permission Permission) bool {
|
||||
for _, p := range k.Permissions {
|
||||
if p == permission {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// getPermissionsForRole returns permissions for a role
|
||||
func getPermissionsForRole(role Role) []Permission {
|
||||
switch role {
|
||||
case RoleReadOnly:
|
||||
return []Permission{
|
||||
PermissionReadPackage,
|
||||
PermissionViewStats,
|
||||
}
|
||||
case RoleReadWrite:
|
||||
return []Permission{
|
||||
PermissionReadPackage,
|
||||
PermissionWritePackage,
|
||||
PermissionViewStats,
|
||||
}
|
||||
case RoleAdmin:
|
||||
return []Permission{
|
||||
PermissionReadPackage,
|
||||
PermissionWritePackage,
|
||||
PermissionDeletePackage,
|
||||
PermissionViewStats,
|
||||
PermissionManageKeys,
|
||||
PermissionManageSettings,
|
||||
PermissionScanPackages,
|
||||
PermissionManageBypasses,
|
||||
}
|
||||
default:
|
||||
return []Permission{}
|
||||
}
|
||||
}
|
||||
|
||||
// generateID generates a unique ID
|
||||
func generateID() string {
|
||||
b := make([]byte, 16)
|
||||
_, _ = rand.Read(b) // #nosec G104 -- Rand read always succeeds
|
||||
return base64.URLEncoding.EncodeToString(b)
|
||||
}
|
||||
@@ -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() // #nosec G104 -- Cleanup, error not critical
|
||||
|
||||
// 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() // #nosec G104 -- Cleanup, error not critical
|
||||
|
||||
// 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") // #nosec G204 -- git command with validated URL
|
||||
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") // #nosec G204 -- git command with validated URL
|
||||
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") // #nosec G204 -- git command with validated URL
|
||||
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