Compare commits

...

2 Commits

Author SHA1 Message Date
henrygd
be70840609 test: update tests that use os.Setenv to t.Setenv 2026-03-20 15:00:28 -04:00
henrygd
565162ef5f refactor(hub): harden/enforce pb api rules and add tests
- separate collection related code from hub.go
- ensure hub is bootstrapped and collections updated automatically when
calling NewHub
2026-03-20 14:39:05 -04:00
19 changed files with 827 additions and 430 deletions

View File

@@ -70,19 +70,11 @@ func TestNewWebSocketClient(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
// Set up environment // Set up environment
if tc.hubURL != "" { if tc.hubURL != "" {
os.Setenv("BESZEL_AGENT_HUB_URL", tc.hubURL) t.Setenv("BESZEL_AGENT_HUB_URL", tc.hubURL)
} else {
os.Unsetenv("BESZEL_AGENT_HUB_URL")
} }
if tc.token != "" { if tc.token != "" {
os.Setenv("BESZEL_AGENT_TOKEN", tc.token) t.Setenv("BESZEL_AGENT_TOKEN", tc.token)
} else {
os.Unsetenv("BESZEL_AGENT_TOKEN")
} }
defer func() {
os.Unsetenv("BESZEL_AGENT_HUB_URL")
os.Unsetenv("BESZEL_AGENT_TOKEN")
}()
client, err := newWebSocketClient(agent) client, err := newWebSocketClient(agent)
@@ -138,12 +130,8 @@ func TestWebSocketClient_GetOptions(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
// Set up environment // Set up environment
os.Setenv("BESZEL_AGENT_HUB_URL", tc.inputURL) t.Setenv("BESZEL_AGENT_HUB_URL", tc.inputURL)
os.Setenv("BESZEL_AGENT_TOKEN", "test-token") t.Setenv("BESZEL_AGENT_TOKEN", "test-token")
defer func() {
os.Unsetenv("BESZEL_AGENT_HUB_URL")
os.Unsetenv("BESZEL_AGENT_TOKEN")
}()
client, err := newWebSocketClient(agent) client, err := newWebSocketClient(agent)
require.NoError(t, err) require.NoError(t, err)
@@ -185,12 +173,8 @@ func TestWebSocketClient_VerifySignature(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Set up environment // Set up environment
os.Setenv("BESZEL_AGENT_HUB_URL", "http://localhost:8080") t.Setenv("BESZEL_AGENT_HUB_URL", "http://localhost:8080")
os.Setenv("BESZEL_AGENT_TOKEN", "test-token") t.Setenv("BESZEL_AGENT_TOKEN", "test-token")
defer func() {
os.Unsetenv("BESZEL_AGENT_HUB_URL")
os.Unsetenv("BESZEL_AGENT_TOKEN")
}()
client, err := newWebSocketClient(agent) client, err := newWebSocketClient(agent)
require.NoError(t, err) require.NoError(t, err)
@@ -258,12 +242,8 @@ func TestWebSocketClient_HandleHubRequest(t *testing.T) {
agent := createTestAgent(t) agent := createTestAgent(t)
// Set up environment // Set up environment
os.Setenv("BESZEL_AGENT_HUB_URL", "http://localhost:8080") t.Setenv("BESZEL_AGENT_HUB_URL", "http://localhost:8080")
os.Setenv("BESZEL_AGENT_TOKEN", "test-token") t.Setenv("BESZEL_AGENT_TOKEN", "test-token")
defer func() {
os.Unsetenv("BESZEL_AGENT_HUB_URL")
os.Unsetenv("BESZEL_AGENT_TOKEN")
}()
client, err := newWebSocketClient(agent) client, err := newWebSocketClient(agent)
require.NoError(t, err) require.NoError(t, err)
@@ -350,13 +330,8 @@ func TestGetUserAgent(t *testing.T) {
func TestWebSocketClient_Close(t *testing.T) { func TestWebSocketClient_Close(t *testing.T) {
agent := createTestAgent(t) agent := createTestAgent(t)
// Set up environment t.Setenv("BESZEL_AGENT_HUB_URL", "http://localhost:8080")
os.Setenv("BESZEL_AGENT_HUB_URL", "http://localhost:8080") t.Setenv("BESZEL_AGENT_TOKEN", "test-token")
os.Setenv("BESZEL_AGENT_TOKEN", "test-token")
defer func() {
os.Unsetenv("BESZEL_AGENT_HUB_URL")
os.Unsetenv("BESZEL_AGENT_TOKEN")
}()
client, err := newWebSocketClient(agent) client, err := newWebSocketClient(agent)
require.NoError(t, err) require.NoError(t, err)
@@ -371,13 +346,8 @@ func TestWebSocketClient_Close(t *testing.T) {
func TestWebSocketClient_ConnectRateLimit(t *testing.T) { func TestWebSocketClient_ConnectRateLimit(t *testing.T) {
agent := createTestAgent(t) agent := createTestAgent(t)
// Set up environment t.Setenv("BESZEL_AGENT_HUB_URL", "http://localhost:8080")
os.Setenv("BESZEL_AGENT_HUB_URL", "http://localhost:8080") t.Setenv("BESZEL_AGENT_TOKEN", "test-token")
os.Setenv("BESZEL_AGENT_TOKEN", "test-token")
defer func() {
os.Unsetenv("BESZEL_AGENT_HUB_URL")
os.Unsetenv("BESZEL_AGENT_TOKEN")
}()
client, err := newWebSocketClient(agent) client, err := newWebSocketClient(agent)
require.NoError(t, err) require.NoError(t, err)
@@ -393,20 +363,10 @@ func TestWebSocketClient_ConnectRateLimit(t *testing.T) {
// TestGetToken tests the getToken function with various scenarios // TestGetToken tests the getToken function with various scenarios
func TestGetToken(t *testing.T) { func TestGetToken(t *testing.T) {
unsetEnvVars := func() {
os.Unsetenv("BESZEL_AGENT_TOKEN")
os.Unsetenv("TOKEN")
os.Unsetenv("BESZEL_AGENT_TOKEN_FILE")
os.Unsetenv("TOKEN_FILE")
}
t.Run("token from TOKEN environment variable", func(t *testing.T) { t.Run("token from TOKEN environment variable", func(t *testing.T) {
unsetEnvVars()
// Set TOKEN env var // Set TOKEN env var
expectedToken := "test-token-from-env" expectedToken := "test-token-from-env"
os.Setenv("TOKEN", expectedToken) t.Setenv("TOKEN", expectedToken)
defer os.Unsetenv("TOKEN")
token, err := getToken() token, err := getToken()
assert.NoError(t, err) assert.NoError(t, err)
@@ -414,12 +374,9 @@ func TestGetToken(t *testing.T) {
}) })
t.Run("token from BESZEL_AGENT_TOKEN environment variable", func(t *testing.T) { t.Run("token from BESZEL_AGENT_TOKEN environment variable", func(t *testing.T) {
unsetEnvVars()
// Set BESZEL_AGENT_TOKEN env var (should take precedence) // Set BESZEL_AGENT_TOKEN env var (should take precedence)
expectedToken := "test-token-from-beszel-env" expectedToken := "test-token-from-beszel-env"
os.Setenv("BESZEL_AGENT_TOKEN", expectedToken) t.Setenv("BESZEL_AGENT_TOKEN", expectedToken)
defer os.Unsetenv("BESZEL_AGENT_TOKEN")
token, err := getToken() token, err := getToken()
assert.NoError(t, err) assert.NoError(t, err)
@@ -427,8 +384,6 @@ func TestGetToken(t *testing.T) {
}) })
t.Run("token from TOKEN_FILE", func(t *testing.T) { t.Run("token from TOKEN_FILE", func(t *testing.T) {
unsetEnvVars()
// Create a temporary token file // Create a temporary token file
expectedToken := "test-token-from-file" expectedToken := "test-token-from-file"
tokenFile, err := os.CreateTemp("", "token-test-*.txt") tokenFile, err := os.CreateTemp("", "token-test-*.txt")
@@ -440,8 +395,7 @@ func TestGetToken(t *testing.T) {
tokenFile.Close() tokenFile.Close()
// Set TOKEN_FILE env var // Set TOKEN_FILE env var
os.Setenv("TOKEN_FILE", tokenFile.Name()) t.Setenv("TOKEN_FILE", tokenFile.Name())
defer os.Unsetenv("TOKEN_FILE")
token, err := getToken() token, err := getToken()
assert.NoError(t, err) assert.NoError(t, err)
@@ -449,8 +403,6 @@ func TestGetToken(t *testing.T) {
}) })
t.Run("token from BESZEL_AGENT_TOKEN_FILE", func(t *testing.T) { t.Run("token from BESZEL_AGENT_TOKEN_FILE", func(t *testing.T) {
unsetEnvVars()
// Create a temporary token file // Create a temporary token file
expectedToken := "test-token-from-beszel-file" expectedToken := "test-token-from-beszel-file"
tokenFile, err := os.CreateTemp("", "token-test-*.txt") tokenFile, err := os.CreateTemp("", "token-test-*.txt")
@@ -462,8 +414,7 @@ func TestGetToken(t *testing.T) {
tokenFile.Close() tokenFile.Close()
// Set BESZEL_AGENT_TOKEN_FILE env var (should take precedence) // Set BESZEL_AGENT_TOKEN_FILE env var (should take precedence)
os.Setenv("BESZEL_AGENT_TOKEN_FILE", tokenFile.Name()) t.Setenv("BESZEL_AGENT_TOKEN_FILE", tokenFile.Name())
defer os.Unsetenv("BESZEL_AGENT_TOKEN_FILE")
token, err := getToken() token, err := getToken()
assert.NoError(t, err) assert.NoError(t, err)
@@ -471,8 +422,6 @@ func TestGetToken(t *testing.T) {
}) })
t.Run("TOKEN takes precedence over TOKEN_FILE", func(t *testing.T) { t.Run("TOKEN takes precedence over TOKEN_FILE", func(t *testing.T) {
unsetEnvVars()
// Create a temporary token file // Create a temporary token file
fileToken := "token-from-file" fileToken := "token-from-file"
tokenFile, err := os.CreateTemp("", "token-test-*.txt") tokenFile, err := os.CreateTemp("", "token-test-*.txt")
@@ -485,12 +434,8 @@ func TestGetToken(t *testing.T) {
// Set both TOKEN and TOKEN_FILE // Set both TOKEN and TOKEN_FILE
envToken := "token-from-env" envToken := "token-from-env"
os.Setenv("TOKEN", envToken) t.Setenv("TOKEN", envToken)
os.Setenv("TOKEN_FILE", tokenFile.Name()) t.Setenv("TOKEN_FILE", tokenFile.Name())
defer func() {
os.Unsetenv("TOKEN")
os.Unsetenv("TOKEN_FILE")
}()
token, err := getToken() token, err := getToken()
assert.NoError(t, err) assert.NoError(t, err)
@@ -498,7 +443,10 @@ func TestGetToken(t *testing.T) {
}) })
t.Run("error when neither TOKEN nor TOKEN_FILE is set", func(t *testing.T) { t.Run("error when neither TOKEN nor TOKEN_FILE is set", func(t *testing.T) {
unsetEnvVars() t.Setenv("BESZEL_AGENT_TOKEN", "")
t.Setenv("TOKEN", "")
t.Setenv("BESZEL_AGENT_TOKEN_FILE", "")
t.Setenv("TOKEN_FILE", "")
token, err := getToken() token, err := getToken()
assert.Error(t, err) assert.Error(t, err)
@@ -507,11 +455,8 @@ func TestGetToken(t *testing.T) {
}) })
t.Run("error when TOKEN_FILE points to non-existent file", func(t *testing.T) { t.Run("error when TOKEN_FILE points to non-existent file", func(t *testing.T) {
unsetEnvVars()
// Set TOKEN_FILE to a non-existent file // Set TOKEN_FILE to a non-existent file
os.Setenv("TOKEN_FILE", "/non/existent/file.txt") t.Setenv("TOKEN_FILE", "/non/existent/file.txt")
defer os.Unsetenv("TOKEN_FILE")
token, err := getToken() token, err := getToken()
assert.Error(t, err) assert.Error(t, err)
@@ -520,8 +465,6 @@ func TestGetToken(t *testing.T) {
}) })
t.Run("handles empty token file", func(t *testing.T) { t.Run("handles empty token file", func(t *testing.T) {
unsetEnvVars()
// Create an empty token file // Create an empty token file
tokenFile, err := os.CreateTemp("", "token-test-*.txt") tokenFile, err := os.CreateTemp("", "token-test-*.txt")
require.NoError(t, err) require.NoError(t, err)
@@ -529,8 +472,7 @@ func TestGetToken(t *testing.T) {
tokenFile.Close() tokenFile.Close()
// Set TOKEN_FILE env var // Set TOKEN_FILE env var
os.Setenv("TOKEN_FILE", tokenFile.Name()) t.Setenv("TOKEN_FILE", tokenFile.Name())
defer os.Unsetenv("TOKEN_FILE")
token, err := getToken() token, err := getToken()
assert.NoError(t, err) assert.NoError(t, err)
@@ -538,8 +480,6 @@ func TestGetToken(t *testing.T) {
}) })
t.Run("strips whitespace from TOKEN_FILE", func(t *testing.T) { t.Run("strips whitespace from TOKEN_FILE", func(t *testing.T) {
unsetEnvVars()
tokenWithWhitespace := " test-token-with-whitespace \n\t" tokenWithWhitespace := " test-token-with-whitespace \n\t"
expectedToken := "test-token-with-whitespace" expectedToken := "test-token-with-whitespace"
tokenFile, err := os.CreateTemp("", "token-test-*.txt") tokenFile, err := os.CreateTemp("", "token-test-*.txt")
@@ -550,8 +490,7 @@ func TestGetToken(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
tokenFile.Close() tokenFile.Close()
os.Setenv("TOKEN_FILE", tokenFile.Name()) t.Setenv("TOKEN_FILE", tokenFile.Name())
defer os.Unsetenv("TOKEN_FILE")
token, err := getToken() token, err := getToken()
assert.NoError(t, err) assert.NoError(t, err)

View File

@@ -7,7 +7,6 @@ import (
"fmt" "fmt"
"net" "net"
"net/url" "net/url"
"os"
"testing" "testing"
"time" "time"
@@ -183,10 +182,6 @@ func TestConnectionManager_TickerManagement(t *testing.T) {
// TestConnectionManager_WebSocketConnectionFlow tests WebSocket connection logic // TestConnectionManager_WebSocketConnectionFlow tests WebSocket connection logic
func TestConnectionManager_WebSocketConnectionFlow(t *testing.T) { func TestConnectionManager_WebSocketConnectionFlow(t *testing.T) {
if testing.Short() {
t.Skip("Skipping WebSocket connection test in short mode")
}
agent := createTestAgent(t) agent := createTestAgent(t)
cm := agent.connectionManager cm := agent.connectionManager
@@ -196,19 +191,18 @@ func TestConnectionManager_WebSocketConnectionFlow(t *testing.T) {
assert.Equal(t, Disconnected, cm.State, "State should remain Disconnected after failed connection") assert.Equal(t, Disconnected, cm.State, "State should remain Disconnected after failed connection")
// Test with invalid URL // Test with invalid URL
os.Setenv("BESZEL_AGENT_HUB_URL", "invalid-url") t.Setenv("BESZEL_AGENT_HUB_URL", "1,33%")
os.Setenv("BESZEL_AGENT_TOKEN", "test-token") t.Setenv("BESZEL_AGENT_TOKEN", "test-token")
defer func() {
os.Unsetenv("BESZEL_AGENT_HUB_URL")
os.Unsetenv("BESZEL_AGENT_TOKEN")
}()
// Test with missing token
os.Setenv("BESZEL_AGENT_HUB_URL", "http://localhost:8080")
os.Unsetenv("BESZEL_AGENT_TOKEN")
_, err2 := newWebSocketClient(agent) _, err2 := newWebSocketClient(agent)
assert.Error(t, err2, "WebSocket client creation should fail without token") assert.Error(t, err2, "WebSocket client creation should fail with invalid URL")
// Test with missing token
t.Setenv("BESZEL_AGENT_HUB_URL", "http://localhost:8080")
t.Setenv("BESZEL_AGENT_TOKEN", "")
_, err3 := newWebSocketClient(agent)
assert.Error(t, err3, "WebSocket client creation should fail without token")
} }
// TestConnectionManager_ReconnectionLogic tests reconnection prevention logic // TestConnectionManager_ReconnectionLogic tests reconnection prevention logic
@@ -234,12 +228,8 @@ func TestConnectionManager_ConnectWithRateLimit(t *testing.T) {
cm := agent.connectionManager cm := agent.connectionManager
// Set up environment for WebSocket client creation // Set up environment for WebSocket client creation
os.Setenv("BESZEL_AGENT_HUB_URL", "ws://localhost:8080") t.Setenv("BESZEL_AGENT_HUB_URL", "ws://localhost:8080")
os.Setenv("BESZEL_AGENT_TOKEN", "test-token") t.Setenv("BESZEL_AGENT_TOKEN", "test-token")
defer func() {
os.Unsetenv("BESZEL_AGENT_HUB_URL")
os.Unsetenv("BESZEL_AGENT_TOKEN")
}()
// Create WebSocket client // Create WebSocket client
wsClient, err := newWebSocketClient(agent) wsClient, err := newWebSocketClient(agent)
@@ -285,12 +275,8 @@ func TestConnectionManager_CloseWebSocket(t *testing.T) {
}, "Should not panic when closing nil WebSocket client") }, "Should not panic when closing nil WebSocket client")
// Set up environment and create WebSocket client // Set up environment and create WebSocket client
os.Setenv("BESZEL_AGENT_HUB_URL", "ws://localhost:8080") t.Setenv("BESZEL_AGENT_HUB_URL", "ws://localhost:8080")
os.Setenv("BESZEL_AGENT_TOKEN", "test-token") t.Setenv("BESZEL_AGENT_TOKEN", "test-token")
defer func() {
os.Unsetenv("BESZEL_AGENT_HUB_URL")
os.Unsetenv("BESZEL_AGENT_TOKEN")
}()
wsClient, err := newWebSocketClient(agent) wsClient, err := newWebSocketClient(agent)
require.NoError(t, err) require.NoError(t, err)

View File

@@ -39,17 +39,7 @@ func TestGetDataDir(t *testing.T) {
t.Run("DATA_DIR environment variable", func(t *testing.T) { t.Run("DATA_DIR environment variable", func(t *testing.T) {
tempDir := t.TempDir() tempDir := t.TempDir()
// Set environment variable t.Setenv("BESZEL_AGENT_DATA_DIR", tempDir)
oldValue := os.Getenv("DATA_DIR")
defer func() {
if oldValue == "" {
os.Unsetenv("BESZEL_AGENT_DATA_DIR")
} else {
os.Setenv("BESZEL_AGENT_DATA_DIR", oldValue)
}
}()
os.Setenv("BESZEL_AGENT_DATA_DIR", tempDir)
result, err := GetDataDir() result, err := GetDataDir()
require.NoError(t, err) require.NoError(t, err)
@@ -65,17 +55,6 @@ func TestGetDataDir(t *testing.T) {
// Test fallback behavior (empty dataDir, no env var) // Test fallback behavior (empty dataDir, no env var)
t.Run("fallback to default directories", func(t *testing.T) { t.Run("fallback to default directories", func(t *testing.T) {
// Clear DATA_DIR environment variable
oldValue := os.Getenv("DATA_DIR")
defer func() {
if oldValue == "" {
os.Unsetenv("DATA_DIR")
} else {
os.Setenv("DATA_DIR", oldValue)
}
}()
os.Unsetenv("DATA_DIR")
// This will try platform-specific defaults, which may or may not work // This will try platform-specific defaults, which may or may not work
// We're mainly testing that it doesn't panic and returns some result // We're mainly testing that it doesn't panic and returns some result
result, err := GetDataDir() result, err := GetDataDir()

View File

@@ -687,18 +687,8 @@ func TestIsDockerSpecialMountpoint(t *testing.T) {
} }
func TestInitializeDiskInfoWithCustomNames(t *testing.T) { func TestInitializeDiskInfoWithCustomNames(t *testing.T) {
// Set up environment variables
oldEnv := os.Getenv("EXTRA_FILESYSTEMS")
defer func() {
if oldEnv != "" {
os.Setenv("EXTRA_FILESYSTEMS", oldEnv)
} else {
os.Unsetenv("EXTRA_FILESYSTEMS")
}
}()
// Test with custom names // Test with custom names
os.Setenv("EXTRA_FILESYSTEMS", "sda1__my-storage,/dev/sdb1__backup-drive,nvme0n1p2") t.Setenv("EXTRA_FILESYSTEMS", "sda1__my-storage,/dev/sdb1__backup-drive,nvme0n1p2")
// Mock disk partitions (we'll just test the parsing logic) // Mock disk partitions (we'll just test the parsing logic)
// Since the actual disk operations are system-dependent, we'll focus on the parsing // Since the actual disk operations are system-dependent, we'll focus on the parsing
@@ -726,7 +716,7 @@ func TestInitializeDiskInfoWithCustomNames(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run("env_"+tc.envValue, func(t *testing.T) { t.Run("env_"+tc.envValue, func(t *testing.T) {
os.Setenv("EXTRA_FILESYSTEMS", tc.envValue) t.Setenv("EXTRA_FILESYSTEMS", tc.envValue)
// Create mock partitions that would match our test cases // Create mock partitions that would match our test cases
partitions := []disk.PartitionStat{} partitions := []disk.PartitionStat{}

View File

@@ -1083,8 +1083,6 @@ func TestCalculateGPUAverage(t *testing.T) {
func TestGPUCapabilitiesAndLegacyPriority(t *testing.T) { func TestGPUCapabilitiesAndLegacyPriority(t *testing.T) {
// Save original PATH // Save original PATH
origPath := os.Getenv("PATH")
defer os.Setenv("PATH", origPath)
hasAmdSysfs := (&GPUManager{}).hasAmdSysfs() hasAmdSysfs := (&GPUManager{}).hasAmdSysfs()
tests := []struct { tests := []struct {
@@ -1178,7 +1176,7 @@ echo "[]"`
{ {
name: "no gpu tools available", name: "no gpu tools available",
setupCommands: func(_ string) error { setupCommands: func(_ string) error {
os.Setenv("PATH", "") t.Setenv("PATH", "")
return nil return nil
}, },
wantErr: true, wantErr: true,
@@ -1188,7 +1186,7 @@ echo "[]"`
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
tempDir := t.TempDir() tempDir := t.TempDir()
os.Setenv("PATH", tempDir) t.Setenv("PATH", tempDir)
if err := tt.setupCommands(tempDir); err != nil { if err := tt.setupCommands(tempDir); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -1234,13 +1232,9 @@ echo "[]"`
} }
func TestCollectorStartHelpers(t *testing.T) { func TestCollectorStartHelpers(t *testing.T) {
// Save original PATH
origPath := os.Getenv("PATH")
defer os.Setenv("PATH", origPath)
// Set up temp dir with the commands // Set up temp dir with the commands
dir := t.TempDir() dir := t.TempDir()
os.Setenv("PATH", dir) t.Setenv("PATH", dir)
tests := []struct { tests := []struct {
name string name string
@@ -1370,11 +1364,8 @@ echo '[{"device_name":"NVIDIA Test GPU","temp":"52C","power_draw":"31W","gpu_uti
} }
func TestNewGPUManagerPriorityNvtopFallback(t *testing.T) { func TestNewGPUManagerPriorityNvtopFallback(t *testing.T) {
origPath := os.Getenv("PATH")
defer os.Setenv("PATH", origPath)
dir := t.TempDir() dir := t.TempDir()
os.Setenv("PATH", dir) t.Setenv("PATH", dir)
t.Setenv("BESZEL_AGENT_GPU_COLLECTOR", "nvtop,nvidia-smi") t.Setenv("BESZEL_AGENT_GPU_COLLECTOR", "nvtop,nvidia-smi")
nvtopPath := filepath.Join(dir, "nvtop") nvtopPath := filepath.Join(dir, "nvtop")
@@ -1399,11 +1390,8 @@ echo "0, NVIDIA Priority GPU, 45, 512, 2048, 12, 25"`
} }
func TestNewGPUManagerPriorityMixedCollectors(t *testing.T) { func TestNewGPUManagerPriorityMixedCollectors(t *testing.T) {
origPath := os.Getenv("PATH")
defer os.Setenv("PATH", origPath)
dir := t.TempDir() dir := t.TempDir()
os.Setenv("PATH", dir) t.Setenv("PATH", dir)
t.Setenv("BESZEL_AGENT_GPU_COLLECTOR", "intel_gpu_top,rocm-smi") t.Setenv("BESZEL_AGENT_GPU_COLLECTOR", "intel_gpu_top,rocm-smi")
intelPath := filepath.Join(dir, "intel_gpu_top") intelPath := filepath.Join(dir, "intel_gpu_top")
@@ -1433,11 +1421,8 @@ echo '{"card0": {"Temperature (Sensor edge) (C)": "49.0", "Current Socket Graphi
} }
func TestNewGPUManagerPriorityNvmlFallbackToNvidiaSmi(t *testing.T) { func TestNewGPUManagerPriorityNvmlFallbackToNvidiaSmi(t *testing.T) {
origPath := os.Getenv("PATH")
defer os.Setenv("PATH", origPath)
dir := t.TempDir() dir := t.TempDir()
os.Setenv("PATH", dir) t.Setenv("PATH", dir)
t.Setenv("BESZEL_AGENT_GPU_COLLECTOR", "nvml,nvidia-smi") t.Setenv("BESZEL_AGENT_GPU_COLLECTOR", "nvml,nvidia-smi")
nvidiaPath := filepath.Join(dir, "nvidia-smi") nvidiaPath := filepath.Join(dir, "nvidia-smi")
@@ -1456,11 +1441,8 @@ echo "0, NVIDIA Fallback GPU, 41, 256, 1024, 8, 14"`
} }
func TestNewGPUManagerConfiguredCollectorsMustStart(t *testing.T) { func TestNewGPUManagerConfiguredCollectorsMustStart(t *testing.T) {
origPath := os.Getenv("PATH")
defer os.Setenv("PATH", origPath)
dir := t.TempDir() dir := t.TempDir()
os.Setenv("PATH", dir) t.Setenv("PATH", dir)
t.Run("configured valid collector unavailable", func(t *testing.T) { t.Run("configured valid collector unavailable", func(t *testing.T) {
t.Setenv("BESZEL_AGENT_GPU_COLLECTOR", "nvidia-smi") t.Setenv("BESZEL_AGENT_GPU_COLLECTOR", "nvidia-smi")
@@ -1480,11 +1462,8 @@ func TestNewGPUManagerConfiguredCollectorsMustStart(t *testing.T) {
} }
func TestNewGPUManagerJetsonIgnoresCollectorConfig(t *testing.T) { func TestNewGPUManagerJetsonIgnoresCollectorConfig(t *testing.T) {
origPath := os.Getenv("PATH")
defer os.Setenv("PATH", origPath)
dir := t.TempDir() dir := t.TempDir()
os.Setenv("PATH", dir) t.Setenv("PATH", dir)
t.Setenv("BESZEL_AGENT_GPU_COLLECTOR", "nvidia-smi") t.Setenv("BESZEL_AGENT_GPU_COLLECTOR", "nvidia-smi")
tegraPath := filepath.Join(dir, "tegrastats") tegraPath := filepath.Join(dir, "tegrastats")
@@ -1719,12 +1698,8 @@ func TestIntelUpdateFromStats(t *testing.T) {
} }
func TestIntelCollectorStreaming(t *testing.T) { func TestIntelCollectorStreaming(t *testing.T) {
// Save and override PATH
origPath := os.Getenv("PATH")
defer os.Setenv("PATH", origPath)
dir := t.TempDir() dir := t.TempDir()
os.Setenv("PATH", dir) t.Setenv("PATH", dir)
// Create a fake intel_gpu_top that prints -l format with four samples (first will be skipped) and exits // Create a fake intel_gpu_top that prints -l format with four samples (first will be skipped) and exits
scriptPath := filepath.Join(dir, "intel_gpu_top") scriptPath := filepath.Join(dir, "intel_gpu_top")

View File

@@ -5,7 +5,6 @@ package agent
import ( import (
"context" "context"
"fmt" "fmt"
"os"
"testing" "testing"
"github.com/henrygd/beszel/internal/entities/system" "github.com/henrygd/beszel/internal/entities/system"
@@ -329,34 +328,10 @@ func TestNewSensorConfigWithEnv(t *testing.T) {
} }
func TestNewSensorConfig(t *testing.T) { func TestNewSensorConfig(t *testing.T) {
// Save original environment variables
originalPrimary, hasPrimary := os.LookupEnv("BESZEL_AGENT_PRIMARY_SENSOR")
originalSys, hasSys := os.LookupEnv("BESZEL_AGENT_SYS_SENSORS")
originalSensors, hasSensors := os.LookupEnv("BESZEL_AGENT_SENSORS")
// Restore environment variables after the test
defer func() {
// Clean up test environment variables
os.Unsetenv("BESZEL_AGENT_PRIMARY_SENSOR")
os.Unsetenv("BESZEL_AGENT_SYS_SENSORS")
os.Unsetenv("BESZEL_AGENT_SENSORS")
// Restore original values if they existed
if hasPrimary {
os.Setenv("BESZEL_AGENT_PRIMARY_SENSOR", originalPrimary)
}
if hasSys {
os.Setenv("BESZEL_AGENT_SYS_SENSORS", originalSys)
}
if hasSensors {
os.Setenv("BESZEL_AGENT_SENSORS", originalSensors)
}
}()
// Set test environment variables // Set test environment variables
os.Setenv("BESZEL_AGENT_PRIMARY_SENSOR", "test_primary") t.Setenv("BESZEL_AGENT_PRIMARY_SENSOR", "test_primary")
os.Setenv("BESZEL_AGENT_SYS_SENSORS", "/test/path") t.Setenv("BESZEL_AGENT_SYS_SENSORS", "/test/path")
os.Setenv("BESZEL_AGENT_SENSORS", "test_sensor1,test_*,test_sensor3") t.Setenv("BESZEL_AGENT_SENSORS", "test_sensor1,test_*,test_sensor3")
agent := &Agent{} agent := &Agent{}
result := agent.newSensorConfig() result := agent.newSensorConfig()

View File

@@ -183,8 +183,7 @@ func TestStartServer(t *testing.T) {
} }
func TestStartServerDisableSSH(t *testing.T) { func TestStartServerDisableSSH(t *testing.T) {
os.Setenv("BESZEL_AGENT_DISABLE_SSH", "true") t.Setenv("BESZEL_AGENT_DISABLE_SSH", "true")
defer os.Unsetenv("BESZEL_AGENT_DISABLE_SSH")
agent, err := NewAgent("") agent, err := NewAgent("")
require.NoError(t, err) require.NoError(t, err)

View File

@@ -1035,7 +1035,7 @@ func TestRefreshExcludedDevices(t *testing.T) {
t.Setenv("EXCLUDE_SMART", tt.envValue) t.Setenv("EXCLUDE_SMART", tt.envValue)
} else { } else {
// Ensure env var is not set for empty test // Ensure env var is not set for empty test
os.Unsetenv("EXCLUDE_SMART") t.Setenv("EXCLUDE_SMART", "")
} }
sm := &SmartManager{} sm := &SmartManager{}

View File

@@ -167,16 +167,12 @@ func TestGetServicePatterns(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
// Clean up any existing env vars
os.Unsetenv("BESZEL_AGENT_SERVICE_PATTERNS")
os.Unsetenv("SERVICE_PATTERNS")
// Set up environment variables // Set up environment variables
if tt.prefixedEnv != "" { if tt.prefixedEnv != "" {
os.Setenv("BESZEL_AGENT_SERVICE_PATTERNS", tt.prefixedEnv) t.Setenv("BESZEL_AGENT_SERVICE_PATTERNS", tt.prefixedEnv)
} }
if tt.unprefixedEnv != "" { if tt.unprefixedEnv != "" {
os.Setenv("SERVICE_PATTERNS", tt.unprefixedEnv) t.Setenv("SERVICE_PATTERNS", tt.unprefixedEnv)
} }
// Run the function // Run the function
@@ -184,12 +180,6 @@ func TestGetServicePatterns(t *testing.T) {
// Verify results // Verify results
assert.Equal(t, tt.expected, result, "Patterns should match expected values") assert.Equal(t, tt.expected, result, "Patterns should match expected values")
// Cleanup
if tt.cleanupEnvVars {
os.Unsetenv("BESZEL_AGENT_SERVICE_PATTERNS")
os.Unsetenv("SERVICE_PATTERNS")
}
}) })
} }
} }

View File

@@ -134,10 +134,8 @@ func TestGetEnv(t *testing.T) {
prefixedKey := "BESZEL_AGENT_" + key prefixedKey := "BESZEL_AGENT_" + key
t.Run("prefixed variable exists", func(t *testing.T) { t.Run("prefixed variable exists", func(t *testing.T) {
os.Setenv(prefixedKey, "prefixed_val") t.Setenv(prefixedKey, "prefixed_val")
os.Setenv(key, "unprefixed_val") t.Setenv(key, "unprefixed_val")
defer os.Unsetenv(prefixedKey)
defer os.Unsetenv(key)
val, exists := GetEnv(key) val, exists := GetEnv(key)
assert.True(t, exists) assert.True(t, exists)
@@ -145,9 +143,7 @@ func TestGetEnv(t *testing.T) {
}) })
t.Run("only unprefixed variable exists", func(t *testing.T) { t.Run("only unprefixed variable exists", func(t *testing.T) {
os.Unsetenv(prefixedKey) t.Setenv(key, "unprefixed_val")
os.Setenv(key, "unprefixed_val")
defer os.Unsetenv(key)
val, exists := GetEnv(key) val, exists := GetEnv(key)
assert.True(t, exists) assert.True(t, exists)
@@ -155,9 +151,6 @@ func TestGetEnv(t *testing.T) {
}) })
t.Run("neither variable exists", func(t *testing.T) { t.Run("neither variable exists", func(t *testing.T) {
os.Unsetenv(prefixedKey)
os.Unsetenv(key)
val, exists := GetEnv(key) val, exists := GetEnv(key)
assert.False(t, exists) assert.False(t, exists)
assert.Empty(t, val) assert.Empty(t, val)

View File

@@ -28,7 +28,7 @@ func main() {
} }
baseApp := getBaseApp() baseApp := getBaseApp()
h := hub.NewHub(baseApp) h, _ := hub.NewHub(baseApp)
if err := h.StartHub(); err != nil { if err := h.StartHub(); err != nil {
log.Fatal(err) log.Fatal(err)
} }

View File

@@ -32,7 +32,8 @@ func createTestHub(t testing.TB) (*Hub, *pbtests.TestApp, error) {
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
return NewHub(testApp), testApp, nil h, err := NewHub(testApp)
return h, testApp, err
} }
// cleanupTestHub stops background system goroutines before tearing down the app. // cleanupTestHub stops background system goroutines before tearing down the app.
@@ -897,12 +898,8 @@ func TestAgentWebSocketIntegration(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Set up environment variables for the agent // Set up environment variables for the agent
os.Setenv("BESZEL_AGENT_HUB_URL", ts.URL) t.Setenv("BESZEL_AGENT_HUB_URL", ts.URL)
os.Setenv("BESZEL_AGENT_TOKEN", tc.agentToken) t.Setenv("BESZEL_AGENT_TOKEN", tc.agentToken)
defer func() {
os.Unsetenv("BESZEL_AGENT_HUB_URL")
os.Unsetenv("BESZEL_AGENT_TOKEN")
}()
// Start agent in background // Start agent in background
done := make(chan error, 1) done := make(chan error, 1)
@@ -1080,12 +1077,8 @@ func TestMultipleSystemsWithSameUniversalToken(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Set up environment variables for the agent // Set up environment variables for the agent
os.Setenv("BESZEL_AGENT_HUB_URL", ts.URL) t.Setenv("BESZEL_AGENT_HUB_URL", ts.URL)
os.Setenv("BESZEL_AGENT_TOKEN", universalToken) t.Setenv("BESZEL_AGENT_TOKEN", universalToken)
defer func() {
os.Unsetenv("BESZEL_AGENT_HUB_URL")
os.Unsetenv("BESZEL_AGENT_TOKEN")
}()
// Count systems before connection // Count systems before connection
systemsBefore, err := testApp.FindRecordsByFilter("systems", "users ~ {:userId}", "", -1, 0, map[string]any{"userId": userRecord.Id}) systemsBefore, err := testApp.FindRecordsByFilter("systems", "users ~ {:userId}", "", -1, 0, map[string]any{"userId": userRecord.Id})
@@ -1243,12 +1236,8 @@ func TestPermanentUniversalTokenFromDB(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Set up environment variables for the agent // Set up environment variables for the agent
os.Setenv("BESZEL_AGENT_HUB_URL", ts.URL) t.Setenv("BESZEL_AGENT_HUB_URL", ts.URL)
os.Setenv("BESZEL_AGENT_TOKEN", universalToken) t.Setenv("BESZEL_AGENT_TOKEN", universalToken)
defer func() {
os.Unsetenv("BESZEL_AGENT_HUB_URL")
os.Unsetenv("BESZEL_AGENT_TOKEN")
}()
// Start agent in background // Start agent in background
done := make(chan error, 1) done := make(chan error, 1)

128
internal/hub/collections.go Normal file
View File

@@ -0,0 +1,128 @@
package hub
import "github.com/pocketbase/pocketbase/core"
type collectionRules struct {
list *string
view *string
create *string
update *string
delete *string
}
// setCollectionAuthSettings applies Beszel's collection auth settings.
func setCollectionAuthSettings(app core.App) error {
usersCollection, err := app.FindCollectionByNameOrId("users")
if err != nil {
return err
}
superusersCollection, err := app.FindCollectionByNameOrId(core.CollectionNameSuperusers)
if err != nil {
return err
}
// disable email auth if DISABLE_PASSWORD_AUTH env var is set
disablePasswordAuth, _ := GetEnv("DISABLE_PASSWORD_AUTH")
usersCollection.PasswordAuth.Enabled = disablePasswordAuth != "true"
usersCollection.PasswordAuth.IdentityFields = []string{"email"}
// allow oauth user creation if USER_CREATION is set
if userCreation, _ := GetEnv("USER_CREATION"); userCreation == "true" {
cr := "@request.context = 'oauth2'"
usersCollection.CreateRule = &cr
} else {
usersCollection.CreateRule = nil
}
// enable mfaOtp mfa if MFA_OTP env var is set
mfaOtp, _ := GetEnv("MFA_OTP")
usersCollection.OTP.Length = 6
superusersCollection.OTP.Length = 6
usersCollection.OTP.Enabled = mfaOtp == "true"
usersCollection.MFA.Enabled = mfaOtp == "true"
superusersCollection.OTP.Enabled = mfaOtp == "true" || mfaOtp == "superusers"
superusersCollection.MFA.Enabled = mfaOtp == "true" || mfaOtp == "superusers"
if err := app.Save(superusersCollection); err != nil {
return err
}
if err := app.Save(usersCollection); err != nil {
return err
}
// When SHARE_ALL_SYSTEMS is enabled, any authenticated user can read
// system-scoped data. Write rules continue to block readonly users.
shareAllSystems, _ := GetEnv("SHARE_ALL_SYSTEMS")
authenticatedRule := "@request.auth.id != \"\""
systemsMemberRule := authenticatedRule + " && users.id ?= @request.auth.id"
systemMemberRule := authenticatedRule + " && system.users.id ?= @request.auth.id"
systemsReadRule := systemsMemberRule
systemScopedReadRule := systemMemberRule
if shareAllSystems == "true" {
systemsReadRule = authenticatedRule
systemScopedReadRule = authenticatedRule
}
systemsWriteRule := systemsReadRule + " && @request.auth.role != \"readonly\""
systemScopedWriteRule := systemScopedReadRule + " && @request.auth.role != \"readonly\""
if err := applyCollectionRules(app, []string{"systems"}, collectionRules{
list: &systemsReadRule,
view: &systemsReadRule,
create: &systemsWriteRule,
update: &systemsWriteRule,
delete: &systemsWriteRule,
}); err != nil {
return err
}
if err := applyCollectionRules(app, []string{"containers", "container_stats", "system_stats", "systemd_services"}, collectionRules{
list: &systemScopedReadRule,
}); err != nil {
return err
}
if err := applyCollectionRules(app, []string{"smart_devices"}, collectionRules{
list: &systemScopedReadRule,
view: &systemScopedReadRule,
delete: &systemScopedWriteRule,
}); err != nil {
return err
}
if err := applyCollectionRules(app, []string{"fingerprints"}, collectionRules{
list: &systemScopedReadRule,
view: &systemScopedReadRule,
create: &systemScopedWriteRule,
update: &systemScopedWriteRule,
delete: &systemScopedWriteRule,
}); err != nil {
return err
}
if err := applyCollectionRules(app, []string{"system_details"}, collectionRules{
list: &systemScopedReadRule,
view: &systemScopedReadRule,
}); err != nil {
return err
}
return nil
}
func applyCollectionRules(app core.App, collectionNames []string, rules collectionRules) error {
for _, collectionName := range collectionNames {
collection, err := app.FindCollectionByNameOrId(collectionName)
if err != nil {
return err
}
collection.ListRule = rules.list
collection.ViewRule = rules.view
collection.CreateRule = rules.create
collection.UpdateRule = rules.update
collection.DeleteRule = rules.delete
if err := app.Save(collection); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,527 @@
//go:build testing
package hub_test
import (
"fmt"
"net/http"
"testing"
beszelTests "github.com/henrygd/beszel/internal/tests"
"github.com/pocketbase/pocketbase/core"
pbTests "github.com/pocketbase/pocketbase/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCollectionRulesDefault(t *testing.T) {
hub, _ := beszelTests.NewTestHub(t.TempDir())
defer hub.Cleanup()
const isUserMatchesUser = `@request.auth.id != "" && user = @request.auth.id`
const isUserInUsers = `@request.auth.id != "" && users.id ?= @request.auth.id`
const isUserInUsersNotReadonly = `@request.auth.id != "" && users.id ?= @request.auth.id && @request.auth.role != "readonly"`
const isUserInSystemUsers = `@request.auth.id != "" && system.users.id ?= @request.auth.id`
const isUserInSystemUsersNotReadonly = `@request.auth.id != "" && system.users.id ?= @request.auth.id && @request.auth.role != "readonly"`
// users collection
usersCollection, err := hub.FindCollectionByNameOrId("users")
assert.NoError(t, err, "Failed to find users collection")
assert.True(t, usersCollection.PasswordAuth.Enabled)
assert.Equal(t, usersCollection.PasswordAuth.IdentityFields, []string{"email"})
assert.Nil(t, usersCollection.CreateRule)
assert.False(t, usersCollection.MFA.Enabled)
// superusers collection
superusersCollection, err := hub.FindCollectionByNameOrId(core.CollectionNameSuperusers)
assert.NoError(t, err, "Failed to find superusers collection")
assert.True(t, superusersCollection.PasswordAuth.Enabled)
assert.Equal(t, superusersCollection.PasswordAuth.IdentityFields, []string{"email"})
assert.Nil(t, superusersCollection.CreateRule)
assert.False(t, superusersCollection.MFA.Enabled)
// alerts collection
alertsCollection, err := hub.FindCollectionByNameOrId("alerts")
require.NoError(t, err, "Failed to find alerts collection")
assert.Equal(t, isUserMatchesUser, *alertsCollection.ListRule)
assert.Nil(t, alertsCollection.ViewRule)
assert.Equal(t, isUserMatchesUser, *alertsCollection.CreateRule)
assert.Equal(t, isUserMatchesUser, *alertsCollection.UpdateRule)
assert.Equal(t, isUserMatchesUser, *alertsCollection.DeleteRule)
// alerts_history collection
alertsHistoryCollection, err := hub.FindCollectionByNameOrId("alerts_history")
require.NoError(t, err, "Failed to find alerts_history collection")
assert.Equal(t, isUserMatchesUser, *alertsHistoryCollection.ListRule)
assert.Nil(t, alertsHistoryCollection.ViewRule)
assert.Nil(t, alertsHistoryCollection.CreateRule)
assert.Nil(t, alertsHistoryCollection.UpdateRule)
assert.Equal(t, isUserMatchesUser, *alertsHistoryCollection.DeleteRule)
// containers collection
containersCollection, err := hub.FindCollectionByNameOrId("containers")
require.NoError(t, err, "Failed to find containers collection")
assert.Equal(t, isUserInSystemUsers, *containersCollection.ListRule)
assert.Nil(t, containersCollection.ViewRule)
assert.Nil(t, containersCollection.CreateRule)
assert.Nil(t, containersCollection.UpdateRule)
assert.Nil(t, containersCollection.DeleteRule)
// container_stats collection
containerStatsCollection, err := hub.FindCollectionByNameOrId("container_stats")
require.NoError(t, err, "Failed to find container_stats collection")
assert.Equal(t, isUserInSystemUsers, *containerStatsCollection.ListRule)
assert.Nil(t, containerStatsCollection.ViewRule)
assert.Nil(t, containerStatsCollection.CreateRule)
assert.Nil(t, containerStatsCollection.UpdateRule)
assert.Nil(t, containerStatsCollection.DeleteRule)
// fingerprints collection
fingerprintsCollection, err := hub.FindCollectionByNameOrId("fingerprints")
require.NoError(t, err, "Failed to find fingerprints collection")
assert.Equal(t, isUserInSystemUsers, *fingerprintsCollection.ListRule)
assert.Equal(t, isUserInSystemUsers, *fingerprintsCollection.ViewRule)
assert.Equal(t, isUserInSystemUsersNotReadonly, *fingerprintsCollection.CreateRule)
assert.Equal(t, isUserInSystemUsersNotReadonly, *fingerprintsCollection.UpdateRule)
assert.Equal(t, isUserInSystemUsersNotReadonly, *fingerprintsCollection.DeleteRule)
// quiet_hours collection
quietHoursCollection, err := hub.FindCollectionByNameOrId("quiet_hours")
require.NoError(t, err, "Failed to find quiet_hours collection")
assert.Equal(t, isUserMatchesUser, *quietHoursCollection.ListRule)
assert.Equal(t, isUserMatchesUser, *quietHoursCollection.ViewRule)
assert.Equal(t, isUserMatchesUser, *quietHoursCollection.CreateRule)
assert.Equal(t, isUserMatchesUser, *quietHoursCollection.UpdateRule)
assert.Equal(t, isUserMatchesUser, *quietHoursCollection.DeleteRule)
// smart_devices collection
smartDevicesCollection, err := hub.FindCollectionByNameOrId("smart_devices")
require.NoError(t, err, "Failed to find smart_devices collection")
assert.Equal(t, isUserInSystemUsers, *smartDevicesCollection.ListRule)
assert.Equal(t, isUserInSystemUsers, *smartDevicesCollection.ViewRule)
assert.Nil(t, smartDevicesCollection.CreateRule)
assert.Nil(t, smartDevicesCollection.UpdateRule)
assert.Equal(t, isUserInSystemUsersNotReadonly, *smartDevicesCollection.DeleteRule)
// system_details collection
systemDetailsCollection, err := hub.FindCollectionByNameOrId("system_details")
require.NoError(t, err, "Failed to find system_details collection")
assert.Equal(t, isUserInSystemUsers, *systemDetailsCollection.ListRule)
assert.Equal(t, isUserInSystemUsers, *systemDetailsCollection.ViewRule)
assert.Nil(t, systemDetailsCollection.CreateRule)
assert.Nil(t, systemDetailsCollection.UpdateRule)
assert.Nil(t, systemDetailsCollection.DeleteRule)
// system_stats collection
systemStatsCollection, err := hub.FindCollectionByNameOrId("system_stats")
require.NoError(t, err, "Failed to find system_stats collection")
assert.Equal(t, isUserInSystemUsers, *systemStatsCollection.ListRule)
assert.Nil(t, systemStatsCollection.ViewRule)
assert.Nil(t, systemStatsCollection.CreateRule)
assert.Nil(t, systemStatsCollection.UpdateRule)
assert.Nil(t, systemStatsCollection.DeleteRule)
// systemd_services collection
systemdServicesCollection, err := hub.FindCollectionByNameOrId("systemd_services")
require.NoError(t, err, "Failed to find systemd_services collection")
assert.Equal(t, isUserInSystemUsers, *systemdServicesCollection.ListRule)
assert.Nil(t, systemdServicesCollection.ViewRule)
assert.Nil(t, systemdServicesCollection.CreateRule)
assert.Nil(t, systemdServicesCollection.UpdateRule)
assert.Nil(t, systemdServicesCollection.DeleteRule)
// systems collection
systemsCollection, err := hub.FindCollectionByNameOrId("systems")
require.NoError(t, err, "Failed to find systems collection")
assert.Equal(t, isUserInUsers, *systemsCollection.ListRule)
assert.Equal(t, isUserInUsers, *systemsCollection.ViewRule)
assert.Equal(t, isUserInUsersNotReadonly, *systemsCollection.CreateRule)
assert.Equal(t, isUserInUsersNotReadonly, *systemsCollection.UpdateRule)
assert.Equal(t, isUserInUsersNotReadonly, *systemsCollection.DeleteRule)
// universal_tokens collection
universalTokensCollection, err := hub.FindCollectionByNameOrId("universal_tokens")
require.NoError(t, err, "Failed to find universal_tokens collection")
assert.Nil(t, universalTokensCollection.ListRule)
assert.Nil(t, universalTokensCollection.ViewRule)
assert.Nil(t, universalTokensCollection.CreateRule)
assert.Nil(t, universalTokensCollection.UpdateRule)
assert.Nil(t, universalTokensCollection.DeleteRule)
// user_settings collection
userSettingsCollection, err := hub.FindCollectionByNameOrId("user_settings")
require.NoError(t, err, "Failed to find user_settings collection")
assert.Equal(t, isUserMatchesUser, *userSettingsCollection.ListRule)
assert.Nil(t, userSettingsCollection.ViewRule)
assert.Equal(t, isUserMatchesUser, *userSettingsCollection.CreateRule)
assert.Equal(t, isUserMatchesUser, *userSettingsCollection.UpdateRule)
assert.Nil(t, userSettingsCollection.DeleteRule)
}
func TestCollectionRulesShareAllSystems(t *testing.T) {
t.Setenv("SHARE_ALL_SYSTEMS", "true")
hub, _ := beszelTests.NewTestHub(t.TempDir())
defer hub.Cleanup()
const isUser = `@request.auth.id != ""`
const isUserNotReadonly = `@request.auth.id != "" && @request.auth.role != "readonly"`
const isUserMatchesUser = `@request.auth.id != "" && user = @request.auth.id`
// alerts collection
alertsCollection, err := hub.FindCollectionByNameOrId("alerts")
require.NoError(t, err, "Failed to find alerts collection")
assert.Equal(t, isUserMatchesUser, *alertsCollection.ListRule)
assert.Nil(t, alertsCollection.ViewRule)
assert.Equal(t, isUserMatchesUser, *alertsCollection.CreateRule)
assert.Equal(t, isUserMatchesUser, *alertsCollection.UpdateRule)
assert.Equal(t, isUserMatchesUser, *alertsCollection.DeleteRule)
// alerts_history collection
alertsHistoryCollection, err := hub.FindCollectionByNameOrId("alerts_history")
require.NoError(t, err, "Failed to find alerts_history collection")
assert.Equal(t, isUserMatchesUser, *alertsHistoryCollection.ListRule)
assert.Nil(t, alertsHistoryCollection.ViewRule)
assert.Nil(t, alertsHistoryCollection.CreateRule)
assert.Nil(t, alertsHistoryCollection.UpdateRule)
assert.Equal(t, isUserMatchesUser, *alertsHistoryCollection.DeleteRule)
// containers collection
containersCollection, err := hub.FindCollectionByNameOrId("containers")
require.NoError(t, err, "Failed to find containers collection")
assert.Equal(t, isUser, *containersCollection.ListRule)
assert.Nil(t, containersCollection.ViewRule)
assert.Nil(t, containersCollection.CreateRule)
assert.Nil(t, containersCollection.UpdateRule)
assert.Nil(t, containersCollection.DeleteRule)
// container_stats collection
containerStatsCollection, err := hub.FindCollectionByNameOrId("container_stats")
require.NoError(t, err, "Failed to find container_stats collection")
assert.Equal(t, isUser, *containerStatsCollection.ListRule)
assert.Nil(t, containerStatsCollection.ViewRule)
assert.Nil(t, containerStatsCollection.CreateRule)
assert.Nil(t, containerStatsCollection.UpdateRule)
assert.Nil(t, containerStatsCollection.DeleteRule)
// fingerprints collection
fingerprintsCollection, err := hub.FindCollectionByNameOrId("fingerprints")
require.NoError(t, err, "Failed to find fingerprints collection")
assert.Equal(t, isUser, *fingerprintsCollection.ListRule)
assert.Equal(t, isUser, *fingerprintsCollection.ViewRule)
assert.Equal(t, isUserNotReadonly, *fingerprintsCollection.CreateRule)
assert.Equal(t, isUserNotReadonly, *fingerprintsCollection.UpdateRule)
assert.Equal(t, isUserNotReadonly, *fingerprintsCollection.DeleteRule)
// quiet_hours collection
quietHoursCollection, err := hub.FindCollectionByNameOrId("quiet_hours")
require.NoError(t, err, "Failed to find quiet_hours collection")
assert.Equal(t, isUserMatchesUser, *quietHoursCollection.ListRule)
assert.Equal(t, isUserMatchesUser, *quietHoursCollection.ViewRule)
assert.Equal(t, isUserMatchesUser, *quietHoursCollection.CreateRule)
assert.Equal(t, isUserMatchesUser, *quietHoursCollection.UpdateRule)
assert.Equal(t, isUserMatchesUser, *quietHoursCollection.DeleteRule)
// smart_devices collection
smartDevicesCollection, err := hub.FindCollectionByNameOrId("smart_devices")
require.NoError(t, err, "Failed to find smart_devices collection")
assert.Equal(t, isUser, *smartDevicesCollection.ListRule)
assert.Equal(t, isUser, *smartDevicesCollection.ViewRule)
assert.Nil(t, smartDevicesCollection.CreateRule)
assert.Nil(t, smartDevicesCollection.UpdateRule)
assert.Equal(t, isUserNotReadonly, *smartDevicesCollection.DeleteRule)
// system_details collection
systemDetailsCollection, err := hub.FindCollectionByNameOrId("system_details")
require.NoError(t, err, "Failed to find system_details collection")
assert.Equal(t, isUser, *systemDetailsCollection.ListRule)
assert.Equal(t, isUser, *systemDetailsCollection.ViewRule)
assert.Nil(t, systemDetailsCollection.CreateRule)
assert.Nil(t, systemDetailsCollection.UpdateRule)
assert.Nil(t, systemDetailsCollection.DeleteRule)
// system_stats collection
systemStatsCollection, err := hub.FindCollectionByNameOrId("system_stats")
require.NoError(t, err, "Failed to find system_stats collection")
assert.Equal(t, isUser, *systemStatsCollection.ListRule)
assert.Nil(t, systemStatsCollection.ViewRule)
assert.Nil(t, systemStatsCollection.CreateRule)
assert.Nil(t, systemStatsCollection.UpdateRule)
assert.Nil(t, systemStatsCollection.DeleteRule)
// systemd_services collection
systemdServicesCollection, err := hub.FindCollectionByNameOrId("systemd_services")
require.NoError(t, err, "Failed to find systemd_services collection")
assert.Equal(t, isUser, *systemdServicesCollection.ListRule)
assert.Nil(t, systemdServicesCollection.ViewRule)
assert.Nil(t, systemdServicesCollection.CreateRule)
assert.Nil(t, systemdServicesCollection.UpdateRule)
assert.Nil(t, systemdServicesCollection.DeleteRule)
// systems collection
systemsCollection, err := hub.FindCollectionByNameOrId("systems")
require.NoError(t, err, "Failed to find systems collection")
assert.Equal(t, isUser, *systemsCollection.ListRule)
assert.Equal(t, isUser, *systemsCollection.ViewRule)
assert.Equal(t, isUserNotReadonly, *systemsCollection.CreateRule)
assert.Equal(t, isUserNotReadonly, *systemsCollection.UpdateRule)
assert.Equal(t, isUserNotReadonly, *systemsCollection.DeleteRule)
// universal_tokens collection
universalTokensCollection, err := hub.FindCollectionByNameOrId("universal_tokens")
require.NoError(t, err, "Failed to find universal_tokens collection")
assert.Nil(t, universalTokensCollection.ListRule)
assert.Nil(t, universalTokensCollection.ViewRule)
assert.Nil(t, universalTokensCollection.CreateRule)
assert.Nil(t, universalTokensCollection.UpdateRule)
assert.Nil(t, universalTokensCollection.DeleteRule)
// user_settings collection
userSettingsCollection, err := hub.FindCollectionByNameOrId("user_settings")
require.NoError(t, err, "Failed to find user_settings collection")
assert.Equal(t, isUserMatchesUser, *userSettingsCollection.ListRule)
assert.Nil(t, userSettingsCollection.ViewRule)
assert.Equal(t, isUserMatchesUser, *userSettingsCollection.CreateRule)
assert.Equal(t, isUserMatchesUser, *userSettingsCollection.UpdateRule)
assert.Nil(t, userSettingsCollection.DeleteRule)
}
func TestDisablePasswordAuth(t *testing.T) {
t.Setenv("DISABLE_PASSWORD_AUTH", "true")
hub, _ := beszelTests.NewTestHub(t.TempDir())
defer hub.Cleanup()
usersCollection, err := hub.FindCollectionByNameOrId("users")
assert.NoError(t, err)
assert.False(t, usersCollection.PasswordAuth.Enabled)
}
func TestUserCreation(t *testing.T) {
t.Setenv("USER_CREATION", "true")
hub, _ := beszelTests.NewTestHub(t.TempDir())
defer hub.Cleanup()
usersCollection, err := hub.FindCollectionByNameOrId("users")
assert.NoError(t, err)
assert.Equal(t, "@request.context = 'oauth2'", *usersCollection.CreateRule)
}
func TestMFAOtp(t *testing.T) {
t.Setenv("MFA_OTP", "true")
hub, _ := beszelTests.NewTestHub(t.TempDir())
defer hub.Cleanup()
usersCollection, err := hub.FindCollectionByNameOrId("users")
assert.NoError(t, err)
assert.True(t, usersCollection.OTP.Enabled)
assert.True(t, usersCollection.MFA.Enabled)
superusersCollection, err := hub.FindCollectionByNameOrId(core.CollectionNameSuperusers)
assert.NoError(t, err)
assert.True(t, superusersCollection.OTP.Enabled)
assert.True(t, superusersCollection.MFA.Enabled)
}
func TestApiCollectionsAuthRules(t *testing.T) {
hub, _ := beszelTests.NewTestHub(t.TempDir())
defer hub.Cleanup()
hub.StartHub()
user1, _ := beszelTests.CreateUser(hub, "user1@example.com", "password")
user1Token, _ := user1.NewAuthToken()
user2, _ := beszelTests.CreateUser(hub, "user2@example.com", "password")
// user2Token, _ := user2.NewAuthToken()
userReadonly, _ := beszelTests.CreateUserWithRole(hub, "userreadonly@example.com", "password", "readonly")
userReadonlyToken, _ := userReadonly.NewAuthToken()
userOneSystem, _ := beszelTests.CreateRecord(hub, "systems", map[string]any{
"name": "system1",
"users": []string{user1.Id},
"host": "127.0.0.1",
})
sharedSystem, _ := beszelTests.CreateRecord(hub, "systems", map[string]any{
"name": "system2",
"users": []string{user1.Id, user2.Id},
"host": "127.0.0.2",
})
userTwoSystem, _ := beszelTests.CreateRecord(hub, "systems", map[string]any{
"name": "system3",
"users": []string{user2.Id},
"host": "127.0.0.2",
})
userRecords, _ := hub.CountRecords("users")
assert.EqualValues(t, 3, userRecords, "all users should be created")
systemRecords, _ := hub.CountRecords("systems")
assert.EqualValues(t, 3, systemRecords, "all systems should be created")
testAppFactory := func(t testing.TB) *pbTests.TestApp {
return hub.TestApp
}
scenarios := []beszelTests.ApiScenario{
{
Name: "Unauthorized user cannot list systems",
Method: http.MethodGet,
URL: "/api/collections/systems/records",
ExpectedStatus: 200, // https://github.com/pocketbase/pocketbase/discussions/1570
TestAppFactory: testAppFactory,
ExpectedContent: []string{`"items":[]`, `"totalItems":0`},
NotExpectedContent: []string{userOneSystem.Id, sharedSystem.Id, userTwoSystem.Id},
},
{
Name: "Unauthorized user cannot delete a system",
Method: http.MethodDelete,
URL: fmt.Sprintf("/api/collections/systems/records/%s", userOneSystem.Id),
ExpectedStatus: 404,
TestAppFactory: testAppFactory,
ExpectedContent: []string{"resource wasn't found"},
NotExpectedContent: []string{userOneSystem.Id},
BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {
systemsCount, _ := app.CountRecords("systems")
assert.EqualValues(t, 3, systemsCount, "should have 3 systems before deletion")
},
AfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) {
systemsCount, _ := app.CountRecords("systems")
assert.EqualValues(t, 3, systemsCount, "should still have 3 systems after failed deletion")
},
},
{
Name: "User 1 can list their own systems",
Method: http.MethodGet,
URL: "/api/collections/systems/records",
Headers: map[string]string{
"Authorization": user1Token,
},
ExpectedStatus: 200,
ExpectedContent: []string{userOneSystem.Id, sharedSystem.Id},
NotExpectedContent: []string{userTwoSystem.Id},
TestAppFactory: testAppFactory,
},
{
Name: "User 1 cannot list user 2's system",
Method: http.MethodGet,
URL: "/api/collections/systems/records",
Headers: map[string]string{
"Authorization": user1Token,
},
ExpectedStatus: 200,
ExpectedContent: []string{userOneSystem.Id, sharedSystem.Id},
NotExpectedContent: []string{userTwoSystem.Id},
TestAppFactory: testAppFactory,
},
{
Name: "User 1 can see user 2's system if SHARE_ALL_SYSTEMS is enabled",
Method: http.MethodGet,
URL: "/api/collections/systems/records",
Headers: map[string]string{
"Authorization": user1Token,
},
ExpectedStatus: 200,
ExpectedContent: []string{userOneSystem.Id, sharedSystem.Id, userTwoSystem.Id},
TestAppFactory: testAppFactory,
BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {
t.Setenv("SHARE_ALL_SYSTEMS", "true")
hub.SetCollectionAuthSettings()
},
AfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) {
t.Setenv("SHARE_ALL_SYSTEMS", "")
hub.SetCollectionAuthSettings()
},
},
{
Name: "User 1 can delete their own system",
Method: http.MethodDelete,
URL: fmt.Sprintf("/api/collections/systems/records/%s", userOneSystem.Id),
Headers: map[string]string{
"Authorization": user1Token,
},
ExpectedStatus: 204,
TestAppFactory: testAppFactory,
BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {
systemsCount, _ := app.CountRecords("systems")
assert.EqualValues(t, 3, systemsCount, "should have 3 systems before deletion")
},
AfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) {
systemsCount, _ := app.CountRecords("systems")
assert.EqualValues(t, 2, systemsCount, "should have 2 systems after deletion")
},
},
{
Name: "User 1 cannot delete user 2's system",
Method: http.MethodDelete,
URL: fmt.Sprintf("/api/collections/systems/records/%s", userTwoSystem.Id),
Headers: map[string]string{
"Authorization": user1Token,
},
ExpectedStatus: 404,
TestAppFactory: testAppFactory,
ExpectedContent: []string{"resource wasn't found"},
BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {
systemsCount, _ := app.CountRecords("systems")
assert.EqualValues(t, 2, systemsCount)
},
AfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) {
systemsCount, _ := app.CountRecords("systems")
assert.EqualValues(t, 2, systemsCount)
},
},
{
Name: "Readonly cannot delete a system even if SHARE_ALL_SYSTEMS is enabled",
Method: http.MethodDelete,
URL: fmt.Sprintf("/api/collections/systems/records/%s", sharedSystem.Id),
Headers: map[string]string{
"Authorization": userReadonlyToken,
},
ExpectedStatus: 404,
ExpectedContent: []string{"resource wasn't found"},
TestAppFactory: testAppFactory,
BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {
t.Setenv("SHARE_ALL_SYSTEMS", "true")
hub.SetCollectionAuthSettings()
systemsCount, _ := app.CountRecords("systems")
assert.EqualValues(t, 2, systemsCount)
},
AfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) {
t.Setenv("SHARE_ALL_SYSTEMS", "")
hub.SetCollectionAuthSettings()
systemsCount, _ := app.CountRecords("systems")
assert.EqualValues(t, 2, systemsCount)
},
},
{
Name: "User 1 can delete user 2's system if SHARE_ALL_SYSTEMS is enabled",
Method: http.MethodDelete,
URL: fmt.Sprintf("/api/collections/systems/records/%s", userTwoSystem.Id),
Headers: map[string]string{
"Authorization": user1Token,
},
ExpectedStatus: 204,
TestAppFactory: testAppFactory,
BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {
t.Setenv("SHARE_ALL_SYSTEMS", "true")
hub.SetCollectionAuthSettings()
systemsCount, _ := app.CountRecords("systems")
assert.EqualValues(t, 2, systemsCount)
},
AfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) {
t.Setenv("SHARE_ALL_SYSTEMS", "")
hub.SetCollectionAuthSettings()
systemsCount, _ := app.CountRecords("systems")
assert.EqualValues(t, 1, systemsCount)
},
},
}
for _, scenario := range scenarios {
scenario.Test(t)
}
}

View File

@@ -4,6 +4,7 @@ package hub
import ( import (
"crypto/ed25519" "crypto/ed25519"
"encoding/pem" "encoding/pem"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
@@ -45,20 +46,17 @@ type Hub struct {
var containerIDPattern = regexp.MustCompile(`^[a-fA-F0-9]{12,64}$`) var containerIDPattern = regexp.MustCompile(`^[a-fA-F0-9]{12,64}$`)
// NewHub creates a new Hub instance with default configuration // NewHub creates a new Hub instance with default configuration
func NewHub(app core.App) *Hub { func NewHub(app core.App) (*Hub, error) {
hub := &Hub{} hub := &Hub{App: app}
hub.App = app
hub.AlertManager = alerts.NewAlertManager(hub) hub.AlertManager = alerts.NewAlertManager(hub)
hub.um = users.NewUserManager(hub) hub.um = users.NewUserManager(hub)
hub.rm = records.NewRecordManager(hub) hub.rm = records.NewRecordManager(hub)
hub.sm = systems.NewSystemManager(hub) hub.sm = systems.NewSystemManager(hub)
hub.appURL, _ = GetEnv("APP_URL")
hub.hb = heartbeat.New(app, GetEnv) hub.hb = heartbeat.New(app, GetEnv)
if hub.hb != nil { if hub.hb != nil {
hub.hbStop = make(chan struct{}) hub.hbStop = make(chan struct{})
} }
return hub return hub, initialize(hub)
} }
// GetEnv retrieves an environment variable with a "BESZEL_HUB_" prefix, or falls back to the unprefixed key. // GetEnv retrieves an environment variable with a "BESZEL_HUB_" prefix, or falls back to the unprefixed key.
@@ -72,10 +70,6 @@ func GetEnv(key string) (value string, exists bool) {
func (h *Hub) StartHub() error { func (h *Hub) StartHub() error {
h.App.OnServe().BindFunc(func(e *core.ServeEvent) error { h.App.OnServe().BindFunc(func(e *core.ServeEvent) error {
// initialize settings / collections
if err := h.initialize(e); err != nil {
return err
}
// sync systems with config // sync systems with config
if err := config.SyncSystems(e); err != nil { if err := config.SyncSystems(e); err != nil {
return err return err
@@ -110,132 +104,32 @@ func (h *Hub) StartHub() error {
h.App.OnRecordCreate("users").BindFunc(h.um.InitializeUserRole) h.App.OnRecordCreate("users").BindFunc(h.um.InitializeUserRole)
h.App.OnRecordCreate("user_settings").BindFunc(h.um.InitializeUserSettings) h.App.OnRecordCreate("user_settings").BindFunc(h.um.InitializeUserSettings)
if pb, ok := h.App.(*pocketbase.PocketBase); ok { pb, ok := h.App.(*pocketbase.PocketBase)
// log.Println("Starting pocketbase") if !ok {
err := pb.Start() return errors.New("not a pocketbase app")
if err != nil {
return err
} }
} return pb.Start()
return nil
} }
// initialize sets up initial configuration (collections, settings, etc.) // initialize sets up initial configuration (collections, settings, etc.)
func (h *Hub) initialize(e *core.ServeEvent) error { func initialize(hub *Hub) error {
// set general settings if !hub.App.IsBootstrapped() {
settings := e.App.Settings() hub.App.Bootstrap()
// batch requests (for global alerts)
settings.Batch.Enabled = true
// set URL if BASE_URL env is set
if h.appURL != "" {
settings.Meta.AppURL = h.appURL
} }
if err := e.App.Save(settings); err != nil { // set general settings
settings := hub.App.Settings()
// batch requests (for alerts)
settings.Batch.Enabled = true
// set URL if APP_URL env is set
if appURL, isSet := GetEnv("APP_URL"); isSet {
hub.appURL = appURL
settings.Meta.AppURL = hub.appURL
}
if err := hub.App.Save(settings); err != nil {
return err return err
} }
// set auth settings // set auth settings
if err := setCollectionAuthSettings(e.App); err != nil { return setCollectionAuthSettings(hub.App)
return err
}
return nil
}
// setCollectionAuthSettings sets up default authentication settings for the app
func setCollectionAuthSettings(app core.App) error {
usersCollection, err := app.FindCollectionByNameOrId("users")
if err != nil {
return err
}
superusersCollection, err := app.FindCollectionByNameOrId(core.CollectionNameSuperusers)
if err != nil {
return err
}
// disable email auth if DISABLE_PASSWORD_AUTH env var is set
disablePasswordAuth, _ := GetEnv("DISABLE_PASSWORD_AUTH")
usersCollection.PasswordAuth.Enabled = disablePasswordAuth != "true"
usersCollection.PasswordAuth.IdentityFields = []string{"email"}
// allow oauth user creation if USER_CREATION is set
if userCreation, _ := GetEnv("USER_CREATION"); userCreation == "true" {
cr := "@request.context = 'oauth2'"
usersCollection.CreateRule = &cr
} else {
usersCollection.CreateRule = nil
}
// enable mfaOtp mfa if MFA_OTP env var is set
mfaOtp, _ := GetEnv("MFA_OTP")
usersCollection.OTP.Length = 6
superusersCollection.OTP.Length = 6
usersCollection.OTP.Enabled = mfaOtp == "true"
usersCollection.MFA.Enabled = mfaOtp == "true"
superusersCollection.OTP.Enabled = mfaOtp == "true" || mfaOtp == "superusers"
superusersCollection.MFA.Enabled = mfaOtp == "true" || mfaOtp == "superusers"
if err := app.Save(superusersCollection); err != nil {
return err
}
if err := app.Save(usersCollection); err != nil {
return err
}
shareAllSystems, _ := GetEnv("SHARE_ALL_SYSTEMS")
// allow all users to access systems if SHARE_ALL_SYSTEMS is set
systemsCollection, err := app.FindCollectionByNameOrId("systems")
if err != nil {
return err
}
var systemsReadRule string
if shareAllSystems == "true" {
systemsReadRule = "@request.auth.id != \"\""
} else {
systemsReadRule = "@request.auth.id != \"\" && users.id ?= @request.auth.id"
}
updateDeleteRule := systemsReadRule + " && @request.auth.role != \"readonly\""
systemsCollection.ListRule = &systemsReadRule
systemsCollection.ViewRule = &systemsReadRule
systemsCollection.UpdateRule = &updateDeleteRule
systemsCollection.DeleteRule = &updateDeleteRule
if err := app.Save(systemsCollection); err != nil {
return err
}
// allow all users to access all containers if SHARE_ALL_SYSTEMS is set
containersCollection, err := app.FindCollectionByNameOrId("containers")
if err != nil {
return err
}
containersListRule := strings.Replace(systemsReadRule, "users.id", "system.users.id", 1)
containersCollection.ListRule = &containersListRule
if err := app.Save(containersCollection); err != nil {
return err
}
// allow all users to access system-related collections if SHARE_ALL_SYSTEMS is set
// these collections all have a "system" relation field
systemRelatedCollections := []string{"system_details", "smart_devices", "systemd_services"}
for _, collectionName := range systemRelatedCollections {
collection, err := app.FindCollectionByNameOrId(collectionName)
if err != nil {
return err
}
collection.ListRule = &containersListRule
// set viewRule for collections that need it (system_details, smart_devices)
if collection.ViewRule != nil {
collection.ViewRule = &containersListRule
}
// set deleteRule for smart_devices (allows user to dismiss disk warnings)
if collectionName == "smart_devices" {
deleteRule := containersListRule + " && @request.auth.role != \"readonly\""
collection.DeleteRule = &deleteRule
}
if err := app.Save(collection); err != nil {
return err
}
}
return nil
} }
// registerCronJobs sets up scheduled tasks // registerCronJobs sets up scheduled tasks

View File

@@ -733,10 +733,8 @@ func TestFirstUserCreation(t *testing.T) {
}) })
t.Run("CreateUserEndpoint not available when USER_EMAIL, USER_PASSWORD are set", func(t *testing.T) { t.Run("CreateUserEndpoint not available when USER_EMAIL, USER_PASSWORD are set", func(t *testing.T) {
os.Setenv("BESZEL_HUB_USER_EMAIL", "me@example.com") t.Setenv("BESZEL_HUB_USER_EMAIL", "me@example.com")
os.Setenv("BESZEL_HUB_USER_PASSWORD", "password123") t.Setenv("BESZEL_HUB_USER_PASSWORD", "password123")
defer os.Unsetenv("BESZEL_HUB_USER_EMAIL")
defer os.Unsetenv("BESZEL_HUB_USER_PASSWORD")
hub, _ := beszelTests.NewTestHub(t.TempDir()) hub, _ := beszelTests.NewTestHub(t.TempDir())
defer hub.Cleanup() defer hub.Cleanup()
@@ -852,13 +850,12 @@ func TestAutoLoginMiddleware(t *testing.T) {
var hubs []*beszelTests.TestHub var hubs []*beszelTests.TestHub
defer func() { defer func() {
defer os.Unsetenv("AUTO_LOGIN")
for _, hub := range hubs { for _, hub := range hubs {
hub.Cleanup() hub.Cleanup()
} }
}() }()
os.Setenv("AUTO_LOGIN", "user@test.com") t.Setenv("AUTO_LOGIN", "user@test.com")
testAppFactory := func(t testing.TB) *pbTests.TestApp { testAppFactory := func(t testing.TB) *pbTests.TestApp {
hub, _ := beszelTests.NewTestHub(t.TempDir()) hub, _ := beszelTests.NewTestHub(t.TempDir())
@@ -906,13 +903,12 @@ func TestTrustedHeaderMiddleware(t *testing.T) {
var hubs []*beszelTests.TestHub var hubs []*beszelTests.TestHub
defer func() { defer func() {
defer os.Unsetenv("TRUSTED_AUTH_HEADER")
for _, hub := range hubs { for _, hub := range hubs {
hub.Cleanup() hub.Cleanup()
} }
}() }()
os.Setenv("TRUSTED_AUTH_HEADER", "X-Beszel-Trusted") t.Setenv("TRUSTED_AUTH_HEADER", "X-Beszel-Trusted")
testAppFactory := func(t testing.TB) *pbTests.TestApp { testAppFactory := func(t testing.TB) *pbTests.TestApp {
hub, _ := beszelTests.NewTestHub(t.TempDir()) hub, _ := beszelTests.NewTestHub(t.TempDir())
@@ -961,3 +957,21 @@ func TestTrustedHeaderMiddleware(t *testing.T) {
scenario.Test(t) scenario.Test(t)
} }
} }
func TestAppUrl(t *testing.T) {
t.Run("no APP_URL does't change app url", func(t *testing.T) {
hub, _ := beszelTests.NewTestHub(t.TempDir())
defer hub.Cleanup()
settings := hub.Settings()
assert.Equal(t, "http://localhost:8090", settings.Meta.AppURL)
})
t.Run("APP_URL changes app url", func(t *testing.T) {
t.Setenv("APP_URL", "http://example.com/app")
hub, _ := beszelTests.NewTestHub(t.TempDir())
defer hub.Cleanup()
settings := hub.Settings()
assert.Equal(t, "http://example.com/app", settings.Meta.AppURL)
})
}

View File

@@ -2,7 +2,9 @@
package hub package hub
import "github.com/henrygd/beszel/internal/hub/systems" import (
"github.com/henrygd/beszel/internal/hub/systems"
)
// TESTING ONLY: GetSystemManager returns the system manager // TESTING ONLY: GetSystemManager returns the system manager
func (h *Hub) GetSystemManager() *systems.SystemManager { func (h *Hub) GetSystemManager() *systems.SystemManager {
@@ -18,3 +20,7 @@ func (h *Hub) GetPubkey() string {
func (h *Hub) SetPubkey(pubkey string) { func (h *Hub) SetPubkey(pubkey string) {
h.pubKey = pubkey h.pubKey = pubkey
} }
func (h *Hub) SetCollectionAuthSettings() error {
return setCollectionAuthSettings(h)
}

View File

@@ -11,11 +11,11 @@ func init() {
jsonData := `[ jsonData := `[
{ {
"id": "elngm8x1l60zi2v", "id": "elngm8x1l60zi2v",
"listRule": "@request.auth.id != \"\" && user.id = @request.auth.id", "listRule": "@request.auth.id != \"\" && user = @request.auth.id",
"viewRule": "", "viewRule": null,
"createRule": "@request.auth.id != \"\" && user.id = @request.auth.id", "createRule": "@request.auth.id != \"\" && user = @request.auth.id",
"updateRule": "@request.auth.id != \"\" && user.id = @request.auth.id", "updateRule": "@request.auth.id != \"\" && user = @request.auth.id",
"deleteRule": "@request.auth.id != \"\" && user.id = @request.auth.id", "deleteRule": "@request.auth.id != \"\" && user = @request.auth.id",
"name": "alerts", "name": "alerts",
"type": "base", "type": "base",
"fields": [ "fields": [
@@ -143,11 +143,11 @@ func init() {
}, },
{ {
"id": "pbc_1697146157", "id": "pbc_1697146157",
"listRule": "@request.auth.id != \"\" && user.id = @request.auth.id", "listRule": "@request.auth.id != \"\" && user = @request.auth.id",
"viewRule": "@request.auth.id != \"\" && user.id = @request.auth.id", "viewRule": null,
"createRule": null, "createRule": null,
"updateRule": null, "updateRule": null,
"deleteRule": "@request.auth.id != \"\" && user.id = @request.auth.id", "deleteRule": "@request.auth.id != \"\" && user = @request.auth.id",
"name": "alerts_history", "name": "alerts_history",
"type": "base", "type": "base",
"fields": [ "fields": [
@@ -261,7 +261,7 @@ func init() {
}, },
{ {
"id": "juohu4jipgc13v7", "id": "juohu4jipgc13v7",
"listRule": "@request.auth.id != \"\"", "listRule": null,
"viewRule": null, "viewRule": null,
"createRule": null, "createRule": null,
"updateRule": null, "updateRule": null,
@@ -351,10 +351,10 @@ func init() {
}, },
{ {
"id": "pbc_3663931638", "id": "pbc_3663931638",
"listRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id", "listRule": null,
"viewRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id", "viewRule": null,
"createRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id && @request.auth.role != \"readonly\"", "createRule": null,
"updateRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id && @request.auth.role != \"readonly\"", "updateRule": null,
"deleteRule": null, "deleteRule": null,
"name": "fingerprints", "name": "fingerprints",
"type": "base", "type": "base",
@@ -433,7 +433,7 @@ func init() {
}, },
{ {
"id": "ej9oowivz8b2mht", "id": "ej9oowivz8b2mht",
"listRule": "@request.auth.id != \"\"", "listRule": null,
"viewRule": null, "viewRule": null,
"createRule": null, "createRule": null,
"updateRule": null, "updateRule": null,
@@ -523,10 +523,10 @@ func init() {
}, },
{ {
"id": "4afacsdnlu8q8r2", "id": "4afacsdnlu8q8r2",
"listRule": "@request.auth.id != \"\" && user.id = @request.auth.id", "listRule": "@request.auth.id != \"\" && user = @request.auth.id",
"viewRule": null, "viewRule": null,
"createRule": "@request.auth.id != \"\" && user.id = @request.auth.id", "createRule": "@request.auth.id != \"\" && user = @request.auth.id",
"updateRule": "@request.auth.id != \"\" && user.id = @request.auth.id", "updateRule": "@request.auth.id != \"\" && user = @request.auth.id",
"deleteRule": null, "deleteRule": null,
"name": "user_settings", "name": "user_settings",
"type": "base", "type": "base",
@@ -596,11 +596,11 @@ func init() {
}, },
{ {
"id": "2hz5ncl8tizk5nx", "id": "2hz5ncl8tizk5nx",
"listRule": "@request.auth.id != \"\" && users.id ?= @request.auth.id", "listRule": null,
"viewRule": "@request.auth.id != \"\" && users.id ?= @request.auth.id", "viewRule": null,
"createRule": "@request.auth.id != \"\" && users.id ?= @request.auth.id && @request.auth.role != \"readonly\"", "createRule": null,
"updateRule": "@request.auth.id != \"\" && users.id ?= @request.auth.id && @request.auth.role != \"readonly\"", "updateRule": null,
"deleteRule": "@request.auth.id != \"\" && users.id ?= @request.auth.id && @request.auth.role != \"readonly\"", "deleteRule": null,
"name": "systems", "name": "systems",
"type": "base", "type": "base",
"fields": [ "fields": [
@@ -866,7 +866,7 @@ func init() {
}, },
{ {
"id": "pbc_1864144027", "id": "pbc_1864144027",
"listRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id", "listRule": null,
"viewRule": null, "viewRule": null,
"createRule": null, "createRule": null,
"updateRule": null, "updateRule": null,
@@ -1159,7 +1159,7 @@ func init() {
"CREATE INDEX ` + "`" + `idx_4Z7LuLNdQb` + "`" + ` ON ` + "`" + `systemd_services` + "`" + ` (` + "`" + `system` + "`" + `)", "CREATE INDEX ` + "`" + `idx_4Z7LuLNdQb` + "`" + ` ON ` + "`" + `systemd_services` + "`" + ` (` + "`" + `system` + "`" + `)",
"CREATE INDEX ` + "`" + `idx_pBp1fF837e` + "`" + ` ON ` + "`" + `systemd_services` + "`" + ` (` + "`" + `updated` + "`" + `)" "CREATE INDEX ` + "`" + `idx_pBp1fF837e` + "`" + ` ON ` + "`" + `systemd_services` + "`" + ` (` + "`" + `updated` + "`" + `)"
], ],
"listRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id", "listRule": null,
"name": "systemd_services", "name": "systemd_services",
"system": false, "system": false,
"type": "base", "type": "base",
@@ -1167,8 +1167,8 @@ func init() {
"viewRule": null "viewRule": null
}, },
{ {
"createRule": "@request.auth.id != \"\" && user.id = @request.auth.id", "createRule": "@request.auth.id != \"\" && user = @request.auth.id",
"deleteRule": "@request.auth.id != \"\" && user.id = @request.auth.id", "deleteRule": "@request.auth.id != \"\" && user = @request.auth.id",
"fields": [ "fields": [
{ {
"autogeneratePattern": "[a-z0-9]{10}", "autogeneratePattern": "[a-z0-9]{10}",
@@ -1252,16 +1252,16 @@ func init() {
"CREATE INDEX ` + "`" + `idx_q0iKnRP9v8` + "`" + ` ON ` + "`" + `quiet_hours` + "`" + ` (\n ` + "`" + `user` + "`" + `,\n ` + "`" + `system` + "`" + `\n)", "CREATE INDEX ` + "`" + `idx_q0iKnRP9v8` + "`" + ` ON ` + "`" + `quiet_hours` + "`" + ` (\n ` + "`" + `user` + "`" + `,\n ` + "`" + `system` + "`" + `\n)",
"CREATE INDEX ` + "`" + `idx_6T7ljT7FJd` + "`" + ` ON ` + "`" + `quiet_hours` + "`" + ` (\n ` + "`" + `type` + "`" + `,\n ` + "`" + `end` + "`" + `\n)" "CREATE INDEX ` + "`" + `idx_6T7ljT7FJd` + "`" + ` ON ` + "`" + `quiet_hours` + "`" + ` (\n ` + "`" + `type` + "`" + `,\n ` + "`" + `end` + "`" + `\n)"
], ],
"listRule": "@request.auth.id != \"\" && user.id = @request.auth.id", "listRule": "@request.auth.id != \"\" && user = @request.auth.id",
"name": "quiet_hours", "name": "quiet_hours",
"system": false, "system": false,
"type": "base", "type": "base",
"updateRule": "@request.auth.id != \"\" && user.id = @request.auth.id", "updateRule": "@request.auth.id != \"\" && user = @request.auth.id",
"viewRule": "@request.auth.id != \"\" && user.id = @request.auth.id" "viewRule": "@request.auth.id != \"\" && user = @request.auth.id"
}, },
{ {
"createRule": null, "createRule": null,
"deleteRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id", "deleteRule": null,
"fields": [ "fields": [
{ {
"autogeneratePattern": "[a-z0-9]{10}", "autogeneratePattern": "[a-z0-9]{10}",
@@ -1447,16 +1447,16 @@ func init() {
"indexes": [ "indexes": [
"CREATE INDEX ` + "`" + `idx_DZ9yhvgl44` + "`" + ` ON ` + "`" + `smart_devices` + "`" + ` (` + "`" + `system` + "`" + `)" "CREATE INDEX ` + "`" + `idx_DZ9yhvgl44` + "`" + ` ON ` + "`" + `smart_devices` + "`" + ` (` + "`" + `system` + "`" + `)"
], ],
"listRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id", "listRule": null,
"name": "smart_devices", "name": "smart_devices",
"system": false, "system": false,
"type": "base", "type": "base",
"updateRule": null, "updateRule": null,
"viewRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id" "viewRule": null
}, },
{ {
"createRule": "", "createRule": null,
"deleteRule": "", "deleteRule": null,
"fields": [ "fields": [
{ {
"autogeneratePattern": "[a-z0-9]{15}", "autogeneratePattern": "[a-z0-9]{15}",
@@ -1625,12 +1625,12 @@ func init() {
], ],
"id": "pbc_3116237454", "id": "pbc_3116237454",
"indexes": [], "indexes": [],
"listRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id",
"name": "system_details", "name": "system_details",
"system": false, "system": false,
"type": "base", "type": "base",
"updateRule": "", "updateRule": null,
"viewRule": "@request.auth.id != \"\" && system.users.id ?= @request.auth.id" "listRule": null,
"viewRule": null
}, },
{ {
"createRule": null, "createRule": null,

View File

@@ -52,7 +52,10 @@ func NewTestHubWithConfig(config core.BaseAppConfig) (*TestHub, error) {
return nil, err return nil, err
} }
hub := hub.NewHub(testApp) hub, err := hub.NewHub(testApp)
if err != nil {
return nil, err
}
t := &TestHub{ t := &TestHub{
App: testApp, App: testApp,
@@ -77,6 +80,16 @@ func CreateUser(app core.App, email string, password string) (*core.Record, erro
return user, app.Save(user) return user, app.Save(user)
} }
func CreateUserWithRole(app core.App, email string, password string, roleName string) (*core.Record, error) {
user, err := CreateUser(app, email, password)
if err != nil {
return nil, err
}
user.Set("role", roleName)
return user, app.Save(user)
}
// Helper function to create a test record // Helper function to create a test record
func CreateRecord(app core.App, collectionName string, fields map[string]any) (*core.Record, error) { func CreateRecord(app core.App, collectionName string, fields map[string]any) (*core.Record, error) {
collection, err := app.FindCachedCollectionByNameOrId(collectionName) collection, err := app.FindCachedCollectionByNameOrId(collectionName)