[CLD-7029] Add a new system console page for configuring custom export FileSettings (#26034)

* Add a new system console page for configuring custom export FileSettings

* Remove NONE option for ExportDriverName dropdown

* Fix tests

* Fix i18n

* Update webapp/channels/src/components/admin_console/admin_definition.tsx

Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com>

* PR Feedback

* Fix formatting

* gofmt

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
Co-authored-by: Ben Schumacher <ben.schumacher@mattermost.com>
This commit is contained in:
Nick Misasi 2024-02-28 11:06:17 -05:00 committed by GitHub
parent 344c5a7528
commit 9ffec5eab6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 228 additions and 31 deletions

View File

@ -535,11 +535,6 @@ func testS3(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if *c.App.Config().ExperimentalSettings.RestrictSystemAdmin {
c.Err = model.NewAppError("testS3", "api.restricted_system_admin", nil, "", http.StatusForbidden)
return
}
appErr := c.App.CheckMandatoryS3Fields(&cfg.FileSettings)
if appErr != nil {
c.Err = appErr

View File

@ -589,15 +589,6 @@ func TestS3TestConnection(t *testing.T) {
CheckErrorID(t, err, "api.file.test_connection_s3_auth.app_error")
})
t.Run("as restricted system admin", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = true })
defer th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = false })
resp, err := th.SystemAdminClient.TestS3Connection(context.Background(), &config)
require.Error(t, err)
CheckForbiddenStatus(t, resp)
})
t.Run("empty file settings", func(t *testing.T) {
config.FileSettings = model.FileSettings{}
resp, err := th.SystemAdminClient.TestS3Connection(context.Background(), &config)

View File

@ -57,7 +57,13 @@ func (a *App) ExportFileBackend() filestore.FileBackend {
}
func (a *App) CheckMandatoryS3Fields(settings *model.FileSettings) *model.AppError {
fileBackendSettings := filestore.NewFileBackendSettingsFromConfig(settings, false, false)
var fileBackendSettings filestore.FileBackendSettings
if a.License().IsCloud() && a.Config().FeatureFlags.CloudDedicatedExportUI && a.Config().FileSettings.DedicatedExportStore != nil && *a.Config().FileSettings.DedicatedExportStore {
fileBackendSettings = filestore.NewExportFileBackendSettingsFromConfig(settings, false, false)
} else {
fileBackendSettings = filestore.NewFileBackendSettingsFromConfig(settings, false, false)
}
err := fileBackendSettings.CheckMandatoryS3Fields()
if err != nil {
return model.NewAppError("CheckMandatoryS3Fields", "api.admin.test_s3.missing_s3_bucket", nil, "", http.StatusBadRequest).Wrap(err)
@ -87,7 +93,15 @@ func (a *App) TestFileStoreConnection() *model.AppError {
func (a *App) TestFileStoreConnectionWithConfig(cfg *model.FileSettings) *model.AppError {
license := a.Srv().License()
insecure := a.Config().ServiceSettings.EnableInsecureOutgoingConnections
backend, err := filestore.NewFileBackend(filestore.NewFileBackendSettingsFromConfig(cfg, license != nil && *license.Features.Compliance, insecure != nil && *insecure))
var backend filestore.FileBackend
var err error
complianceEnabled := license != nil && *license.Features.Compliance
if license.IsCloud() && a.Config().FeatureFlags.CloudDedicatedExportUI && a.Config().FileSettings.DedicatedExportStore != nil && *a.Config().FileSettings.DedicatedExportStore {
allowInsecure := a.Config().ServiceSettings.EnableInsecureOutgoingConnections != nil && *a.Config().ServiceSettings.EnableInsecureOutgoingConnections
backend, err = filestore.NewFileBackend(filestore.NewExportFileBackendSettingsFromConfig(cfg, complianceEnabled && license.IsCloud(), allowInsecure))
} else {
backend, err = filestore.NewFileBackend(filestore.NewFileBackendSettingsFromConfig(cfg, complianceEnabled, insecure != nil && *insecure))
}
if err != nil {
return model.NewAppError("FileBackend", "api.file.no_driver.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
}

View File

@ -1600,21 +1600,21 @@ type FileSettings struct {
AmazonS3Trace *bool `access:"environment_file_storage,write_restrictable,cloud_restrictable"`
AmazonS3RequestTimeoutMilliseconds *int64 `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none
// Export store settings
DedicatedExportStore *bool `access:"environment_file_storage,write_restrictable,cloud_restrictable"`
ExportDriverName *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"`
ExportDirectory *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none
ExportAmazonS3AccessKeyId *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none
ExportAmazonS3SecretAccessKey *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none
ExportAmazonS3Bucket *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none
ExportAmazonS3PathPrefix *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none
ExportAmazonS3Region *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none
ExportAmazonS3Endpoint *string `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none
ExportAmazonS3SSL *bool `access:"environment_file_storage,write_restrictable,cloud_restrictable"`
ExportAmazonS3SignV2 *bool `access:"environment_file_storage,write_restrictable,cloud_restrictable"`
ExportAmazonS3SSE *bool `access:"environment_file_storage,write_restrictable,cloud_restrictable"`
ExportAmazonS3Trace *bool `access:"environment_file_storage,write_restrictable,cloud_restrictable"`
ExportAmazonS3RequestTimeoutMilliseconds *int64 `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none
ExportAmazonS3PresignExpiresSeconds *int64 `access:"environment_file_storage,write_restrictable,cloud_restrictable"` // telemetry: none
DedicatedExportStore *bool `access:"environment_file_storage,write_restrictable"`
ExportDriverName *string `access:"environment_file_storage,write_restrictable"`
ExportDirectory *string `access:"environment_file_storage,write_restrictable"` // telemetry: none
ExportAmazonS3AccessKeyId *string `access:"environment_file_storage,write_restrictable"` // telemetry: none
ExportAmazonS3SecretAccessKey *string `access:"environment_file_storage,write_restrictable"` // telemetry: none
ExportAmazonS3Bucket *string `access:"environment_file_storage,write_restrictable"` // telemetry: none
ExportAmazonS3PathPrefix *string `access:"environment_file_storage,write_restrictable"` // telemetry: none
ExportAmazonS3Region *string `access:"environment_file_storage,write_restrictable"` // telemetry: none
ExportAmazonS3Endpoint *string `access:"environment_file_storage,write_restrictable"` // telemetry: none
ExportAmazonS3SSL *bool `access:"environment_file_storage,write_restrictable"`
ExportAmazonS3SignV2 *bool `access:"environment_file_storage,write_restrictable"`
ExportAmazonS3SSE *bool `access:"environment_file_storage,write_restrictable"`
ExportAmazonS3Trace *bool `access:"environment_file_storage,write_restrictable"`
ExportAmazonS3RequestTimeoutMilliseconds *int64 `access:"environment_file_storage,write_restrictable"` // telemetry: none
ExportAmazonS3PresignExpiresSeconds *int64 `access:"environment_file_storage,write_restrictable"` // telemetry: none
}
func (s *FileSettings) SetDefaults(isUpdate bool) {

View File

@ -50,6 +50,9 @@ type FeatureFlags struct {
ConsumePostHook bool
CloudAnnualRenewals bool
CloudDedicatedExportUI bool
WebSocketEventScope bool
}
@ -70,6 +73,7 @@ func (f *FeatureFlags) SetDefaults() {
f.CloudIPFiltering = false
f.ConsumePostHook = false
f.CloudAnnualRenewals = false
f.CloudDedicatedExportUI = false
f.WebSocketEventScope = false
}

View File

@ -1130,6 +1130,191 @@ const AdminDefinition: AdminDefinitionType = {
],
},
},
export_storage: {
url: 'environment/export_storage',
title: defineMessage({id: 'admin.sidebar.exportStorage', defaultMessage: 'Export Storage'}),
isHidden: it.any(
it.not(it.licensedForFeature('Cloud')),
it.not(it.licensedForSku(LicenseSkus.Enterprise)),
it.configIsFalse('FeatureFlags', 'CloudDedicatedExportUI'),
),
schema: {
id: 'ExportFileSettings',
name: defineMessage({id: 'admin.sidebar.exportStorage', defaultMessage: 'Export Storage'}),
settings: [
{
type: 'bool',
key: 'FileSettings.DedicatedExportStore',
label: defineMessage({id: 'admin.exportStorage.dedicatedExportStore', defaultMessage: 'Enable Dedicated Export Store:'}),
help_text: defineMessage({id: 'admin.exportStorage.dedicatedExportStoreDescription', defaultMessage: 'When enabled, Mattermost will use a dedicated export storage bucket for all export operations. This is required for Mattermost Cloud deployments.'}),
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
},
{
type: 'dropdown',
key: 'FileSettings.ExportDriverName',
label: defineMessage({id: 'admin.exportStorage.exportDriverName', defaultMessage: 'Export Storage Driver:'}),
isDisabled: true,
isHidden: it.stateEquals('FileSettings.DedicatedExportStore', false),
options: [
{
value: FILE_STORAGE_DRIVER_S3,
display_name: defineMessage({id: 'admin.image.storeAmazonS3', defaultMessage: 'Amazon S3'}),
},
],
},
{
type: 'text',
key: 'FileSettings.ExportDirectory',
label: defineMessage({id: 'admin.exportStorage.exportDirectory', defaultMessage: 'Export Directory'}),
help_text: defineMessage({id: 'admin.image.exportDirectoryDescription', defaultMessage: 'Directory to which files are written. If blank, defaults to ./data/.'}),
placeholder: defineMessage({id: 'admin.image.localExample', defaultMessage: 'E.g.: "./data/"'}),
isDisabled: it.any(
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
it.stateEquals('FileSettings.DedicatedExportStore', false),
),
isHidden: it.any(it.stateEquals('FileSettings.ExportDriverName', 'NONE'), it.stateEquals('FileSettings.DedicatedExportStore', false)),
},
{
type: 'text',
key: 'FileSettings.ExportAmazonS3AccessKeyId',
label: defineMessage({id: 'admin.image.amazonS3IdTitle', defaultMessage: 'Amazon S3 Access Key ID:'}),
help_text: defineMessage({id: 'admin.image.amazonS3IdDescription', defaultMessage: '(Optional) Only required if you do not want to authenticate to S3 using an <link>IAM role</link>. Enter the Access Key ID provided by your Amazon EC2 administrator.'}),
help_text_values: {
link: (msg: string) => (
<ExternalLink
location='admin_console'
href='https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2_instance-profiles.html'
>
{msg}
</ExternalLink>
),
},
help_text_markdown: false,
placeholder: defineMessage({id: 'admin.image.amazonS3IdExample', defaultMessage: 'E.g.: "AKIADTOVBGERKLCBV"'}),
isDisabled: it.any(
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
it.stateEquals('FileSettings.DedicatedExportStore', false),
),
isHidden: it.any(it.stateEquals('FileSettings.ExportDriverName', 'NONE'), it.stateEquals('FileSettings.DedicatedExportStore', false)),
},
{
type: 'text',
key: 'FileSettings.ExportAmazonS3SecretAccessKey',
label: defineMessage({id: 'admin.image.amazonS3SecretTitle', defaultMessage: 'Amazon S3 Secret Access Key:'}),
help_text: defineMessage({id: 'admin.image.amazonS3SecretDescription', defaultMessage: '(Optional) The secret access key associated with your Amazon S3 Access Key ID.'}),
placeholder: defineMessage({id: 'admin.image.amazonS3SecretExample', defaultMessage: 'E.g.: "jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY"'}),
isDisabled: it.any(
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
it.stateEquals('FileSettings.DedicatedExportStore', false),
),
isHidden: it.any(it.stateEquals('FileSettings.ExportDriverName', 'NONE'), it.stateEquals('FileSettings.DedicatedExportStore', false)),
},
{
type: 'text',
key: 'FileSettings.ExportAmazonS3Bucket',
label: defineMessage({id: 'admin.image.amazonS3BucketTitle', defaultMessage: 'Amazon S3 Bucket:'}),
help_text: defineMessage({id: 'admin.image.amazonS3BucketDescription', defaultMessage: 'Name you selected for your S3 bucket in AWS.'}),
placeholder: defineMessage({id: 'admin.image.amazonS3BucketExample', defaultMessage: 'E.g.: "mattermost-export"'}),
isDisabled: it.any(
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
it.stateEquals('FileSettings.DedicatedExportStore', false),
),
isHidden: it.any(it.stateEquals('FileSettings.ExportDriverName', 'NONE'), it.stateEquals('FileSettings.DedicatedExportStore', false)),
},
{
type: 'text',
key: 'FileSettings.ExportAmazonS3PathPrefix',
label: defineMessage({id: 'admin.image.amazonS3PathPrefixTitle', defaultMessage: 'Amazon S3 Path Prefix:'}),
help_text: defineMessage({id: 'admin.image.amazonS3PathPrefixDescription', defaultMessage: 'Prefix you selected for your S3 bucket in AWS.'}),
placeholder: defineMessage({id: 'admin.image.amazonS3PathPrefixExample', defaultMessage: 'E.g.: "subdir1/" or you can leave it .'}),
isDisabled: it.any(
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
it.stateEquals('FileSettings.DedicatedExportStore', false),
),
isHidden: it.any(it.stateEquals('FileSettings.ExportDriverName', 'NONE'), it.stateEquals('FileSettings.DedicatedExportStore', false)),
},
{
type: 'text',
key: 'FileSettings.ExportAmazonS3Region',
label: defineMessage({id: 'admin.image.amazonS3RegionTitle', defaultMessage: 'Amazon S3 Region:'}),
help_text: defineMessage({id: 'admin.image.amazonS3RegionDescription', defaultMessage: 'AWS region you selected when creating your S3 bucket. If no region is set, Mattermost attempts to get the appropriate region from AWS, or sets it to "us-east-1" if none found.'}),
placeholder: defineMessage({id: 'admin.image.amazonS3RegionExample', defaultMessage: 'E.g.: "us-east-1"'}),
isDisabled: it.any(
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
it.stateEquals('FileSettings.DedicatedExportStore', false),
),
isHidden: it.any(it.stateEquals('FileSettings.ExportDriverName', 'NONE'), it.stateEquals('FileSettings.DedicatedExportStore', false)),
},
{
type: 'text',
key: 'FileSettings.ExportAmazonS3Endpoint',
label: defineMessage({id: 'admin.image.amazonS3EndpointTitle', defaultMessage: 'Amazon S3 Endpoint:'}),
help_text: defineMessage({id: 'admin.image.amazonS3EndpointDescription', defaultMessage: 'Hostname of your S3 Compatible Storage provider. Defaults to "s3.amazonaws.com".'}),
placeholder: defineMessage({id: 'admin.image.amazonS3EndpointExample', defaultMessage: 'E.g.: "s3.amazonaws.com"'}),
isDisabled: it.any(
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
it.stateEquals('FileSettings.DedicatedExportStore', false),
),
isHidden: it.any(it.stateEquals('FileSettings.ExportDriverName', 'NONE'), it.stateEquals('FileSettings.DedicatedExportStore', false)),
},
{
type: 'bool',
key: 'FileSettings.ExportAmazonS3SSL',
label: defineMessage({id: 'admin.image.amazonS3SSLTitle', defaultMessage: 'Enable Secure Amazon S3 Connections:'}),
help_text: defineMessage({id: 'admin.image.amazonS3SSLDescription', defaultMessage: 'When false, allow insecure connections to Amazon S3. Defaults to secure connections only.'}),
isDisabled: it.any(
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
it.stateEquals('FileSettings.DedicatedExportStore', false),
),
isHidden: it.any(it.stateEquals('FileSettings.ExportDriverName', 'NONE'), it.stateEquals('FileSettings.DedicatedExportStore', false)),
},
{
type: 'bool',
key: 'FileSettings.ExportAmazonSignV2',
label: defineMessage({id: 'admin.image.amazonS3SignV2', defaultMessage: 'Enable Sign V2'}),
help_text: defineMessage({id: 'admin.image.amazonS3SignV2Description', defaultMessage: 'When true, use Sign V2 for Amazon S3 connections'}),
isDisabled: it.any(
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
it.stateEquals('FileSettings.DedicatedExportStore', false),
),
isHidden: it.any(it.stateEquals('FileSettings.ExportDriverName', 'NONE'), it.stateEquals('FileSettings.DedicatedExportStore', false)),
},
{
type: 'bool',
key: 'FileSettings.ExportAmazonS3SSE',
label: defineMessage({id: 'admin.image.amazonS3SSETitle', defaultMessage: 'Enable Server-Side Encryption for Amazon S3:'}),
help_text: defineMessage({id: 'admin.image.amazonS3SSEDescription', defaultMessage: 'When true, encrypt files in Amazon S3 using server-side encryption with Amazon S3-managed keys. See <link>documentation</link> to learn more.'}),
help_text_values: {
link: (msg: string) => (
<ExternalLink
location='admin_console'
href={DocLinks.SESSION_LENGTHS}
>
{msg}
</ExternalLink>
),
},
help_text_markdown: false,
isHidden: it.any(it.stateEquals('FileSettings.ExportDriverName', 'NONE'), it.stateEquals('FileSettings.DedicatedExportStore', false)),
isDisabled: it.any(
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
it.stateEquals('FileSettings.DedicatedExportStore', false),
),
},
{
type: 'button',
action: testS3Connection,
key: 'TestS3Connection',
label: defineMessage({id: 'admin.s3.connectionS3Test', defaultMessage: 'Test Connection'}),
loading: defineMessage({id: 'admin.s3.testing', defaultMessage: 'Testing...'}),
error_message: defineMessage({id: 'admin.s3.s3Fail', defaultMessage: 'Connection unsuccessful: {error}'}),
success_message: defineMessage({id: 'admin.s3.s3Success', defaultMessage: 'Connection was successful'}),
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
isHidden: it.any(it.stateEquals('FileSettings.ExportDriverName', 'NONE'), it.stateEquals('FileSettings.DedicatedExportStore', false)),
},
],
},
},
image_proxy: {
url: 'environment/image_proxy',
title: defineMessage({id: 'admin.sidebar.imageProxy', defaultMessage: 'Image Proxy'}),

View File

@ -1033,6 +1033,10 @@
"admin.experimental.userStatusAwayTimeout.desc": "This setting defines the number of seconds after which the users status indicator changes to \"Away\", when they are away from Mattermost.",
"admin.experimental.userStatusAwayTimeout.example": "E.g.: \"300\"",
"admin.experimental.userStatusAwayTimeout.title": "User Status Away Timeout:",
"admin.exportStorage.dedicatedExportStore": "Enable Dedicated Export Store:",
"admin.exportStorage.dedicatedExportStoreDescription": "When enabled, Mattermost will use a dedicated export storage bucket for all export operations. This is required for Mattermost Cloud deployments.",
"admin.exportStorage.exportDirectory": "Export Directory",
"admin.exportStorage.exportDriverName": "Export Storage Driver:",
"admin.false": "false",
"admin.feature_discovery.trial-request.accept-terms": "By clicking <highlight>Start trial</highlight>, I agree to the <linkEvaluation>Mattermost Software Evaluation Agreement</linkEvaluation>, <linkPrivacy>Privacy Policy</linkPrivacy> and receiving product emails.",
"admin.feature_discovery.trial-request.accept-terms.cloudFree": "By selecting <highlight>Try free for {trialLength} days</highlight>, I agree to the <linkEvaluation>Mattermost Software Evaluation Agreement</linkEvaluation>, <linkPrivacy>Privacy Policy</linkPrivacy>, and receiving product emails.",
@ -1187,6 +1191,8 @@
"admin.image.amazonS3SecretDescription": "(Optional) The secret access key associated with your Amazon S3 Access Key ID.",
"admin.image.amazonS3SecretExample": "E.g.: \"jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY\"",
"admin.image.amazonS3SecretTitle": "Amazon S3 Secret Access Key:",
"admin.image.amazonS3SignV2": "Enable Sign V2",
"admin.image.amazonS3SignV2Description": "When true, use Sign V2 for Amazon S3 connections",
"admin.image.amazonS3SSEDescription": "When true, encrypt files in Amazon S3 using server-side encryption with Amazon S3-managed keys. See <link>documentation</link> to learn more.",
"admin.image.amazonS3SSETitle": "Enable Server-Side Encryption for Amazon S3:",
"admin.image.amazonS3SSLDescription": "When false, allow insecure connections to Amazon S3. Defaults to secure connections only.",
@ -1197,6 +1203,7 @@
"admin.image.archiveRecursionTitle": "Enable searching content of documents within ZIP files:",
"admin.image.enableProxy": "Enable Image Proxy:",
"admin.image.enableProxyDescription": "When true, enables an image proxy for loading all Markdown images.",
"admin.image.exportDirectoryDescription": "Directory to which files are written. If blank, defaults to ./data/.",
"admin.image.extractContentDescription": "When enabled, supported document types are searchable by their content. Search results for existing documents may be incomplete <link>until a data migration is executed</link>.",
"admin.image.extractContentTitle": "Enable document search by content:",
"admin.image.localDescription": "Directory to which files and images are written. If blank, defaults to ./data/.",
@ -2361,6 +2368,7 @@
"admin.sidebar.environment": "Environment",
"admin.sidebar.experimental": "Experimental",
"admin.sidebar.experimentalFeatures": "Features",
"admin.sidebar.exportStorage": "Export Storage",
"admin.sidebar.fileSharingDownloads": "File Sharing and Downloads",
"admin.sidebar.fileStorage": "File Storage",
"admin.sidebar.filter": "Find settings",