mirror of
https://github.com/lukaszraczylo/gohoarder.git
synced 2026-10-12 00:45:16 +00:00
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
316 lines
9.3 KiB
Go
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",
|
|
})
|
|
}
|