Ctrl K
ComunidadGuías…

server/internal/handlers/file_watcher_settings.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package handlers

import (
	"encoding/json"
	"errors"
	"log"
	"net/http"
	"strings"
	"time"

	"blackbox/server/internal/middleware"
	"blackbox/server/internal/models"
	"gorm.io/gorm"
	"gorm.io/gorm/clause"
)

const fileWatcherRedactSecretsKey = "file_watcher_redact_secrets"

func getFileWatcherRedactSecrets(db *gorm.DB) (bool, error) {
	var setting models.AppSetting
	if err := db.First(&setting, "key = ?", fileWatcherRedactSecretsKey).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return true, nil
		}
		return false, err
	}
	switch setting.Value {
	case "", "true":
		return true, nil
	case "false":
		return false, nil
	default:
		return false, errors.New("invalid file watcher redaction value")
	}
}

func setFileWatcherRedactSecrets(db *gorm.DB, enabled bool) error {
	value := "false"
	if enabled {
		value = "true"
	}
	setting := models.AppSetting{
		Key:       fileWatcherRedactSecretsKey,
		Value:     value,
		UpdatedAt: time.Now(),
	}
	return db.Clauses(clause.OnConflict{
		Columns: []clause.Column{{Name: "key"}},
		DoUpdates: clause.Assignments(map[string]interface{}{
			"value":      value,
			"updated_at": time.Now(),
		}),
	}).Create(&setting).Error
}

func UpdateFileWatcherSettings(db *gorm.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		var req struct {
			RedactSecrets *bool `json:"redact_secrets"`
		}
		if !decodeJSONBody(w, r, 8<<10, &req) {
			return
		}
		if req.RedactSecrets == nil {
			writeError(w, http.StatusBadRequest, "redact_secrets is required")
			return
		}

		if err := setFileWatcherRedactSecrets(db, *req.RedactSecrets); err != nil {
			writeError(w, http.StatusInternalServerError, "failed to update file watcher settings")
			return
		}

		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(map[string]bool{"redact_secrets": *req.RedactSecrets})
	}
}

// getFileWatcherSettingsForNode reads from data_source_instances first (type="filewatcher",
// node_id=nodeName). An enabled row turns collection on; a disabled row is authoritative and
// suppresses file events even when WATCH_PATHS is configured locally. A missing row falls back
// to enabled=true so legacy WATCH_PATHS-only nodes keep working until migration seeds a source.
func getFileWatcherSettingsForNode(db *gorm.DB, nodeName string) (bool, bool, error) {
	globalRedactSecrets, err := getFileWatcherRedactSecrets(db)
	if err != nil {
		return false, false, err
	}

	var inst models.DataSourceInstance
	err = db.Where("type = ? AND node_id = ?", "filewatcher", nodeName).Order("created_at ASC").First(&inst).Error
	if err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return true, globalRedactSecrets, nil
		}
		return false, false, err
	}
	if !inst.Enabled {
		return false, globalRedactSecrets, nil
	}

	var cfg struct {
		RedactSecrets *bool `json:"redact_secrets"`
	}
	if jsonErr := json.Unmarshal([]byte(inst.Config), &cfg); jsonErr == nil {
		if cfg.RedactSecrets != nil {
			return true, *cfg.RedactSecrets, nil
		}
	} else {
		log.Printf("getFileWatcherSettingsForNode: failed to parse config for source %s: %v", inst.ID, jsonErr)
	}

	return true, globalRedactSecrets, nil
}

