diff --git a/server/channels/api4/system.go b/server/channels/api4/system.go index 5103daccb7..bf234ccfeb 100644 --- a/server/channels/api4/system.go +++ b/server/channels/api4/system.go @@ -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 diff --git a/server/channels/api4/system_test.go b/server/channels/api4/system_test.go index d33a7a2993..9e6901cd8d 100644 --- a/server/channels/api4/system_test.go +++ b/server/channels/api4/system_test.go @@ -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) diff --git a/server/channels/app/file.go b/server/channels/app/file.go index fddbe4483f..19de7b4ae0 100644 --- a/server/channels/app/file.go +++ b/server/channels/app/file.go @@ -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) } diff --git a/server/public/model/config.go b/server/public/model/config.go index 9fca265c98..ed4ff64a00 100644 --- a/server/public/model/config.go +++ b/server/public/model/config.go @@ -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) { diff --git a/server/public/model/feature_flags.go b/server/public/model/feature_flags.go index dfccfe43fd..c2f3ea4ce3 100644 --- a/server/public/model/feature_flags.go +++ b/server/public/model/feature_flags.go @@ -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 } diff --git a/webapp/channels/src/components/admin_console/admin_definition.tsx b/webapp/channels/src/components/admin_console/admin_definition.tsx index 494eadaea8..f013517ef0 100644 --- a/webapp/channels/src/components/admin_console/admin_definition.tsx +++ b/webapp/channels/src/components/admin_console/admin_definition.tsx @@ -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 IAM role. Enter the Access Key ID provided by your Amazon EC2 administrator.'}), + help_text_values: { + link: (msg: string) => ( + + {msg} + + ), + }, + 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 documentation to learn more.'}), + help_text_values: { + link: (msg: string) => ( + + {msg} + + ), + }, + 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'}), diff --git a/webapp/channels/src/i18n/en.json b/webapp/channels/src/i18n/en.json index 6ef8465de5..0ca044f585 100644 --- a/webapp/channels/src/i18n/en.json +++ b/webapp/channels/src/i18n/en.json @@ -1033,6 +1033,10 @@ "admin.experimental.userStatusAwayTimeout.desc": "This setting defines the number of seconds after which the user’s 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 Start trial, I agree to the Mattermost Software Evaluation Agreement, Privacy Policy and receiving product emails.", "admin.feature_discovery.trial-request.accept-terms.cloudFree": "By selecting Try free for {trialLength} days, I agree to the Mattermost Software Evaluation Agreement, Privacy Policy, 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 documentation 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 until a data migration is executed.", "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",