mirror of
https://github.com/lukaszraczylo/gohoarder.git
synced 2026-06-05 22:53:53 +00:00
194 lines
4.4 KiB
Go
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)
|
|
}
|