diff --git a/api4/team.go b/api4/team.go index e6e014e367..8cc73beaa4 100644 --- a/api4/team.go +++ b/api4/team.go @@ -522,6 +522,8 @@ func addUserToTeamFromInvite(c *Context, w http.ResponseWriter, r *http.Request) } func addTeamMembers(c *Context, w http.ResponseWriter, r *http.Request) { + graceful := r.URL.Query().Get("graceful") != "" + c.RequireTeamId() if c.Err != nil { return @@ -587,7 +589,7 @@ func addTeamMembers(c *Context, w http.ResponseWriter, r *http.Request) { return } - members, err = c.App.AddTeamMembers(c.Params.TeamId, userIds, c.App.Session.UserId) + membersWithErrors, err := c.App.AddTeamMembers(c.Params.TeamId, userIds, c.App.Session.UserId, graceful) if err != nil { c.Err = err @@ -595,7 +597,14 @@ func addTeamMembers(c *Context, w http.ResponseWriter, r *http.Request) { } w.WriteHeader(http.StatusCreated) - w.Write([]byte(model.TeamMembersToJson(members))) + + if graceful { + // in 'graceful' mode we allow a different return value, notifying the client which users were not added + w.Write([]byte(model.TeamMembersWithErrorToJson(membersWithErrors))) + } else { + w.Write([]byte(model.TeamMembersToJson(model.TeamMembersWithErrorToTeamMembers(membersWithErrors)))) + } + } func removeTeamMember(c *Context, w http.ResponseWriter, r *http.Request) { diff --git a/api4/team_test.go b/api4/team_test.go index cc25ed4bd9..f3aa850362 100644 --- a/api4/team_test.go +++ b/api4/team_test.go @@ -1651,6 +1651,66 @@ func TestAddTeamMemberMyself(t *testing.T) { } +func TestAddTeamMembersDomainConstrained(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + client := th.SystemAdminClient + team := th.BasicTeam + team.AllowedDomains = "domain1.com, domain2.com" + _, response := client.UpdateTeam(team) + require.Nil(t, response.Error) + + // create two users on allowed domains + user1, response := client.CreateUser(&model.User{ + Email: "user@domain1.com", + Password: "Pa$$word11", + Username: GenerateTestUsername(), + }) + require.Nil(t, response.Error) + user2, response := client.CreateUser(&model.User{ + Email: "user@domain2.com", + Password: "Pa$$word11", + Username: GenerateTestUsername(), + }) + require.Nil(t, response.Error) + + userList := []string{ + user1.Id, + user2.Id, + } + + // validate that they can be added + tm, response := client.AddTeamMembers(team.Id, userList) + require.Nil(t, response.Error) + require.Len(t, tm, 2) + + // cleanup + _, response = client.RemoveTeamMember(team.Id, user1.Id) + require.Nil(t, response.Error) + _, response = client.RemoveTeamMember(team.Id, user2.Id) + require.Nil(t, response.Error) + + // disable one of the allowed domains + team.AllowedDomains = "domain1.com" + _, response = client.UpdateTeam(team) + require.Nil(t, response.Error) + + // validate that they cannot be added + _, response = client.AddTeamMembers(team.Id, userList) + require.NotNil(t, response.Error) + + // validate that one user can be added gracefully + members, response := client.AddTeamMembersGracefully(team.Id, userList) + require.Nil(t, response.Error) + require.Len(t, members, 2) + require.NotNil(t, members[0].Member) + require.NotNil(t, members[1].Error) + require.Equal(t, members[0].UserId, user1.Id) + require.Equal(t, members[1].UserId, user2.Id) + require.Nil(t, members[0].Error) + require.Nil(t, members[1].Member) +} + func TestAddTeamMembers(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() diff --git a/app/plugin_api.go b/app/plugin_api.go index d2219686d6..fa99755c62 100644 --- a/app/plugin_api.go +++ b/app/plugin_api.go @@ -174,7 +174,15 @@ func (api *PluginAPI) CreateTeamMember(teamId, userId string) (*model.TeamMember } func (api *PluginAPI) CreateTeamMembers(teamId string, userIds []string, requestorId string) ([]*model.TeamMember, *model.AppError) { - return api.app.AddTeamMembers(teamId, userIds, requestorId) + members, err := api.app.AddTeamMembers(teamId, userIds, requestorId, false) + if err != nil { + return nil, err + } + return model.TeamMembersWithErrorToTeamMembers(members), nil +} + +func (api *PluginAPI) CreateTeamMembersGracefully(teamId string, userIds []string, requestorId string) ([]*model.TeamMemberWithError, *model.AppError) { + return api.app.AddTeamMembers(teamId, userIds, requestorId, true) } func (api *PluginAPI) DeleteTeamMember(teamId, userId, requestorId string) *model.AppError { diff --git a/app/team.go b/app/team.go index 03fdcc0c0b..e9ec165058 100644 --- a/app/team.go +++ b/app/team.go @@ -777,11 +777,18 @@ func (a *App) AddTeamMember(teamId, userId string) (*model.TeamMember, *model.Ap return teamMember, nil } -func (a *App) AddTeamMembers(teamId string, userIds []string, userRequestorId string) ([]*model.TeamMember, *model.AppError) { - var members []*model.TeamMember +func (a *App) AddTeamMembers(teamId string, userIds []string, userRequestorId string, graceful bool) ([]*model.TeamMemberWithError, *model.AppError) { + var membersWithErrors []*model.TeamMemberWithError for _, userId := range userIds { if _, err := a.AddUserToTeam(teamId, userId, userRequestorId); err != nil { + if graceful { + membersWithErrors = append(membersWithErrors, &model.TeamMemberWithError{ + UserId: userId, + Error: err, + }) + continue + } return nil, err } @@ -789,7 +796,10 @@ func (a *App) AddTeamMembers(teamId string, userIds []string, userRequestorId st if err != nil { return nil, err } - members = append(members, teamMember) + membersWithErrors = append(membersWithErrors, &model.TeamMemberWithError{ + UserId: userId, + Member: teamMember, + }) message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_ADDED_TO_TEAM, "", "", userId, nil) message.Add("team_id", teamId) @@ -797,7 +807,7 @@ func (a *App) AddTeamMembers(teamId string, userIds []string, userRequestorId st a.Publish(message) } - return members, nil + return membersWithErrors, nil } func (a *App) AddTeamMemberByToken(userId, tokenId string) (*model.TeamMember, *model.AppError) { diff --git a/go.mod b/go.mod index 916e8049b7..5191fedecd 100644 --- a/go.mod +++ b/go.mod @@ -39,7 +39,7 @@ require ( github.com/icrowley/fake v0.0.0-20180203215853-4178557ae428 github.com/jaytaylor/html2text v0.0.0-20190408195923-01ec452cbe43 github.com/jmoiron/sqlx v1.2.0 - github.com/jonboulle/clockwork v0.1.0 + github.com/jonboulle/clockwork v0.1.0 // indirect github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect github.com/lib/pq v1.2.0 github.com/magiconair/properties v1.8.1 // indirect diff --git a/model/client4.go b/model/client4.go index 1d46889065..31518f3611 100644 --- a/model/client4.go +++ b/model/client4.go @@ -1861,6 +1861,22 @@ func (c *Client4) AddTeamMembers(teamId string, userIds []string) ([]*TeamMember return TeamMembersFromJson(r.Body), BuildResponse(r) } +// AddTeamMembers adds a number of users to a team and returns the team members. +func (c *Client4) AddTeamMembersGracefully(teamId string, userIds []string) ([]*TeamMemberWithError, *Response) { + var members []*TeamMember + for _, userId := range userIds { + member := &TeamMember{TeamId: teamId, UserId: userId} + members = append(members, member) + } + + r, err := c.DoApiPost(c.GetTeamMembersRoute(teamId)+"/batch?graceful=true", TeamMembersToJson(members)) + if err != nil { + return nil, BuildErrorResponse(r, err) + } + defer closeBody(r) + return TeamMembersWithErrorFromJson(r.Body), BuildResponse(r) +} + // RemoveTeamMember will remove a user from a team. func (c *Client4) RemoveTeamMember(teamId, userId string) (bool, *Response) { r, err := c.DoApiDelete(c.GetTeamMemberRoute(teamId, userId)) diff --git a/model/team_member.go b/model/team_member.go index 1be2ab5569..ad42fb9769 100644 --- a/model/team_member.go +++ b/model/team_member.go @@ -32,6 +32,12 @@ type TeamMemberForExport struct { TeamName string } +type TeamMemberWithError struct { + UserId string `json:"user_id"` + Member *TeamMember `json:"member"` + Error *AppError `json:"error"` +} + func (o *TeamMember) ToJson() string { b, _ := json.Marshal(o) return string(b) @@ -54,6 +60,30 @@ func TeamUnreadFromJson(data io.Reader) *TeamUnread { return o } +func TeamMembersWithErrorToTeamMembers(o []*TeamMemberWithError) []*TeamMember { + var ret []*TeamMember + for _, o := range o { + if o.Error == nil { + ret = append(ret, o.Member) + } + } + return ret +} + +func TeamMembersWithErrorToJson(o []*TeamMemberWithError) string { + if b, err := json.Marshal(o); err != nil { + return "[]" + } else { + return string(b) + } +} + +func TeamMembersWithErrorFromJson(data io.Reader) []*TeamMemberWithError { + var o []*TeamMemberWithError + json.NewDecoder(data).Decode(&o) + return o +} + func TeamMembersToJson(o []*TeamMember) string { if b, err := json.Marshal(o); err != nil { return "[]" diff --git a/plugin/api.go b/plugin/api.go index 30059a3562..3df873a20f 100644 --- a/plugin/api.go +++ b/plugin/api.go @@ -288,6 +288,13 @@ type API interface { // Minimum server version: 5.2 CreateTeamMembers(teamId string, userIds []string, requestorId string) ([]*model.TeamMember, *model.AppError) + // CreateTeamMembersGracefully creates a team membership for all provided user ids and reports the users that were not added. + // + // @tag Team + // @tag User + // Minimum server version: 5.20 + CreateTeamMembersGracefully(teamId string, userIds []string, requestorId string) ([]*model.TeamMemberWithError, *model.AppError) + // DeleteTeamMember deletes a team membership. // // @tag Team diff --git a/plugin/client_rpc_generated.go b/plugin/client_rpc_generated.go index 98671b4cc1..288854eabb 100644 --- a/plugin/client_rpc_generated.go +++ b/plugin/client_rpc_generated.go @@ -1682,6 +1682,37 @@ func (s *apiRPCServer) CreateTeamMembers(args *Z_CreateTeamMembersArgs, returns return nil } +type Z_CreateTeamMembersGracefullyArgs struct { + A string + B []string + C string +} + +type Z_CreateTeamMembersGracefullyReturns struct { + A []*model.TeamMemberWithError + B *model.AppError +} + +func (g *apiRPCClient) CreateTeamMembersGracefully(teamId string, userIds []string, requestorId string) ([]*model.TeamMemberWithError, *model.AppError) { + _args := &Z_CreateTeamMembersGracefullyArgs{teamId, userIds, requestorId} + _returns := &Z_CreateTeamMembersGracefullyReturns{} + if err := g.client.Call("Plugin.CreateTeamMembersGracefully", _args, _returns); err != nil { + log.Printf("RPC call to CreateTeamMembersGracefully API failed: %s", err.Error()) + } + return _returns.A, _returns.B +} + +func (s *apiRPCServer) CreateTeamMembersGracefully(args *Z_CreateTeamMembersGracefullyArgs, returns *Z_CreateTeamMembersGracefullyReturns) error { + if hook, ok := s.impl.(interface { + CreateTeamMembersGracefully(teamId string, userIds []string, requestorId string) ([]*model.TeamMemberWithError, *model.AppError) + }); ok { + returns.A, returns.B = hook.CreateTeamMembersGracefully(args.A, args.B, args.C) + } else { + return encodableError(fmt.Errorf("API CreateTeamMembersGracefully called but not implemented.")) + } + return nil +} + type Z_DeleteTeamMemberArgs struct { A string B string diff --git a/plugin/plugintest/api.go b/plugin/plugintest/api.go index e7f942d750..c2c5417d28 100644 --- a/plugin/plugintest/api.go +++ b/plugin/plugintest/api.go @@ -268,6 +268,31 @@ func (_m *API) CreateTeamMembers(teamId string, userIds []string, requestorId st return r0, r1 } +// CreateTeamMembersGracefully provides a mock function with given fields: teamId, userIds, requestorId +func (_m *API) CreateTeamMembersGracefully(teamId string, userIds []string, requestorId string) ([]*model.TeamMemberWithError, *model.AppError) { + ret := _m.Called(teamId, userIds, requestorId) + + var r0 []*model.TeamMemberWithError + if rf, ok := ret.Get(0).(func(string, []string, string) []*model.TeamMemberWithError); ok { + r0 = rf(teamId, userIds, requestorId) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.TeamMemberWithError) + } + } + + var r1 *model.AppError + if rf, ok := ret.Get(1).(func(string, []string, string) *model.AppError); ok { + r1 = rf(teamId, userIds, requestorId) + } else { + if ret.Get(1) != nil { + r1 = ret.Get(1).(*model.AppError) + } + } + + return r0, r1 +} + // CreateUser provides a mock function with given fields: user func (_m *API) CreateUser(user *model.User) (*model.User, *model.AppError) { ret := _m.Called(user)