From 28e445c6e08d0aa931fcf0be0098dfd47aa87eb4 Mon Sep 17 00:00:00 2001 From: George Goldberg Date: Thu, 2 Feb 2017 15:03:41 +0000 Subject: PLT-5407: Bulk importing of User memberships. (#5273) --- app/channel.go | 12 +- app/import.go | 144 ++++++++++++++++++++ app/import_test.go | 393 ++++++++++++++++++++++++++++++++++++++++++++++++++++- app/team.go | 30 ++-- i18n/en.json | 16 +++ 5 files changed, 581 insertions(+), 14 deletions(-) diff --git a/app/channel.go b/app/channel.go index aba65143b..1f5d308bf 100644 --- a/app/channel.go +++ b/app/channel.go @@ -336,7 +336,7 @@ func DeleteChannel(channel *model.Channel, userId string) *model.AppError { return nil } -func AddUserToChannel(user *model.User, channel *model.Channel) (*model.ChannelMember, *model.AppError) { +func addUserToChannel(user *model.User, channel *model.Channel) (*model.ChannelMember, *model.AppError) { if channel.DeleteAt > 0 { return nil, model.NewLocAppError("AddUserToChannel", "api.channel.add_user_to_channel.deleted.app_error", nil, "") } @@ -380,6 +380,16 @@ func AddUserToChannel(user *model.User, channel *model.Channel) (*model.ChannelM InvalidateCacheForUser(user.Id) InvalidateCacheForChannelMembers(channel.Id) + return newMember, nil +} + +func AddUserToChannel(user *model.User, channel *model.Channel) (*model.ChannelMember, *model.AppError) { + + newMember, err := addUserToChannel(user, channel) + if err != nil { + return nil, err + } + message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_ADDED, "", channel.Id, "", nil) message.Add("user_id", user.Id) message.Add("team_id", channel.TeamId) diff --git a/app/import.go b/app/import.go index e99dd79dc..4a27bcd7e 100644 --- a/app/import.go +++ b/app/import.go @@ -55,6 +55,19 @@ type UserImportData struct { Position *string `json:"position"` Roles *string `json:"roles"` Locale *string `json:"locale"` + + Teams *[]UserTeamImportData `json:"teams"` +} + +type UserTeamImportData struct { + Name *string `json:"name"` + Roles *string `json:"roles"` + Channels *[]UserChannelImportData `json:"channels"` +} + +type UserChannelImportData struct { + Name *string `json:"name"` + Roles *string `json:"roles"` } // @@ -376,6 +389,91 @@ func ImportUser(data *UserImportData, dryRun bool) *model.AppError { } } + return ImportUserTeams(*data.Username, data.Teams) +} + +func ImportUserTeams(username string, data *[]UserTeamImportData) *model.AppError { + if data == nil { + return nil + } + + user, err := GetUserByUsername(username) + if err != nil { + return err + } + + for _, tdata := range *data { + team, err := GetTeamByName(*tdata.Name) + if err != nil { + return err + } + + var roles string + if tdata.Roles == nil { + roles = model.ROLE_TEAM_USER.Id + } else { + roles = *tdata.Roles + } + + if _, err := GetTeamMember(team.Id, user.Id); err != nil { + if _, err := joinUserToTeam(team, user); err != nil { + return err + } + } + + if member, err := GetTeamMember(team.Id, user.Id); err != nil { + return err + } else { + if member.Roles != roles { + if _, err := UpdateTeamMemberRoles(team.Id, user.Id, roles); err != nil { + return err + } + } + } + + if err := ImportUserChannels(user, team, tdata.Channels); err != nil { + return err + } + } + + return nil +} + +func ImportUserChannels(user *model.User, team *model.Team, data *[]UserChannelImportData) *model.AppError { + if data == nil { + return nil + } + + // Loop through all channels. + for _, cdata := range *data { + channel, err := GetChannelByName(*cdata.Name, team.Id) + if err != nil { + return err + } + + var roles string + if cdata.Roles == nil { + roles = model.ROLE_CHANNEL_USER.Id + } else { + roles = *cdata.Roles + } + + var member *model.ChannelMember + member, err = GetChannelMember(channel.Id, user.Id) + if err != nil { + member, err = addUserToChannel(user, channel) + if err != nil { + return err + } + } + + if member.Roles != roles { + if _, err := UpdateChannelMemberRoles(channel.Id, user.Id, roles); err != nil { + return err + } + } + } + return nil } @@ -421,6 +519,52 @@ func validateUserImportData(data *UserImportData) *model.AppError { return model.NewAppError("BulkImport", "app.import.validate_user_import_data.roles_invalid.error", nil, "", http.StatusBadRequest) } + if data.Teams != nil { + return validateUserTeamsImportData(data.Teams) + } else { + return nil + } +} + +func validateUserTeamsImportData(data *[]UserTeamImportData) *model.AppError { + if data == nil { + return nil + } + + for _, tdata := range *data { + if tdata.Name == nil { + return model.NewAppError("BulkImport", "app.import.validate_user_teams_import_data.team_name_missing.error", nil, "", http.StatusBadRequest) + } + + if tdata.Roles != nil && !model.IsValidUserRoles(*tdata.Roles) { + return model.NewAppError("BulkImport", "app.import.validate_user_teams_import_data.invalid_roles.error", nil, "", http.StatusBadRequest) + } + + if tdata.Channels != nil { + if err := validateUserChannelsImportData(tdata.Channels); err != nil { + return err + } + } + } + + return nil +} + +func validateUserChannelsImportData(data *[]UserChannelImportData) *model.AppError { + if data == nil { + return nil + } + + for _, cdata := range *data { + if cdata.Name == nil { + return model.NewAppError("BulkImport", "app.import.validate_user_channels_import_data.channel_name_missing.error", nil, "", http.StatusBadRequest) + } + + if cdata.Roles != nil && !model.IsValidUserRoles(*cdata.Roles) { + return model.NewAppError("BulkImport", "app.import.validate_user_channels_import_data.invalid_roles.error", nil, "", http.StatusBadRequest) + } + } + return nil } diff --git a/app/import_test.go b/app/import_test.go index f3add56b2..d4ffad69f 100644 --- a/app/import_test.go +++ b/app/import_test.go @@ -349,6 +349,82 @@ func TestImportValidateUserImportData(t *testing.T) { data.Roles = ptrStr("system_user") } +func TestImportValidateUserTeamsImportData(t *testing.T) { + + // Invalid Name. + data := []UserTeamImportData{ + { + Roles: ptrStr("team_admin team_user"), + }, + } + if err := validateUserTeamsImportData(&data); err == nil { + t.Fatal("Should have failed due to invalid name.") + } + data[0].Name = ptrStr("teamname") + + // Invalid Roles + data[0].Roles = ptrStr("wtf") + if err := validateUserTeamsImportData(&data); err == nil { + t.Fatal("Should have failed due to invalid roles.") + } + + // Valid (nil roles) + data[0].Roles = nil + if err := validateUserTeamsImportData(&data); err != nil { + t.Fatal("Should have succeeded with empty roles.") + } + + // Valid (empty roles) + data[0].Roles = ptrStr("") + if err := validateUserTeamsImportData(&data); err != nil { + t.Fatal("Should have succeeded with empty roles.") + } + + // Valid (with roles) + data[0].Roles = ptrStr("team_admin team_user") + if err := validateUserTeamsImportData(&data); err != nil { + t.Fatal("Should have succeeded with valid roles.") + } +} + +func TestImportValidateUserChannelsImportData(t *testing.T) { + + // Invalid Name. + data := []UserChannelImportData{ + { + Roles: ptrStr("channel_admin channel_user"), + }, + } + if err := validateUserChannelsImportData(&data); err == nil { + t.Fatal("Should have failed due to invalid name.") + } + data[0].Name = ptrStr("channelname") + + // Invalid Roles + data[0].Roles = ptrStr("wtf") + if err := validateUserChannelsImportData(&data); err == nil { + t.Fatal("Should have failed due to invalid roles.") + } + + // Valid (nil roles) + data[0].Roles = nil + if err := validateUserChannelsImportData(&data); err != nil { + t.Fatal("Should have succeeded with empty roles.") + } + + // Valid (empty roles) + data[0].Roles = ptrStr("") + if err := validateUserChannelsImportData(&data); err != nil { + t.Fatal("Should have succeeded with empty roles.") + } + + // Valid (with roles) + data[0].Roles = ptrStr("channel_admin channel_user") + if err := validateUserChannelsImportData(&data); err != nil { + t.Fatal("Should have succeeded with valid roles.") + } +} + func TestImportImportTeam(t *testing.T) { _ = Setup() @@ -467,11 +543,9 @@ func TestImportImportChannel(t *testing.T) { DisplayName: ptrStr("Display Name"), Type: ptrStr("O"), }, false) - var team *model.Team - if te, err := GetTeamByName(teamName); err != nil { + team, err := GetTeamByName(teamName); + if err != nil { t.Fatalf("Failed to get team from database.") - } else { - team = te } // Check how many channels are in the database. @@ -792,6 +866,314 @@ func TestImportImportUser(t *testing.T) { t.Fatalf("Expected roles to be set: %v", user.Roles) } } + + // Test team and channel memberships + teamName := model.NewId() + ImportTeam(&TeamImportData{ + Name: &teamName, + DisplayName: ptrStr("Display Name"), + Type: ptrStr("O"), + }, false) + team, err := GetTeamByName(teamName); + if err != nil { + t.Fatalf("Failed to get team from database.") + } + + channelName := model.NewId() + ImportChannel(&ChannelImportData{ + Team: &teamName, + Name: &channelName, + DisplayName: ptrStr("Display Name"), + Type: ptrStr("O"), + }, false) + channel, err := GetChannelByName(channelName, team.Id); + if err != nil { + t.Fatalf("Failed to get channel from database.") + } + + 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()), + } + + teamMembers, err := GetTeamMembers(team.Id, 0, 1000) + if err != nil { + t.Fatalf("Failed to get team member count") + } + teamMemberCount := len(teamMembers) + + channelMemberCount, err := GetChannelMemberCount(channel.Id) + if err != nil { + t.Fatalf("Failed to get channel member count") + } + + // Test with an invalid team & channel membership in dry-run mode. + data.Teams = &[]UserTeamImportData{ + { + Roles: ptrStr("invalid"), + Channels: &[]UserChannelImportData{ + { + Roles: ptrStr("invalid"), + }, + }, + }, + } + if err := ImportUser(&data, true); err == nil { + t.Fatalf("Should have failed.") + } + + // Test with an unknown team name & invalid channel membership in dry-run mode. + data.Teams = &[]UserTeamImportData{ + { + Name: ptrStr(model.NewId()), + Channels: &[]UserChannelImportData{ + { + Roles: ptrStr("invalid"), + }, + }, + }, + } + if err := ImportUser(&data, true); err == nil { + t.Fatalf("Should have failed.") + } + + // Test with a valid team & invalid channel membership in dry-run mode. + data.Teams = &[]UserTeamImportData{ + { + Name: &teamName, + Channels: &[]UserChannelImportData{ + { + Roles: ptrStr("invalid"), + }, + }, + }, + } + if err := ImportUser(&data, true); err == nil { + t.Fatalf("Should have failed.") + } + + // Test with a valid team & unknown channel name in dry-run mode. + data.Teams = &[]UserTeamImportData{ + { + Name: &teamName, + Channels: &[]UserChannelImportData{ + { + Name: ptrStr(model.NewId()), + }, + }, + }, + } + if err := ImportUser(&data, true); err != nil { + t.Fatalf("Should have succeeded.") + } + + // Test with a valid team & valid channel name in dry-run mode. + data.Teams = &[]UserTeamImportData{ + { + Name: &teamName, + Channels: &[]UserChannelImportData{ + { + Name: &channelName, + }, + }, + }, + } + if err := ImportUser(&data, true); err != nil { + t.Fatalf("Should have succeeded.") + } + + // Check no new member objects were created because dry run mode. + if tmc, err := GetTeamMembers(team.Id, 0, 1000); err != nil { + t.Fatalf("Failed to get Team Member Count") + } else if len(tmc) != teamMemberCount { + t.Fatalf("Number of team members not as expected") + } + + if cmc, err := GetChannelMemberCount(channel.Id); err != nil { + t.Fatalf("Failed to get Channel Member Count") + } else if cmc != channelMemberCount { + t.Fatalf("Number of channel members not as expected") + } + + // Test with an invalid team & channel membership in apply mode. + data.Teams = &[]UserTeamImportData{ + { + Roles: ptrStr("invalid"), + Channels: &[]UserChannelImportData{ + { + Roles: ptrStr("invalid"), + }, + }, + }, + } + if err := ImportUser(&data, false); err == nil { + t.Fatalf("Should have failed.") + } + + // Test with an unknown team name & invalid channel membership in apply mode. + data.Teams = &[]UserTeamImportData{ + { + Name: ptrStr(model.NewId()), + Channels: &[]UserChannelImportData{ + { + Roles: ptrStr("invalid"), + }, + }, + }, + } + if err := ImportUser(&data, false); err == nil { + t.Fatalf("Should have failed.") + } + + // Test with a valid team & invalid channel membership in apply mode. + data.Teams = &[]UserTeamImportData{ + { + Name: &teamName, + Channels: &[]UserChannelImportData{ + { + Roles: ptrStr("invalid"), + }, + }, + }, + } + if err := ImportUser(&data, false); err == nil { + t.Fatalf("Should have failed.") + } + + // Check no new member objects were created because all tests should have failed so far. + if tmc, err := GetTeamMembers(team.Id, 0, 1000); err != nil { + t.Fatalf("Failed to get Team Member Count") + } else if len(tmc) != teamMemberCount { + t.Fatalf("Number of team members not as expected") + } + + if cmc, err := GetChannelMemberCount(channel.Id); err != nil { + t.Fatalf("Failed to get Channel Member Count") + } else if cmc != channelMemberCount { + t.Fatalf("Number of channel members not as expected") + } + + // Test with a valid team & unknown channel name in apply mode. + data.Teams = &[]UserTeamImportData{ + { + Name: &teamName, + Channels: &[]UserChannelImportData{ + { + Name: ptrStr(model.NewId()), + }, + }, + }, + } + if err := ImportUser(&data, false); err == nil { + t.Fatalf("Should have failed.") + } + + // Check only new team member object created because dry run mode. + if tmc, err := GetTeamMembers(team.Id, 0, 1000); err != nil { + t.Fatalf("Failed to get Team Member Count") + } else if len(tmc) != teamMemberCount + 1 { + t.Fatalf("Number of team members not as expected") + } + + if cmc, err := GetChannelMemberCount(channel.Id); err != nil { + t.Fatalf("Failed to get Channel Member Count") + } else if cmc != channelMemberCount { + t.Fatalf("Number of channel members not as expected") + } + + // Check team member properties. + user, err := GetUserByUsername(username); + if err != nil { + t.Fatalf("Failed to get user from database.") + } + if teamMember, err := GetTeamMember(team.Id, user.Id); err != nil { + t.Fatalf("Failed to get team member from database.") + } else if teamMember.Roles != "team_user" { + t.Fatalf("Team member properties not as expected") + } + + // Test with a valid team & valid channel name in apply mode. + data.Teams = &[]UserTeamImportData{ + { + Name: &teamName, + Channels: &[]UserChannelImportData{ + { + Name: &channelName, + }, + }, + }, + } + if err := ImportUser(&data, false); err != nil { + t.Fatalf("Should have succeeded.") + } + + // Check only new channel member object created because dry run mode. + if tmc, err := GetTeamMembers(team.Id, 0, 1000); err != nil { + t.Fatalf("Failed to get Team Member Count") + } else if len(tmc) != teamMemberCount + 1 { + t.Fatalf("Number of team members not as expected") + } + + if cmc, err := GetChannelMemberCount(channel.Id); err != nil { + t.Fatalf("Failed to get Channel Member Count") + } else if cmc != channelMemberCount + 1 { + t.Fatalf("Number of channel members not as expected") + } + + // Check channel member properties. + if channelMember, err := GetChannelMember(channel.Id, user.Id); err != nil { + t.Fatalf("Failed to get channel member from database.") + } else if channelMember.Roles != "channel_user" { + t.Fatalf("Channel member properties not as expected") + } + + // Test with the properties of the team and channel membership changed. + data.Teams = &[]UserTeamImportData{ + { + Name: &teamName, + Roles: ptrStr("team_user team_admin"), + Channels: &[]UserChannelImportData{ + { + Name: &channelName, + Roles: ptrStr("channel_user channel_admin"), + }, + }, + }, + } + if err := ImportUser(&data, false); err != nil { + t.Fatalf("Should have succeeded.") + } + + // Check both member properties. + if teamMember, err := GetTeamMember(team.Id, user.Id); err != nil { + t.Fatalf("Failed to get team member from database.") + } else if teamMember.Roles != "team_user team_admin" { + t.Fatalf("Team member properties not as expected: %v", teamMember.Roles) + } + + if channelMember, err := GetChannelMember(channel.Id, user.Id); err != nil { + t.Fatalf("Failed to get channel member from database.") + } else if channelMember.Roles != "channel_user channel_admin" { + t.Fatalf("Channel member properties not as expected") + } + + // No more new member objects. + if tmc, err := GetTeamMembers(team.Id, 0, 1000); err != nil { + t.Fatalf("Failed to get Team Member Count") + } else if len(tmc) != teamMemberCount + 1 { + t.Fatalf("Number of team members not as expected") + } + + if cmc, err := GetChannelMemberCount(channel.Id); err != nil { + t.Fatalf("Failed to get Channel Member Count") + } else if cmc != channelMemberCount + 1 { + t.Fatalf("Number of channel members not as expected") + } } func TestImportImportLine(t *testing.T) { @@ -834,7 +1216,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": "user", "user": {"username": "kufjgnkxkrhhfgbrip6qxkfsaa", "email": "kufjgnkxkrhhfgbrip6qxkfsaa@example.com"}}` +{"type": "user", "user": {"username": "kufjgnkxkrhhfgbrip6qxkfsaa", "email": "kufjgnkxkrhhfgbrip6qxkfsaa@example.com"}} +{"type": "user", "user": {"username": "bwshaim6qnc2ne7oqkd5b2s2rq", "email": "bwshaim6qnc2ne7oqkd5b2s2rq@example.com", "teams": [{"name": "` + teamName + `", "channels": [{"name": "` + channelName + `"}]}]}}` 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/team.go b/app/team.go index 3c7145818..6225f3ee0 100644 --- a/app/team.go +++ b/app/team.go @@ -231,7 +231,7 @@ func AddUserToTeamByInviteId(inviteId string, userId string) (*model.Team, *mode return team, nil } -func JoinUserToTeam(team *model.Team, user *model.User) *model.AppError { +func joinUserToTeam(team *model.Team, user *model.User) (bool, *model.AppError) { tm := &model.TeamMember{ TeamId: team.Id, @@ -239,11 +239,8 @@ func JoinUserToTeam(team *model.Team, user *model.User) *model.AppError { Roles: model.ROLE_TEAM_USER.Id, } - channelRole := model.ROLE_CHANNEL_USER.Id - if team.Email == user.Email { tm.Roles = model.ROLE_TEAM_USER.Id + " " + model.ROLE_TEAM_ADMIN.Id - channelRole = model.ROLE_CHANNEL_USER.Id + " " + model.ROLE_CHANNEL_ADMIN.Id } if etmr := <-Srv.Store.Team().GetMember(team.Id, user.Id); etmr.Err == nil { @@ -252,21 +249,38 @@ func JoinUserToTeam(team *model.Team, user *model.User) *model.AppError { // Do nothing if already added if rtm.DeleteAt == 0 { - return nil + return true, nil } if tmr := <-Srv.Store.Team().UpdateMember(tm); tmr.Err != nil { - return tmr.Err + return false, tmr.Err } } else { // Membership appears to be missing. Lets try to add. if tmr := <-Srv.Store.Team().SaveMember(tm); tmr.Err != nil { - return tmr.Err + return false, tmr.Err } } if uua := <-Srv.Store.User().UpdateUpdateAt(user.Id); uua.Err != nil { - return uua.Err + return false, uua.Err + } + + return false, nil +} + +func JoinUserToTeam(team *model.Team, user *model.User) *model.AppError { + + if alreadyAdded, err := joinUserToTeam(team, user); err != nil { + return err + } else if alreadyAdded { + return nil + } + + channelRole := model.ROLE_CHANNEL_USER.Id + + if team.Email == user.Email { + channelRole = model.ROLE_CHANNEL_USER.Id + " " + model.ROLE_CHANNEL_ADMIN.Id } // Soft error if there is an issue joining the default channels diff --git a/i18n/en.json b/i18n/en.json index cd109f860..c58e113d2 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -2951,6 +2951,22 @@ "id": "app.import.validate_user_import_data.roles_invalid.error", "translation": "User roles are not valid." }, + { + "id": "app.import.validate_user_teams_import_data.team_name_missing.error", + "translation": "Team name missing from User's Team Membership." + }, + { + "id": "app.import.validate_user_teams_import_data.invalid_roles.error", + "translation": "Invalid roles for User's Team Membership." + }, + { + "id": "app.import.validate_user_channels_import_data.channel_name_missing.error", + "translation": "Channel name missing from User's Channel Membership." + }, + { + "id": "app.import.validate_user_channels_import_data.invalid_roles.error", + "translation": "Invalid roles for User's Channel Membership." + }, { "id": "authentication.permissions.create_team_roles.description", "translation": "Ability to create new teams" -- cgit v1.2.3-1-g7c22