[MM-53269] Add configuration setting for integration requests timeout (#23805)
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
7874daa6f7
Коммит
1fdddfe678
@@ -96,6 +96,7 @@ const defaultServerConfig: AdminConfig = {
|
||||
EnableIncomingWebhooks: true,
|
||||
EnableOutgoingWebhooks: true,
|
||||
EnableCommands: true,
|
||||
OutgoingIntegrationRequestsTimeout: 30,
|
||||
EnablePostUsernameOverride: false,
|
||||
EnablePostIconOverride: false,
|
||||
GoogleDeveloperKey: '',
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -175,41 +176,62 @@ func TestOpenDialog(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
_, err := client.OpenInteractiveDialog(context.Background(), request)
|
||||
require.NoError(t, err)
|
||||
t.Run("Should pass with valid request", func(t *testing.T) {
|
||||
_, err := client.OpenInteractiveDialog(context.Background(), request)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
// Should fail on bad trigger ID
|
||||
request.TriggerId = "junk"
|
||||
resp, err := client.OpenInteractiveDialog(context.Background(), request)
|
||||
require.Error(t, err)
|
||||
CheckBadRequestStatus(t, resp)
|
||||
t.Run("Should fail on bad trigger ID", func(t *testing.T) {
|
||||
request.TriggerId = "junk"
|
||||
resp, err := client.OpenInteractiveDialog(context.Background(), request)
|
||||
require.Error(t, err)
|
||||
CheckBadRequestStatus(t, resp)
|
||||
})
|
||||
|
||||
// URL is required
|
||||
request.TriggerId = triggerId
|
||||
request.URL = ""
|
||||
resp, err = client.OpenInteractiveDialog(context.Background(), request)
|
||||
require.Error(t, err)
|
||||
CheckBadRequestStatus(t, resp)
|
||||
t.Run("URL is required", func(t *testing.T) {
|
||||
request.TriggerId = triggerId
|
||||
request.URL = ""
|
||||
resp, err := client.OpenInteractiveDialog(context.Background(), request)
|
||||
require.Error(t, err)
|
||||
CheckBadRequestStatus(t, resp)
|
||||
})
|
||||
|
||||
// Should pass with markdown formatted introduction text
|
||||
request.URL = "http://localhost:8065"
|
||||
request.Dialog.IntroductionText = "**Some** _introduction text"
|
||||
_, err = client.OpenInteractiveDialog(context.Background(), request)
|
||||
require.NoError(t, err)
|
||||
t.Run("Should pass with markdown formatted introduction text", func(t *testing.T) {
|
||||
request.URL = "http://localhost:8065"
|
||||
request.Dialog.IntroductionText = "**Some** _introduction text"
|
||||
_, err := client.OpenInteractiveDialog(context.Background(), request)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
// Should pass with empty introduction text
|
||||
request.Dialog.IntroductionText = ""
|
||||
_, err = client.OpenInteractiveDialog(context.Background(), request)
|
||||
require.NoError(t, err)
|
||||
t.Run("Should pass with empty introduction text", func(t *testing.T) {
|
||||
request.Dialog.IntroductionText = ""
|
||||
_, err := client.OpenInteractiveDialog(context.Background(), request)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
// Should pass with no elements
|
||||
request.Dialog.Elements = nil
|
||||
_, err = client.OpenInteractiveDialog(context.Background(), request)
|
||||
require.NoError(t, err)
|
||||
t.Run("Should pass with nil elements slice", func(t *testing.T) {
|
||||
request.Dialog.Elements = nil
|
||||
_, err := client.OpenInteractiveDialog(context.Background(), request)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
request.Dialog.Elements = []model.DialogElement{}
|
||||
_, err = client.OpenInteractiveDialog(context.Background(), request)
|
||||
require.NoError(t, err)
|
||||
t.Run("Should pass with empty elements slice", func(t *testing.T) {
|
||||
request.Dialog.Elements = []model.DialogElement{}
|
||||
_, err := client.OpenInteractiveDialog(context.Background(), request)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("Should fail if trigger timeout is extended", func(t *testing.T) {
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.ServiceSettings.OutgoingIntegrationRequestsTimeout = model.NewInt64(1)
|
||||
})
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
_, err := client.OpenInteractiveDialog(context.Background(), request)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "Trigger ID for interactive dialog is expired.")
|
||||
})
|
||||
}
|
||||
|
||||
func TestSubmitDialog(t *testing.T) {
|
||||
|
||||
@@ -554,12 +554,11 @@ type AppIface interface {
|
||||
DisableUserAccessToken(c request.CTX, token *model.UserAccessToken) *model.AppError
|
||||
DoAppMigrations()
|
||||
DoCheckForAdminNotifications(trial bool) *model.AppError
|
||||
DoCommandRequest(cmd *model.Command, p url.Values) (*model.Command, *model.CommandResponse, *model.AppError)
|
||||
DoCommandRequest(rctx request.CTX, cmd *model.Command, p url.Values) (*model.Command, *model.CommandResponse, *model.AppError)
|
||||
DoEmojisPermissionsMigration()
|
||||
DoGuestRolesCreationMigration()
|
||||
DoLocalRequest(c request.CTX, rawURL string, body []byte) (*http.Response, *model.AppError)
|
||||
DoLogin(c request.CTX, w http.ResponseWriter, r *http.Request, user *model.User, deviceID string, isMobile, isOAuthUser, isSaml bool) (*model.Session, *model.AppError)
|
||||
DoPostAction(c request.CTX, postID, actionId, userID, selectedOption string) (string, *model.AppError)
|
||||
DoPostActionWithCookie(c request.CTX, postID, actionId, userID, selectedOption string, cookie *model.PostActionCookie) (string, *model.AppError)
|
||||
DoSystemConsoleRolesCreationMigration()
|
||||
DoUploadFile(c request.CTX, now time.Time, rawTeamId string, rawChannelId string, rawUserId string, rawFilename string, data []byte) (*model.FileInfo, *model.AppError)
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
@@ -477,17 +478,20 @@ func (a *App) tryExecuteCustomCommand(c request.CTX, args *model.CommandArgs, tr
|
||||
}
|
||||
p.Set("response_url", args.SiteURL+"/hooks/commands/"+hook.Id)
|
||||
|
||||
return a.DoCommandRequest(cmd, p)
|
||||
return a.DoCommandRequest(c, cmd, p)
|
||||
}
|
||||
|
||||
func (a *App) DoCommandRequest(cmd *model.Command, p url.Values) (*model.Command, *model.CommandResponse, *model.AppError) {
|
||||
func (a *App) DoCommandRequest(rctx request.CTX, cmd *model.Command, p url.Values) (*model.Command, *model.CommandResponse, *model.AppError) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*a.Config().ServiceSettings.OutgoingIntegrationRequestsTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// Prepare the request
|
||||
var req *http.Request
|
||||
var err error
|
||||
if cmd.Method == model.CommandMethodGet {
|
||||
req, err = http.NewRequest(http.MethodGet, cmd.URL, nil)
|
||||
req, err = http.NewRequestWithContext(ctx, http.MethodGet, cmd.URL, nil)
|
||||
} else {
|
||||
req, err = http.NewRequest(http.MethodPost, cmd.URL, strings.NewReader(p.Encode()))
|
||||
req, err = http.NewRequestWithContext(ctx, http.MethodPost, cmd.URL, strings.NewReader(p.Encode()))
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -507,9 +511,11 @@ func (a *App) DoCommandRequest(cmd *model.Command, p url.Values) (*model.Command
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
|
||||
// Send the request
|
||||
resp, err := a.Srv().outgoingWebhookClient.Do(req)
|
||||
if err != nil {
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
rctx.Logger().Info("Outgoing Command request timed out. Consider increasing ServiceSettings.OutgoingIntegrationRequestsTimeout.")
|
||||
}
|
||||
return cmd, nil, model.NewAppError("command", "api.command.execute_command.failed.app_error", map[string]any{"Trigger": cmd.Trigger}, "", http.StatusInternalServerError).Wrap(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
//
|
||||
// 1. An integration creates an interactive message button or menu.
|
||||
// 2. A user clicks on a button or selects an option from the menu.
|
||||
// 3. The client sends a request to server to complete the post action, calling DoPostAction below.
|
||||
// 4. DoPostAction will send an HTTP POST request to the integration containing contextual data, including
|
||||
// 3. The client sends a request to server to complete the post action, calling DoPostActionWithCookie below.
|
||||
// 4. DoPostActionWithCookie will send an HTTP POST request to the integration containing contextual data, including
|
||||
// an encoded and signed trigger ID. Slash commands also include trigger IDs in their payloads.
|
||||
// 5. The integration performs any actions it needs to and optionally makes a request back to the MM server
|
||||
// using the trigger ID to open an interactive dialog.
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
|
||||
@@ -40,10 +41,6 @@ import (
|
||||
"github.com/mattermost/mattermost/server/v8/channels/utils"
|
||||
)
|
||||
|
||||
func (a *App) DoPostAction(c request.CTX, postID, actionId, userID, selectedOption string) (string, *model.AppError) {
|
||||
return a.DoPostActionWithCookie(c, postID, actionId, userID, selectedOption, nil)
|
||||
}
|
||||
|
||||
func (a *App) DoPostActionWithCookie(c request.CTX, postID, actionId, userID, selectedOption string, cookie *model.PostActionCookie) (string, *model.AppError) {
|
||||
// PostAction may result in the original post being updated. For the
|
||||
// updated post, we need to unconditionally preserve the original
|
||||
@@ -320,8 +317,14 @@ func (a *App) DoActionRequest(c request.CTX, rawURL string, body []byte) (*http.
|
||||
return a.DoLocalRequest(c, rawURLPath, body)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", rawURL, bytes.NewReader(body))
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*a.Config().ServiceSettings.OutgoingIntegrationRequestsTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", rawURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
c.Logger().Info("Outgoing Integration Action request timed out. Consider increasing ServiceSettings.OutgoingIntegrationRequestsTimeout.")
|
||||
}
|
||||
return nil, model.NewAppError("DoActionRequest", "api.post.do_action.action_integration.app_error", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
@@ -584,7 +587,8 @@ func (a *App) DoLocalRequest(c request.CTX, rawURL string, body []byte) (*http.R
|
||||
}
|
||||
|
||||
func (a *App) OpenInteractiveDialog(request model.OpenDialogRequest) *model.AppError {
|
||||
clientTriggerId, userID, appErr := request.DecodeAndVerifyTriggerId(a.AsymmetricSigningKey())
|
||||
timeout := time.Duration(*a.Config().ServiceSettings.OutgoingIntegrationRequestsTimeout) * time.Second
|
||||
clientTriggerId, userID, appErr := request.DecodeAndVerifyTriggerId(a.AsymmetricSigningKey(), timeout)
|
||||
if appErr != nil {
|
||||
return appErr
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -65,22 +66,21 @@ func TestPostActionInvalidURL(t *testing.T) {
|
||||
require.NotEmpty(t, attachments[0].Actions)
|
||||
require.NotEmpty(t, attachments[0].Actions[0].Id)
|
||||
|
||||
_, err = th.App.DoPostAction(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "")
|
||||
_, err = th.App.DoPostActionWithCookie(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "", nil)
|
||||
require.NotNil(t, err)
|
||||
require.True(t, strings.Contains(err.Error(), "missing protocol scheme"))
|
||||
}
|
||||
|
||||
func TestPostActionEmptyResponse(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
channel := th.BasicChannel
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost,127.0.0.1"
|
||||
})
|
||||
|
||||
t.Run("Empty response on post action", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
|
||||
channel := th.BasicChannel
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost,127.0.0.1"
|
||||
})
|
||||
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}))
|
||||
defer ts.Close()
|
||||
|
||||
@@ -118,9 +118,58 @@ func TestPostActionEmptyResponse(t *testing.T) {
|
||||
attachments, ok := post.GetProp("attachments").([]*model.SlackAttachment)
|
||||
require.True(t, ok)
|
||||
|
||||
_, err = th.App.DoPostAction(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "")
|
||||
_, err = th.App.DoPostActionWithCookie(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "", nil)
|
||||
require.Nil(t, err)
|
||||
})
|
||||
|
||||
t.Run("Empty response on post action, timeout", func(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(2 * time.Second)
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
interactivePost := model.Post{
|
||||
Message: "Interactive post",
|
||||
ChannelId: channel.Id,
|
||||
PendingPostId: model.NewId() + ":" + fmt.Sprint(model.GetMillis()),
|
||||
UserId: th.BasicUser.Id,
|
||||
Props: model.StringInterface{
|
||||
"attachments": []*model.SlackAttachment{
|
||||
{
|
||||
Text: "hello",
|
||||
Actions: []*model.PostAction{
|
||||
{
|
||||
Integration: &model.PostActionIntegration{
|
||||
Context: model.StringInterface{
|
||||
"s": "foo",
|
||||
"n": 3,
|
||||
},
|
||||
URL: ts.URL,
|
||||
},
|
||||
Name: "action",
|
||||
Type: "some_type",
|
||||
DataSource: "some_source",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
post, err := th.App.CreatePostAsUser(th.Context, &interactivePost, "", true)
|
||||
require.Nil(t, err)
|
||||
|
||||
attachments, ok := post.GetProp("attachments").([]*model.SlackAttachment)
|
||||
require.True(t, ok)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.ServiceSettings.OutgoingIntegrationRequestsTimeout = model.NewInt64(1)
|
||||
})
|
||||
|
||||
_, err = th.App.DoPostActionWithCookie(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "", nil)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.DetailedError, "context deadline exceeded")
|
||||
})
|
||||
}
|
||||
|
||||
func TestPostAction(t *testing.T) {
|
||||
@@ -258,16 +307,16 @@ func TestPostAction(t *testing.T) {
|
||||
require.NotEmpty(t, attachments2[0].Actions)
|
||||
require.NotEmpty(t, attachments2[0].Actions[0].Id)
|
||||
|
||||
clientTriggerId, err := th.App.DoPostAction(th.Context, post.Id, "notavalidid", th.BasicUser.Id, "")
|
||||
clientTriggerId, err := th.App.DoPostActionWithCookie(th.Context, post.Id, "notavalidid", th.BasicUser.Id, "", nil)
|
||||
require.NotNil(t, err)
|
||||
assert.Equal(t, http.StatusNotFound, err.StatusCode)
|
||||
assert.True(t, clientTriggerId == "")
|
||||
|
||||
clientTriggerId, err = th.App.DoPostAction(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "")
|
||||
clientTriggerId, err = th.App.DoPostActionWithCookie(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "", nil)
|
||||
require.Nil(t, err)
|
||||
assert.True(t, len(clientTriggerId) == 26)
|
||||
|
||||
clientTriggerId, err = th.App.DoPostAction(th.Context, post2.Id, attachments2[0].Actions[0].Id, th.BasicUser.Id, "selected")
|
||||
clientTriggerId, err = th.App.DoPostActionWithCookie(th.Context, post2.Id, attachments2[0].Actions[0].Id, th.BasicUser.Id, "selected", nil)
|
||||
require.Nil(t, err)
|
||||
assert.True(t, len(clientTriggerId) == 26)
|
||||
|
||||
@@ -275,7 +324,7 @@ func TestPostAction(t *testing.T) {
|
||||
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = ""
|
||||
})
|
||||
|
||||
_, err = th.App.DoPostAction(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "")
|
||||
_, err = th.App.DoPostActionWithCookie(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "", nil)
|
||||
require.NotNil(t, err)
|
||||
require.True(t, strings.Contains(err.Error(), "address forbidden"))
|
||||
|
||||
@@ -313,14 +362,14 @@ func TestPostAction(t *testing.T) {
|
||||
attachmentsPlugin, ok := postplugin.GetProp("attachments").([]*model.SlackAttachment)
|
||||
require.True(t, ok)
|
||||
|
||||
_, err = th.App.DoPostAction(th.Context, postplugin.Id, attachmentsPlugin[0].Actions[0].Id, th.BasicUser.Id, "")
|
||||
_, err = th.App.DoPostActionWithCookie(th.Context, postplugin.Id, attachmentsPlugin[0].Actions[0].Id, th.BasicUser.Id, "", nil)
|
||||
require.Equal(t, "api.post.do_action.action_integration.app_error", err.Id)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.AllowedUntrustedInternalConnections = "localhost,127.0.0.1"
|
||||
})
|
||||
|
||||
_, err = th.App.DoPostAction(th.Context, postplugin.Id, attachmentsPlugin[0].Actions[0].Id, th.BasicUser.Id, "")
|
||||
_, err = th.App.DoPostActionWithCookie(th.Context, postplugin.Id, attachmentsPlugin[0].Actions[0].Id, th.BasicUser.Id, "", nil)
|
||||
require.Nil(t, err)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
@@ -361,7 +410,7 @@ func TestPostAction(t *testing.T) {
|
||||
attachmentsSiteURL, ok := postSiteURL.GetProp("attachments").([]*model.SlackAttachment)
|
||||
require.True(t, ok)
|
||||
|
||||
_, err = th.App.DoPostAction(th.Context, postSiteURL.Id, attachmentsSiteURL[0].Actions[0].Id, th.BasicUser.Id, "")
|
||||
_, err = th.App.DoPostActionWithCookie(th.Context, postSiteURL.Id, attachmentsSiteURL[0].Actions[0].Id, th.BasicUser.Id, "", nil)
|
||||
require.NotNil(t, err)
|
||||
require.False(t, strings.Contains(err.Error(), "address forbidden"))
|
||||
|
||||
@@ -403,7 +452,7 @@ func TestPostAction(t *testing.T) {
|
||||
attachmentsSubpath, ok := postSubpath.GetProp("attachments").([]*model.SlackAttachment)
|
||||
require.True(t, ok)
|
||||
|
||||
_, err = th.App.DoPostAction(th.Context, postSubpath.Id, attachmentsSubpath[0].Actions[0].Id, th.BasicUser.Id, "")
|
||||
_, err = th.App.DoPostActionWithCookie(th.Context, postSubpath.Id, attachmentsSubpath[0].Actions[0].Id, th.BasicUser.Id, "", nil)
|
||||
require.Nil(t, err)
|
||||
})
|
||||
}
|
||||
@@ -477,7 +526,7 @@ func TestPostActionProps(t *testing.T) {
|
||||
attachments, ok := post.GetProp("attachments").([]*model.SlackAttachment)
|
||||
require.True(t, ok)
|
||||
|
||||
clientTriggerId, err := th.App.DoPostAction(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "")
|
||||
clientTriggerId, err := th.App.DoPostActionWithCookie(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "", nil)
|
||||
require.Nil(t, err)
|
||||
assert.True(t, len(clientTriggerId) == 26)
|
||||
|
||||
@@ -661,7 +710,7 @@ func TestPostActionRelativeURL(t *testing.T) {
|
||||
require.NotEmpty(t, attachments[0].Actions)
|
||||
require.NotEmpty(t, attachments[0].Actions[0].Id)
|
||||
|
||||
_, err = th.App.DoPostAction(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "")
|
||||
_, err = th.App.DoPostActionWithCookie(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "", nil)
|
||||
require.NotNil(t, err)
|
||||
})
|
||||
|
||||
@@ -701,7 +750,7 @@ func TestPostActionRelativeURL(t *testing.T) {
|
||||
require.NotEmpty(t, attachments[0].Actions)
|
||||
require.NotEmpty(t, attachments[0].Actions[0].Id)
|
||||
|
||||
_, err = th.App.DoPostAction(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "")
|
||||
_, err = th.App.DoPostActionWithCookie(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "", nil)
|
||||
require.NotNil(t, err)
|
||||
})
|
||||
|
||||
@@ -741,7 +790,7 @@ func TestPostActionRelativeURL(t *testing.T) {
|
||||
require.NotEmpty(t, attachments[0].Actions)
|
||||
require.NotEmpty(t, attachments[0].Actions[0].Id)
|
||||
|
||||
_, err = th.App.DoPostAction(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "")
|
||||
_, err = th.App.DoPostActionWithCookie(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "", nil)
|
||||
require.NotNil(t, err)
|
||||
})
|
||||
|
||||
@@ -781,7 +830,7 @@ func TestPostActionRelativeURL(t *testing.T) {
|
||||
require.NotEmpty(t, attachments[0].Actions)
|
||||
require.NotEmpty(t, attachments[0].Actions[0].Id)
|
||||
|
||||
_, err = th.App.DoPostAction(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "")
|
||||
_, err = th.App.DoPostActionWithCookie(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "", nil)
|
||||
require.NotNil(t, err)
|
||||
})
|
||||
|
||||
@@ -821,7 +870,7 @@ func TestPostActionRelativeURL(t *testing.T) {
|
||||
require.NotEmpty(t, attachments[0].Actions)
|
||||
require.NotEmpty(t, attachments[0].Actions[0].Id)
|
||||
|
||||
_, err = th.App.DoPostAction(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "")
|
||||
_, err = th.App.DoPostActionWithCookie(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "", nil)
|
||||
require.NotNil(t, err)
|
||||
})
|
||||
}
|
||||
@@ -898,7 +947,7 @@ func TestPostActionRelativePluginURL(t *testing.T) {
|
||||
require.NotEmpty(t, attachments[0].Actions)
|
||||
require.NotEmpty(t, attachments[0].Actions[0].Id)
|
||||
|
||||
_, err = th.App.DoPostAction(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "")
|
||||
_, err = th.App.DoPostActionWithCookie(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "", nil)
|
||||
require.NotNil(t, err)
|
||||
})
|
||||
|
||||
@@ -938,7 +987,7 @@ func TestPostActionRelativePluginURL(t *testing.T) {
|
||||
require.NotEmpty(t, attachments[0].Actions)
|
||||
require.NotEmpty(t, attachments[0].Actions[0].Id)
|
||||
|
||||
_, err = th.App.DoPostAction(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "")
|
||||
_, err = th.App.DoPostActionWithCookie(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "", nil)
|
||||
require.Nil(t, err)
|
||||
})
|
||||
|
||||
@@ -978,7 +1027,7 @@ func TestPostActionRelativePluginURL(t *testing.T) {
|
||||
require.NotEmpty(t, attachments[0].Actions)
|
||||
require.NotEmpty(t, attachments[0].Actions[0].Id)
|
||||
|
||||
_, err = th.App.DoPostAction(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "")
|
||||
_, err = th.App.DoPostActionWithCookie(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "", nil)
|
||||
require.Nil(t, err)
|
||||
})
|
||||
|
||||
@@ -1018,7 +1067,7 @@ func TestPostActionRelativePluginURL(t *testing.T) {
|
||||
require.NotEmpty(t, attachments[0].Actions)
|
||||
require.NotEmpty(t, attachments[0].Actions[0].Id)
|
||||
|
||||
_, err = th.App.DoPostAction(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "")
|
||||
_, err = th.App.DoPostActionWithCookie(th.Context, post.Id, attachments[0].Actions[0].Id, th.BasicUser.Id, "", nil)
|
||||
require.Nil(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3776,7 +3776,7 @@ func (a *OpenTracingAppLayer) DoCheckForAdminNotifications(trial bool) *model.Ap
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) DoCommandRequest(cmd *model.Command, p url.Values) (*model.Command, *model.CommandResponse, *model.AppError) {
|
||||
func (a *OpenTracingAppLayer) DoCommandRequest(rctx request.CTX, cmd *model.Command, p url.Values) (*model.Command, *model.CommandResponse, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoCommandRequest")
|
||||
|
||||
@@ -3788,7 +3788,7 @@ func (a *OpenTracingAppLayer) DoCommandRequest(cmd *model.Command, p url.Values)
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0, resultVar1, resultVar2 := a.app.DoCommandRequest(cmd, p)
|
||||
resultVar0, resultVar1, resultVar2 := a.app.DoCommandRequest(rctx, cmd, p)
|
||||
|
||||
if resultVar2 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar2))
|
||||
@@ -3894,28 +3894,6 @@ func (a *OpenTracingAppLayer) DoPermissionsMigrations() error {
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) DoPostAction(c request.CTX, postID string, actionId string, userID string, selectedOption string) (string, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoPostAction")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store().SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store().SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0, resultVar1 := a.app.DoPostAction(c, postID, actionId, userID, selectedOption)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) DoPostActionWithCookie(c request.CTX, postID string, actionId string, userID string, selectedOption string, cookie *model.PostActionCookie) (string, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.DoPostActionWithCookie")
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/platform/services/httpservice"
|
||||
)
|
||||
|
||||
type InfiniteReader struct {
|
||||
@@ -363,7 +362,7 @@ func TestDoCommandRequest(t *testing.T) {
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
_, resp, err := th.App.DoCommandRequest(&model.Command{URL: server.URL}, url.Values{})
|
||||
_, resp, err := th.App.DoCommandRequest(th.Context, &model.Command{URL: server.URL}, url.Values{})
|
||||
require.Nil(t, err)
|
||||
|
||||
assert.NotNil(t, resp)
|
||||
@@ -378,7 +377,7 @@ func TestDoCommandRequest(t *testing.T) {
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
_, resp, err := th.App.DoCommandRequest(&model.Command{URL: server.URL}, url.Values{})
|
||||
_, resp, err := th.App.DoCommandRequest(th.Context, &model.Command{URL: server.URL}, url.Values{})
|
||||
require.Nil(t, err)
|
||||
|
||||
assert.NotNil(t, resp)
|
||||
@@ -393,7 +392,7 @@ func TestDoCommandRequest(t *testing.T) {
|
||||
|
||||
// Since we limit the length of the response, no error will be returned and resp.Text will be a finite string
|
||||
|
||||
_, resp, err := th.App.DoCommandRequest(&model.Command{URL: server.URL}, url.Values{})
|
||||
_, resp, err := th.App.DoCommandRequest(th.Context, &model.Command{URL: server.URL}, url.Values{})
|
||||
require.Nil(t, err)
|
||||
require.NotNil(t, resp)
|
||||
})
|
||||
@@ -406,7 +405,7 @@ func TestDoCommandRequest(t *testing.T) {
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
_, _, err := th.App.DoCommandRequest(&model.Command{URL: server.URL}, url.Values{})
|
||||
_, _, err := th.App.DoCommandRequest(th.Context, &model.Command{URL: server.URL}, url.Values{})
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, "api.command.execute_command.failed.app_error", err.Id)
|
||||
})
|
||||
@@ -419,29 +418,47 @@ func TestDoCommandRequest(t *testing.T) {
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
_, _, err := th.App.DoCommandRequest(&model.Command{URL: server.URL}, url.Values{})
|
||||
_, _, err := th.App.DoCommandRequest(th.Context, &model.Command{URL: server.URL}, url.Values{})
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, "api.command.execute_command.failed.app_error", err.Id)
|
||||
})
|
||||
|
||||
t.Run("with a slow response", func(t *testing.T) {
|
||||
t.Run("with a too slow response", func(t *testing.T) {
|
||||
done := make(chan bool)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
<-done
|
||||
io.Copy(w, strings.NewReader(`{"text": "Hello, World!"}`))
|
||||
io.Copy(w, strings.NewReader("Hello, World!"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
th.App.HTTPService().(*httpservice.HTTPServiceImpl).RequestTimeout = 100 * time.Millisecond
|
||||
defer func() {
|
||||
th.App.HTTPService().(*httpservice.HTTPServiceImpl).RequestTimeout = httpservice.RequestTimeout
|
||||
}()
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.ServiceSettings.OutgoingIntegrationRequestsTimeout = model.NewInt64(1)
|
||||
})
|
||||
|
||||
_, _, err := th.App.DoCommandRequest(&model.Command{URL: server.URL}, url.Values{})
|
||||
_, _, err := th.App.DoCommandRequest(th.Context, &model.Command{URL: server.URL}, url.Values{})
|
||||
require.NotNil(t, err)
|
||||
require.Equal(t, "api.command.execute_command.failed.app_error", err.Id)
|
||||
close(done)
|
||||
})
|
||||
|
||||
t.Run("with a too slow response, long timeout configured", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
io.Copy(w, strings.NewReader("Hello, World!"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.ServiceSettings.OutgoingIntegrationRequestsTimeout = model.NewInt64(2)
|
||||
})
|
||||
|
||||
_, resp, err := th.App.DoCommandRequest(th.Context, &model.Command{URL: server.URL}, url.Values{})
|
||||
require.Nil(t, err)
|
||||
|
||||
require.NotNil(t, resp)
|
||||
assert.Equal(t, "Hello, World!", resp.Text)
|
||||
})
|
||||
}
|
||||
|
||||
func TestMentionsToTeamMembers(t *testing.T) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
@@ -118,7 +119,11 @@ func (a *App) TriggerWebhook(c request.CTX, payload *model.OutgoingWebhookPayloa
|
||||
defer wg.Done()
|
||||
webhookResp, err := a.doOutgoingWebhookRequest(url, body, contentType)
|
||||
if err != nil {
|
||||
c.Logger().Error("Event POST failed.", mlog.Err(err))
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
c.Logger().Error("Outgoing Webhook POST timed out. Consider increasing ServiceSettings.OutgoingIntegrationRequestsTimeout.", mlog.Err(err))
|
||||
} else {
|
||||
c.Logger().Error("Outgoing Webhook POST failed", mlog.Err(err))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -158,7 +163,10 @@ func (a *App) TriggerWebhook(c request.CTX, payload *model.OutgoingWebhookPayloa
|
||||
}
|
||||
|
||||
func (a *App) doOutgoingWebhookRequest(url string, body io.Reader, contentType string) (*model.OutgoingWebhookResponse, error) {
|
||||
req, err := http.NewRequest("POST", url, body)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(*a.Config().ServiceSettings.OutgoingIntegrationRequestsTimeout)*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", url, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/mattermost/mattermost/server/v8/channels/testlib"
|
||||
"github.com/mattermost/mattermost/server/v8/platform/services/httpservice"
|
||||
)
|
||||
|
||||
func TestCreateIncomingWebhookForChannel(t *testing.T) {
|
||||
@@ -787,7 +786,7 @@ func TestDoOutgoingWebhookRequest(t *testing.T) {
|
||||
resp, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotNil(t, resp)
|
||||
require.NotNil(t, resp)
|
||||
assert.NotNil(t, resp.Text)
|
||||
assert.Equal(t, "Hello, World!", *resp.Text)
|
||||
})
|
||||
@@ -835,16 +834,34 @@ func TestDoOutgoingWebhookRequest(t *testing.T) {
|
||||
defer server.Close()
|
||||
defer close(releaseHandler)
|
||||
|
||||
th.App.HTTPService().(*httpservice.HTTPServiceImpl).RequestTimeout = 500 * time.Millisecond
|
||||
defer func() {
|
||||
th.App.HTTPService().(*httpservice.HTTPServiceImpl).RequestTimeout = httpservice.RequestTimeout
|
||||
}()
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.ServiceSettings.OutgoingIntegrationRequestsTimeout = model.NewInt64(1)
|
||||
})
|
||||
|
||||
_, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json")
|
||||
require.Error(t, err)
|
||||
require.IsType(t, &url.Error{}, err)
|
||||
})
|
||||
|
||||
t.Run("with a slow response, long timeout configured", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
io.Copy(w, strings.NewReader(`{"text": "Hello, World!"}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
cfg.ServiceSettings.OutgoingIntegrationRequestsTimeout = model.NewInt64(2)
|
||||
})
|
||||
|
||||
resp, err := th.App.doOutgoingWebhookRequest(server.URL, strings.NewReader(""), "application/json")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
assert.NotNil(t, resp.Text)
|
||||
assert.Equal(t, "Hello, World!", *resp.Text)
|
||||
})
|
||||
|
||||
t.Run("without response", func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
}))
|
||||
|
||||
@@ -8264,7 +8264,7 @@
|
||||
},
|
||||
{
|
||||
"id": "interactive_message.decode_trigger_id.expired",
|
||||
"translation": "Trigger ID for interactive dialog is expired. Trigger IDs live for a maximum of {{.Seconds}} seconds."
|
||||
"translation": "Trigger ID for interactive dialog is expired. Trigger IDs live for a maximum of {{.Duration}}."
|
||||
},
|
||||
{
|
||||
"id": "interactive_message.decode_trigger_id.missing_data",
|
||||
@@ -8922,6 +8922,10 @@
|
||||
"id": "model.config.is_valid.move_thread.domain_invalid.app_error",
|
||||
"translation": "Invalid domain for move thread settings"
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.outgoing_integrations_request_timeout.app_error",
|
||||
"translation": "Invalid Outgoing Integrations Request Timeout for service settings. Must be a positive number."
|
||||
},
|
||||
{
|
||||
"id": "model.config.is_valid.password_length.app_error",
|
||||
"translation": "Minimum password length must be a whole number greater than or equal to {{.MinLength}} and less than or equal to {{.MaxLength}}."
|
||||
|
||||
@@ -411,6 +411,7 @@ func (ts *TelemetryService) trackConfig() {
|
||||
"enable_incoming_webhooks": cfg.ServiceSettings.EnableIncomingWebhooks,
|
||||
"enable_outgoing_webhooks": cfg.ServiceSettings.EnableOutgoingWebhooks,
|
||||
"enable_commands": *cfg.ServiceSettings.EnableCommands,
|
||||
"outgoing_integrations_requests_timeout": cfg.ServiceSettings.OutgoingIntegrationRequestsTimeout,
|
||||
"enable_post_username_override": cfg.ServiceSettings.EnablePostUsernameOverride,
|
||||
"enable_post_icon_override": cfg.ServiceSettings.EnablePostIconOverride,
|
||||
"enable_user_access_tokens": *cfg.ServiceSettings.EnableUserAccessTokens,
|
||||
|
||||
@@ -214,6 +214,8 @@ const (
|
||||
DataRetentionSettingsDefaultTimeBetweenBatchesMilliseconds = 100
|
||||
DataRetentionSettingsDefaultRetentionIdsBatchSize = 100
|
||||
|
||||
OutgoingIntegrationRequestsDefaultTimeout = 30
|
||||
|
||||
PluginSettingsDefaultDirectory = "./plugins"
|
||||
PluginSettingsDefaultClientDirectory = "./client/plugins"
|
||||
PluginSettingsDefaultEnableMarketplace = true
|
||||
@@ -309,6 +311,7 @@ type ServiceSettings struct {
|
||||
EnableIncomingWebhooks *bool `access:"integrations_integration_management"`
|
||||
EnableOutgoingWebhooks *bool `access:"integrations_integration_management"`
|
||||
EnableCommands *bool `access:"integrations_integration_management"`
|
||||
OutgoingIntegrationRequestsTimeout *int64 `access:"integrations_integration_management"` // In seconds.
|
||||
EnablePostUsernameOverride *bool `access:"integrations_integration_management"`
|
||||
EnablePostIconOverride *bool `access:"integrations_integration_management"`
|
||||
GoogleDeveloperKey *string `access:"site_posts,write_restrictable,cloud_restrictable"`
|
||||
@@ -509,6 +512,10 @@ func (s *ServiceSettings) SetDefaults(isUpdate bool) {
|
||||
s.EnableOutgoingWebhooks = NewBool(true)
|
||||
}
|
||||
|
||||
if s.OutgoingIntegrationRequestsTimeout == nil {
|
||||
s.OutgoingIntegrationRequestsTimeout = NewInt64(OutgoingIntegrationRequestsDefaultTimeout)
|
||||
}
|
||||
|
||||
if s.ConnectionSecurity == nil {
|
||||
s.ConnectionSecurity = NewString("")
|
||||
}
|
||||
@@ -4011,6 +4018,10 @@ func (s *ServiceSettings) isValid() *AppError {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.listen_address.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if *s.OutgoingIntegrationRequestsTimeout <= 0 {
|
||||
return NewAppError("Config.IsValid", "model.config.is_valid.outgoing_integrations_request_timeout.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if *s.ExperimentalGroupUnreadChannels != GroupUnreadChannelsDisabled &&
|
||||
*s.ExperimentalGroupUnreadChannels != GroupUnreadChannelsDefaultOn &&
|
||||
*s.ExperimentalGroupUnreadChannels != GroupUnreadChannelsDefaultOff {
|
||||
|
||||
@@ -74,6 +74,47 @@ func TestConfigEmptySiteName(t *testing.T) {
|
||||
require.Equal(t, *c1.TeamSettings.SiteName, TeamSettingsDefaultSiteName)
|
||||
}
|
||||
|
||||
func TestServiceSettingsIsValid(t *testing.T) {
|
||||
for name, test := range map[string]struct {
|
||||
ServiceSettings ServiceSettings
|
||||
ExpectError bool
|
||||
}{
|
||||
"empty": {
|
||||
ServiceSettings: ServiceSettings{},
|
||||
ExpectError: false,
|
||||
},
|
||||
"OutgoingIntegrationRequestsTimeout is negative": {
|
||||
ServiceSettings: ServiceSettings{
|
||||
OutgoingIntegrationRequestsTimeout: NewInt64(-1),
|
||||
},
|
||||
ExpectError: true,
|
||||
},
|
||||
"OutgoingIntegrationRequestsTimeout is zero": {
|
||||
ServiceSettings: ServiceSettings{
|
||||
OutgoingIntegrationRequestsTimeout: NewInt64(0),
|
||||
},
|
||||
ExpectError: true,
|
||||
},
|
||||
"OutgoingIntegrationRequestsTimeout is positiv": {
|
||||
ServiceSettings: ServiceSettings{
|
||||
OutgoingIntegrationRequestsTimeout: NewInt64(1),
|
||||
},
|
||||
ExpectError: false,
|
||||
},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
test.ServiceSettings.SetDefaults(false)
|
||||
|
||||
appErr := test.ServiceSettings.isValid()
|
||||
if test.ExpectError {
|
||||
assert.NotNil(t, appErr)
|
||||
} else {
|
||||
assert.Nil(t, appErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigEnableDeveloper(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Description string
|
||||
|
||||
@@ -19,12 +19,12 @@ import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
PostActionTypeButton = "button"
|
||||
PostActionTypeSelect = "select"
|
||||
InteractiveDialogTriggerTimeoutMilliseconds = 3000
|
||||
PostActionTypeButton = "button"
|
||||
PostActionTypeSelect = "select"
|
||||
)
|
||||
|
||||
var PostActionRetainPropKeys = []string{"from_webhook", "override_username", "override_icon_url"}
|
||||
@@ -280,7 +280,7 @@ func (r *PostActionIntegrationRequest) GenerateTriggerId(s crypto.Signer) (strin
|
||||
return clientTriggerId, triggerId, nil
|
||||
}
|
||||
|
||||
func DecodeAndVerifyTriggerId(triggerId string, s *ecdsa.PrivateKey) (string, string, *AppError) {
|
||||
func DecodeAndVerifyTriggerId(triggerId string, s *ecdsa.PrivateKey, timeout time.Duration) (string, string, *AppError) {
|
||||
triggerIdBytes, err := base64.StdEncoding.DecodeString(triggerId)
|
||||
if err != nil {
|
||||
return "", "", NewAppError("DecodeAndVerifyTriggerId", "interactive_message.decode_trigger_id.base64_decode_failed", nil, "", http.StatusBadRequest).Wrap(err)
|
||||
@@ -296,9 +296,8 @@ func DecodeAndVerifyTriggerId(triggerId string, s *ecdsa.PrivateKey) (string, st
|
||||
timestampStr := split[2]
|
||||
timestamp, _ := strconv.ParseInt(timestampStr, 10, 64)
|
||||
|
||||
now := GetMillis()
|
||||
if now-timestamp > InteractiveDialogTriggerTimeoutMilliseconds {
|
||||
return "", "", NewAppError("DecodeAndVerifyTriggerId", "interactive_message.decode_trigger_id.expired", map[string]any{"Seconds": InteractiveDialogTriggerTimeoutMilliseconds / 1000}, "", http.StatusBadRequest)
|
||||
if time.Since(time.UnixMilli(timestamp)) > timeout {
|
||||
return "", "", NewAppError("DecodeAndVerifyTriggerId", "interactive_message.decode_trigger_id.expired", map[string]any{"Duration": timeout.String()}, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
signature, err := base64.StdEncoding.DecodeString(split[3])
|
||||
@@ -327,8 +326,8 @@ func DecodeAndVerifyTriggerId(triggerId string, s *ecdsa.PrivateKey) (string, st
|
||||
return clientTriggerId, userId, nil
|
||||
}
|
||||
|
||||
func (r *OpenDialogRequest) DecodeAndVerifyTriggerId(s *ecdsa.PrivateKey) (string, string, *AppError) {
|
||||
return DecodeAndVerifyTriggerId(r.TriggerId, s)
|
||||
func (r *OpenDialogRequest) DecodeAndVerifyTriggerId(s *ecdsa.PrivateKey, timeout time.Duration) (string, string, *AppError) {
|
||||
return DecodeAndVerifyTriggerId(r.TriggerId, s, timeout)
|
||||
}
|
||||
|
||||
func (o *Post) StripActionIntegrations() {
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -22,7 +23,7 @@ func TestTriggerIdDecodeAndVerification(t *testing.T) {
|
||||
userId := NewId()
|
||||
clientTriggerId, triggerId, appErr := GenerateTriggerId(userId, key)
|
||||
require.Nil(t, appErr)
|
||||
decodedClientTriggerId, decodedUserId, appErr := DecodeAndVerifyTriggerId(triggerId, key)
|
||||
decodedClientTriggerId, decodedUserId, appErr := DecodeAndVerifyTriggerId(triggerId, key, OutgoingIntegrationRequestsDefaultTimeout*time.Second)
|
||||
assert.Nil(t, appErr)
|
||||
assert.Equal(t, clientTriggerId, decodedClientTriggerId)
|
||||
assert.Equal(t, userId, decodedUserId)
|
||||
@@ -35,38 +36,38 @@ func TestTriggerIdDecodeAndVerification(t *testing.T) {
|
||||
clientTriggerId, triggerId, appErr := actionReq.GenerateTriggerId(key)
|
||||
require.Nil(t, appErr)
|
||||
dialogReq := &OpenDialogRequest{TriggerId: triggerId}
|
||||
decodedClientTriggerId, decodedUserId, appErr := dialogReq.DecodeAndVerifyTriggerId(key)
|
||||
decodedClientTriggerId, decodedUserId, appErr := dialogReq.DecodeAndVerifyTriggerId(key, OutgoingIntegrationRequestsDefaultTimeout*time.Second)
|
||||
assert.Nil(t, appErr)
|
||||
assert.Equal(t, clientTriggerId, decodedClientTriggerId)
|
||||
assert.Equal(t, actionReq.UserId, decodedUserId)
|
||||
})
|
||||
|
||||
t.Run("should fail on base64 decode", func(t *testing.T) {
|
||||
_, _, appErr := DecodeAndVerifyTriggerId("junk!", key)
|
||||
_, _, appErr := DecodeAndVerifyTriggerId("junk!", key, OutgoingIntegrationRequestsDefaultTimeout*time.Second)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "interactive_message.decode_trigger_id.base64_decode_failed", appErr.Id)
|
||||
})
|
||||
|
||||
t.Run("should fail on trigger parsing", func(t *testing.T) {
|
||||
_, _, appErr := DecodeAndVerifyTriggerId(base64.StdEncoding.EncodeToString([]byte("junk!")), key)
|
||||
_, _, appErr := DecodeAndVerifyTriggerId(base64.StdEncoding.EncodeToString([]byte("junk!")), key, OutgoingIntegrationRequestsDefaultTimeout*time.Second)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "interactive_message.decode_trigger_id.missing_data", appErr.Id)
|
||||
})
|
||||
|
||||
t.Run("should fail on expired timestamp", func(t *testing.T) {
|
||||
_, _, appErr := DecodeAndVerifyTriggerId(base64.StdEncoding.EncodeToString([]byte("some-trigger-id:some-user-id:1234567890:junksignature")), key)
|
||||
_, _, appErr := DecodeAndVerifyTriggerId(base64.StdEncoding.EncodeToString([]byte("some-trigger-id:some-user-id:1234567890:junksignature")), key, OutgoingIntegrationRequestsDefaultTimeout*time.Second)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "interactive_message.decode_trigger_id.expired", appErr.Id)
|
||||
})
|
||||
|
||||
t.Run("should fail on base64 decoding signature", func(t *testing.T) {
|
||||
_, _, appErr := DecodeAndVerifyTriggerId(base64.StdEncoding.EncodeToString([]byte("some-trigger-id:some-user-id:12345678900000:junk!")), key)
|
||||
_, _, appErr := DecodeAndVerifyTriggerId(base64.StdEncoding.EncodeToString([]byte("some-trigger-id:some-user-id:12345678900000:junk!")), key, OutgoingIntegrationRequestsDefaultTimeout*time.Second)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "interactive_message.decode_trigger_id.base64_decode_failed_signature", appErr.Id)
|
||||
})
|
||||
|
||||
t.Run("should fail on bad signature", func(t *testing.T) {
|
||||
_, _, appErr := DecodeAndVerifyTriggerId(base64.StdEncoding.EncodeToString([]byte("some-trigger-id:some-user-id:12345678900000:junk")), key)
|
||||
_, _, appErr := DecodeAndVerifyTriggerId(base64.StdEncoding.EncodeToString([]byte("some-trigger-id:some-user-id:12345678900000:junk")), key, OutgoingIntegrationRequestsDefaultTimeout*time.Second)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "interactive_message.decode_trigger_id.signature_decode_failed", appErr.Id)
|
||||
})
|
||||
@@ -76,7 +77,7 @@ func TestTriggerIdDecodeAndVerification(t *testing.T) {
|
||||
require.Nil(t, appErr)
|
||||
newKey, keyErr := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
require.NoError(t, keyErr)
|
||||
_, _, appErr = DecodeAndVerifyTriggerId(triggerId, newKey)
|
||||
_, _, appErr = DecodeAndVerifyTriggerId(triggerId, newKey, OutgoingIntegrationRequestsDefaultTimeout*time.Second)
|
||||
require.NotNil(t, appErr)
|
||||
assert.Equal(t, "interactive_message.decode_trigger_id.verify_signature_failed", appErr.Id)
|
||||
})
|
||||
|
||||
@@ -6204,6 +6204,50 @@ const AdminDefinition: AdminDefinitionType = {
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.INTEGRATIONS.INTEGRATION_MANAGEMENT)),
|
||||
isHidden: it.licensedForFeature('Cloud'),
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
key: 'ServiceSettings.OutgoingIntegrationRequestsTimeout',
|
||||
label: t('admin.service.integrationRequestTitle'),
|
||||
label_default: 'Integration request timeout: ',
|
||||
help_text: t('admin.service.integrationRequestDesc'),
|
||||
help_text_default: 'The number of seconds to wait for Integration requests. That includes <slashCommands>Slash Commands</slashCommands>, <outgoingWebhooks>Outgoing Webhooks</outgoingWebhooks>, <interactiveMessages>Interactive Messages</interactiveMessages> and <interactiveDialogs>Interactive Dialogs</interactiveDialogs>.',
|
||||
help_text_values: {
|
||||
slashCommands: (msg: string) => (
|
||||
<ExternalLink
|
||||
location='admin_console'
|
||||
href={DeveloperLinks.CUSTOM_SLASH_COMMANDS}
|
||||
>
|
||||
{msg}
|
||||
</ExternalLink>
|
||||
),
|
||||
outgoingWebhooks: (msg: string) => (
|
||||
<ExternalLink
|
||||
location='admin_console'
|
||||
href={DeveloperLinks.OUTGOING_WEBHOOKS}
|
||||
>
|
||||
{msg}
|
||||
</ExternalLink>
|
||||
),
|
||||
interactiveMessages: (msg: string) => (
|
||||
<ExternalLink
|
||||
location='admin_console'
|
||||
href={DeveloperLinks.INTERACTIVE_MESSAGES}
|
||||
>
|
||||
{msg}
|
||||
</ExternalLink>
|
||||
),
|
||||
interactiveDialogs: (msg: string) => (
|
||||
<ExternalLink
|
||||
location='admin_console'
|
||||
href={DeveloperLinks.INTERACTIVE_DIALOGS}
|
||||
>
|
||||
{msg}
|
||||
</ExternalLink>
|
||||
),
|
||||
},
|
||||
help_text_markdown: false,
|
||||
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.INTEGRATIONS.INTEGRATION_MANAGEMENT)),
|
||||
},
|
||||
{
|
||||
type: 'bool',
|
||||
key: 'ServiceSettings.EnablePostUsernameOverride',
|
||||
|
||||
@@ -2244,6 +2244,8 @@
|
||||
"admin.service.iconTitle": "Enable integrations to override profile picture icons:",
|
||||
"admin.service.insecureTlsDesc": "When true, any outgoing HTTPS requests will accept unverified, self-signed certificates. For example, outgoing webhooks to a server with a self-signed TLS certificate, using any domain, will be allowed. Note that this makes these connections susceptible to man-in-the-middle attacks.",
|
||||
"admin.service.insecureTlsTitle": "Enable Insecure Outgoing Connections: ",
|
||||
"admin.service.integrationRequestDesc": "The number of seconds to wait for Integration requests. That includes <slashCommands>Slash Commands</slashCommands>, <outgoingWebhooks>Outgoing Webhooks</outgoingWebhooks>, <interactiveMessages>Interactive Messages</interactiveMessages> and <interactiveDialogs>Interactive Dialogs</interactiveDialogs>.",
|
||||
"admin.service.integrationRequestTitle": "Integration request timeout: ",
|
||||
"admin.service.internalConnectionsDesc": "A whitelist of local network addresses that can be requested by the Mattermost server on behalf of a client. Care should be used when configuring this setting to prevent unintended access to your local network. See <link>documentation</link> to learn more. Changing this requires a server restart before taking effect.",
|
||||
"admin.service.internalConnectionsEx": "webhooks.internal.example.com 127.0.0.1 10.0.16.0/28",
|
||||
"admin.service.internalConnectionsTitle": "Allow untrusted internal connections to: ",
|
||||
|
||||
@@ -1139,6 +1139,8 @@ export const DeveloperLinks = {
|
||||
ENABLE_OAUTH2: 'https://mattermost.com/pl/enable-oauth',
|
||||
INCOMING_WEBHOOKS: 'https://mattermost.com/pl/incoming-webhooks',
|
||||
OUTGOING_WEBHOOKS: 'https://mattermost.com/pl/outgoing-webhooks',
|
||||
INTERACTIVE_MESSAGES: 'https://mattermost.com/pl/interactive-messages',
|
||||
INTERACTIVE_DIALOGS: 'https://mattermost.com/pl/interactive-dialogs',
|
||||
PERSONAL_ACCESS_TOKENS: 'https://mattermost.com/pl/personal-access-tokens',
|
||||
PLUGIN_SIGNING: 'https://mattermost.com/pl/sign-plugins',
|
||||
PLUGINS: 'https://mattermost.com/pl/plugins',
|
||||
|
||||
@@ -303,6 +303,7 @@ export type ServiceSettings = {
|
||||
EnableIncomingWebhooks: boolean;
|
||||
EnableOutgoingWebhooks: boolean;
|
||||
EnableCommands: boolean;
|
||||
OutgoingIntegrationRequestsTimeout: number;
|
||||
EnablePostUsernameOverride: boolean;
|
||||
EnablePostIconOverride: boolean;
|
||||
EnableLinkPreviews: boolean;
|
||||
|
||||
Ссылка в новой задаче
Block a user