diff --git a/server/channels/api4/command.go b/server/channels/api4/command.go index cd2fe01d49..312c0d093f 100644 --- a/server/channels/api4/command.go +++ b/server/channels/api4/command.go @@ -323,7 +323,14 @@ func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) { defer c.LogAuditRec(auditRec) audit.AddEventParameterAuditable(auditRec, "command_args", &commandArgs) - // checks that user is a member of the specified channel, and that they have permission to use slash commands in it + // Checks that user is a member of the specified channel, and that they have permission to create a post in it. + if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), commandArgs.ChannelId, model.PermissionCreatePost) { + c.SetPermissionError(model.PermissionCreatePost) + return + } + + // For compatibility reasons, PermissionCreatePost is also checked. + // TODO: Remove in 8.0: https://mattermost.atlassian.net/browse/MM-51274 if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), commandArgs.ChannelId, model.PermissionUseSlashCommands) { c.SetPermissionError(model.PermissionUseSlashCommands) return @@ -343,6 +350,13 @@ func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) { // if the slash command was used in a DM or GM, ensure that the user is a member of the specified team, so that // they can't just execute slash commands against arbitrary teams if c.AppContext.Session().GetTeamByTeamId(commandArgs.TeamId) == nil { + if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionCreatePost) { + c.SetPermissionError(model.PermissionCreatePost) + return + } + + // For compatibility reasons, PermissionCreatePost is also checked. + // TODO: Remove in 8.0: https://mattermost.atlassian.net/browse/MM-51274 if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionUseSlashCommands) { c.SetPermissionError(model.PermissionUseSlashCommands) return diff --git a/server/channels/api4/command_test.go b/server/channels/api4/command_test.go index 8517a344fe..b1328da3b1 100644 --- a/server/channels/api4/command_test.go +++ b/server/channels/api4/command_test.go @@ -14,6 +14,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/mattermost/mattermost-server/server/v8/channels/app/request" "github.com/mattermost/mattermost-server/server/v8/model" "github.com/mattermost/mattermost-server/server/v8/platform/shared/mlog" ) @@ -1065,3 +1066,80 @@ func TestExecuteCommandInTeamUserIsNotOn(t *testing.T) { require.Error(t, err) CheckForbiddenStatus(t, resp) } + +func TestExecuteCommandReadOnly(t *testing.T) { + th := Setup(t).InitBasic() + ctx := request.EmptyContext(th.TestLogger) + defer th.TearDown() + client := th.Client + + enableCommands := *th.App.Config().ServiceSettings.EnableCommands + allowedInternalConnections := *th.App.Config().ServiceSettings.AllowedUntrustedInternalConnections + defer func() { + th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.EnableCommands = &enableCommands }) + th.App.UpdateConfig(func(cfg *model.Config) { + cfg.ServiceSettings.AllowedUntrustedInternalConnections = &allowedInternalConnections + }) + }() + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableCommands = true }) + th.App.UpdateConfig(func(cfg *model.Config) { + *cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost,127.0.0.1" + }) + + expectedCommandResponse := &model.CommandResponse{ + Text: "test post command response", + ResponseType: model.CommandResponseTypeInChannel, + Type: "custom_test", + Props: map[string]any{"someprop": "somevalue"}, + } + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + r.ParseForm() + require.Equal(t, th.BasicTeam.Name, r.FormValue("team_domain")) + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(expectedCommandResponse); err != nil { + th.TestLogger.Warn("Error while writing response", mlog.Err(err)) + } + })) + defer ts.Close() + + // create a slash command on that team + postCmd := &model.Command{ + CreatorId: th.BasicUser.Id, + TeamId: th.BasicTeam.Id, + URL: ts.URL, + Method: model.CommandMethodPost, + Trigger: "postcommand", + } + _, appErr := th.App.CreateCommand(postCmd) + require.Nil(t, appErr, "failed to create post command") + + // Confirm that the command works when the channel is not read only + _, resp, err := client.ExecuteCommandWithTeam(th.BasicChannel.Id, th.BasicChannel.TeamId, "/postcommand") + require.NoError(t, err) + CheckOKStatus(t, resp) + + // Enable Enterprise features + th.App.Srv().SetLicense(model.NewTestLicense()) + + th.App.SetPhase2PermissionsMigrationStatus(true) + + _, appErr = th.App.PatchChannelModerationsForChannel( + ctx, + th.BasicChannel, + []*model.ChannelModerationPatch{{ + Name: &model.PermissionCreatePost.Id, + Roles: &model.ChannelModeratedRolesPatch{ + Guests: model.NewBool(false), + Members: model.NewBool(false), + }, + }}) + require.Nil(t, appErr) + + // Confirm that the command fails when the channel is read only + _, resp, err = client.ExecuteCommandWithTeam(th.BasicChannel.Id, th.BasicChannel.TeamId, "/postcommand") + require.Error(t, err) + CheckForbiddenStatus(t, resp) +} diff --git a/server/model/permission.go b/server/model/permission.go index a44a566964..231154e2d4 100644 --- a/server/model/permission.go +++ b/server/model/permission.go @@ -21,6 +21,9 @@ type Permission struct { var PermissionInviteUser *Permission var PermissionAddUserToTeam *Permission + +// Deprecated: PermissionCreatePost should be used to determine if a slash command can be executed. +// TODO: Remove in 8.0: https://mattermost.atlassian.net/browse/MM-51274 var PermissionUseSlashCommands *Permission var PermissionManageSlashCommands *Permission var PermissionManageOthersSlashCommands *Permission