// getSystemdUnitsForNode reads from data_source_instances first (type="systemd",
// enabled=true, node_id=nodeName). If a disabled per-node source exists it is authoritative and
// returns an empty unit list; legacy systemd_unit_configs are only consulted when no source row exists yet.
func getSystemdUnitsForNode(db *gorm.DB, nodeName string) ([]string, error) {
	var inst models.DataSourceInstance
	err := db.Where("type = ? AND enabled = ? AND node_id = ?", "systemd", true, nodeName).First(&inst).Error
	if err == nil {
		var cfg struct {
			Units []string `json:"units"`
		}
		if jsonErr := json.Unmarshal([]byte(inst.Config), &cfg); jsonErr == nil {
			if cfg.Units == nil {
				cfg.Units = []string{}
			}
			return cfg.Units, nil
		} else {
			log.Printf("getSystemdUnitsForNode: failed to parse config for source %s: %v", inst.ID, jsonErr)
		}
	}
	if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, err
	}

	// Intentional asymmetry:
	// Systemd fail-closes, so an enabled source with malformed inst.Config yields no units.
	// getFileWatcherSettingsForNode fail-opens, keeping collection enabled and falling back
	// to the global redact setting when filewatcher config cannot be parsed.
	var existingSource models.DataSourceInstance
	if err := db.Where("type = ? AND node_id = ?", "systemd", nodeName).Order("created_at ASC").First(&existingSource).Error; err == nil {
		return []string{}, nil
	} else if !errors.Is(err, gorm.ErrRecordNotFound) {
		return nil, err
	}
	// Fall back to legacy table
	var config models.SystemdUnitConfig
	if err := db.First(&config, "node_name = ?", nodeName).Error; err != nil {
		if errors.Is(err, gorm.ErrRecordNotFound) {
			return []string{}, nil
		}
		return nil, err
	}
	var units []string
	if err := json.Unmarshal([]byte(config.Units), &units); err != nil {
		return nil, err
	}
	return units, nil
}

func AgentConfig(db *gorm.DB) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// AgentAuth must set the node name in context; unauthenticated requests are rejected.
		nodeName, ok := middleware.AgentNodeFromContext(r.Context())
		if !ok {
			writeError(w, http.StatusUnauthorized, "unauthorized")
			return
		}

		if capsHeader := r.Header.Get("X-Blackbox-Agent-Capabilities"); capsHeader != "" {
			const maxCapsHeader = 4 * 1024 // 4 KiB
			if len(capsHeader) > maxCapsHeader {
				capsHeader = capsHeader[:maxCapsHeader]
				if idx := strings.LastIndex(capsHeader, ","); idx >= 0 {
					capsHeader = capsHeader[:idx]
				} else {
					capsHeader = ""
				}
			}
			parts := strings.Split(capsHeader, ",")
			const maxCaps = 32
			const maxCapLen = 64
			caps := make([]string, 0, min(len(parts), maxCaps))
			for _, c := range parts {
				c = strings.TrimSpace(c)
				if c == "" {
					continue
				}
				if len(c) > maxCapLen {
					c = c[:maxCapLen]
				}
				caps = append(caps, c)
				if len(caps) >= maxCaps {
					break
				}
			}
			if len(caps) == 0 {
				log.Printf("AgentConfig: ignoring degenerate capabilities header for node %s", nodeName)
			} else if capsJSON, err := json.Marshal(caps); err == nil {
				newValue := string(capsJSON)
				result := db.Model(&models.Node{}).
					Where("name = ? AND (capabilities IS NULL OR capabilities != ?)", nodeName, newValue).
					Update("capabilities", newValue)
				if result.Error != nil {
					log.Printf("AgentConfig: failed to store capabilities for node %s: %v", nodeName, result.Error)
				}
			}
		}

		fileWatcherEnabled, redactSecrets, err := getFileWatcherSettingsForNode(db, nodeName)
		if err != nil {
			writeError(w, http.StatusInternalServerError, "failed to load agent config")
			return
		}

		systemdUnits, err := getSystemdUnitsForNode(db, nodeName)
		if err != nil {
			writeError(w, http.StatusInternalServerError, "failed to load agent config")
			return
		}

		w.Header().Set("Content-Type", "application/json")
		_ = json.NewEncoder(w).Encode(map[string]interface{}{
			"file_watcher_enabled":        fileWatcherEnabled,
			"file_watcher_redact_secrets": redactSecrets,
			"systemd_units":               systemdUnits,
		})
	}
}