mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-02-18 00:17:39 +01:00
Harden setup token flow and enforce encrypted persistence
This commit is contained in:
@@ -137,13 +137,14 @@ See [Configuration Guide](docs/CONFIGURATION.md#automated-setup-skip-ui) for det
|
||||
2. Discovered nodes appear automatically
|
||||
3. Click "Setup Script" next to any node
|
||||
4. Click "Generate Setup Code" button (creates a 6-character code valid for 5 minutes)
|
||||
5. Copy and run the provided one-liner on your Proxmox/PBS host
|
||||
5. Copy and run the provided one-liner on your Proxmox/PBS host (the script prompts for your setup token securely)
|
||||
6. Node is configured and monitoring starts automatically
|
||||
|
||||
**Example:**
|
||||
```bash
|
||||
curl -sSL "http://pulse:7655/api/setup-script?type=pve&host=https://pve:8006&auth_token=ABC123" | bash
|
||||
curl -sSL "http://pulse:7655/api/setup-script?type=pve&host=https://pve:8006" | bash
|
||||
```
|
||||
> Tip: For non-interactive installs, export `PULSE_SETUP_TOKEN` before running the script or supply `auth_token=YOUR_API_TOKEN` as shown in Method 2.
|
||||
|
||||
#### Method 2: Automated Setup (For scripts/automation)
|
||||
Use your permanent API token directly in the URL for automation:
|
||||
|
||||
@@ -758,8 +758,9 @@ Response:
|
||||
{
|
||||
"url": "http://pulse.local:7655/api/setup-script?type=pve&host=...",
|
||||
"command": "curl -sSL \"http://pulse.local:7655/api/setup-script?...\" | bash",
|
||||
"setupCode": "A7K9P2", // 6-character one-time code
|
||||
"expires": 1755123456 // Unix timestamp when code expires (5 minutes)
|
||||
"setupToken": "4c7f3e8c1c5f4b0da580c4477f4b1c2d",
|
||||
"tokenHint": "4c7…c2d",
|
||||
"expires": 1755123456 // Unix timestamp when token expires (5 minutes)
|
||||
}
|
||||
```
|
||||
|
||||
@@ -774,8 +775,8 @@ The script will:
|
||||
1. Create a monitoring user (pulse-monitor@pam or pulse-monitor@pbs)
|
||||
2. Generate an API token for that user
|
||||
3. Set appropriate permissions
|
||||
4. Prompt for the setup code
|
||||
5. Auto-register with Pulse if a valid code is provided
|
||||
4. Prompt for the setup token (or read `PULSE_SETUP_TOKEN` if set)
|
||||
5. Auto-register with Pulse if a valid token is provided
|
||||
|
||||
### Auto-Register Node
|
||||
Register a node automatically (used by setup scripts). Requires either a valid setup code or API token.
|
||||
|
||||
@@ -74,6 +74,19 @@ export const NodeModal: Component<NodeModalProps> = (props) => {
|
||||
|
||||
const [formData, setFormData] = createSignal(getCleanFormData());
|
||||
const [quickSetupCommand, setQuickSetupCommand] = createSignal('');
|
||||
const [quickSetupToken, setQuickSetupToken] = createSignal('');
|
||||
const [quickSetupExpiry, setQuickSetupExpiry] = createSignal<number | null>(null);
|
||||
const quickSetupExpiryLabel = () => {
|
||||
const expiry = quickSetupExpiry();
|
||||
if (!expiry) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
return new Date(expiry * 1000).toLocaleTimeString();
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
// Track previous state to detect changes
|
||||
let previousResetKey: number | undefined = undefined;
|
||||
@@ -91,6 +104,8 @@ export const NodeModal: Component<NodeModalProps> = (props) => {
|
||||
previousResetKey = key;
|
||||
setFormData(() => getCleanFormData(props.nodeType));
|
||||
setQuickSetupCommand('');
|
||||
setQuickSetupToken('');
|
||||
setQuickSetupExpiry(null);
|
||||
setTestResult(null);
|
||||
return;
|
||||
}
|
||||
@@ -100,6 +115,8 @@ export const NodeModal: Component<NodeModalProps> = (props) => {
|
||||
previousNodeType = nodeType;
|
||||
setFormData(() => getCleanFormData(props.nodeType));
|
||||
setQuickSetupCommand('');
|
||||
setQuickSetupToken('');
|
||||
setQuickSetupExpiry(null);
|
||||
setTestResult(null);
|
||||
return;
|
||||
}
|
||||
@@ -109,6 +126,8 @@ export const NodeModal: Component<NodeModalProps> = (props) => {
|
||||
if (isOpen && !editingNode) {
|
||||
setFormData(() => getCleanFormData(props.nodeType));
|
||||
setQuickSetupCommand('');
|
||||
setQuickSetupToken('');
|
||||
setQuickSetupExpiry(null);
|
||||
setTestResult(null);
|
||||
}
|
||||
});
|
||||
@@ -659,6 +678,9 @@ export const NodeModal: Component<NodeModalProps> = (props) => {
|
||||
const data = await response.json();
|
||||
console.log('[Quick Setup] Setup data received:', data);
|
||||
|
||||
setQuickSetupToken(data.setupToken ?? '');
|
||||
setQuickSetupExpiry(typeof data.expires === 'number' ? data.expires : null);
|
||||
|
||||
// Backend returns url, command, and expires
|
||||
// Just copy the command - don't show the modal
|
||||
if (data.command) {
|
||||
@@ -667,7 +689,7 @@ export const NodeModal: Component<NodeModalProps> = (props) => {
|
||||
const copied = await copyToClipboard(data.command);
|
||||
console.log('[Quick Setup] Copy result:', copied);
|
||||
if (copied) {
|
||||
showSuccess('Command copied to clipboard!');
|
||||
showSuccess('Command copied to clipboard! Paste the setup token shown below when prompted.');
|
||||
} else {
|
||||
showError('Failed to copy to clipboard');
|
||||
}
|
||||
@@ -675,10 +697,14 @@ export const NodeModal: Component<NodeModalProps> = (props) => {
|
||||
console.log('[Quick Setup] No command in response');
|
||||
}
|
||||
} else {
|
||||
setQuickSetupToken('');
|
||||
setQuickSetupExpiry(null);
|
||||
showError('Failed to generate setup URL');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[Quick Setup] Error:', error);
|
||||
setQuickSetupToken('');
|
||||
setQuickSetupExpiry(null);
|
||||
showError('Failed to copy command');
|
||||
}
|
||||
}}
|
||||
@@ -718,6 +744,19 @@ export const NodeModal: Component<NodeModalProps> = (props) => {
|
||||
{quickSetupCommand()}
|
||||
</code>
|
||||
</Show>
|
||||
<Show when={quickSetupToken().length > 0}>
|
||||
<div class="mt-2 text-xs text-blue-800 dark:text-blue-200">
|
||||
<span class="font-semibold">Setup token:</span>
|
||||
<code class="ml-1 font-mono break-all text-blue-900 dark:text-blue-100">
|
||||
{quickSetupToken()}
|
||||
</code>
|
||||
<Show when={quickSetupExpiry()}>
|
||||
<span class="ml-2">
|
||||
Expires at {quickSetupExpiryLabel()}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-3">
|
||||
@@ -1175,6 +1214,19 @@ export const NodeModal: Component<NodeModalProps> = (props) => {
|
||||
{quickSetupCommand()}
|
||||
</code>
|
||||
</Show>
|
||||
<Show when={quickSetupToken().length > 0}>
|
||||
<div class="mt-2 text-xs text-blue-800 dark:text-blue-200">
|
||||
<span class="font-semibold">Setup token:</span>
|
||||
<code class="ml-1 font-mono break-all text-blue-900 dark:text-blue-100">
|
||||
{quickSetupToken()}
|
||||
</code>
|
||||
<Show when={quickSetupExpiry()}>
|
||||
<span class="ml-2">
|
||||
Expires at {quickSetupExpiryLabel()}
|
||||
</span>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-3">
|
||||
|
||||
@@ -3577,9 +3577,32 @@ else
|
||||
# Try auto-registration
|
||||
echo "Registering node with Pulse..."
|
||||
|
||||
# Use auth token from URL parameter (much simpler!)
|
||||
# Use auth token from URL parameter when provided (automation workflows)
|
||||
AUTH_TOKEN="%s"
|
||||
|
||||
|
||||
# Allow non-interactive override via environment variable
|
||||
if [ -z "$AUTH_TOKEN" ] && [ -n "$PULSE_SETUP_TOKEN" ]; then
|
||||
AUTH_TOKEN="$PULSE_SETUP_TOKEN"
|
||||
fi
|
||||
|
||||
# Prompt the operator if we still don't have a token and a TTY is available
|
||||
if [ -z "$AUTH_TOKEN" ]; then
|
||||
if [ -t 0 ]; then
|
||||
printf "Pulse setup token: "
|
||||
if command -v stty >/dev/null 2>&1; then stty -echo; fi
|
||||
IFS= read -r AUTH_TOKEN
|
||||
if command -v stty >/dev/null 2>&1; then stty echo; fi
|
||||
printf "\n"
|
||||
elif { exec 3</dev/tty; } 2>/dev/null; then
|
||||
printf "Pulse setup token: " >&3
|
||||
if command -v stty >/dev/null 2>&1; then stty -echo <&3 2>/dev/null || true; fi
|
||||
IFS= read -r AUTH_TOKEN <&3 || true
|
||||
if command -v stty >/dev/null 2>&1; then stty echo <&3 2>/dev/null || true; fi
|
||||
printf "\n" >&3
|
||||
exec 3<&-
|
||||
fi
|
||||
fi
|
||||
|
||||
# Only proceed with auto-registration if we have an auth token
|
||||
if [ -n "$AUTH_TOKEN" ]; then
|
||||
# Get the server's hostname
|
||||
@@ -3618,8 +3641,7 @@ else
|
||||
-H "Content-Type: application/json" \
|
||||
-d @- 2>&1)
|
||||
else
|
||||
echo "Warning: No authentication token provided"
|
||||
echo "Auto-registration skipped"
|
||||
echo "⚠️ Auto-registration skipped: no setup token provided"
|
||||
AUTO_REG_SUCCESS=false
|
||||
REGISTER_RESPONSE=""
|
||||
fi
|
||||
@@ -4615,9 +4637,32 @@ else
|
||||
echo "🔄 Attempting auto-registration with Pulse..."
|
||||
echo ""
|
||||
|
||||
# Use auth token from URL parameter (much simpler!)
|
||||
# Use auth token from URL parameter when provided (automation workflows)
|
||||
AUTH_TOKEN="%s"
|
||||
|
||||
|
||||
# Allow non-interactive override via environment variable
|
||||
if [ -z "$AUTH_TOKEN" ] && [ -n "$PULSE_SETUP_TOKEN" ]; then
|
||||
AUTH_TOKEN="$PULSE_SETUP_TOKEN"
|
||||
fi
|
||||
|
||||
# Prompt the operator if we still don't have a token and a TTY is available
|
||||
if [ -z "$AUTH_TOKEN" ]; then
|
||||
if [ -t 0 ]; then
|
||||
printf "Pulse setup token: "
|
||||
if command -v stty >/dev/null 2>&1; then stty -echo; fi
|
||||
IFS= read -r AUTH_TOKEN
|
||||
if command -v stty >/dev/null 2>&1; then stty echo; fi
|
||||
printf "\n"
|
||||
elif { exec 3</dev/tty; } 2>/dev/null; then
|
||||
printf "Pulse setup token: " >&3
|
||||
if command -v stty >/dev/null 2>&1; then stty -echo <&3 2>/dev/null || true; fi
|
||||
IFS= read -r AUTH_TOKEN <&3 || true
|
||||
if command -v stty >/dev/null 2>&1; then stty echo <&3 2>/dev/null || true; fi
|
||||
printf "\n" >&3
|
||||
exec 3<&-
|
||||
fi
|
||||
fi
|
||||
|
||||
# Only proceed with auto-registration if we have an auth token
|
||||
if [ -n "$AUTH_TOKEN" ]; then
|
||||
# Get the server's hostname
|
||||
@@ -4667,7 +4712,7 @@ EOF
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$REGISTER_JSON" 2>&1)
|
||||
else
|
||||
echo "⚠️ No setup code provided - skipping auto-registration"
|
||||
echo "⚠️ Auto-registration skipped: no setup token provided"
|
||||
AUTO_REG_SUCCESS=false
|
||||
REGISTER_RESPONSE=""
|
||||
fi
|
||||
@@ -4815,16 +4860,23 @@ func (h *ConfigHandlers) HandleSetupScriptURL(w http.ResponseWriter, r *http.Req
|
||||
backupPerms = "&backup_perms=true"
|
||||
}
|
||||
|
||||
// Include the token directly in the URL - much simpler!
|
||||
scriptURL := fmt.Sprintf("%s/api/setup-script?type=%s%s&pulse_url=%s%s&auth_token=%s",
|
||||
pulseURL, req.Type, encodedHost, pulseURL, backupPerms, token)
|
||||
// Build script URL without embedding the secret token directly
|
||||
scriptURL := fmt.Sprintf("%s/api/setup-script?type=%s%s&pulse_url=%s%s",
|
||||
pulseURL, req.Type, encodedHost, pulseURL, backupPerms)
|
||||
|
||||
// Return a simple curl command - no environment variables needed
|
||||
// Don't include setupCode since it's already embedded in the URL
|
||||
// The setup token is returned separately so the script can prompt the user
|
||||
tokenHint := token
|
||||
if len(token) > 6 {
|
||||
tokenHint = fmt.Sprintf("%s…%s", token[:3], token[len(token)-3:])
|
||||
}
|
||||
|
||||
response := map[string]interface{}{
|
||||
"url": scriptURL,
|
||||
"command": fmt.Sprintf(`curl -sSL "%s" | bash`, scriptURL),
|
||||
"expires": expiry.Unix(),
|
||||
"url": scriptURL,
|
||||
"command": fmt.Sprintf(`curl -sSL "%s" | bash`, scriptURL),
|
||||
"expires": expiry.Unix(),
|
||||
"setupToken": token,
|
||||
"tokenHint": tokenHint,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
@@ -5052,17 +5104,19 @@ func (h *ConfigHandlers) HandleAutoRegister(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
}
|
||||
|
||||
// If still not authenticated and auth is required, reject
|
||||
// BUT: Always allow if a valid setup code/auth token was provided (even if expired/used)
|
||||
// This ensures the error message is accurate
|
||||
if !authenticated && h.config.HasAPITokens() && authCode == "" {
|
||||
log.Warn().Str("ip", r.RemoteAddr).Msg("Unauthorized auto-register attempt - no authentication provided")
|
||||
http.Error(w, "Pulse requires authentication", http.StatusUnauthorized)
|
||||
return
|
||||
} else if !authenticated && h.config.HasAPITokens() {
|
||||
// Had a code but it didn't validate
|
||||
log.Warn().Str("ip", r.RemoteAddr).Msg("Unauthorized auto-register attempt - invalid or expired setup code")
|
||||
http.Error(w, "Invalid or expired setup code", http.StatusUnauthorized)
|
||||
// Abort when no authentication succeeded. This applies even when API tokens
|
||||
// are not configured to ensure one-time setup tokens are always required.
|
||||
if !authenticated {
|
||||
log.Warn().
|
||||
Str("ip", r.RemoteAddr).
|
||||
Bool("has_auth_code", authCode != "").
|
||||
Msg("Unauthorized auto-register attempt rejected")
|
||||
|
||||
if authCode == "" && r.Header.Get("X-API-Token") == "" {
|
||||
http.Error(w, "Pulse requires authentication", http.StatusUnauthorized)
|
||||
} else {
|
||||
http.Error(w, "Invalid or expired setup code", http.StatusUnauthorized)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
110
internal/api/config_handlers_auto_register_test.go
Normal file
110
internal/api/config_handlers_auto_register_test.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
internalauth "github.com/rcourtman/pulse-go-rewrite/internal/auth"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
)
|
||||
|
||||
func newTestConfigHandlers(t *testing.T, cfg *config.Config) *ConfigHandlers {
|
||||
t.Helper()
|
||||
|
||||
h := &ConfigHandlers{
|
||||
config: cfg,
|
||||
persistence: config.NewConfigPersistence(cfg.DataPath),
|
||||
setupCodes: make(map[string]*SetupCode),
|
||||
recentSetupTokens: make(map[string]time.Time),
|
||||
lastClusterDetection: make(map[string]time.Time),
|
||||
recentAutoRegistered: make(map[string]time.Time),
|
||||
}
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func TestHandleAutoRegisterRejectsWithoutAuth(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
t.Setenv("PULSE_DATA_DIR", tempDir)
|
||||
|
||||
cfg := &config.Config{
|
||||
DataPath: tempDir,
|
||||
ConfigPath: tempDir,
|
||||
}
|
||||
|
||||
handler := newTestConfigHandlers(t, cfg)
|
||||
|
||||
reqBody := AutoRegisterRequest{
|
||||
Type: "pve",
|
||||
Host: "https://pve.local:8006",
|
||||
TokenID: "pulse-monitor@pam!token",
|
||||
TokenValue: "secret-token",
|
||||
ServerName: "pve.local",
|
||||
}
|
||||
|
||||
body, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal request: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/auto-register", bytes.NewReader(body))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
handler.HandleAutoRegister(rec, req)
|
||||
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Fatalf("expected status 401, got %d, body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHandleAutoRegisterAcceptsWithSetupToken(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
t.Setenv("PULSE_DATA_DIR", tempDir)
|
||||
|
||||
cfg := &config.Config{
|
||||
DataPath: tempDir,
|
||||
ConfigPath: tempDir,
|
||||
}
|
||||
|
||||
handler := newTestConfigHandlers(t, cfg)
|
||||
|
||||
const tokenValue = "TEMP-TOKEN"
|
||||
tokenHash := internalauth.HashAPIToken(tokenValue)
|
||||
handler.codeMutex.Lock()
|
||||
handler.setupCodes[tokenHash] = &SetupCode{
|
||||
ExpiresAt: time.Now().Add(5 * time.Minute),
|
||||
NodeType: "pve",
|
||||
}
|
||||
handler.codeMutex.Unlock()
|
||||
|
||||
reqBody := AutoRegisterRequest{
|
||||
Type: "pve",
|
||||
Host: "https://pve.local:8006",
|
||||
TokenID: "pulse-monitor@pam!token",
|
||||
TokenValue: "secret-token",
|
||||
ServerName: "pve.local",
|
||||
AuthToken: tokenValue,
|
||||
}
|
||||
|
||||
body, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to marshal request: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/auto-register", bytes.NewReader(body))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
handler.HandleAutoRegister(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected status 200, got %d, body=%s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
if len(cfg.PVEInstances) != 1 {
|
||||
t.Fatalf("expected 1 PVE instance stored, got %d", len(cfg.PVEInstances))
|
||||
}
|
||||
}
|
||||
@@ -33,17 +33,33 @@ type ConfigPersistence struct {
|
||||
crypto *crypto.CryptoManager
|
||||
}
|
||||
|
||||
// NewConfigPersistence creates a new config persistence manager
|
||||
// NewConfigPersistence creates a new config persistence manager.
|
||||
// The process terminates if encryption cannot be initialized to avoid
|
||||
// writing secrets to disk in plaintext.
|
||||
func NewConfigPersistence(configDir string) *ConfigPersistence {
|
||||
cp, err := newConfigPersistence(configDir)
|
||||
if err != nil {
|
||||
log.Fatal().
|
||||
Str("configDir", configDir).
|
||||
Err(err).
|
||||
Msg("Failed to initialize config persistence")
|
||||
}
|
||||
return cp
|
||||
}
|
||||
|
||||
func newConfigPersistence(configDir string) (*ConfigPersistence, error) {
|
||||
if configDir == "" {
|
||||
configDir = "/etc/pulse"
|
||||
if envDir := os.Getenv("PULSE_DATA_DIR"); envDir != "" {
|
||||
configDir = envDir
|
||||
} else {
|
||||
configDir = "/etc/pulse"
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize crypto manager
|
||||
cryptoMgr, err := crypto.NewCryptoManager()
|
||||
cryptoMgr, err := crypto.NewCryptoManagerAt(configDir)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to initialize crypto manager, using unencrypted storage")
|
||||
cryptoMgr = nil
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cp := &ConfigPersistence{
|
||||
@@ -66,7 +82,7 @@ func NewConfigPersistence(configDir string) *ConfigPersistence {
|
||||
Bool("encryptionEnabled", cryptoMgr != nil).
|
||||
Msg("Config persistence initialized")
|
||||
|
||||
return cp
|
||||
return cp, nil
|
||||
}
|
||||
|
||||
// EnsureConfigDir ensures the configuration directory exists
|
||||
|
||||
20
internal/config/persistence_fail_test.go
Normal file
20
internal/config/persistence_fail_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewConfigPersistenceFailsWhenEncryptedDataPresentWithoutKey(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
// Simulate existing encrypted data without providing the encryption key.
|
||||
if err := os.WriteFile(filepath.Join(dir, "nodes.enc"), []byte("ciphertext"), 0600); err != nil {
|
||||
t.Fatalf("failed to write simulated encrypted file: %v", err)
|
||||
}
|
||||
|
||||
if _, err := newConfigPersistence(dir); err == nil {
|
||||
t.Fatalf("expected error when initializing persistence without encryption key")
|
||||
}
|
||||
}
|
||||
@@ -19,9 +19,14 @@ type CryptoManager struct {
|
||||
key []byte
|
||||
}
|
||||
|
||||
// NewCryptoManager creates a new crypto manager
|
||||
// NewCryptoManager creates a new crypto manager using the default data directory.
|
||||
func NewCryptoManager() (*CryptoManager, error) {
|
||||
key, err := getOrCreateKey()
|
||||
return NewCryptoManagerAt(utils.GetDataDir())
|
||||
}
|
||||
|
||||
// NewCryptoManagerAt creates a new crypto manager with an explicit data directory override.
|
||||
func NewCryptoManagerAt(dataDir string) (*CryptoManager, error) {
|
||||
key, err := getOrCreateKeyAt(dataDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get encryption key: %w", err)
|
||||
}
|
||||
@@ -31,10 +36,12 @@ func NewCryptoManager() (*CryptoManager, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getOrCreateKey gets the encryption key or creates one if it doesn't exist
|
||||
func getOrCreateKey() ([]byte, error) {
|
||||
// Use data directory for key storage (for Docker persistence)
|
||||
dataDir := utils.GetDataDir()
|
||||
// getOrCreateKeyAt gets the encryption key or creates one if it doesn't exist
|
||||
func getOrCreateKeyAt(dataDir string) ([]byte, error) {
|
||||
if dataDir == "" {
|
||||
dataDir = utils.GetDataDir()
|
||||
}
|
||||
|
||||
keyPath := filepath.Join(dataDir, ".encryption.key")
|
||||
oldKeyPath := "/etc/pulse/.encryption.key"
|
||||
|
||||
|
||||
20
internal/monitoring/main_test.go
Normal file
20
internal/monitoring/main_test.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package monitoring
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
dataDir, err := os.MkdirTemp("", "monitoring-test-data-*")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer os.RemoveAll(dataDir)
|
||||
|
||||
if err := os.Setenv("PULSE_DATA_DIR", dataDir); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
Reference in New Issue
Block a user