From 24a90356e614c08d51c451985b4be8510a8ea69d Mon Sep 17 00:00:00 2001 From: Mattermost Build Date: Wed, 18 Mar 2026 14:30:54 +0100 Subject: [PATCH] Automated cherry pick of #35490 (#35650) Automatic Merge --- server/channels/app/slack.go | 8 ++ server/i18n/en.json | 12 +- .../services/slackimport/slackimport.go | 19 ++- .../services/slackimport/slackimport_test.go | 112 ++++++++++++++++++ 4 files changed, 142 insertions(+), 9 deletions(-) diff --git a/server/channels/app/slack.go b/server/channels/app/slack.go index ff9f42a0fd..8614d5f756 100644 --- a/server/channels/app/slack.go +++ b/server/channels/app/slack.go @@ -40,6 +40,14 @@ func (a *App) SlackImport(c request.CTX, fileData multipart.File, fileSize int64 GeneratePreviewImage: a.generatePreviewImage, InvalidateAllCaches: func() *model.AppError { return a.ch.srv.platform.InvalidateAllCaches() }, MaxPostSize: func() int { return a.ch.srv.platform.MaxPostSize() }, + SendPasswordReset: func(email string) (bool, *model.AppError) { + sent, err := a.SendPasswordReset(c, email, a.GetSiteURL()) + if err != nil { + return false, err + } + + return sent, nil + }, PrepareImage: func(fileData []byte) (image.Image, string, func(), error) { img, imgType, release, err := prepareImage(c, a.ch.imgDecoder, bytes.NewReader(fileData)) if err != nil { diff --git a/server/i18n/en.json b/server/i18n/en.json index dab7933352..237dd57193 100644 --- a/server/i18n/en.json +++ b/server/i18n/en.json @@ -3031,8 +3031,8 @@ "translation": "Could not uninvite remote to channel" }, { - "id": "api.slackimport.slack_add_bot_user.email_pwd", - "translation": "The Integration/Slack Bot user with email {{.Email}} and password {{.Password}} has been imported.\r\n" + "id": "api.slackimport.slack_add_bot_user.email", + "translation": "The Integration/Slack Bot user with email {{.Email}} has been imported.\r\n" }, { "id": "api.slackimport.slack_add_bot_user.unable_import", @@ -3059,8 +3059,8 @@ "translation": "\r\nUsers created:\r\n" }, { - "id": "api.slackimport.slack_add_users.email_pwd", - "translation": "Slack user with email {{.Email}} and password {{.Password}} has been imported.\r\n" + "id": "api.slackimport.slack_add_users.email", + "translation": "Slack user with email {{.Email}} has been imported.\r\n" }, { "id": "api.slackimport.slack_add_users.merge_existing", @@ -3074,6 +3074,10 @@ "id": "api.slackimport.slack_add_users.missing_email_address", "translation": "User {{.Username}} does not have an email address in the Slack export. Used {{.Email}} as a placeholder. The user should update their email address once logged in to the system.\r\n" }, + { + "id": "api.slackimport.slack_add_users.send_reset_email_failed", + "translation": "Unable to send password reset email to {{.Username}} at {{.Email}}.\r\n" + }, { "id": "api.slackimport.slack_add_users.unable_import", "translation": "Unable to import Slack user: {{.Username}}.\r\n" diff --git a/server/platform/services/slackimport/slackimport.go b/server/platform/services/slackimport/slackimport.go index c67345c431..794a26c7c4 100644 --- a/server/platform/services/slackimport/slackimport.go +++ b/server/platform/services/slackimport/slackimport.go @@ -95,6 +95,7 @@ type Actions struct { GeneratePreviewImage func(request.CTX, image.Image, string, string) InvalidateAllCaches func() *model.AppError MaxPostSize func() int + SendPasswordReset func(string) (bool, *model.AppError) PrepareImage func(fileData []byte) (image.Image, string, func(), error) } @@ -268,8 +269,6 @@ func (si *SlackImporter) slackAddUsers(rctx request.CTX, teamId string, slackuse rctx.Logger().Warn("Slack Import: User does not have an email address in the Slack export. Used username as a placeholder. The user should update their email address once logged in to the system.", mlog.String("user_email", email), mlog.String("user_name", sUser.Username)) } - password := model.NewId() - // Check for email conflict and use existing user if found if existingUser, err := si.store.User().GetByEmail(email); err == nil { addedUsers[sUser.Id] = existingUser @@ -287,7 +286,7 @@ func (si *SlackImporter) slackAddUsers(rctx request.CTX, teamId string, slackuse FirstName: firstName, LastName: lastName, Email: email, - Password: password, + Password: "", } mUser := si.oldImportUser(rctx, team, &newUser) @@ -295,8 +294,18 @@ func (si *SlackImporter) slackAddUsers(rctx request.CTX, teamId string, slackuse importerLog.WriteString(i18n.T("api.slackimport.slack_add_users.unable_import", map[string]any{"Username": sUser.Username})) continue } + + sent, err := si.actions.SendPasswordReset(email) + if err != nil { + rctx.Logger().Warn("Slack Import: Cannot send password reset email to user. An admin should update their email address once logged in to the system.", mlog.String("user_email", email), mlog.String("user_name", sUser.Username)) + } + + if !sent { + importerLog.WriteString(i18n.T("api.slackimport.slack_add_users.send_reset_email_failed", map[string]any{"Username": sUser.Username, "Email": newUser.Email})) + } + addedUsers[sUser.Id] = mUser - importerLog.WriteString(i18n.T("api.slackimport.slack_add_users.email_pwd", map[string]any{"Email": newUser.Email, "Password": password})) + importerLog.WriteString(i18n.T("api.slackimport.slack_add_users.email", map[string]any{"Email": newUser.Email})) } return addedUsers @@ -327,7 +336,7 @@ func (si *SlackImporter) slackAddBotUser(rctx request.CTX, teamId string, log *b return nil } - log.WriteString(i18n.T("api.slackimport.slack_add_bot_user.email_pwd", map[string]any{"Email": botUser.Email, "Password": password})) + log.WriteString(i18n.T("api.slackimport.slack_add_bot_user.email", map[string]any{"Email": botUser.Email})) return mUser } diff --git a/server/platform/services/slackimport/slackimport_test.go b/server/platform/services/slackimport/slackimport_test.go index 30e82e56c3..1872475730 100644 --- a/server/platform/services/slackimport/slackimport_test.go +++ b/server/platform/services/slackimport/slackimport_test.go @@ -6,6 +6,7 @@ package slackimport import ( "archive/zip" "bytes" + "fmt" "os" "path/filepath" "strings" @@ -760,3 +761,114 @@ func TestSlackImportEnhancedSecurityBackwardsCompatibility(t *testing.T) { // Verify VerifyEmail was NOT called userStore.AssertNotCalled(t, "VerifyEmail") } + +type slackAddUsersTestSetup struct { + store *mocks.Store + teamStore *mocks.TeamStore + userStore *mocks.UserStore + team *model.Team + savedUser *model.User +} + +func newSlackAddUsersTestSetup(t *testing.T) *slackAddUsersTestSetup { + t.Helper() + + s := &slackAddUsersTestSetup{} + s.store = &mocks.Store{} + s.teamStore = &mocks.TeamStore{} + s.userStore = &mocks.UserStore{} + s.store.On("Team").Return(s.teamStore) + s.store.On("User").Return(s.userStore) + + s.team = &model.Team{Id: "test-team-id", Name: "test-team"} + s.teamStore.On("Get", "test-team-id").Return(s.team, nil) + s.userStore.On("GetByEmail", mock.AnythingOfType("string")).Return(nil, fmt.Errorf("not found")) + + s.savedUser = &model.User{Id: "test-user-id", Username: "testuser", Email: "testuser@example.com"} + s.userStore.On("Save", mock.AnythingOfType("*request.Context"), mock.AnythingOfType("*model.User")).Return(s.savedUser, nil) + + return s +} + +func (s *slackAddUsersTestSetup) newImporter(actions Actions) *SlackImporter { + config := &model.Config{} + config.SetDefaults() + return New(s.store, actions, config) +} + +func (s *slackAddUsersTestSetup) defaultActions() Actions { + return Actions{ + JoinUserToTeam: func(team *model.Team, user *model.User, userRequestorId string) (*model.TeamMember, *model.AppError) { + return &model.TeamMember{}, nil + }, + SendPasswordReset: func(email string) (bool, *model.AppError) { + return true, nil + }, + } +} + +func defaultSlackUsers() []slackUser { + return []slackUser{ + {Id: "U001", Username: "testuser", Profile: slackProfile{FirstName: "Test", LastName: "User", Email: "testuser@example.com"}}, + } +} + +func TestSlackAddUsersLogContainsProperUserCreationMessage(t *testing.T) { + rctx := request.TestContext(t) + s := newSlackAddUsersTestSetup(t) + + importer := s.newImporter(s.defaultActions()) + importerLog := new(bytes.Buffer) + importer.slackAddUsers(rctx, "test-team-id", defaultSlackUsers(), importerLog) + + logOutput := importerLog.String() + assert.Contains(t, logOutput, "api.slackimport.slack_add_users.email", "import log should contain the user creation message") + assert.NotContains(t, logOutput, "api.slackimport.slack_add_users.email_pwd", "import log must not use the old user creation message") +} + +func TestSlackAddUsersLogsSendResetEmailFailure(t *testing.T) { + rctx := request.TestContext(t) + s := newSlackAddUsersTestSetup(t) + + actions := s.defaultActions() + actions.SendPasswordReset = func(email string) (bool, *model.AppError) { + return false, nil + } + + importer := s.newImporter(actions) + importerLog := new(bytes.Buffer) + importer.slackAddUsers(rctx, "test-team-id", defaultSlackUsers(), importerLog) + + assert.Contains(t, importerLog.String(), "api.slackimport.slack_add_users.send_reset_email_failed") +} + +func TestSlackAddUsersGeneratesUserWithEmptyPassword(t *testing.T) { + rctx := request.TestContext(t) + s := newSlackAddUsersTestSetup(t) + + importer := s.newImporter(s.defaultActions()) + importerLog := new(bytes.Buffer) + importer.slackAddUsers(rctx, "test-team-id", defaultSlackUsers(), importerLog) + + s.userStore.AssertCalled(t, "Save", mock.AnythingOfType("*request.Context"), mock.MatchedBy(func(u *model.User) bool { + return u.Password == "" + })) +} + +func TestSlackAddUsersTriggersPasswordResetFlow(t *testing.T) { + rctx := request.TestContext(t) + s := newSlackAddUsersTestSetup(t) + + passwordResetCalled := false + actions := s.defaultActions() + actions.SendPasswordReset = func(email string) (bool, *model.AppError) { + passwordResetCalled = true + return true, nil + } + + importer := s.newImporter(actions) + importerLog := new(bytes.Buffer) + importer.slackAddUsers(rctx, "test-team-id", defaultSlackUsers(), importerLog) + + assert.True(t, passwordResetCalled, "SendPasswordReset should be called for each imported user") +}