Files
gohoarder/pkg/auth/auth.go
T
2026-01-02 23:14:23 +00:00

194 lines
4.4 KiB
Go

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)
}