mirror of
https://github.com/lukaszraczylo/gohoarder.git
synced 2026-06-05 22:53:53 +00:00
6b037a92b4
- [x] Reorder struct fields across codebase for consistency - [x] Add analytics event handlers and tests - [x] Add authentication API key management handlers and tests - [x] Add pre-warming control handlers and tests - [x] Implement S3 storage backend with tests - [x] Implement SMB/CIFS storage backend with tests - [x] Add CDN middleware tests - [x] Integrate analytics tracking into cache manager - [x] Add S3 and SMB storage initialization in app setup - [x] Add CDN caching to proxy handlers - [x] Remove distributed locking (Redis lock manager) - [x] Remove proxy common package and utilities - [x] Remove standalone HTTP server package - [x] Remove logger middleware - [x] Simplify error handling utilities - [x] Update config with S3 and SMB options - [x] Update cache manager signature to include analytics
324 lines
9.3 KiB
Go
324 lines
9.3 KiB
Go
package app
|
|
|
|
import (
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gofiber/fiber/v2"
|
|
"github.com/lukaszraczylo/gohoarder/pkg/auth"
|
|
"github.com/lukaszraczylo/gohoarder/pkg/metadata"
|
|
"github.com/lukaszraczylo/gohoarder/pkg/uuid"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// requireAdmin middleware checks for admin authentication
|
|
func (a *App) requireAdmin(c *fiber.Ctx) error {
|
|
// Get API key from Authorization header
|
|
authHeader := c.Get("Authorization")
|
|
if authHeader == "" {
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
|
"error": "missing authorization header",
|
|
})
|
|
}
|
|
|
|
// Extract bearer token
|
|
parts := strings.SplitN(authHeader, " ", 2)
|
|
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
|
"error": "invalid authorization header format, expected: Bearer <token>",
|
|
})
|
|
}
|
|
|
|
apiKey := parts[1]
|
|
|
|
// Validate API key
|
|
key, err := a.authManager.ValidateAPIKey(c.Context(), apiKey)
|
|
if err != nil {
|
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
|
|
"error": "invalid or expired API key",
|
|
})
|
|
}
|
|
|
|
// Check if user has admin role or bypass management permission
|
|
if key.Role != auth.RoleAdmin && !key.HasPermission(auth.PermissionManageBypasses) {
|
|
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{
|
|
"error": "insufficient permissions, admin role required",
|
|
})
|
|
}
|
|
|
|
// Continue to next handler
|
|
return c.Next()
|
|
}
|
|
|
|
// handleAdminBypasses handles /api/admin/bypasses endpoint
|
|
func (a *App) handleAdminBypasses(c *fiber.Ctx) error {
|
|
c.Set("Content-Type", "application/json")
|
|
c.Set("Access-Control-Allow-Origin", "*")
|
|
c.Set("Access-Control-Allow-Methods", "GET, POST, PATCH, DELETE, OPTIONS")
|
|
c.Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
|
|
|
if c.Method() == "OPTIONS" {
|
|
return c.SendStatus(fiber.StatusOK)
|
|
}
|
|
|
|
// Check if there's an ID parameter
|
|
id := c.Params("id")
|
|
|
|
switch c.Method() {
|
|
case "GET":
|
|
if id != "" {
|
|
return a.handleGetBypass(c)
|
|
}
|
|
return a.handleListBypasses(c)
|
|
case "POST":
|
|
return a.handleCreateBypass(c)
|
|
case "PATCH":
|
|
return a.handleUpdateBypass(c)
|
|
case "DELETE":
|
|
return a.handleDeleteBypass(c)
|
|
default:
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "method not allowed"})
|
|
}
|
|
}
|
|
|
|
// handleListBypasses lists all CVE bypasses
|
|
func (a *App) handleListBypasses(c *fiber.Ctx) error {
|
|
ctx := c.Context()
|
|
|
|
// Parse query parameters
|
|
includeExpired := c.Query("include_expired") == "true"
|
|
activeOnly := c.Query("active_only") == "true"
|
|
bypassType := metadata.BypassType(c.Query("type"))
|
|
|
|
opts := &metadata.BypassListOptions{
|
|
IncludeExpired: includeExpired,
|
|
ActiveOnly: activeOnly,
|
|
Type: bypassType,
|
|
}
|
|
|
|
bypasses, err := a.metadata.ListCVEBypasses(ctx, opts)
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to list CVE bypasses")
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to list bypasses"})
|
|
}
|
|
|
|
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
|
"bypasses": bypasses,
|
|
"total": len(bypasses),
|
|
})
|
|
}
|
|
|
|
// CreateBypassRequest represents the request body for creating a bypass
|
|
type CreateBypassRequest struct {
|
|
Type metadata.BypassType `json:"type"`
|
|
Target string `json:"target"`
|
|
Reason string `json:"reason"`
|
|
CreatedBy string `json:"created_by"`
|
|
AppliesTo string `json:"applies_to,omitempty"`
|
|
ExpiresInHours int `json:"expires_in_hours"`
|
|
NotifyOnExpiry bool `json:"notify_on_expiry"`
|
|
}
|
|
|
|
// handleCreateBypass creates a new CVE bypass
|
|
func (a *App) handleCreateBypass(c *fiber.Ctx) error {
|
|
ctx := c.Context()
|
|
|
|
var req CreateBypassRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid JSON in request body"})
|
|
}
|
|
|
|
// Validate request
|
|
if req.Type != metadata.BypassTypeCVE && req.Type != metadata.BypassTypePackage {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "type must be 'cve' or 'package'"})
|
|
}
|
|
|
|
if req.Target == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "target is required"})
|
|
}
|
|
|
|
if req.Reason == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "reason is required"})
|
|
}
|
|
|
|
if req.CreatedBy == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "created_by is required"})
|
|
}
|
|
|
|
if req.ExpiresInHours <= 0 {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "expires_in_hours must be greater than 0"})
|
|
}
|
|
|
|
// Create bypass
|
|
now := time.Now()
|
|
expiresAt := now.Add(time.Duration(req.ExpiresInHours) * time.Hour)
|
|
|
|
bypass := &metadata.CVEBypass{
|
|
ID: uuid.New().String(),
|
|
Type: req.Type,
|
|
Target: req.Target,
|
|
Reason: req.Reason,
|
|
CreatedBy: req.CreatedBy,
|
|
CreatedAt: now,
|
|
ExpiresAt: expiresAt,
|
|
AppliesTo: req.AppliesTo,
|
|
NotifyOnExpiry: req.NotifyOnExpiry,
|
|
Active: true,
|
|
}
|
|
|
|
// Save to database
|
|
if err := a.metadata.SaveCVEBypass(ctx, bypass); err != nil {
|
|
log.Error().Err(err).Msg("Failed to save CVE bypass")
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to create bypass"})
|
|
}
|
|
|
|
log.Info().
|
|
Str("bypass_id", bypass.ID).
|
|
Str("type", string(bypass.Type)).
|
|
Str("target", bypass.Target).
|
|
Str("created_by", bypass.CreatedBy).
|
|
Time("expires_at", bypass.ExpiresAt).
|
|
Msg("CVE bypass created")
|
|
|
|
return c.Status(fiber.StatusCreated).JSON(fiber.Map{
|
|
"bypass": bypass,
|
|
"message": "Bypass created successfully",
|
|
})
|
|
}
|
|
|
|
// handleGetBypass gets a specific bypass by ID
|
|
func (a *App) handleGetBypass(c *fiber.Ctx) error {
|
|
ctx := c.Context()
|
|
|
|
// Extract ID from parameter
|
|
bypassID := c.Params("id")
|
|
|
|
if bypassID == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "bypass ID is required"})
|
|
}
|
|
|
|
// Get all bypasses and find the one with matching ID
|
|
bypasses, err := a.metadata.ListCVEBypasses(ctx, &metadata.BypassListOptions{
|
|
IncludeExpired: true,
|
|
})
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to list bypasses")
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to get bypass"})
|
|
}
|
|
|
|
for _, bypass := range bypasses {
|
|
if bypass.ID == bypassID {
|
|
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
|
"bypass": bypass,
|
|
})
|
|
}
|
|
}
|
|
|
|
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "bypass not found"})
|
|
}
|
|
|
|
// UpdateBypassRequest represents the request body for updating a bypass
|
|
type UpdateBypassRequest struct {
|
|
Active *bool `json:"active,omitempty"`
|
|
Reason string `json:"reason,omitempty"`
|
|
ExpiresInHours int `json:"expires_in_hours,omitempty"`
|
|
}
|
|
|
|
// handleUpdateBypass updates a bypass (activate/deactivate or extend expiration)
|
|
func (a *App) handleUpdateBypass(c *fiber.Ctx) error {
|
|
ctx := c.Context()
|
|
|
|
// Extract ID from parameter
|
|
bypassID := c.Params("id")
|
|
|
|
if bypassID == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "bypass ID is required"})
|
|
}
|
|
|
|
var req UpdateBypassRequest
|
|
if err := c.BodyParser(&req); err != nil {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid JSON in request body"})
|
|
}
|
|
|
|
// Get current bypass
|
|
bypasses, err := a.metadata.ListCVEBypasses(ctx, &metadata.BypassListOptions{
|
|
IncludeExpired: true,
|
|
})
|
|
if err != nil {
|
|
log.Error().Err(err).Msg("Failed to list bypasses")
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to get bypass"})
|
|
}
|
|
|
|
var currentBypass *metadata.CVEBypass
|
|
for _, bypass := range bypasses {
|
|
if bypass.ID == bypassID {
|
|
currentBypass = bypass
|
|
break
|
|
}
|
|
}
|
|
|
|
if currentBypass == nil {
|
|
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "bypass not found"})
|
|
}
|
|
|
|
// Update fields
|
|
if req.Active != nil {
|
|
currentBypass.Active = *req.Active
|
|
}
|
|
|
|
if req.Reason != "" {
|
|
currentBypass.Reason = req.Reason
|
|
}
|
|
|
|
if req.ExpiresInHours > 0 {
|
|
currentBypass.ExpiresAt = time.Now().Add(time.Duration(req.ExpiresInHours) * time.Hour)
|
|
}
|
|
|
|
// Save updated bypass
|
|
if err := a.metadata.SaveCVEBypass(ctx, currentBypass); err != nil {
|
|
log.Error().Err(err).Msg("Failed to update bypass")
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to update bypass"})
|
|
}
|
|
|
|
log.Info().
|
|
Str("bypass_id", currentBypass.ID).
|
|
Bool("active", currentBypass.Active).
|
|
Msg("CVE bypass updated")
|
|
|
|
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
|
"bypass": currentBypass,
|
|
"message": "Bypass updated successfully",
|
|
})
|
|
}
|
|
|
|
// handleDeleteBypass deletes a bypass
|
|
func (a *App) handleDeleteBypass(c *fiber.Ctx) error {
|
|
ctx := c.Context()
|
|
|
|
// Extract ID from parameter
|
|
bypassID := c.Params("id")
|
|
|
|
if bypassID == "" {
|
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "bypass ID is required"})
|
|
}
|
|
|
|
// Delete bypass
|
|
if err := a.metadata.DeleteCVEBypass(ctx, bypassID); err != nil {
|
|
if strings.Contains(err.Error(), "not found") {
|
|
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "bypass not found"})
|
|
}
|
|
log.Error().Err(err).Msg("Failed to delete bypass")
|
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to delete bypass"})
|
|
}
|
|
|
|
log.Info().
|
|
Str("bypass_id", bypassID).
|
|
Msg("CVE bypass deleted")
|
|
|
|
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
|
"deleted": true,
|
|
"bypass_id": bypassID,
|
|
"message": "Bypass deleted successfully",
|
|
})
|
|
}
|