From 67739cb516309e06a7cb08cc5807140ac9af9b13 Mon Sep 17 00:00:00 2001 From: George Goldberg Date: Tue, 31 Jan 2017 13:04:17 +0000 Subject: PLT-5365 Import of basic user properties. (#5231) --- app/import.go | 175 ++++++++++++++++++++++++++++++- app/import_test.go | 298 ++++++++++++++++++++++++++++++++++++++++++++++++++++- app/slackimport.go | 4 +- app/user.go | 59 ++++++----- 4 files changed, 508 insertions(+), 28 deletions(-) (limited to 'app') diff --git a/app/import.go b/app/import.go index 26981f0c2..e99dd79dc 100644 --- a/app/import.go +++ b/app/import.go @@ -15,6 +15,7 @@ import ( l4g "github.com/alecthomas/log4go" "github.com/mattermost/platform/model" "github.com/mattermost/platform/utils" + "net/http" ) // Import Data Models @@ -23,6 +24,7 @@ type LineImportData struct { Type string `json:"type"` Team *TeamImportData `json:"team"` Channel *ChannelImportData `json:"channel"` + User *UserImportData `json:"user"` } type TeamImportData struct { @@ -42,6 +44,19 @@ type ChannelImportData struct { Purpose *string `json:"purpose"` } +type UserImportData struct { + Username *string `json:"username"` + Email *string `json:"email"` + AuthService *string `json:"auth_service"` + AuthData *string `json:"auth_data"` + Nickname *string `json:"nickname"` + FirstName *string `json:"first_name"` + LastName *string `json:"last_name"` + Position *string `json:"position"` + Roles *string `json:"roles"` + Locale *string `json:"locale"` +} + // // -- Bulk Import Functions -- // These functions import data directly into the database. Security and permission checks are bypassed but validity is @@ -86,6 +101,12 @@ func ImportLine(line LineImportData, dryRun bool) *model.AppError { } else { return ImportChannel(line.Channel, dryRun) } + case line.Type == "user": + if line.User == nil { + return model.NewAppError("BulkImport", "app.import.import_line.null_user.error", nil, "", http.StatusBadRequest) + } else { + return ImportUser(line.User, dryRun) + } default: return model.NewLocAppError("BulkImport", "app.import.import_line.unknown_line_type.error", map[string]interface{}{"Type": line.Type}, "") } @@ -251,6 +272,158 @@ func validateChannelImportData(data *ChannelImportData) *model.AppError { return nil } +func ImportUser(data *UserImportData, dryRun bool) *model.AppError { + if err := validateUserImportData(data); err != nil { + return err + } + + // If this is a Dry Run, do not continue any further. + if dryRun { + return nil + } + + var user *model.User + if result := <-Srv.Store.User().GetByUsername(*data.Username); result.Err == nil { + user = result.Data.(*model.User) + } else { + user = &model.User{} + } + + user.Username = *data.Username + user.Email = *data.Email + + var password string + var authService string + var authData *string + + if data.AuthService != nil { + authService = *data.AuthService + } + + // AuthData and Password are mutually exclusive. + if data.AuthData != nil { + authData = data.AuthData + password = "" + } else { + // If no Auth Data is specified, we must generate a password. + password = model.NewId() + authData = nil + } + + user.Password = password + user.AuthService = authService + user.AuthData = authData + + // Automatically assume all emails are verified. + emailVerified := true + user.EmailVerified = emailVerified + + if data.Nickname != nil { + user.Nickname = *data.Nickname + } + + if data.FirstName != nil { + user.FirstName = *data.FirstName + } + + if data.LastName != nil { + user.LastName = *data.LastName + } + + if data.Position != nil { + user.Position = *data.Position + } + + if data.Locale != nil { + user.Locale = *data.Locale + } else { + user.Locale = *utils.Cfg.LocalizationSettings.DefaultClientLocale + } + + var roles string + if data.Roles != nil { + roles = *data.Roles + } else if len(user.Roles) == 0 { + // Set SYSTEM_USER roles on newly created users by default. + roles = model.ROLE_SYSTEM_USER.Id + } + user.Roles = roles + + if user.Id == "" { + if _, err := createUser(user); err != nil { + return err + } + } else { + if _, err := UpdateUser(user, utils.GetSiteURL(), false); err != nil { + return err + } + if _, err := UpdateUserRoles(user.Id, roles); err != nil { + return err + } + if len(password) > 0 { + if err := UpdatePassword(user, password); err != nil { + return err + } + } else { + if res := <-Srv.Store.User().UpdateAuthData(user.Id, authService, authData, user.Email, false); res.Err != nil { + return res.Err + } + } + if emailVerified { + if err := VerifyUserEmail(user.Id); err != nil { + return err + } + } + } + + return nil +} + +func validateUserImportData(data *UserImportData) *model.AppError { + + if data.Username == nil { + return model.NewAppError("BulkImport", "app.import.validate_user_import_data.username_missing.error", nil, "", http.StatusBadRequest) + } else if !model.IsValidUsername(*data.Username) { + return model.NewAppError("BulkImport", "app.import.validate_user_import_data.username_invalid.error", nil, "", http.StatusBadRequest) + } + + if data.Email == nil { + return model.NewAppError("BulkImport", "app.import.validate_user_import_data.email_missing.error", nil, "", http.StatusBadRequest) + } else if len(*data.Email) == 0 || len(*data.Email) > model.USER_EMAIL_MAX_LENGTH { + return model.NewAppError("BulkImport", "app.import.validate_user_import_data.email_length.error", nil, "", http.StatusBadRequest) + } + + if data.AuthService != nil && len(*data.AuthService) == 0 { + return model.NewAppError("BulkImport", "app.import.validate_user_import_data.auth_service_length.error", nil, "", http.StatusBadRequest) + } + + if data.AuthData != nil && len(*data.AuthData) > model.USER_AUTH_DATA_MAX_LENGTH { + return model.NewAppError("BulkImport", "app.import.validate_user_import_data.auth_data_length.error", nil, "", http.StatusBadRequest) + } + + if data.Nickname != nil && utf8.RuneCountInString(*data.Nickname) > model.USER_NICKNAME_MAX_RUNES { + return model.NewAppError("BulkImport", "app.import.validate_user_import_data.nickname_length.error", nil, "", http.StatusBadRequest) + } + + if data.FirstName != nil && utf8.RuneCountInString(*data.FirstName) > model.USER_FIRST_NAME_MAX_RUNES { + return model.NewAppError("BulkImport", "app.import.validate_user_import_data.first_name_length.error", nil, "", http.StatusBadRequest) + } + + if data.LastName != nil && utf8.RuneCountInString(*data.LastName) > model.USER_LAST_NAME_MAX_RUNES { + return model.NewAppError("BulkImport", "app.import.validate_user_import_data.last_name_length.error", nil, "", http.StatusBadRequest) + } + + if data.Position != nil && utf8.RuneCountInString(*data.Position) > model.USER_POSITION_MAX_RUNES { + return model.NewAppError("BulkImport", "app.import.validate_user_import_data.position_length.error", nil, "", http.StatusBadRequest) + } + + if data.Roles != nil && !model.IsValidUserRoles(*data.Roles) { + return model.NewAppError("BulkImport", "app.import.validate_user_import_data.roles_invalid.error", nil, "", http.StatusBadRequest) + } + + return nil +} + // // -- Old SlackImport Functions -- // Import functions are sutible for entering posts and users into the database without @@ -288,7 +461,7 @@ func ImportPost(post *model.Post) { } } -func ImportUser(team *model.Team, user *model.User) *model.User { +func OldImportUser(team *model.Team, user *model.User) *model.User { user.MakeNonNil() user.Roles = model.ROLE_SYSTEM_USER.Id diff --git a/app/import_test.go b/app/import_test.go index 8f08ff34a..f3add56b2 100644 --- a/app/import_test.go +++ b/app/import_test.go @@ -7,6 +7,7 @@ import ( "github.com/mattermost/platform/model" "strings" "testing" + "github.com/mattermost/platform/utils" ) func ptrStr(s string) *string { @@ -236,6 +237,118 @@ func TestImportValidateChannelImportData(t *testing.T) { } } +func TestImportValidateUserImportData(t *testing.T) { + + // Test with minimum required valid properties. + data := UserImportData{ + Username: ptrStr("bob"), + Email: ptrStr("bob@example.com"), + } + if err := validateUserImportData(&data); err != nil { + t.Fatal("Validation failed but should have been valid.") + } + + // Invalid Usernames. + data.Username = nil + if err := validateUserImportData(&data); err == nil { + t.Fatal("Validation should have failed due to nil Username.") + } + + data.Username = ptrStr("") + if err := validateUserImportData(&data); err == nil { + t.Fatal("Validation should have failed due to 0 length Username.") + } + + data.Username = ptrStr(strings.Repeat("abcdefghij", 7)) + if err := validateUserImportData(&data); err == nil { + t.Fatal("Validation should have failed due to too long Username.") + } + + data.Username = ptrStr("i am a username with spaces and !!!") + if err := validateUserImportData(&data); err == nil { + t.Fatal("Validation should have failed due to invalid characters in Username.") + } + + data.Username = ptrStr("bob") + + // Invalid Emails + data.Email = nil + if err := validateUserImportData(&data); err == nil { + t.Fatal("Validation should have failed due to nil Email.") + } + + data.Email = ptrStr("") + if err := validateUserImportData(&data); err == nil { + t.Fatal("Validation should have failed due to 0 length Email.") + } + + data.Email = ptrStr(strings.Repeat("abcdefghij", 13)) + if err := validateUserImportData(&data); err == nil { + t.Fatal("Validation should have failed due to too long Email.") + } + + data.Email = ptrStr("bob@example.com") + + // TODO: Auth Service and Auth Data. + + // Test a valid User with all fields populated. + data = UserImportData{ + Username: ptrStr("bob"), + Email: ptrStr("bob@example.com"), + AuthService: ptrStr("ldap"), + AuthData: ptrStr("bob"), + Nickname: ptrStr("BobNick"), + FirstName: ptrStr("Bob"), + LastName: ptrStr("Blob"), + Position: ptrStr("The Boss"), + Roles: ptrStr("system_user"), + Locale: ptrStr("en"), + } + if err := validateUserImportData(&data); err != nil { + t.Fatal("Validation failed but should have been valid.") + } + + // Test various invalid optional field values. + data.Nickname = ptrStr(strings.Repeat("abcdefghij", 7)) + if err := validateUserImportData(&data); err == nil { + t.Fatal("Validation should have failed due to too long Nickname.") + } + data.Nickname = ptrStr("BobNick") + + data.FirstName = ptrStr(strings.Repeat("abcdefghij", 7)) + if err := validateUserImportData(&data); err == nil { + t.Fatal("Validation should have failed due to too long First Name.") + } + data.FirstName = ptrStr("Bob") + + data.LastName = ptrStr(strings.Repeat("abcdefghij", 7)) + if err := validateUserImportData(&data); err == nil { + t.Fatal("Validation should have failed due to too long Last name.") + } + data.LastName = ptrStr("Blob") + + data.Position = ptrStr(strings.Repeat("abcdefghij", 7)) + if err := validateUserImportData(&data); err == nil { + t.Fatal("Validation should have failed due to too long Position.") + } + data.Position = ptrStr("The Boss") + + data.Roles = ptrStr("system_user wat") + if err := validateUserImportData(&data); err == nil { + t.Fatal("Validation should have failed due to too unrecognised role.") + } + data.Roles = nil + if err := validateUserImportData(&data); err != nil { + t.Fatal("Validation failed but should have been valid.") + } + + data.Roles = ptrStr("") + if err := validateUserImportData(&data); err != nil { + t.Fatal("Validation failed but should have been valid.") + } + data.Roles = ptrStr("system_user") +} + func TestImportImportTeam(t *testing.T) { _ = Setup() @@ -505,6 +618,182 @@ func TestImportImportChannel(t *testing.T) { } +func TestImportImportUser(t *testing.T) { + _ = Setup() + + // Check how many users are in the database. + var userCount int64 + if r := <-Srv.Store.User().GetTotalUsersCount(); r.Err == nil { + userCount = r.Data.(int64) + } else { + t.Fatalf("Failed to get user count.") + } + + // Do an invalid user in dry-run mode. + data := UserImportData{ + Username: ptrStr(model.NewId()), + } + if err := ImportUser(&data, true); err == nil { + t.Fatalf("Should have failed to import invalid user.") + } + + // Check that no more users are in the DB. + if r := <-Srv.Store.User().GetTotalUsersCount(); r.Err == nil { + if r.Data.(int64) != userCount { + t.Fatalf("Unexpected number of users") + } + } else { + t.Fatalf("Failed to get user count.") + } + + // Do a valid user in dry-run mode. + data = UserImportData{ + Username: ptrStr(model.NewId()), + Email: ptrStr(model.NewId() + "@example.com"), + } + if err := ImportUser(&data, true); err != nil { + t.Fatalf("Should have succeeded to import valid user.") + } + + // Check that no more users are in the DB. + if r := <-Srv.Store.User().GetTotalUsersCount(); r.Err == nil { + if r.Data.(int64) != userCount { + t.Fatalf("Unexpected number of users") + } + } else { + t.Fatalf("Failed to get user count.") + } + + // Do an invalid user in apply mode. + data = UserImportData{ + Username: ptrStr(model.NewId()), + } + if err := ImportUser(&data, false); err == nil { + t.Fatalf("Should have failed to import invalid user.") + } + + // Check that no more users are in the DB. + if r := <-Srv.Store.User().GetTotalUsersCount(); r.Err == nil { + if r.Data.(int64) != userCount { + t.Fatalf("Unexpected number of users") + } + } else { + t.Fatalf("Failed to get user count.") + } + + // Do a valid user in apply mode. + username := model.NewId() + data = UserImportData{ + Username: &username, + Email: ptrStr(model.NewId() + "@example.com"), + Nickname: ptrStr(model.NewId()), + FirstName: ptrStr(model.NewId()), + LastName: ptrStr(model.NewId()), + Position: ptrStr(model.NewId()), + } + if err := ImportUser(&data, false); err != nil { + t.Fatalf("Should have succeeded to import valid user.") + } + + // Check that one more user is in the DB. + if r := <-Srv.Store.User().GetTotalUsersCount(); r.Err == nil { + if r.Data.(int64) != userCount + 1 { + t.Fatalf("Unexpected number of users") + } + } else { + t.Fatalf("Failed to get user count.") + } + + // Get the user and check all the fields are correct. + if user, err := GetUserByUsername(username); err != nil { + t.Fatalf("Failed to get user from database.") + } else { + if user.Email != *data.Email || user.Nickname != *data.Nickname || user.FirstName != *data.FirstName || user.LastName != *data.LastName || user.Position != *data.Position { + t.Fatalf("User properties do not match Import Data.") + } + // Check calculated properties. + if user.AuthService != "" { + t.Fatalf("Expected Auth Service to be empty.") + } + + if ! (user.AuthData == nil || *user.AuthData == "") { + t.Fatalf("Expected AuthData to be empty.") + } + + if len(user.Password) == 0 { + t.Fatalf("Expected password to be set.") + } + + if !user.EmailVerified { + t.Fatalf("Expected EmailVerified to be true.") + } + + if user.Locale != *utils.Cfg.LocalizationSettings.DefaultClientLocale { + t.Fatalf("Expected Locale to be the default.") + } + + if user.Roles != "system_user" { + t.Fatalf("Expected roles to be system_user") + } + } + + // Alter all the fields of that user. + data.Email = ptrStr(model.NewId() + "@example.com") + data.AuthService = ptrStr("ldap") + data.AuthData = &username + data.Nickname = ptrStr(model.NewId()) + data.FirstName = ptrStr(model.NewId()) + data.LastName = ptrStr(model.NewId()) + data.Position = ptrStr(model.NewId()) + data.Roles = ptrStr("system_admin system_user") + data.Locale = ptrStr("zh_CN") + if err := ImportUser(&data, false); err != nil { + t.Fatalf("Should have succeeded to update valid user %v", err) + } + + // Check user count the same. + if r := <-Srv.Store.User().GetTotalUsersCount(); r.Err == nil { + if r.Data.(int64) != userCount + 1 { + t.Fatalf("Unexpected number of users") + } + } else { + t.Fatalf("Failed to get user count.") + } + + // Get the user and check all the fields are correct. + if user, err := GetUserByUsername(username); err != nil { + t.Fatalf("Failed to get user from database.") + } else { + if user.Email != *data.Email || user.Nickname != *data.Nickname || user.FirstName != *data.FirstName || user.LastName != *data.LastName || user.Position != *data.Position { + t.Fatalf("Updated User properties do not match Import Data.") + } + // Check calculated properties. + if user.AuthService != "ldap" { + t.Fatalf("Expected Auth Service to be ldap \"%v\"", user.AuthService) + } + + if ! (user.AuthData == data.AuthData || *user.AuthData == *data.AuthData) { + t.Fatalf("Expected AuthData to be set.") + } + + if len(user.Password) != 0 { + t.Fatalf("Expected password to be empty.") + } + + if !user.EmailVerified { + t.Fatalf("Expected EmailVerified to be true.") + } + + if user.Locale != *data.Locale { + t.Fatalf("Expected Locale to be the set.") + } + + if user.Roles != *data.Roles { + t.Fatalf("Expected roles to be set: %v", user.Roles) + } + } +} + func TestImportImportLine(t *testing.T) { _ = Setup() @@ -528,6 +817,12 @@ func TestImportImportLine(t *testing.T) { if err := ImportLine(line, false); err == nil { t.Fatalf("Expected an error when importing a line with type channel with a nil channel.") } + + // Try import line with user type but nil user. + line.Type = "user" + if err := ImportLine(line, false); err == nil { + t.Fatalf("Expected an error when importing a line with type uesr with a nil user.") + } } func TestImportBulkImport(t *testing.T) { @@ -538,7 +833,8 @@ func TestImportBulkImport(t *testing.T) { // Run bulk import with a valid 1 of everything. data1 := `{"type": "team", "team": {"type": "O", "display_name": "lskmw2d7a5ao7ppwqh5ljchvr4", "name": "` + teamName + `"}} -{"type": "channel", "channel": {"type": "O", "display_name": "xr6m6udffngark2uekvr3hoeny", "team": "` + teamName + `", "name": "` + channelName + `"}}` +{"type": "channel", "channel": {"type": "O", "display_name": "xr6m6udffngark2uekvr3hoeny", "team": "` + teamName + `", "name": "` + channelName + `"}} +{"type": "user", "user": {"username": "kufjgnkxkrhhfgbrip6qxkfsaa", "email": "kufjgnkxkrhhfgbrip6qxkfsaa@example.com"}}` if err, line := BulkImport(strings.NewReader(data1), false); err != nil || line != 0 { t.Fatalf("BulkImport should have succeeded: %v, %v", err.Error(), line) diff --git a/app/slackimport.go b/app/slackimport.go index e4beb3d3d..75a1606af 100644 --- a/app/slackimport.go +++ b/app/slackimport.go @@ -178,7 +178,7 @@ func SlackAddUsers(teamId string, slackusers []SlackUser, log *bytes.Buffer) map Password: password, } - if mUser := ImportUser(team, &newUser); mUser != nil { + if mUser := OldImportUser(team, &newUser); mUser != nil { addedUsers[sUser.Id] = mUser log.WriteString(utils.T("api.slackimport.slack_add_users.email_pwd", map[string]interface{}{"Email": newUser.Email, "Password": password})) } else { @@ -210,7 +210,7 @@ func SlackAddBotUser(teamId string, log *bytes.Buffer) *model.User { Password: password, } - if mUser := ImportUser(team, &botUser); mUser != nil { + if mUser := OldImportUser(team, &botUser); mUser != nil { log.WriteString(utils.T("api.slackimport.slack_add_bot_user.email_pwd", map[string]interface{}{"Email": botUser.Email, "Password": password})) return mUser } else { diff --git a/app/user.go b/app/user.go index 848f7c9fc..5422d0b67 100644 --- a/app/user.go +++ b/app/user.go @@ -173,9 +173,23 @@ func CreateUser(user *model.User) (*model.User, *model.AppError) { } } - user.MakeNonNil() user.Locale = *utils.Cfg.LocalizationSettings.DefaultClientLocale + if ruser, err := createUser(user); err != nil { + return nil, err + } else { + // This message goes to everyone, so the teamId, channelId and userId are irrelevant + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_NEW_USER, "", "", "", nil) + message.Add("user_id", ruser.Id) + go Publish(message) + + return ruser, nil + } +} + +func createUser(user *model.User) (*model.User, *model.AppError) { + user.MakeNonNil() + if err := utils.IsPasswordValid(user.Password); user.AuthService == "" && err != nil { return nil, err } @@ -199,11 +213,6 @@ func CreateUser(user *model.User) (*model.User, *model.AppError) { ruser.Sanitize(map[string]bool{}) - // This message goes to everyone, so the teamId, channelId and userId are irrelevant - message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_NEW_USER, "", "", "", nil) - message.Add("user_id", ruser.Id) - go Publish(message) - return ruser, nil } } @@ -716,7 +725,7 @@ func SanitizeProfile(user *model.User, asAdmin bool) { } func UpdateUserAsUser(user *model.User, siteURL string, asAdmin bool) (*model.User, *model.AppError) { - updatedUser, err := UpdateUser(user, siteURL) + updatedUser, err := UpdateUser(user, siteURL, true) if err != nil { return nil, err } @@ -732,36 +741,38 @@ func UpdateUserAsUser(user *model.User, siteURL string, asAdmin bool) (*model.Us return updatedUser, nil } -func UpdateUser(user *model.User, siteURL string) (*model.User, *model.AppError) { +func UpdateUser(user *model.User, siteURL string, sendNotifications bool) (*model.User, *model.AppError) { if result := <-Srv.Store.User().Update(user, false); result.Err != nil { return nil, result.Err } else { rusers := result.Data.([2]*model.User) - if rusers[0].Email != rusers[1].Email { - go func() { - if err := SendEmailChangeEmail(rusers[1].Email, rusers[0].Email, rusers[0].Locale, siteURL); err != nil { - l4g.Error(err.Error()) + if sendNotifications { + if rusers[0].Email != rusers[1].Email { + go func() { + if err := SendEmailChangeEmail(rusers[1].Email, rusers[0].Email, rusers[0].Locale, siteURL); err != nil { + l4g.Error(err.Error()) + } + }() + + if utils.Cfg.EmailSettings.RequireEmailVerification { + go func() { + if err := SendEmailChangeVerifyEmail(rusers[0].Id, rusers[0].Email, rusers[0].Locale, siteURL); err != nil { + l4g.Error(err.Error()) + } + }() } - }() + } - if utils.Cfg.EmailSettings.RequireEmailVerification { + if rusers[0].Username != rusers[1].Username { go func() { - if err := SendEmailChangeVerifyEmail(rusers[0].Id, rusers[0].Email, rusers[0].Locale, siteURL); err != nil { + if err := SendChangeUsernameEmail(rusers[1].Username, rusers[0].Username, rusers[0].Email, rusers[0].Locale, siteURL); err != nil { l4g.Error(err.Error()) } }() } } - if rusers[0].Username != rusers[1].Username { - go func() { - if err := SendChangeUsernameEmail(rusers[1].Username, rusers[0].Username, rusers[0].Email, rusers[0].Locale, siteURL); err != nil { - l4g.Error(err.Error()) - } - }() - } - InvalidateCacheForUser(user.Id) return rusers[0], nil @@ -778,7 +789,7 @@ func UpdateUserNotifyProps(userId string, props map[string]string, siteURL strin user.NotifyProps = props var ruser *model.User - if ruser, err = UpdateUser(user, siteURL); err != nil { + if ruser, err = UpdateUser(user, siteURL, true); err != nil { return nil, err } -- cgit v1.2.3-1-g7c22