mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 03:20:11 +00:00
Harden alert history and tenant storage paths
This commit is contained in:
parent
a7326d7047
commit
dcc4747215
5 changed files with 64 additions and 19 deletions
|
|
@ -4,7 +4,6 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
|
|
@ -126,7 +125,11 @@ func NewIncidentStore(cfg IncidentStoreConfig) *IncidentStore {
|
|||
store.dataDir = ""
|
||||
} else {
|
||||
store.dataDir = normalizedDataDir
|
||||
store.filePath = filepath.Join(store.dataDir, incidentFileName)
|
||||
if filePath, pathErr := pathutil.JoinBaseFile(store.dataDir, incidentFileName); pathErr != nil {
|
||||
log.Warn().Err(pathErr).Str("dataDir", store.dataDir).Msg("Failed to build incident store file path")
|
||||
} else {
|
||||
store.filePath = filePath
|
||||
}
|
||||
}
|
||||
if store.filePath != "" {
|
||||
if err := store.loadFromDisk(); err != nil {
|
||||
|
|
|
|||
|
|
@ -54,14 +54,31 @@ func NewHistoryManager(dataDir string) *HistoryManager {
|
|||
normalizedDataDir, err := pathutil.NormalizeDir(dataDir)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("dir", dataDir).Msg("Invalid alert history data directory")
|
||||
normalizedDataDir = filepath.Clean(dataDir)
|
||||
fallbackDir, fallbackErr := pathutil.NormalizeDir(utils.GetDataDir())
|
||||
if fallbackErr != nil {
|
||||
log.Error().Err(fallbackErr).Msg("Failed to normalize fallback alert history data directory")
|
||||
normalizedDataDir = filepath.Clean(utils.GetDataDir())
|
||||
} else {
|
||||
normalizedDataDir = fallbackDir
|
||||
}
|
||||
}
|
||||
dataDir = normalizedDataDir
|
||||
|
||||
historyFile, err := pathutil.JoinBaseFile(dataDir, HistoryFileName)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("dir", dataDir).Msg("Invalid alert history file path")
|
||||
historyFile = filepath.Join(dataDir, HistoryFileName)
|
||||
}
|
||||
backupFile, err := pathutil.JoinBaseFile(dataDir, HistoryBackupFileName)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("dir", dataDir).Msg("Invalid alert history backup path")
|
||||
backupFile = filepath.Join(dataDir, HistoryBackupFileName)
|
||||
}
|
||||
|
||||
hm := &HistoryManager{
|
||||
dataDir: dataDir,
|
||||
historyFile: filepath.Join(dataDir, HistoryFileName),
|
||||
backupFile: filepath.Join(dataDir, HistoryBackupFileName),
|
||||
historyFile: historyFile,
|
||||
backupFile: backupFile,
|
||||
history: make([]HistoryEntry, 0),
|
||||
saveInterval: 5 * time.Minute,
|
||||
stopChan: make(chan struct{}),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"sync"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/pathutil"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
|
|
@ -20,6 +21,9 @@ type MultiTenantPersistence struct {
|
|||
|
||||
// NewMultiTenantPersistence creates a new multi-tenant persistence manager.
|
||||
func NewMultiTenantPersistence(baseDataDir string) *MultiTenantPersistence {
|
||||
if normalized, err := pathutil.NormalizeDir(baseDataDir); err == nil {
|
||||
baseDataDir = normalized
|
||||
}
|
||||
return &MultiTenantPersistence{
|
||||
baseDataDir: baseDataDir,
|
||||
tenants: make(map[string]*ConfigPersistence),
|
||||
|
|
@ -50,16 +54,9 @@ func (mtp *MultiTenantPersistence) GetPersistence(orgID string) (*ConfigPersiste
|
|||
return nil, fmt.Errorf("invalid organization ID: %s", orgID)
|
||||
}
|
||||
|
||||
// Determine org data directory
|
||||
// Global/Default org uses the root data dir (legacy compatibility)
|
||||
// New orgs use /data/orgs/<org-id>
|
||||
var orgDir string
|
||||
if orgID == "default" {
|
||||
// IMPORTANT: Default org uses root data dir for backward compatibility
|
||||
// This ensures existing users' configs (nodes.enc, ai.enc, etc.) continue to work
|
||||
orgDir = mtp.baseDataDir
|
||||
} else {
|
||||
orgDir = filepath.Join(mtp.baseDataDir, "orgs", orgID)
|
||||
orgDir, err := mtp.orgDir(orgID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Info().Str("org_id", orgID).Str("dir", orgDir).Msg("Initializing tenant persistence")
|
||||
|
|
@ -89,11 +86,25 @@ func (mtp *MultiTenantPersistence) OrgExists(orgID string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
orgDir := filepath.Join(mtp.baseDataDir, "orgs", orgID)
|
||||
orgDir, err := mtp.orgDir(orgID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
stat, err := os.Stat(orgDir)
|
||||
return err == nil && stat.IsDir()
|
||||
}
|
||||
|
||||
func (mtp *MultiTenantPersistence) orgDir(orgID string) (string, error) {
|
||||
if orgID == "default" {
|
||||
return mtp.baseDataDir, nil
|
||||
}
|
||||
orgsRoot, err := pathutil.JoinBaseFile(mtp.baseDataDir, "orgs")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to resolve org root: %w", err)
|
||||
}
|
||||
return pathutil.JoinBaseFile(orgsRoot, orgID)
|
||||
}
|
||||
|
||||
// LoadOrganization loads the organization metadata including members.
|
||||
// Org metadata is stored in <orgDir>/org.json.
|
||||
func (mtp *MultiTenantPersistence) LoadOrganization(orgID string) (*models.Organization, error) {
|
||||
|
|
|
|||
|
|
@ -1536,12 +1536,23 @@ func (n *NotificationManager) sendAppriseViaHTTP(cfg AppriseConfig, title, body,
|
|||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(cfg.TimeoutSeconds)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
validatedBaseURL, err := n.validatedWebhookRequestURL(serverURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("apprise server URL validation failed: %w", err)
|
||||
}
|
||||
|
||||
notifyEndpoint := "/notify"
|
||||
if cfg.ConfigKey != "" {
|
||||
notifyEndpoint = "/notify/" + url.PathEscape(cfg.ConfigKey)
|
||||
}
|
||||
|
||||
requestURL := strings.TrimRight(serverURL, "/") + notifyEndpoint
|
||||
requestURL := *validatedBaseURL
|
||||
if requestURL.Path == "" || requestURL.Path == "/" {
|
||||
requestURL.Path = notifyEndpoint
|
||||
} else {
|
||||
requestURL.Path = strings.TrimRight(requestURL.Path, "/") + notifyEndpoint
|
||||
}
|
||||
requestURL.Fragment = ""
|
||||
|
||||
payload := map[string]any{
|
||||
"body": body,
|
||||
|
|
@ -1559,7 +1570,7 @@ func (n *NotificationManager) sendAppriseViaHTTP(cfg AppriseConfig, title, body,
|
|||
return fmt.Errorf("failed to marshal Apprise payload: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL, bytes.NewReader(payloadBytes))
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, requestURL.String(), bytes.NewReader(payloadBytes))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create Apprise request: %w", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,10 @@ func NewNotificationQueue(dataDir string) (*NotificationQueue, error) {
|
|||
return nil, fmt.Errorf("failed to create notification queue directory: %w", err)
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(dataDir, "notification_queue.db")
|
||||
dbPath, err := pathutil.JoinBaseFile(dataDir, "notification_queue.db")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build notification queue database path: %w", err)
|
||||
}
|
||||
|
||||
// Open database with pragmas in DSN so every pool connection is configured
|
||||
dsn := dbPath + "?" + url.Values{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue