[MM-53269] Add configuration setting for integration requests timeout (#23805)
Co-authored-by: Michael Kochell <6913320+mickmister@users.noreply.github.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
7874daa6f7
Коммит
1fdddfe678
@@ -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)
|
||||
})
|
||||
|
||||
Ссылка в новой задаче
Block a user