Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions server/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ PLUGIN_PACKAGES += mattermost-plugin-calls-v1.11.1
PLUGIN_PACKAGES += mattermost-plugin-github-v2.6.0
PLUGIN_PACKAGES += mattermost-plugin-gitlab-v1.12.0
PLUGIN_PACKAGES += mattermost-plugin-jira-v4.5.1
PLUGIN_PACKAGES += mattermost-plugin-playbooks-v2.7.0
PLUGIN_PACKAGES += mattermost-plugin-playbooks-v2.8.0
PLUGIN_PACKAGES += mattermost-plugin-servicenow-v2.4.0
PLUGIN_PACKAGES += mattermost-plugin-zoom-v1.12.0
PLUGIN_PACKAGES += mattermost-plugin-agents-v1.7.2
Expand All @@ -174,7 +174,7 @@ PLUGIN_PACKAGES += mattermost-plugin-channel-export-v1.3.0
# download the package from to work. This will no longer be needed when we unify
# the way we pre-package FIPS and non-FIPS plugins.
ifeq ($(FIPS_ENABLED),true)
PLUGIN_PACKAGES = mattermost-plugin-playbooks-v2.7.0%2B1031c5e-fips
PLUGIN_PACKAGES = mattermost-plugin-playbooks-v2.8.0%2Bc4449ac-fips
PLUGIN_PACKAGES += mattermost-plugin-agents-v1.7.2%2B866e2dd-fips
PLUGIN_PACKAGES += mattermost-plugin-boards-v9.2.2%2B4282c63-fips
endif
Expand Down
8 changes: 8 additions & 0 deletions server/channels/app/slack.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ func (a *App) SlackImport(rctx request.CTX, fileData multipart.File, fileSize in
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(rctx, 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(rctx, a.ch.imgDecoder, bytes.NewReader(fileData))
if err != nil {
Expand Down
12 changes: 8 additions & 4 deletions server/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3279,8 +3279,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",
Expand All @@ -3307,8 +3307,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",
Expand All @@ -3322,6 +3322,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"
Expand Down
19 changes: 14 additions & 5 deletions server/platform/services/slackimport/slackimport.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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
Expand All @@ -287,16 +286,26 @@ 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)
if mUser == nil {
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
Expand Down Expand Up @@ -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
}

Expand Down
112 changes: 112 additions & 0 deletions server/platform/services/slackimport/slackimport_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package slackimport
import (
"archive/zip"
"bytes"
"fmt"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -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")
}
Loading