Files
Redflag/aggregator-server/internal/api/handlers/security_settings.go
jpetree331 4c62de8d8b fix(security): A-3 auth middleware coverage fixes
Fixes 9 auth middleware findings from the A-3 recon audit.

F-A3-11 CRITICAL: Removed JWT secret from WebAuthMiddleware log output.
  Replaced emoji-prefixed fmt.Printf with ETHOS-compliant log.Printf.
  No secret values in any log output.

F-A3-7 CRITICAL: Config download now requires WebAuthMiddleware.
  GET /downloads/config/:agent_id is admin-only (agents never call it).

F-A3-6 HIGH: Update package download now requires AuthMiddleware.
  GET /downloads/updates/:package_id requires valid agent JWT.

F-A3-10 HIGH: Scheduler stats changed from AuthMiddleware to
  WebAuthMiddleware. Agent JWTs can no longer view scheduler internals.

F-A3-13 LOW: RequireAdmin() middleware implemented. 7 security settings
  routes re-enabled (GET/PUT/POST under /security/settings).
  security_settings.go.broken renamed to .go, API mismatches fixed.

F-A3-12 MEDIUM: JWT issuer claims added for token type separation.
  Agent tokens: issuer=redflag-agent, Web tokens: issuer=redflag-web.
  AuthMiddleware rejects tokens with wrong issuer.
  Grace period: tokens with no issuer still accepted (backward compat).

F-A3-2 MEDIUM: /auth/verify now has WebAuthMiddleware applied.
  Endpoint returns 200 with valid=true for valid admin tokens.

F-A3-9 MEDIUM: Agent self-unregister (DELETE /:id) now rate-limited
  using the same agent_reports rate limiter as other agent routes.

F-A3-14 LOW: CORS origin configurable via REDFLAG_CORS_ORIGIN env var.
  Defaults to http://localhost:3000 for development.
  Added PATCH method and agent-specific headers to CORS config.

All 27 server tests pass. All 14 agent tests pass. No regressions.
See docs/A3_Fix_Implementation.md and docs/Deviations_Report.md
(DEV-020 through DEV-022).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 22:17:40 -04:00

199 lines
5.8 KiB
Go

package handlers
import (
"fmt"
"net/http"
"github.com/Fimeg/RedFlag/aggregator-server/internal/services"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
// SecuritySettingsHandler handles security settings API endpoints
type SecuritySettingsHandler struct {
securitySettingsService *services.SecuritySettingsService
}
// NewSecuritySettingsHandler creates a new security settings handler
func NewSecuritySettingsHandler(securitySettingsService *services.SecuritySettingsService) *SecuritySettingsHandler {
return &SecuritySettingsHandler{
securitySettingsService: securitySettingsService,
}
}
// GetAllSecuritySettings returns all security settings
func (h *SecuritySettingsHandler) GetAllSecuritySettings(c *gin.Context) {
settings, err := h.securitySettingsService.GetAllSettings()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"settings": settings,
"user_has_permission": true,
})
}
// GetSecuritySettingsByCategory returns settings for a specific category
func (h *SecuritySettingsHandler) GetSecuritySettingsByCategory(c *gin.Context) {
category := c.Param("category")
settings, err := h.securitySettingsService.GetSettingsByCategory(category)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, settings)
}
// UpdateSecuritySetting updates a specific security setting
func (h *SecuritySettingsHandler) UpdateSecuritySetting(c *gin.Context) {
var req struct {
Value interface{} `json:"value" binding:"required"`
Reason string `json:"reason"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
category := c.Param("category")
key := c.Param("key")
userIDStr := c.GetString("user_id")
// Parse user_id to UUID (the service expects uuid.UUID)
userID, err := uuid.Parse(userIDStr)
if err != nil {
// Fallback for numeric user IDs — use a deterministic UUID
userID = uuid.NewSHA1(uuid.NameSpaceURL, []byte("user:"+userIDStr))
}
if err := h.securitySettingsService.ValidateSetting(category, key, req.Value); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.securitySettingsService.SetSetting(category, key, req.Value, userID, req.Reason); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
setting, err := h.securitySettingsService.GetSetting(category, key)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Setting updated",
"setting": map[string]interface{}{
"category": category,
"key": key,
"value": setting,
},
})
}
// ValidateSecuritySettings validates settings without applying them
func (h *SecuritySettingsHandler) ValidateSecuritySettings(c *gin.Context) {
var req struct {
Category string `json:"category" binding:"required"`
Key string `json:"key" binding:"required"`
Value interface{} `json:"value" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
err := h.securitySettingsService.ValidateSetting(req.Category, req.Key, req.Value)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"valid": false, "error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"valid": true, "message": "Setting is valid"})
}
// GetSecurityAuditTrail returns audit trail of security setting changes
// Note: GetAuditTrail not yet implemented in service — returns placeholder
func (h *SecuritySettingsHandler) GetSecurityAuditTrail(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"audit_entries": []interface{}{},
"pagination": gin.H{
"page": 1,
"page_size": 50,
"total": 0,
"total_pages": 0,
},
})
}
// GetSecurityOverview returns current security status overview
// Note: GetSecurityOverview not yet implemented in service — returns all settings
func (h *SecuritySettingsHandler) GetSecurityOverview(c *gin.Context) {
settings, err := h.securitySettingsService.GetAllSettings()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"overview": settings,
})
}
// ApplySecuritySettings applies batch of setting changes
func (h *SecuritySettingsHandler) ApplySecuritySettings(c *gin.Context) {
var req struct {
Settings map[string]map[string]interface{} `json:"settings" binding:"required"`
Reason string `json:"reason"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
userIDStr := c.GetString("user_id")
userID, err := uuid.Parse(userIDStr)
if err != nil {
userID = uuid.NewSHA1(uuid.NameSpaceURL, []byte("user:"+userIDStr))
}
// Validate all settings first
for category, settings := range req.Settings {
for key, value := range settings {
if err := h.securitySettingsService.ValidateSetting(category, key, value); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": fmt.Sprintf("Validation failed for %s.%s: %v", category, key, err),
})
return
}
}
}
// Apply settings one by one (batch method not available in service)
applied := 0
for category, settings := range req.Settings {
for key, value := range settings {
if err := h.securitySettingsService.SetSetting(category, key, value, userID, req.Reason); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"error": fmt.Sprintf("Failed to apply %s.%s: %v", category, key, err),
})
return
}
applied++
}
}
c.JSON(http.StatusOK, gin.H{
"message": "All settings applied",
"applied_count": applied,
})
}