Files
gohoarder/pkg/app/handlers_admin.go
T
lukaszraczylo f72b481720 chore(deps): migrate fiber from v2 to v3
The automated update changed only go.mod, so the build failed on the old v2 imports. This commit moves the code to the v3 API.

The commit replaces BodyParser with Bind().Body, Static with the static middleware, and ListenTLS with ListenConfig. It also updates the tests for TestConfig.

Fixes #179
2026-10-04 00:50:37 +01:00

316 lines
9.3 KiB
Go

package app
import (
"time"
"github.com/gofiber/fiber/v3"
"github.com/lukaszraczylo/gohoarder/pkg/auth"
"github.com/lukaszraczylo/gohoarder/pkg/errors"
"github.com/lukaszraczylo/gohoarder/pkg/metadata"
"github.com/lukaszraczylo/gohoarder/pkg/uuid"
"github.com/rs/zerolog/log"
)
// requireAdmin middleware checks for admin authentication. It accepts the API
// key via either the Authorization Bearer header or the X-API-Key header
// (shared extractAPIKey), so admin clients are not limited to the bearer form.
func (a *App) requireAdmin(c fiber.Ctx) error {
// Get API key from Authorization or X-API-Key header
apiKey, ok := extractAPIKey(c)
if !ok {
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{
"error": "missing API key; provide Authorization: Bearer <key> or X-API-Key header",
})
}
// 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.Bind().Body(&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.Bind().Body(&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 appErr, ok := err.(*errors.Error); ok && appErr.Code == errors.ErrCodeNotFound {
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",
})
}