From 4b39d8487b3761e6d57cc1f9df5f07f48e95b2b0 Mon Sep 17 00:00:00 2001 From: Eli Yukelzon Date: Wed, 29 Jan 2020 12:56:59 +0200 Subject: [PATCH] =?UTF-8?q?MM-20977=20-=20Inviting=20multiple=20users=20wi?= =?UTF-8?q?th=20valid/allowed=20and=20inva=E2=80=A6=20(#13779)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api4/team.go | 46 +++++++--- api4/team_test.go | 19 +++++ app/team.go | 198 +++++++++++++++++++++++++++++++------------ model/client4.go | 25 ++++++ model/team_member.go | 29 +++++++ 5 files changed, 248 insertions(+), 69 deletions(-) diff --git a/api4/team.go b/api4/team.go index e08bd5f718..460316d958 100644 --- a/api4/team.go +++ b/api4/team.go @@ -945,6 +945,8 @@ func importTeam(c *Context, w http.ResponseWriter, r *http.Request) { } func inviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request) { + graceful := r.URL.Query().Get("graceful") != "" + c.RequireTeamId() if c.Err != nil { return @@ -967,16 +969,26 @@ func inviteUsersToTeam(c *Context, w http.ResponseWriter, r *http.Request) { return } - err := c.App.InviteNewUsersToTeam(emailList, c.Params.TeamId, c.App.Session.UserId) - if err != nil { - c.Err = err - return + if graceful { + invitesWithError, err := c.App.InviteNewUsersToTeamGracefully(emailList, c.Params.TeamId, c.App.Session.UserId) + if err != nil { + c.Err = err + return + } + // in graceful mode we return both the succesful ones and the failed ones + w.Write([]byte(model.EmailInviteWithErrorToJson(invitesWithError))) + } else { + err := c.App.InviteNewUsersToTeam(emailList, c.Params.TeamId, c.App.Session.UserId) + if err != nil { + c.Err = err + return + } + ReturnStatusOK(w) } - - ReturnStatusOK(w) } func inviteGuestsToChannels(c *Context, w http.ResponseWriter, r *http.Request) { + graceful := r.URL.Query().Get("graceful") != "" if c.App.License() == nil { c.Err = model.NewAppError("Api4.InviteGuestsToChannels", "api.team.invate_guests_to_channels.license.error", nil, "", http.StatusNotImplemented) return @@ -1002,14 +1014,22 @@ func inviteGuestsToChannels(c *Context, w http.ResponseWriter, r *http.Request) c.Err = err return } - - err := c.App.InviteGuestsToChannels(c.Params.TeamId, guestsInvite, c.App.Session.UserId) - if err != nil { - c.Err = err - return + if graceful { + invitesWithError, err := c.App.InviteGuestsToChannelsGracefully(c.Params.TeamId, guestsInvite, c.App.Session.UserId) + if err != nil { + c.Err = err + return + } + // in graceful mode we return both the succesful ones and the failed ones + w.Write([]byte(model.EmailInviteWithErrorToJson(invitesWithError))) + } else { + err := c.App.InviteGuestsToChannels(c.Params.TeamId, guestsInvite, c.App.Session.UserId) + if err != nil { + c.Err = err + return + } + ReturnStatusOK(w) } - - ReturnStatusOK(w) } func getInviteInfo(c *Context, w http.ResponseWriter, r *http.Request) { diff --git a/api4/team_test.go b/api4/team_test.go index f3aa850362..16ab958475 100644 --- a/api4/team_test.go +++ b/api4/team_test.go @@ -2343,6 +2343,13 @@ func TestInviteUsersToTeam(t *testing.T) { require.Equalf(t, err.Where, "InviteNewUsersToTeam", "%v, Got wrong error message!", err) require.Equalf(t, err.Id, "api.team.invite_members.invalid_email.app_error", "%v, Got wrong error message!", err) + + res, err := th.App.InviteNewUsersToTeamGracefully(emailList, th.BasicTeam.Id, th.BasicUser.Id) + + require.Nil(t, err) + require.Len(t, res, 2) + require.NotNil(t, res[0].Error) + require.NotNil(t, res[1].Error) }) t.Run("override restricted domains", func(t *testing.T) { @@ -2363,6 +2370,12 @@ func TestInviteUsersToTeam(t *testing.T) { err = th.App.InviteNewUsersToTeam([]string{"test@invalid.com"}, th.BasicTeam.Id, th.BasicUser.Id) require.NotNilf(t, err, "%v, Should not invite user", err) + + res, err := th.App.InviteNewUsersToTeamGracefully([]string{"test@invalid.com", "test@common.com"}, th.BasicTeam.Id, th.BasicUser.Id) + require.Nil(t, err) + require.Len(t, res, 2) + require.NotNil(t, res[0].Error) + require.Nil(t, res[1].Error) }) } @@ -2457,6 +2470,12 @@ func TestInviteGuestsToTeam(t *testing.T) { err := th.App.InviteGuestsToChannels(th.BasicTeam.Id, &model.GuestsInvite{Emails: []string{"guest1@invalid.com"}, Channels: []string{th.BasicChannel.Id}, Message: "test message"}, th.BasicUser.Id) require.NotNil(t, err, "guest user invites should be affected by the guest domain restrictions") + res, err := th.App.InviteGuestsToChannelsGracefully(th.BasicTeam.Id, &model.GuestsInvite{Emails: []string{"guest1@invalid.com", "guest1@guest.com"}, Channels: []string{th.BasicChannel.Id}, Message: "test message"}, th.BasicUser.Id) + require.Nil(t, err) + require.Len(t, res, 2) + require.NotNil(t, res[0].Error) + require.Nil(t, res[1].Error) + err = th.App.InviteGuestsToChannels(th.BasicTeam.Id, &model.GuestsInvite{Emails: []string{"guest1@guest.com"}, Channels: []string{th.BasicChannel.Id}, Message: "test message"}, th.BasicUser.Id) require.Nil(t, err, "whitelisted guest user email should be allowed by the guest domain restrictions") }) diff --git a/app/team.go b/app/team.go index 44adf4db5e..47ce221cea 100644 --- a/app/team.go +++ b/app/team.go @@ -1043,16 +1043,7 @@ func (a *App) postRemoveFromTeamMessage(user *model.User, channel *model.Channel return nil } -func (a *App) InviteNewUsersToTeam(emailList []string, teamId, senderId string) *model.AppError { - if !*a.Config().ServiceSettings.EnableEmailInvitations { - return model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.disabled.app_error", nil, "", http.StatusNotImplemented) - } - - if len(emailList) == 0 { - err := model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.no_one.app_error", nil, "", http.StatusBadRequest) - return err - } - +func (a *App) prepareInviteNewUsersToTeam(teamId, senderId string) (*model.User, *model.Team, *model.AppError) { tchan := make(chan store.StoreResult, 1) go func() { team, err := a.Srv.Store.Team().Get(teamId) @@ -1069,15 +1060,152 @@ func (a *App) InviteNewUsersToTeam(emailList []string, teamId, senderId string) result := <-tchan if result.Err != nil { - return result.Err + return nil, nil, result.Err } team := result.Data.(*model.Team) result = <-uchan if result.Err != nil { - return result.Err + return nil, nil, result.Err } user := result.Data.(*model.User) + return user, team, nil +} + +func (a *App) InviteNewUsersToTeamGracefully(emailList []string, teamId, senderId string) ([]*model.EmailInviteWithError, *model.AppError) { + if !*a.Config().ServiceSettings.EnableEmailInvitations { + return nil, model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.disabled.app_error", nil, "", http.StatusNotImplemented) + } + + if len(emailList) == 0 { + err := model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.no_one.app_error", nil, "", http.StatusBadRequest) + return nil, err + } + + user, team, err := a.prepareInviteNewUsersToTeam(teamId, senderId) + if err != nil { + return nil, err + } + var inviteListWithErrors []*model.EmailInviteWithError + var goodEmails []string + for _, email := range emailList { + invite := &model.EmailInviteWithError{ + Email: email, + Error: nil, + } + if !a.isTeamEmailAddressAllowed(email, team.AllowedDomains) { + invite.Error = model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.invalid_email.app_error", map[string]interface{}{"Addresses": email}, "", http.StatusBadRequest) + } else { + goodEmails = append(goodEmails, email) + } + inviteListWithErrors = append(inviteListWithErrors, invite) + } + + if len(goodEmails) > 0 { + nameFormat := *a.Config().TeamSettings.TeammateNameDisplay + a.SendInviteEmails(team, user.GetDisplayName(nameFormat), user.Id, goodEmails, a.GetSiteURL()) + } + + return inviteListWithErrors, nil +} + +func (a *App) prepareInviteGuestsToChannels(teamId string, guestsInvite *model.GuestsInvite, senderId string) (*model.User, *model.Team, []*model.Channel, *model.AppError) { + if err := guestsInvite.IsValid(); err != nil { + return nil, nil, nil, err + } + + tchan := make(chan store.StoreResult, 1) + go func() { + team, err := a.Srv.Store.Team().Get(teamId) + tchan <- store.StoreResult{Data: team, Err: err} + close(tchan) + }() + cchan := make(chan store.StoreResult, 1) + go func() { + channels, err := a.Srv.Store.Channel().GetChannelsByIds(guestsInvite.Channels) + cchan <- store.StoreResult{Data: channels, Err: err} + close(cchan) + }() + uchan := make(chan store.StoreResult, 1) + go func() { + user, err := a.Srv.Store.User().Get(senderId) + uchan <- store.StoreResult{Data: user, Err: err} + close(uchan) + }() + + result := <-cchan + if result.Err != nil { + return nil, nil, nil, result.Err + } + channels := result.Data.([]*model.Channel) + + result = <-uchan + if result.Err != nil { + return nil, nil, nil, result.Err + } + user := result.Data.(*model.User) + + result = <-tchan + if result.Err != nil { + return nil, nil, nil, result.Err + } + team := result.Data.(*model.Team) + + for _, channel := range channels { + if channel.TeamId != teamId { + return nil, nil, nil, model.NewAppError("InviteGuestsToChannels", "api.team.invite_guests.channel_in_invalid_team.app_error", nil, "", http.StatusBadRequest) + } + } + return user, team, channels, nil +} + +func (a *App) InviteGuestsToChannelsGracefully(teamId string, guestsInvite *model.GuestsInvite, senderId string) ([]*model.EmailInviteWithError, *model.AppError) { + if !*a.Config().ServiceSettings.EnableEmailInvitations { + return nil, model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.disabled.app_error", nil, "", http.StatusNotImplemented) + } + + user, team, channels, err := a.prepareInviteGuestsToChannels(teamId, guestsInvite, senderId) + if err != nil { + return nil, err + } + + var inviteListWithErrors []*model.EmailInviteWithError + var goodEmails []string + for _, email := range guestsInvite.Emails { + invite := &model.EmailInviteWithError{ + Email: email, + Error: nil, + } + if !CheckEmailDomain(email, *a.Config().GuestAccountsSettings.RestrictCreationToDomains) { + invite.Error = model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.invalid_email.app_error", map[string]interface{}{"Addresses": email}, "", http.StatusBadRequest) + } else { + goodEmails = append(goodEmails, email) + } + inviteListWithErrors = append(inviteListWithErrors, invite) + } + + if len(goodEmails) > 0 { + nameFormat := *a.Config().TeamSettings.TeammateNameDisplay + a.SendGuestInviteEmails(team, channels, user.GetDisplayName(nameFormat), user.Id, goodEmails, a.GetSiteURL(), guestsInvite.Message) + } + + return inviteListWithErrors, nil +} + +func (a *App) InviteNewUsersToTeam(emailList []string, teamId, senderId string) *model.AppError { + if !*a.Config().ServiceSettings.EnableEmailInvitations { + return model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.disabled.app_error", nil, "", http.StatusNotImplemented) + } + + if len(emailList) == 0 { + err := model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.no_one.app_error", nil, "", http.StatusBadRequest) + return err + } + + user, team, err := a.prepareInviteNewUsersToTeam(teamId, senderId) + if err != nil { + return err + } var invalidEmailList []string @@ -1104,53 +1232,11 @@ func (a *App) InviteGuestsToChannels(teamId string, guestsInvite *model.GuestsIn return model.NewAppError("InviteNewUsersToTeam", "api.team.invite_members.disabled.app_error", nil, "", http.StatusNotImplemented) } - if err := guestsInvite.IsValid(); err != nil { + user, team, channels, err := a.prepareInviteGuestsToChannels(teamId, guestsInvite, senderId) + if err != nil { return err } - tchan := make(chan store.StoreResult, 1) - go func() { - team, err := a.Srv.Store.Team().Get(teamId) - tchan <- store.StoreResult{Data: team, Err: err} - close(tchan) - }() - cchan := make(chan store.StoreResult, 1) - go func() { - channels, err := a.Srv.Store.Channel().GetChannelsByIds(guestsInvite.Channels) - cchan <- store.StoreResult{Data: channels, Err: err} - close(cchan) - }() - uchan := make(chan store.StoreResult, 1) - go func() { - user, err := a.Srv.Store.User().Get(senderId) - uchan <- store.StoreResult{Data: user, Err: err} - close(uchan) - }() - - result := <-cchan - if result.Err != nil { - return result.Err - } - channels := result.Data.([]*model.Channel) - - result = <-uchan - if result.Err != nil { - return result.Err - } - user := result.Data.(*model.User) - - result = <-tchan - if result.Err != nil { - return result.Err - } - team := result.Data.(*model.Team) - - for _, channel := range channels { - if channel.TeamId != teamId { - return model.NewAppError("InviteGuestsToChannels", "api.team.invite_guests.channel_in_invalid_team.app_error", nil, "", http.StatusBadRequest) - } - } - var invalidEmailList []string for _, email := range guestsInvite.Emails { if !CheckEmailDomain(email, *a.Config().GuestAccountsSettings.RestrictCreationToDomains) { diff --git a/model/client4.go b/model/client4.go index 7e4abda054..9b325ec0cd 100644 --- a/model/client4.go +++ b/model/client4.go @@ -1985,6 +1985,31 @@ func (c *Client4) InviteGuestsToTeam(teamId string, userEmails []string, channel return CheckStatusOK(r), BuildResponse(r) } +// InviteUsersToTeam invite users by email to the team. +func (c *Client4) InviteUsersToTeamGracefully(teamId string, userEmails []string) ([]*EmailInviteWithError, *Response) { + r, err := c.DoApiPost(c.GetTeamRoute(teamId)+"/invite/email?graceful=true", ArrayToJson(userEmails)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return EmailInviteWithErrorFromJson(r.Body), BuildResponse(r) +} + +// InviteGuestsToTeam invite guest by email to some channels in a team. +func (c *Client4) InviteGuestsToTeamGracefully(teamId string, userEmails []string, channels []string, message string) ([]*EmailInviteWithError, *Response) { + guestsInvite := GuestsInvite{ + Emails: userEmails, + Channels: channels, + Message: message, + } + r, err := c.DoApiPost(c.GetTeamRoute(teamId)+"/invite-guests/email?graceful=true", guestsInvite.ToJson()) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return EmailInviteWithErrorFromJson(r.Body), BuildResponse(r) +} + // InvalidateEmailInvites will invalidate active email invitations that have not been accepted by the user. func (c *Client4) InvalidateEmailInvites() (bool, *Response) { r, err := c.DoApiDelete(c.GetTeamsRoute() + "/invites/email") diff --git a/model/team_member.go b/model/team_member.go index ad42fb9769..0c0a3435da 100644 --- a/model/team_member.go +++ b/model/team_member.go @@ -38,6 +38,11 @@ type TeamMemberWithError struct { Error *AppError `json:"error"` } +type EmailInviteWithError struct { + Email string `json:"email"` + Error *AppError `json:"error"` +} + func (o *TeamMember) ToJson() string { b, _ := json.Marshal(o) return string(b) @@ -60,6 +65,30 @@ func TeamUnreadFromJson(data io.Reader) *TeamUnread { return o } +func EmailInviteWithErrorFromJson(data io.Reader) []*EmailInviteWithError { + var o []*EmailInviteWithError + json.NewDecoder(data).Decode(&o) + return o +} + +func EmailInviteWithErrorToEmails(o []*EmailInviteWithError) []string { + var ret []string + for _, o := range o { + if o.Error == nil { + ret = append(ret, o.Email) + } + } + return ret +} + +func EmailInviteWithErrorToJson(o []*EmailInviteWithError) string { + if b, err := json.Marshal(o); err != nil { + return "[]" + } else { + return string(b) + } +} + func TeamMembersWithErrorToTeamMembers(o []*TeamMemberWithError) []*TeamMember { var ret []*TeamMember for _, o := range o {