From 0965e8485af830592561fd0b8fc70cf3dd298052 Mon Sep 17 00:00:00 2001 From: Ibrahim Serdar Acikgoz Date: Wed, 3 Jun 2020 12:20:52 +0300 Subject: [PATCH] [MM-24594] api4/channel: add/remove member & get public/deleted chs for local-mode (#14590) * [MM-24146] Add unix socket listener for mmctl local mode (#14296) * add unix socket listener for mmctl local mode * add a constant for local-mode socket path * reflect review comments * [MM-24401] Base approach for Local Mode (#14333) * add unix socket listener for mmctl local mode * First working PoC * Adds the channel list endpoint * Add team list endpoint * Add a LocalClient to the api test helper and start local mode * Add helper to test with both SystemAdmin and Local clients * Add some docs * Adds TestForAllClients test helper * Incorporating @ashishbhate's proposal for adding test names to the helpers * Fix init errors after merge * Adds create channel tests * Always init local mode to allow for enabling-disabling it via config * Check the RemoteAddr of the request before marking session as local * Mark the request as errored if it's local and the origin is remote * Set the socket permissions to read/write when initialising * Fix linter * Replace RemoteAddr check to ditch connections with the IP:PORT shape Co-authored-by: Ibrahim Serdar Acikgoz * Fix translations order * [MM-24832] Migrate plugin endpoints to local mode (#14543) * [MM-24832] Migrate plugin endpoints to local mode * Fix client reference in helper * [MM-24776] Migrate config endpoints to local mode (#14544) * [MM-24776] Migrate get config endpoint to local mode * [MM-24777] Migrate update config endpoint to local mode * Fix update config to bypass RestrictSystemAdmin flag * Add patchConfig endpoint * MM-24774/MM-24755: local mode for addLicense and removeLicense (#14491) Automatic Merge * api4/channel: add/remove member & get public/deleted chs for local-mode * api4/channel_local: reflect review comments Co-authored-by: Miguel de la Cruz Co-authored-by: Ashish Bhate Co-authored-by: mattermod --- api4/api.go | 13 +++++ api4/channel_local.go | 117 ++++++++++++++++++++++++++++++++++++++++++ api4/channel_test.go | 89 ++++++++++++++++++++------------ api4/user_local.go | 70 +++++++++++++++++++++++++ 4 files changed, 256 insertions(+), 33 deletions(-) create mode 100644 api4/user_local.go diff --git a/api4/api.go b/api4/api.go index 1d06cecf1f..6011673a96 100644 --- a/api4/api.go +++ b/api4/api.go @@ -269,15 +269,27 @@ func InitLocal(configservice configservice.ConfigService, globalOptionsFunc app. api.BaseRoutes.Root = root api.BaseRoutes.ApiRoot = root.PathPrefix(model.API_URL_SUFFIX).Subrouter() + api.BaseRoutes.Users = api.BaseRoutes.ApiRoot.PathPrefix("/users").Subrouter() + api.BaseRoutes.User = api.BaseRoutes.Users.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter() + api.BaseRoutes.UserByUsername = api.BaseRoutes.Users.PathPrefix("/username/{username:[A-Za-z0-9\\_\\-\\.]+}").Subrouter() + api.BaseRoutes.UserByEmail = api.BaseRoutes.Users.PathPrefix("/email/{email:.+}").Subrouter() + api.BaseRoutes.Teams = api.BaseRoutes.ApiRoot.PathPrefix("/teams").Subrouter() api.BaseRoutes.Team = api.BaseRoutes.Teams.PathPrefix("/{team_id:[A-Za-z0-9]+}").Subrouter() + api.BaseRoutes.TeamByName = api.BaseRoutes.Teams.PathPrefix("/name/{team_name:[A-Za-z0-9_-]+}").Subrouter() api.BaseRoutes.TeamMembers = api.BaseRoutes.Team.PathPrefix("/members").Subrouter() api.BaseRoutes.TeamMember = api.BaseRoutes.TeamMembers.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter() api.BaseRoutes.Channels = api.BaseRoutes.ApiRoot.PathPrefix("/channels").Subrouter() api.BaseRoutes.Channel = api.BaseRoutes.Channels.PathPrefix("/{channel_id:[A-Za-z0-9]+}").Subrouter() + api.BaseRoutes.ChannelByName = api.BaseRoutes.Team.PathPrefix("/channels/name/{channel_name:[A-Za-z0-9_-]+}").Subrouter() + api.BaseRoutes.ChannelsForTeam = api.BaseRoutes.Team.PathPrefix("/channels").Subrouter() + api.BaseRoutes.ChannelByNameForTeamName = api.BaseRoutes.TeamByName.PathPrefix("/channels/name/{channel_name:[A-Za-z0-9_-]+}").Subrouter() api.BaseRoutes.ChannelMembers = api.BaseRoutes.Channel.PathPrefix("/members").Subrouter() api.BaseRoutes.ChannelMember = api.BaseRoutes.ChannelMembers.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter() + api.BaseRoutes.ChannelMembersForUser = api.BaseRoutes.User.PathPrefix("/teams/{team_id:[A-Za-z0-9]+}/channels/members").Subrouter() + + api.BaseRoutes.License = api.BaseRoutes.ApiRoot.PathPrefix("/license").Subrouter() api.BaseRoutes.Plugins = api.BaseRoutes.ApiRoot.PathPrefix("/plugins").Subrouter() api.BaseRoutes.Plugin = api.BaseRoutes.Plugins.PathPrefix("/{plugin_id:[A-Za-z0-9\\_\\-\\.]+}").Subrouter() @@ -289,6 +301,7 @@ func InitLocal(configservice configservice.ConfigService, globalOptionsFunc app. api.BaseRoutes.Groups = api.BaseRoutes.ApiRoot.PathPrefix("/groups").Subrouter() + api.InitUserLocal() api.InitTeamLocal() api.InitChannelLocal() api.InitLicenseLocal() diff --git a/api4/channel_local.go b/api4/channel_local.go index 7f09f47b7f..4d8daf0972 100644 --- a/api4/channel_local.go +++ b/api4/channel_local.go @@ -14,8 +14,17 @@ func (api *API) InitChannelLocal() { api.BaseRoutes.Channels.Handle("", api.ApiLocal(getAllChannels)).Methods("GET") api.BaseRoutes.Channels.Handle("", api.ApiLocal(localCreateChannel)).Methods("POST") api.BaseRoutes.Channel.Handle("", api.ApiLocal(deleteChannel)).Methods("DELETE") + + api.BaseRoutes.ChannelMembers.Handle("", api.ApiLocal(localAddChannelMember)).Methods("POST") + api.BaseRoutes.ChannelMember.Handle("", api.ApiLocal(localRemoveChannelMember)).Methods("DELETE") api.BaseRoutes.ChannelMember.Handle("", api.ApiLocal(getChannelMember)).Methods("GET") api.BaseRoutes.ChannelMembers.Handle("", api.ApiLocal(getChannelMembers)).Methods("GET") + + api.BaseRoutes.ChannelsForTeam.Handle("", api.ApiLocal(getPublicChannelsForTeam)).Methods("GET") + api.BaseRoutes.ChannelsForTeam.Handle("/deleted", api.ApiLocal(getDeletedChannelsForTeam)).Methods("GET") + + api.BaseRoutes.ChannelByName.Handle("", api.ApiLocal(getChannelByName)).Methods("GET") + api.BaseRoutes.ChannelByNameForTeamName.Handle("", api.ApiLocal(getChannelByNameForTeamName)).Methods("GET") } func localCreateChannel(c *Context, w http.ResponseWriter, r *http.Request) { @@ -42,3 +51,111 @@ func localCreateChannel(c *Context, w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusCreated) w.Write([]byte(sc.ToJson())) } + +func localAddChannelMember(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireChannelId() + if c.Err != nil { + return + } + + props := model.StringInterfaceFromJson(r.Body) + userId, ok := props["user_id"].(string) + if !ok || !model.IsValidId(userId) { + c.SetInvalidParam("user_id") + return + } + + user, err := c.App.GetUser(userId) + if err != nil { + c.Err = err + return + } + + channel, err := c.App.GetChannel(c.Params.ChannelId) + if err != nil { + c.Err = err + return + } + + auditRec := c.MakeAuditRecord("localAddChannelMember", audit.Fail) + defer c.LogAuditRec(auditRec) + auditRec.AddMeta("channel", channel) + + if channel.Type == model.CHANNEL_DIRECT || channel.Type == model.CHANNEL_GROUP { + c.Err = model.NewAppError("addUserToChannel", "api.channel.add_user_to_channel.type.app_error", nil, "", http.StatusBadRequest) + return + } + + if channel.IsGroupConstrained() { + nonMembers, err := c.App.FilterNonGroupChannelMembers([]string{user.Id}, channel) + if err != nil { + if v, ok := err.(*model.AppError); ok { + c.Err = v + } else { + c.Err = model.NewAppError("addChannelMember", "api.channel.add_members.error", nil, err.Error(), http.StatusBadRequest) + } + return + } + if len(nonMembers) > 0 { + c.Err = model.NewAppError("addChannelMember", "api.channel.add_members.user_denied", map[string]interface{}{"UserIDs": nonMembers}, "", http.StatusBadRequest) + return + } + } + + cm, err := c.App.AddUserToChannel(user, channel) + if err != nil { + c.Err = err + return + } + + auditRec.Success() + auditRec.AddMeta("add_user_id", cm.UserId) + c.LogAudit("name=" + channel.Name + " user_id=" + cm.UserId) + + w.WriteHeader(http.StatusCreated) + w.Write([]byte(cm.ToJson())) +} + +func localRemoveChannelMember(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireChannelId().RequireUserId() + if c.Err != nil { + return + } + + channel, err := c.App.GetChannel(c.Params.ChannelId) + if err != nil { + c.Err = err + return + } + + user, err := c.App.GetUser(c.Params.UserId) + if err != nil { + c.Err = err + return + } + + if !(channel.Type == model.CHANNEL_OPEN || channel.Type == model.CHANNEL_PRIVATE) { + c.Err = model.NewAppError("removeChannelMember", "api.channel.remove_channel_member.type.app_error", nil, "", http.StatusBadRequest) + return + } + + if channel.IsGroupConstrained() && !user.IsBot { + c.Err = model.NewAppError("removeChannelMember", "api.channel.remove_member.group_constrained.app_error", nil, "", http.StatusBadRequest) + return + } + + auditRec := c.MakeAuditRecord("localRemoveChannelMember", audit.Fail) + defer c.LogAuditRec(auditRec) + auditRec.AddMeta("channel", channel) + auditRec.AddMeta("remove_user_id", user.Id) + + if err = c.App.RemoveUserFromChannel(c.Params.UserId, "", channel); err != nil { + c.Err = err + return + } + + auditRec.Success() + c.LogAudit("name=" + channel.Name + " user_id=" + c.Params.UserId) + + ReturnStatusOK(w) +} diff --git a/api4/channel_test.go b/api4/channel_test.go index 26f57e6afa..b5d036b8cb 100644 --- a/api4/channel_test.go +++ b/api4/channel_test.go @@ -668,6 +668,7 @@ func TestGetChannel(t *testing.T) { func TestGetDeletedChannelsForTeam(t *testing.T) { th := Setup(t).InitBasic() defer th.TearDown() + Client := th.Client team := th.BasicTeam @@ -681,16 +682,20 @@ func TestGetDeletedChannelsForTeam(t *testing.T) { publicChannel1 := th.CreatePublicChannel() Client.DeleteChannel(publicChannel1.Id) - channels, resp = Client.GetDeletedChannelsForTeam(team.Id, 0, 100, "") - CheckNoError(t, resp) - require.Len(t, channels, numInitialChannelsForTeam+1, "should be 1 deleted channel") + th.TestForAllClients(t, func(t *testing.T, client *model.Client4) { + channels, resp = client.GetDeletedChannelsForTeam(team.Id, 0, 100, "") + CheckNoError(t, resp) + require.Len(t, channels, numInitialChannelsForTeam+1, "should be 1 deleted channel") + }) publicChannel2 := th.CreatePublicChannel() Client.DeleteChannel(publicChannel2.Id) - channels, resp = Client.GetDeletedChannelsForTeam(team.Id, 0, 100, "") - CheckNoError(t, resp) - require.Len(t, channels, numInitialChannelsForTeam+2, "should be 2 deleted channels") + th.TestForAllClients(t, func(t *testing.T, client *model.Client4) { + channels, resp = client.GetDeletedChannelsForTeam(team.Id, 0, 100, "") + CheckNoError(t, resp) + require.Len(t, channels, numInitialChannelsForTeam+2, "should be 2 deleted channels") + }) th.LoginBasic() @@ -713,6 +718,12 @@ func TestGetDeletedChannelsForTeam(t *testing.T) { CheckNoError(t, resp) require.Len(t, channels, numInitialChannelsForTeam+3) + th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) { + channels, resp = client.GetDeletedChannelsForTeam(team.Id, 0, 100, "") + CheckNoError(t, resp) + require.Len(t, channels, numInitialChannelsForTeam+2) + }) + channels, resp = Client.GetDeletedChannelsForTeam(team.Id, 0, 1, "") CheckNoError(t, resp) require.Len(t, channels, 1, "should be one channel per page") @@ -779,8 +790,10 @@ func TestGetPublicChannelsForTeam(t *testing.T) { _, resp = Client.GetPublicChannelsForTeam(team.Id, 0, 100, "") CheckForbiddenStatus(t, resp) - _, resp = th.SystemAdminClient.GetPublicChannelsForTeam(team.Id, 0, 100, "") - CheckNoError(t, resp) + th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) { + _, resp = client.GetPublicChannelsForTeam(team.Id, 0, 100, "") + CheckNoError(t, resp) + }) } func TestGetPublicChannelsByIdsForTeam(t *testing.T) { @@ -2316,11 +2329,13 @@ func TestAddChannelMember(t *testing.T) { _, resp = Client.AddChannelMember(privateChannel.Id, user2.Id) CheckUnauthorizedStatus(t, resp) - _, resp = th.SystemAdminClient.AddChannelMember(publicChannel.Id, user2.Id) - CheckNoError(t, resp) + th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) { + _, resp = client.AddChannelMember(publicChannel.Id, user2.Id) + CheckNoError(t, resp) - _, resp = th.SystemAdminClient.AddChannelMember(privateChannel.Id, user2.Id) - CheckNoError(t, resp) + _, resp = client.AddChannelMember(privateChannel.Id, user2.Id) + CheckNoError(t, resp) + }) // Check the appropriate permissions are enforced. defaultRolePermissions := th.SaveDefaultRolePermissions() @@ -2370,9 +2385,11 @@ func TestAddChannelMember(t *testing.T) { _, appErr := th.App.UpdateChannel(privateChannel) require.Nil(t, appErr) - // User is not in associated groups so shouldn't be allowed - _, resp = th.SystemAdminClient.AddChannelMember(privateChannel.Id, user.Id) - CheckErrorMessage(t, resp, "api.channel.add_members.user_denied") + th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) { + // User is not in associated groups so shouldn't be allowed + _, resp = client.AddChannelMember(privateChannel.Id, user.Id) + CheckErrorMessage(t, resp, "api.channel.add_members.user_denied") + }) // Associate group to team _, appErr = th.App.UpsertGroupSyncable(&model.GroupSyncable{ @@ -2386,8 +2403,10 @@ func TestAddChannelMember(t *testing.T) { _, appErr = th.App.UpsertGroupMember(th.Group.Id, user.Id) require.Nil(t, appErr) - _, resp = th.SystemAdminClient.AddChannelMember(privateChannel.Id, user.Id) - CheckNoError(t, resp) + th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) { + _, resp = client.AddChannelMember(privateChannel.Id, user.Id) + CheckNoError(t, resp) + }) } func TestAddChannelMemberAddMyself(t *testing.T) { @@ -2595,8 +2614,11 @@ func TestRemoveChannelMember(t *testing.T) { _, resp = Client.RemoveUserFromChannel(private.Id, th.BasicUser.Id) CheckForbiddenStatus(t, resp) - _, resp = th.SystemAdminClient.RemoveUserFromChannel(private.Id, th.BasicUser.Id) - CheckNoError(t, resp) + th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) { + th.App.AddUserToChannel(th.BasicUser, private) + _, resp = client.RemoveUserFromChannel(private.Id, th.BasicUser.Id) + CheckNoError(t, resp) + }) th.LoginBasic() th.UpdateUserToNonTeamAdmin(user1, team) @@ -2610,21 +2632,23 @@ func TestRemoveChannelMember(t *testing.T) { th.AddPermissionToRole(model.PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS.Id, model.CHANNEL_USER_ROLE_ID) - // Check that a regular channel user can remove other users. - privateChannel := th.CreateChannelWithClient(th.SystemAdminClient, model.CHANNEL_PRIVATE) - _, resp = th.SystemAdminClient.AddChannelMember(privateChannel.Id, user1.Id) - CheckNoError(t, resp) - _, resp = th.SystemAdminClient.AddChannelMember(privateChannel.Id, user2.Id) - CheckNoError(t, resp) + th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) { + // Check that a regular channel user can remove other users. + privateChannel := th.CreateChannelWithClient(client, model.CHANNEL_PRIVATE) + _, resp = client.AddChannelMember(privateChannel.Id, user1.Id) + CheckNoError(t, resp) + _, resp = client.AddChannelMember(privateChannel.Id, user2.Id) + CheckNoError(t, resp) - _, resp = Client.RemoveUserFromChannel(privateChannel.Id, user2.Id) - CheckNoError(t, resp) + _, resp = Client.RemoveUserFromChannel(privateChannel.Id, user2.Id) + CheckNoError(t, resp) + }) // Restrict the permission for adding users to Channel Admins th.AddPermissionToRole(model.PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS.Id, model.CHANNEL_ADMIN_ROLE_ID) th.RemovePermissionFromRole(model.PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS.Id, model.CHANNEL_USER_ROLE_ID) - privateChannel = th.CreateChannelWithClient(th.SystemAdminClient, model.CHANNEL_PRIVATE) + privateChannel := th.CreateChannelWithClient(th.SystemAdminClient, model.CHANNEL_PRIVATE) _, resp = th.SystemAdminClient.AddChannelMember(privateChannel.Id, user1.Id) CheckNoError(t, resp) _, resp = th.SystemAdminClient.AddChannelMember(privateChannel.Id, user2.Id) @@ -2677,11 +2701,10 @@ func TestRemoveChannelMember(t *testing.T) { groupChannel, resp := Client.CreateGroupChannel([]string{user1.Id, user2.Id, user3.Id}) CheckNoError(t, resp) - _, resp = Client.RemoveUserFromChannel(groupChannel.Id, user1.Id) - CheckBadRequestStatus(t, resp) - - _, resp = th.SystemAdminClient.RemoveUserFromChannel(groupChannel.Id, user1.Id) - CheckBadRequestStatus(t, resp) + th.TestForAllClients(t, func(t *testing.T, client *model.Client4) { + _, resp = client.RemoveUserFromChannel(groupChannel.Id, user1.Id) + CheckBadRequestStatus(t, resp) + }) } func TestAutocompleteChannels(t *testing.T) { diff --git a/api4/user_local.go b/api4/user_local.go new file mode 100644 index 0000000000..147c72ab4a --- /dev/null +++ b/api4/user_local.go @@ -0,0 +1,70 @@ +// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +package api4 + +import ( + "net/http" + + "github.com/mattermost/mattermost-server/v5/model" +) + +func (api *API) InitUserLocal() { + api.BaseRoutes.Users.Handle("", api.ApiLocal(getUsers)).Methods("GET") + api.BaseRoutes.Users.Handle("/ids", api.ApiLocal(getUsersByIds)).Methods("POST") + api.BaseRoutes.User.Handle("", api.ApiLocal(getUser)).Methods("GET") + + api.BaseRoutes.UserByUsername.Handle("", api.ApiLocal(localGetUserByUsername)).Methods("GET") + api.BaseRoutes.UserByEmail.Handle("", api.ApiLocal(localGetUserByEmail)).Methods("GET") +} + +func localGetUserByUsername(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireUsername() + if c.Err != nil { + return + } + + user, err := c.App.GetUserByUsername(c.Params.Username) + if err != nil { + return + } + + etag := user.Etag(*c.App.Config().PrivacySettings.ShowFullName, *c.App.Config().PrivacySettings.ShowEmailAddress) + + if c.HandleEtag(etag, "Get User", w, r) { + return + } + + c.App.SanitizeProfile(user, c.IsSystemAdmin()) + w.Header().Set(model.HEADER_ETAG_SERVER, etag) + w.Write([]byte(user.ToJson())) +} + +func localGetUserByEmail(c *Context, w http.ResponseWriter, r *http.Request) { + c.SanitizeEmail() + if c.Err != nil { + return + } + + sanitizeOptions := c.App.GetSanitizeOptions(c.IsSystemAdmin()) + if !sanitizeOptions["email"] { + c.Err = model.NewAppError("getUserByEmail", "api.user.get_user_by_email.permissions.app_error", nil, "userId="+c.App.Session().UserId, http.StatusForbidden) + return + } + + user, err := c.App.GetUserByEmail(c.Params.Email) + if err != nil { + c.Err = err + return + } + + etag := user.Etag(*c.App.Config().PrivacySettings.ShowFullName, *c.App.Config().PrivacySettings.ShowEmailAddress) + + if c.HandleEtag(etag, "Get User", w, r) { + return + } + + c.App.SanitizeProfile(user, c.IsSystemAdmin()) + w.Header().Set(model.HEADER_ETAG_SERVER, etag) + w.Write([]byte(user.ToJson())) +}