mirror of
https://github.com/henrygd/beszel.git
synced 2026-04-14 17:01:51 +02:00
fix(hub): System.HasUser - return true if SHARE_ALL_SYSTEMS=true (#1891)
- move hub's GetEnv function to new utils package to more easily share across different hub packages - change System.HasUser to take core.Record instead of user ID string - add tests
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
// Package utils provides utility functions for the agent.
|
||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/henrygd/beszel/internal/ghupdate"
|
"github.com/henrygd/beszel/internal/ghupdate"
|
||||||
"github.com/henrygd/beszel/internal/hub/config"
|
"github.com/henrygd/beszel/internal/hub/config"
|
||||||
"github.com/henrygd/beszel/internal/hub/systems"
|
"github.com/henrygd/beszel/internal/hub/systems"
|
||||||
|
"github.com/henrygd/beszel/internal/hub/utils"
|
||||||
"github.com/pocketbase/dbx"
|
"github.com/pocketbase/dbx"
|
||||||
"github.com/pocketbase/pocketbase/apis"
|
"github.com/pocketbase/pocketbase/apis"
|
||||||
"github.com/pocketbase/pocketbase/core"
|
"github.com/pocketbase/pocketbase/core"
|
||||||
@@ -70,13 +71,13 @@ func (h *Hub) registerMiddlewares(se *core.ServeEvent) {
|
|||||||
return e.Next()
|
return e.Next()
|
||||||
}
|
}
|
||||||
// authenticate with trusted header
|
// authenticate with trusted header
|
||||||
if autoLogin, _ := GetEnv("AUTO_LOGIN"); autoLogin != "" {
|
if autoLogin, _ := utils.GetEnv("AUTO_LOGIN"); autoLogin != "" {
|
||||||
se.Router.BindFunc(func(e *core.RequestEvent) error {
|
se.Router.BindFunc(func(e *core.RequestEvent) error {
|
||||||
return authorizeRequestWithEmail(e, autoLogin)
|
return authorizeRequestWithEmail(e, autoLogin)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
// authenticate with trusted header
|
// authenticate with trusted header
|
||||||
if trustedHeader, _ := GetEnv("TRUSTED_AUTH_HEADER"); trustedHeader != "" {
|
if trustedHeader, _ := utils.GetEnv("TRUSTED_AUTH_HEADER"); trustedHeader != "" {
|
||||||
se.Router.BindFunc(func(e *core.RequestEvent) error {
|
se.Router.BindFunc(func(e *core.RequestEvent) error {
|
||||||
return authorizeRequestWithEmail(e, e.Request.Header.Get(trustedHeader))
|
return authorizeRequestWithEmail(e, e.Request.Header.Get(trustedHeader))
|
||||||
})
|
})
|
||||||
@@ -104,7 +105,7 @@ func (h *Hub) registerApiRoutes(se *core.ServeEvent) error {
|
|||||||
apiAuth.GET("/info", h.getInfo)
|
apiAuth.GET("/info", h.getInfo)
|
||||||
apiAuth.GET("/getkey", h.getInfo) // deprecated - keep for compatibility w/ integrations
|
apiAuth.GET("/getkey", h.getInfo) // deprecated - keep for compatibility w/ integrations
|
||||||
// check for updates
|
// check for updates
|
||||||
if optIn, _ := GetEnv("CHECK_UPDATES"); optIn == "true" {
|
if optIn, _ := utils.GetEnv("CHECK_UPDATES"); optIn == "true" {
|
||||||
var updateInfo UpdateInfo
|
var updateInfo UpdateInfo
|
||||||
apiAuth.GET("/update", updateInfo.getUpdate)
|
apiAuth.GET("/update", updateInfo.getUpdate)
|
||||||
}
|
}
|
||||||
@@ -127,7 +128,7 @@ func (h *Hub) registerApiRoutes(se *core.ServeEvent) error {
|
|||||||
// get systemd service details
|
// get systemd service details
|
||||||
apiAuth.GET("/systemd/info", h.getSystemdInfo)
|
apiAuth.GET("/systemd/info", h.getSystemdInfo)
|
||||||
// /containers routes
|
// /containers routes
|
||||||
if enabled, _ := GetEnv("CONTAINER_DETAILS"); enabled != "false" {
|
if enabled, _ := utils.GetEnv("CONTAINER_DETAILS"); enabled != "false" {
|
||||||
// get container logs
|
// get container logs
|
||||||
apiAuth.GET("/containers/logs", h.getContainerLogs)
|
apiAuth.GET("/containers/logs", h.getContainerLogs)
|
||||||
// get container info
|
// get container info
|
||||||
@@ -147,7 +148,7 @@ func (h *Hub) getInfo(e *core.RequestEvent) error {
|
|||||||
Key: h.pubKey,
|
Key: h.pubKey,
|
||||||
Version: beszel.Version,
|
Version: beszel.Version,
|
||||||
}
|
}
|
||||||
if optIn, _ := GetEnv("CHECK_UPDATES"); optIn == "true" {
|
if optIn, _ := utils.GetEnv("CHECK_UPDATES"); optIn == "true" {
|
||||||
info.CheckUpdate = true
|
info.CheckUpdate = true
|
||||||
}
|
}
|
||||||
return e.JSON(http.StatusOK, info)
|
return e.JSON(http.StatusOK, info)
|
||||||
@@ -315,7 +316,7 @@ func (h *Hub) containerRequestHandler(e *core.RequestEvent, fetchFunc func(*syst
|
|||||||
}
|
}
|
||||||
|
|
||||||
system, err := h.sm.GetSystem(systemID)
|
system, err := h.sm.GetSystem(systemID)
|
||||||
if err != nil || !system.HasUser(e.App, e.Auth.Id) {
|
if err != nil || !system.HasUser(e.App, e.Auth) {
|
||||||
return e.NotFoundError("", nil)
|
return e.NotFoundError("", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,7 +351,7 @@ func (h *Hub) getSystemdInfo(e *core.RequestEvent) error {
|
|||||||
return e.BadRequestError("Invalid system or service parameter", nil)
|
return e.BadRequestError("Invalid system or service parameter", nil)
|
||||||
}
|
}
|
||||||
system, err := h.sm.GetSystem(systemID)
|
system, err := h.sm.GetSystem(systemID)
|
||||||
if err != nil || !system.HasUser(e.App, e.Auth.Id) {
|
if err != nil || !system.HasUser(e.App, e.Auth) {
|
||||||
return e.NotFoundError("", nil)
|
return e.NotFoundError("", nil)
|
||||||
}
|
}
|
||||||
// verify service exists before fetching details
|
// verify service exists before fetching details
|
||||||
@@ -378,7 +379,7 @@ func (h *Hub) refreshSmartData(e *core.RequestEvent) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
system, err := h.sm.GetSystem(systemID)
|
system, err := h.sm.GetSystem(systemID)
|
||||||
if err != nil || !system.HasUser(e.App, e.Auth.Id) {
|
if err != nil || !system.HasUser(e.App, e.Auth) {
|
||||||
return e.NotFoundError("", nil)
|
return e.NotFoundError("", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -344,6 +344,23 @@ func TestApiRoutesAuthentication(t *testing.T) {
|
|||||||
"Authorization": user2Token,
|
"Authorization": user2Token,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "GET /containers/info - SHARE_ALL_SYSTEMS allows non-member user",
|
||||||
|
Method: http.MethodGet,
|
||||||
|
URL: fmt.Sprintf("/api/beszel/containers/info?system=%s&container=abababababab", system.Id),
|
||||||
|
ExpectedStatus: 500,
|
||||||
|
ExpectedContent: []string{"Something went wrong while processing your request."},
|
||||||
|
TestAppFactory: testAppFactory,
|
||||||
|
Headers: map[string]string{
|
||||||
|
"Authorization": user2Token,
|
||||||
|
},
|
||||||
|
BeforeTestFunc: func(t testing.TB, app *pbTests.TestApp, e *core.ServeEvent) {
|
||||||
|
t.Setenv("SHARE_ALL_SYSTEMS", "true")
|
||||||
|
},
|
||||||
|
AfterTestFunc: func(t testing.TB, app *pbTests.TestApp, res *http.Response) {
|
||||||
|
t.Setenv("SHARE_ALL_SYSTEMS", "")
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "GET /containers/logs - with auth but missing system param should fail",
|
Name: "GET /containers/logs - with auth but missing system param should fail",
|
||||||
Method: http.MethodGet,
|
Method: http.MethodGet,
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package hub
|
package hub
|
||||||
|
|
||||||
import "github.com/pocketbase/pocketbase/core"
|
import (
|
||||||
|
"github.com/henrygd/beszel/internal/hub/utils"
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
)
|
||||||
|
|
||||||
type collectionRules struct {
|
type collectionRules struct {
|
||||||
list *string
|
list *string
|
||||||
@@ -22,11 +25,11 @@ func setCollectionAuthSettings(app core.App) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// disable email auth if DISABLE_PASSWORD_AUTH env var is set
|
// disable email auth if DISABLE_PASSWORD_AUTH env var is set
|
||||||
disablePasswordAuth, _ := GetEnv("DISABLE_PASSWORD_AUTH")
|
disablePasswordAuth, _ := utils.GetEnv("DISABLE_PASSWORD_AUTH")
|
||||||
usersCollection.PasswordAuth.Enabled = disablePasswordAuth != "true"
|
usersCollection.PasswordAuth.Enabled = disablePasswordAuth != "true"
|
||||||
usersCollection.PasswordAuth.IdentityFields = []string{"email"}
|
usersCollection.PasswordAuth.IdentityFields = []string{"email"}
|
||||||
// allow oauth user creation if USER_CREATION is set
|
// allow oauth user creation if USER_CREATION is set
|
||||||
if userCreation, _ := GetEnv("USER_CREATION"); userCreation == "true" {
|
if userCreation, _ := utils.GetEnv("USER_CREATION"); userCreation == "true" {
|
||||||
cr := "@request.context = 'oauth2'"
|
cr := "@request.context = 'oauth2'"
|
||||||
usersCollection.CreateRule = &cr
|
usersCollection.CreateRule = &cr
|
||||||
} else {
|
} else {
|
||||||
@@ -34,7 +37,7 @@ func setCollectionAuthSettings(app core.App) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// enable mfaOtp mfa if MFA_OTP env var is set
|
// enable mfaOtp mfa if MFA_OTP env var is set
|
||||||
mfaOtp, _ := GetEnv("MFA_OTP")
|
mfaOtp, _ := utils.GetEnv("MFA_OTP")
|
||||||
usersCollection.OTP.Length = 6
|
usersCollection.OTP.Length = 6
|
||||||
superusersCollection.OTP.Length = 6
|
superusersCollection.OTP.Length = 6
|
||||||
usersCollection.OTP.Enabled = mfaOtp == "true"
|
usersCollection.OTP.Enabled = mfaOtp == "true"
|
||||||
@@ -50,7 +53,7 @@ func setCollectionAuthSettings(app core.App) error {
|
|||||||
|
|
||||||
// When SHARE_ALL_SYSTEMS is enabled, any authenticated user can read
|
// When SHARE_ALL_SYSTEMS is enabled, any authenticated user can read
|
||||||
// system-scoped data. Write rules continue to block readonly users.
|
// system-scoped data. Write rules continue to block readonly users.
|
||||||
shareAllSystems, _ := GetEnv("SHARE_ALL_SYSTEMS")
|
shareAllSystems, _ := utils.GetEnv("SHARE_ALL_SYSTEMS")
|
||||||
|
|
||||||
authenticatedRule := "@request.auth.id != \"\""
|
authenticatedRule := "@request.auth.id != \"\""
|
||||||
systemsMemberRule := authenticatedRule + " && users.id ?= @request.auth.id"
|
systemsMemberRule := authenticatedRule + " && users.id ?= @request.auth.id"
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/henrygd/beszel/internal/hub/config"
|
"github.com/henrygd/beszel/internal/hub/config"
|
||||||
"github.com/henrygd/beszel/internal/hub/heartbeat"
|
"github.com/henrygd/beszel/internal/hub/heartbeat"
|
||||||
"github.com/henrygd/beszel/internal/hub/systems"
|
"github.com/henrygd/beszel/internal/hub/systems"
|
||||||
|
"github.com/henrygd/beszel/internal/hub/utils"
|
||||||
"github.com/henrygd/beszel/internal/records"
|
"github.com/henrygd/beszel/internal/records"
|
||||||
"github.com/henrygd/beszel/internal/users"
|
"github.com/henrygd/beszel/internal/users"
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ func NewHub(app core.App) *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.hb = heartbeat.New(app, GetEnv)
|
hub.hb = heartbeat.New(app, utils.GetEnv)
|
||||||
if hub.hb != nil {
|
if hub.hb != nil {
|
||||||
hub.hbStop = make(chan struct{})
|
hub.hbStop = make(chan struct{})
|
||||||
}
|
}
|
||||||
@@ -52,15 +53,6 @@ func NewHub(app core.App) *Hub {
|
|||||||
return hub
|
return hub
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetEnv retrieves an environment variable with a "BESZEL_HUB_" prefix, or falls back to the unprefixed key.
|
|
||||||
func GetEnv(key string) (value string, exists bool) {
|
|
||||||
if value, exists = os.LookupEnv("BESZEL_HUB_" + key); exists {
|
|
||||||
return value, exists
|
|
||||||
}
|
|
||||||
// Fallback to the old unprefixed key
|
|
||||||
return os.LookupEnv(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// onAfterBootstrapAndMigrations ensures the provided function runs after the database is set up and migrations are applied.
|
// onAfterBootstrapAndMigrations ensures the provided function runs after the database is set up and migrations are applied.
|
||||||
// This is a workaround for behavior in PocketBase where onBootstrap runs before migrations, forcing use of onServe for this purpose.
|
// This is a workaround for behavior in PocketBase where onBootstrap runs before migrations, forcing use of onServe for this purpose.
|
||||||
// However, PB's tests.TestApp is already bootstrapped, generally doesn't serve, but does handle migrations.
|
// However, PB's tests.TestApp is already bootstrapped, generally doesn't serve, but does handle migrations.
|
||||||
@@ -131,7 +123,7 @@ func (h *Hub) initialize(app core.App) error {
|
|||||||
// batch requests (for alerts)
|
// batch requests (for alerts)
|
||||||
settings.Batch.Enabled = true
|
settings.Batch.Enabled = true
|
||||||
// set URL if APP_URL env is set
|
// set URL if APP_URL env is set
|
||||||
if appURL, isSet := GetEnv("APP_URL"); isSet {
|
if appURL, isSet := utils.GetEnv("APP_URL"); isSet {
|
||||||
h.appURL = appURL
|
h.appURL = appURL
|
||||||
settings.Meta.AppURL = appURL
|
settings.Meta.AppURL = appURL
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/henrygd/beszel"
|
"github.com/henrygd/beszel"
|
||||||
|
"github.com/henrygd/beszel/internal/hub/utils"
|
||||||
"github.com/henrygd/beszel/internal/site"
|
"github.com/henrygd/beszel/internal/site"
|
||||||
|
|
||||||
"github.com/pocketbase/pocketbase/apis"
|
"github.com/pocketbase/pocketbase/apis"
|
||||||
@@ -32,7 +33,7 @@ func (h *Hub) startServer(se *core.ServeEvent) error {
|
|||||||
staticPaths := [2]string{"/static/", "/assets/"}
|
staticPaths := [2]string{"/static/", "/assets/"}
|
||||||
serveStatic := apis.Static(site.DistDirFS, false)
|
serveStatic := apis.Static(site.DistDirFS, false)
|
||||||
// get CSP configuration
|
// get CSP configuration
|
||||||
csp, cspExists := GetEnv("CSP")
|
csp, cspExists := utils.GetEnv("CSP")
|
||||||
// add route
|
// add route
|
||||||
se.Router.GET("/{path...}", func(e *core.RequestEvent) error {
|
se.Router.GET("/{path...}", func(e *core.RequestEvent) error {
|
||||||
// serve static assets if path is in staticPaths
|
// serve static assets if path is in staticPaths
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
|
|
||||||
"github.com/henrygd/beszel/internal/common"
|
"github.com/henrygd/beszel/internal/common"
|
||||||
"github.com/henrygd/beszel/internal/hub/transport"
|
"github.com/henrygd/beszel/internal/hub/transport"
|
||||||
|
"github.com/henrygd/beszel/internal/hub/utils"
|
||||||
"github.com/henrygd/beszel/internal/hub/ws"
|
"github.com/henrygd/beszel/internal/hub/ws"
|
||||||
|
|
||||||
"github.com/henrygd/beszel/internal/entities/container"
|
"github.com/henrygd/beszel/internal/entities/container"
|
||||||
@@ -353,14 +354,21 @@ func (sys *System) getRecord(app core.App) (*core.Record, error) {
|
|||||||
return record, nil
|
return record, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// HasUser checks if the given user ID is in the system's users list.
|
// HasUser checks if the given user is in the system's users list.
|
||||||
func (sys *System) HasUser(app core.App, userID string) bool {
|
// Returns true if SHARE_ALL_SYSTEMS is enabled (any authenticated user can access any system).
|
||||||
|
func (sys *System) HasUser(app core.App, user *core.Record) bool {
|
||||||
|
if user == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if v, _ := utils.GetEnv("SHARE_ALL_SYSTEMS"); v == "true" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
record, err := sys.getRecord(app)
|
record, err := sys.getRecord(app)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
users := record.GetStringSlice("users")
|
users := record.GetStringSlice("users")
|
||||||
return slices.Contains(users, userID)
|
return slices.Contains(users, user.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// setDown marks a system as down in the database.
|
// setDown marks a system as down in the database.
|
||||||
|
|||||||
@@ -421,3 +421,51 @@ func testOld(t *testing.T, hub *tests.TestHub) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHasUser(t *testing.T) {
|
||||||
|
hub, err := tests.NewTestHub(t.TempDir())
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer hub.Cleanup()
|
||||||
|
|
||||||
|
sm := hub.GetSystemManager()
|
||||||
|
err = sm.Initialize()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
user1, err := tests.CreateUser(hub, "user1@test.com", "password123")
|
||||||
|
require.NoError(t, err)
|
||||||
|
user2, err := tests.CreateUser(hub, "user2@test.com", "password123")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
record, err := tests.CreateRecord(hub, "systems", map[string]any{
|
||||||
|
"name": "has-user-test",
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": "33914",
|
||||||
|
"users": []string{user1.Id},
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
sys, err := sm.GetSystemFromStore(record.Id)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
t.Run("user in list returns true", func(t *testing.T) {
|
||||||
|
assert.True(t, sys.HasUser(hub, user1))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("user not in list returns false", func(t *testing.T) {
|
||||||
|
assert.False(t, sys.HasUser(hub, user2))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unknown user ID returns false", func(t *testing.T) {
|
||||||
|
assert.False(t, sys.HasUser(hub, nil))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("SHARE_ALL_SYSTEMS=true grants access to non-member", func(t *testing.T) {
|
||||||
|
t.Setenv("SHARE_ALL_SYSTEMS", "true")
|
||||||
|
assert.True(t, sys.HasUser(hub, user2))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("BESZEL_HUB_SHARE_ALL_SYSTEMS=true grants access to non-member", func(t *testing.T) {
|
||||||
|
t.Setenv("BESZEL_HUB_SHARE_ALL_SYSTEMS", "true")
|
||||||
|
assert.True(t, sys.HasUser(hub, user2))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
12
internal/hub/utils/utils.go
Normal file
12
internal/hub/utils/utils.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// Package utils provides utility functions for the hub.
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import "os"
|
||||||
|
|
||||||
|
// GetEnv retrieves an environment variable with a "BESZEL_HUB_" prefix, or falls back to the unprefixed key.
|
||||||
|
func GetEnv(key string) (value string, exists bool) {
|
||||||
|
if value, exists = os.LookupEnv("BESZEL_HUB_" + key); exists {
|
||||||
|
return value, exists
|
||||||
|
}
|
||||||
|
return os.LookupEnv(key)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user