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