From 13ea8106eb3402383d60a8d719884d26335bd810 Mon Sep 17 00:00:00 2001 From: Sven van Ginkel Date: Tue, 13 Oct 2026 14:37:04 +0200 Subject: [PATCH] users: add OAUTH_DEFAULT_USER_ROLE and make USER_EMAIL user admin (#2516) --- internal/migrations/initial-settings.go | 3 ++- internal/users/oauth_test.go | 36 ++++++++++++++++++++++--- internal/users/users.go | 12 ++++++++- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/internal/migrations/initial-settings.go b/internal/migrations/initial-settings.go index 759b77796..e0c02f076 100644 --- a/internal/migrations/initial-settings.go +++ b/internal/migrations/initial-settings.go @@ -44,13 +44,14 @@ func init() { superUser.SetRandomPassword() } - // if user details are provided, we create a regular user as well + // if user details are provided, we create an admin user as well if didProvideUserDetails { usersCollection, _ := app.FindCollectionByNameOrId("users") user := core.NewRecord(usersCollection) user.SetEmail(email) user.SetPassword(password) user.SetVerified(true) + user.Set("role", "admin") err := app.Save(user) if err != nil { return err diff --git a/internal/users/oauth_test.go b/internal/users/oauth_test.go index 99333042f..1f1503531 100644 --- a/internal/users/oauth_test.go +++ b/internal/users/oauth_test.go @@ -33,8 +33,22 @@ func TestOAuthUserRole(t *testing.T) { auth.Providers[provider] = func() auth.Provider { return &roleTestProvider{} } t.Cleanup(func() { delete(auth.Providers, provider) }) - for _, createData := range []string{`{}`, `{"role":"admin"}`, `{"role":"readonly"}`} { - t.Run(createData, func(t *testing.T) { + for _, tc := range []struct { + createData, defaultRole, want string + }{ + {`{}`, "", "user"}, + {`{"role":"admin"}`, "", "user"}, + {`{"role":"readonly"}`, "", "user"}, + {`{}`, "readonly", "readonly"}, + {`{"role":"admin"}`, "readonly", "readonly"}, + {`{}`, "user", "user"}, + // admin and unknown values must never be granted through self-registration + {`{}`, "admin", "user"}, + {`{"role":"readonly"}`, "bogus", "user"}, + } { + t.Run(tc.createData+"/default="+tc.defaultRole, func(t *testing.T) { + t.Setenv("OAUTH_DEFAULT_USER_ROLE", tc.defaultRole) + createData := tc.createData h, err := beszelTests.NewTestHub(t.TempDir()) require.NoError(t, err) defer h.Cleanup() @@ -62,7 +76,7 @@ func TestOAuthUserRole(t *testing.T) { login() user, err := h.FindAuthRecordByEmail("users", "oauth@example.com") require.NoError(t, err) - require.Equal(t, "user", user.GetString("role")) + require.Equal(t, tc.want, user.GetString("role")) // A later OAuth login must preserve a role assigned by an administrator. user.Set("role", "admin") @@ -75,7 +89,23 @@ func TestOAuthUserRole(t *testing.T) { } } +func TestEnvBootstrapUserIsAdmin(t *testing.T) { + t.Setenv("USER_EMAIL", "bootstrap@example.com") + t.Setenv("USER_PASSWORD", "password12345") + t.Setenv("OAUTH_DEFAULT_USER_ROLE", "readonly") + h, err := beszelTests.NewTestHub(t.TempDir()) + require.NoError(t, err) + defer h.Cleanup() + h.StartHub() + + user, err := h.FindAuthRecordByEmail("users", "bootstrap@example.com") + require.NoError(t, err) + require.Equal(t, "admin", user.GetString("role")) +} + func TestInternalUserRole(t *testing.T) { + // OAUTH_DEFAULT_USER_ROLE must not affect users created outside OAuth. + t.Setenv("OAUTH_DEFAULT_USER_ROLE", "readonly") for _, role := range []string{"", "user", "admin", "readonly"} { t.Run("role="+role, func(t *testing.T) { h, err := beszelTests.NewTestHub(t.TempDir()) diff --git a/internal/users/users.go b/internal/users/users.go index 8d4cf8726..1074d5335 100644 --- a/internal/users/users.go +++ b/internal/users/users.go @@ -6,6 +6,7 @@ import ( "log" "net/http" + "github.com/henrygd/beszel/internal/hub/utils" "github.com/henrygd/beszel/internal/migrations" "github.com/pocketbase/dbx" @@ -24,13 +25,22 @@ func NewUserManager(app core.App) *UserManager { } } +// oauthDefaultRole returns the role for new OAuth users from OAUTH_DEFAULT_USER_ROLE. +// Only "user" and "readonly" are accepted; anything else (including "admin") falls back to "user". +func oauthDefaultRole() string { + if role, _ := utils.GetEnv("OAUTH_DEFAULT_USER_ROLE"); role == "readonly" { + return role + } + return "user" +} + // InitializeOAuthUserRole prevents self-registration from assigning a privileged role. func (um *UserManager) InitializeOAuthUserRole(e *core.RecordAuthWithOAuth2RequestEvent) error { if e.IsNewRecord { if e.CreateData == nil { e.CreateData = make(map[string]any) } - e.CreateData["role"] = "user" + e.CreateData["role"] = oauthDefaultRole() } return e.Next() }