From d9a55e394cd5bc15edb49e9606bec8fec03ac4c6 Mon Sep 17 00:00:00 2001 From: Julien Tant <785518+JulienTant@users.noreply.github.com> Date: Tue, 19 May 2026 22:53:39 -0700 Subject: [PATCH] MM-68702: Reject demoting bot accounts to guest (#36487) (#36634) Automatic Merge --- server/channels/api4/user_test.go | 28 ++++++++++++++++++++++++++++ server/channels/app/user.go | 4 ++++ server/channels/app/user_test.go | 12 ++++++++++++ server/i18n/en.json | 4 ++++ 4 files changed, 48 insertions(+) diff --git a/server/channels/api4/user_test.go b/server/channels/api4/user_test.go index cad6f10d88..80aaa75047 100644 --- a/server/channels/api4/user_test.go +++ b/server/channels/api4/user_test.go @@ -6673,6 +6673,34 @@ func TestDemoteUserToGuest(t *testing.T) { require.NoError(t, err) }) + t.Run("cannot demote bot account", func(t *testing.T) { + th.App.Srv().SetLicense(model.NewTestLicense("guest_accounts")) + + prevBotCreation := *th.App.Config().ServiceSettings.EnableBotAccountCreation + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.EnableBotAccountCreation = true + }) + defer th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.EnableBotAccountCreation = prevBotCreation + }) + + createdBot, resp, err := th.SystemAdminClient.CreateBot(context.Background(), &model.Bot{ + Username: "botdemote" + model.NewId(), + DisplayName: "Demote Test Bot", + Description: "test", + }) + require.NoError(t, err) + CheckCreatedStatus(t, resp) + defer func() { + appErr := th.App.PermanentDeleteBot(th.Context, createdBot.UserId) + require.Nil(t, appErr) + }() + + demoteResp, err := th.SystemAdminClient.DemoteUserToGuest(context.Background(), createdBot.UserId) + CheckBadRequestStatus(t, demoteResp) + CheckErrorID(t, err, "api.user.demote_user_to_guest.bot_not_allowed.app_error") + }) + th.TestForSystemAdminAndLocal(t, func(t *testing.T, c *model.Client4) { _, _, err := c.GetUser(context.Background(), user.Id, "") require.NoError(t, err) diff --git a/server/channels/app/user.go b/server/channels/app/user.go index acb1ede8e3..d873f4b430 100644 --- a/server/channels/app/user.go +++ b/server/channels/app/user.go @@ -2570,6 +2570,10 @@ func (a *App) PromoteGuestToUser(c request.CTX, user *model.User, requestorId st // DemoteUserToGuest Convert user's roles and all his membership's roles from // regular user roles to guest roles. func (a *App) DemoteUserToGuest(c request.CTX, user *model.User) *model.AppError { + if user.IsBot { + return model.NewAppError("DemoteUserToGuest", "api.user.demote_user_to_guest.bot_not_allowed.app_error", nil, "", http.StatusBadRequest) + } + demotedUser, nErr := a.ch.srv.userService.DemoteUserToGuest(user) a.InvalidateCacheForUser(user.Id) if nErr != nil { diff --git a/server/channels/app/user_test.go b/server/channels/app/user_test.go index 657f2fb870..0306614901 100644 --- a/server/channels/app/user_test.go +++ b/server/channels/app/user_test.go @@ -1844,6 +1844,18 @@ func TestDemoteUserToGuest(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() + t.Run("Must reject bot user", func(t *testing.T) { + bot := th.CreateBot() + user, err := th.App.GetUser(bot.UserId) + require.Nil(t, err) + require.True(t, user.IsBot) + + appErr := th.App.DemoteUserToGuest(th.Context, user) + require.NotNil(t, appErr) + assert.Equal(t, "api.user.demote_user_to_guest.bot_not_allowed.app_error", appErr.Id) + assert.Equal(t, http.StatusBadRequest, appErr.StatusCode) + }) + t.Run("Must invalidate channel stats cache when demoting a user", func(t *testing.T) { user := th.CreateUser() require.Equal(t, "system_user", user.Roles) diff --git a/server/i18n/en.json b/server/i18n/en.json index 10f92b9e9c..375c7f1a0d 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -4130,6 +4130,10 @@ "id": "api.user.demote_user_to_guest.already_guest.app_error", "translation": "Unable to convert the user to guest because is already a guest." }, + { + "id": "api.user.demote_user_to_guest.bot_not_allowed.app_error", + "translation": "Bot accounts cannot be converted to guest accounts." + }, { "id": "api.user.email_to_ldap.not_available.app_error", "translation": "AD/LDAP not available on this server."