MM-68663: Admin console support and Test Connection generalization for Azure Blob Storage (#36583)

* Generalize the file storage Test Connection endpoint

Replaces the S3-only /api/v4/file/s3_test handler with a backend-agnostic
POST /api/v4/file/test that validates mandatory fields per driver and
runs a write/read/delete probe against the configured backend. The
legacy /file/s3_test route stays as a thin wrapper so existing clients
keep working.

The driver switch validates S3 and Azure mandatory fields explicitly,
treats Local as a no-op (no required credentials), and rejects unknown
or empty driver names with a 400 and a specific error code so admins
get a useful message instead of a generic backend failure.

Reuses config.Desanitize (renamed from the package-private desanitize)
so the FakeSetting placeholder swap for secrets is shared with the
PUT /api/v4/config save path. Adding a new driver-secret in the future
only requires touching config.Desanitize once. Desanitize is also made
nil-safe on every pointer dereference so callers can hand it a partial
config without first running SetDefaults().

Mattermost-redux and the webapp client gain a corresponding
TestFileStoreConnection method that the admin console action layer
calls instead of the deprecated S3-specific method.

------
AI assisted commit

* Wire Azure Blob Storage into the file storage admin console

Adds the Azure Blob Storage option to the File Storage panel in the
System Console. Selecting it enables Azure-specific fields for the
storage account name, container, optional path prefix, shared key,
optional endpoint override, secure-connections toggle, and request
timeout. The fields are hidden and disabled when the driver is set to
Local or S3, matching the existing pattern.

Help text and placeholders are added in the webapp i18n catalog so
admins see the same field labels documented in the admin guide.

The same set of fields is repeated for the Files Export panel when
DedicatedExportStore is enabled, keeping the export backend
configurable independently of the primary file store.

------
AI assisted commit

* Document /api/v4/file/test in the OpenAPI spec

Adds the new backend-agnostic file storage Test Connection endpoint to
the public OpenAPI surface. The request body is optional: callers that
omit it test the running server configuration, callers that include a
full AdminConfig test the supplied configuration without persisting
anything. The deprecated /api/v4/file/s3_test endpoint is left
unchanged in the spec for the existing S3-only flow.

------
AI assisted commit

* Add UI-only Cypress coverage for the Azure file storage panel

Adds a Cypress spec that drives the System Console File Storage panel,
switches the driver to Azure Blob Storage, fills in the Azure fields,
and asserts the expected fields appear (and S3 fields are hidden). The
spec is UI-only and does not depend on an Azure backend or Azurite, so
it can run in CI without external infrastructure.

Updates the existing environment_spec.js so it tolerates the new Azure
option in the driver dropdown.

------
AI assisted commit

* Nil-guard file storage mandatory-field checks

CheckMandatoryS3Fields and CheckMandatoryAzureFields built a
FileBackendSettings via NewFileBackendSettingsFromConfig before
validating, but that constructor dereferences pointers
unconditionally and would panic if a caller skipped the api
handler's reflective nil check. Validate the required pointers
directly against FileSettings instead, dropping the throwaway
constructor call so the methods are safe to call from any path.

------
AI assisted commit

* Check permission before validating file settings

The /file/test handler ran checkHasNilFields before
SessionHasPermissionTo, so an unauthorized caller posting a partial
config got a 400, leaking config shape, rather than a 403. Swap the two
blocks so the permission decision happens first.

------
AI assisted commit

* Preserve FakeSetting when desanitize has no actual

The Azure access key, export Azure access key, and S3 secret access key
branches in Desanitize reassigned target to actual without checking
actual for nil. When the running config had no value, the FakeSetting
placeholder in target was replaced with nil, dropping the field from the
round-trip. Guard the assignment so the placeholder stays in place when
actual is unset.

------
AI assisted commit

---------

Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
Alejandro García Montoro
2026-05-25 11:36:02 +00:00
committed by GitHub
co-authored by Mattermost Build
parent 4d8c25f040
commit c6b59cc9a7
16 changed files with 599 additions and 83 deletions
+44 -4
View File
@@ -550,22 +550,60 @@
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalServerError"
/api/v4/file/test:
post:
tags:
- system
summary: Test the configured file storage backend
description: >
Send a test to validate that the server can connect to the configured
file storage backend (Amazon S3 or Azure Blob Storage). Optionally
provide a configuration in the request body to test. If no valid
configuration is present in the request body the current server
configuration will be tested.
##### Permissions
Must have `manage_system` permission.
__Minimum server version__: 11.10
operationId: TestFileStoreConnection
requestBody:
description: Mattermost configuration
required: false
content:
application/json:
schema:
$ref: "#/components/schemas/Config"
responses:
"200":
description: File storage test successful
content:
application/json:
schema:
$ref: "#/components/schemas/StatusOK"
"400":
$ref: "#/components/responses/BadRequest"
"403":
$ref: "#/components/responses/Forbidden"
"500":
$ref: "#/components/responses/InternalServerError"
/api/v4/file/s3_test:
post:
tags:
- system
summary: Test AWS S3 connection
description: >
Send a test to validate if can connect to AWS S3. Optionally provide a
configuration in the request body to test. If no valid configuration is
present in the request body the current server configuration will be
tested.
Deprecated alias for `/api/v4/file/test` kept for backwards
compatibility. New callers should use `/api/v4/file/test`, which is
backend-agnostic.
##### Permissions
Must have `manage_system` permission.
__Minimum server version__: 4.8
deprecated: true
operationId: TestS3Connection
requestBody:
description: Mattermost configuration
@@ -581,6 +619,8 @@
application/json:
schema:
$ref: "#/components/schemas/StatusOK"
"400":
$ref: "#/components/responses/BadRequest"
"403":
$ref: "#/components/responses/Forbidden"
"500":
@@ -0,0 +1,70 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// ***************************************************************
// - [#] indicates a test step (e.g. # Go to a page)
// - [*] indicates an assertion (e.g. * Check the title)
// - Use element ID when selecting an element. Create one if none.
// ***************************************************************
// Stage: @prod
// Group: @channels @not_cloud @system_console
describe('Environment - File Storage (Azure Blob Storage)', () => {
before(() => {
cy.shouldNotRunOnCloudEdition();
cy.apiAdminLogin();
});
beforeEach(() => {
cy.visit('/admin_console/environment/file_storage');
cy.findByTestId('FileSettings.DriverNamedropdown').should('be.visible');
});
it('shows the Azure Blob Storage option in the File Storage System dropdown', () => {
// * Verify the Azure option is present alongside Local and S3
cy.findByTestId('FileSettings.DriverNamedropdown').
find('option[value="azureblob"]').
should('have.text', 'Azure Blob Storage');
});
it('enables Azure-only fields and disables S3-only fields when Azure is selected', () => {
// # Select the Azure driver
cy.findByTestId('FileSettings.DriverNamedropdown').select('azureblob');
// * Azure fields are enabled
cy.findByTestId('FileSettings.AzureStorageAccountinput').should('not.be.disabled');
cy.findByTestId('FileSettings.AzureContainerinput').should('not.be.disabled');
cy.findByTestId('FileSettings.AzurePathPrefixinput').should('not.be.disabled');
cy.findByTestId('FileSettings.AzureAccessKeyinput').should('not.be.disabled');
cy.findByTestId('FileSettings.AzureEndpointinput').should('not.be.disabled');
cy.findByTestId('FileSettings.AzureRequestTimeoutMillisecondsnumber').should('not.be.disabled');
// * S3 fields are disabled when the driver is not S3
cy.findByTestId('FileSettings.AmazonS3Bucketinput').should('be.disabled');
cy.findByTestId('FileSettings.AmazonS3AccessKeyIdinput').should('be.disabled');
// * Local directory is also disabled
cy.findByTestId('FileSettings.Directoryinput').should('be.disabled');
});
it('hides Azure-only fields when the S3 driver is selected', () => {
// # Select the S3 driver
cy.findByTestId('FileSettings.DriverNamedropdown').select('amazons3');
// * Azure fields are not rendered when the driver is not Azure
cy.findByTestId('FileSettings.AzureStorageAccountinput').should('not.exist');
cy.findByTestId('FileSettings.AzureContainerinput').should('not.exist');
cy.findByTestId('FileSettings.AzureAccessKeyinput').should('not.exist');
});
it('exposes the backend-agnostic Test Connection button when Azure is selected', () => {
// # Select the Azure driver
cy.findByTestId('FileSettings.DriverNamedropdown').select('azureblob');
// * The renamed button is rendered and is no longer S3-named
cy.get('#TestFileStoreConnection').scrollIntoView().should('be.visible');
cy.get('#TestFileStoreConnection').findByText('Test Connection').should('exist');
cy.get('#TestS3Connection').should('not.exist');
});
});
@@ -243,7 +243,7 @@ describe('Environment', () => {
// # Click Save button to save the settings
cy.get('#saveSetting').click().wait(TIMEOUTS.ONE_SEC);
cy.get('#TestS3Connection').scrollIntoView().should('be.visible').within(() => {
cy.get('#TestFileStoreConnection').scrollIntoView().should('be.visible').within(() => {
cy.findByText('Test Connection').should('be.visible').click().wait(TIMEOUTS.ONE_SEC);
waitForAlert('Connection unsuccessful: S3 Bucket is required');
});
@@ -254,7 +254,7 @@ describe('Environment', () => {
// # Click Save button to save the settings
cy.get('#saveSetting').click().wait(TIMEOUTS.ONE_SEC);
cy.get('#TestS3Connection').scrollIntoView().should('be.visible').within(() => {
cy.get('#TestFileStoreConnection').scrollIntoView().should('be.visible').within(() => {
cy.findByText('Test Connection').should('be.visible').click().wait(TIMEOUTS.ONE_SEC);
waitForAlert('Connection unsuccessful: Unable to authenticate against the file storage backend. Verify your credentials and authentication settings.');
});
+39 -14
View File
@@ -47,7 +47,9 @@ func (api *API) InitSystem() {
api.BaseRoutes.APIRoot.Handle("/notifications/test", api.APISessionRequired(testNotifications)).Methods(http.MethodPost)
api.BaseRoutes.APIRoot.Handle("/email/test", api.APISessionRequired(testEmail)).Methods(http.MethodPost)
api.BaseRoutes.APIRoot.Handle("/site_url/test", api.APISessionRequired(testSiteURL)).Methods(http.MethodPost)
api.BaseRoutes.APIRoot.Handle("/file/s3_test", api.APISessionRequired(testS3)).Methods(http.MethodPost)
api.BaseRoutes.APIRoot.Handle("/file/test", api.APISessionRequired(testFileStore)).Methods(http.MethodPost)
// Deprecated: use /file/test instead. Kept as a thin compatibility wrapper.
api.BaseRoutes.APIRoot.Handle("/file/s3_test", api.APISessionRequired(testFileStore)).Methods(http.MethodPost)
api.BaseRoutes.APIRoot.Handle("/database/recycle", api.APISessionRequired(databaseRecycle)).Methods(http.MethodPost)
api.BaseRoutes.APIRoot.Handle("/caches/invalidate", api.APISessionRequired(invalidateCaches)).Methods(http.MethodPost)
@@ -560,7 +562,7 @@ func getSupportedTimezones(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
func testS3(c *Context, w http.ResponseWriter, r *http.Request) {
func testFileStore(c *Context, w http.ResponseWriter, r *http.Request) {
var cfg *model.Config
err := json.NewDecoder(r.Body).Decode(&cfg)
if err != nil {
@@ -570,28 +572,51 @@ func testS3(c *Context, w http.ResponseWriter, r *http.Request) {
cfg = c.App.Config()
}
if checkHasNilFields(&cfg.FileSettings) {
c.Err = model.NewAppError("testS3", "api.file.test_connection_s3_settings_nil.app_error", nil, "", http.StatusBadRequest)
return
}
// PermissionTestS3 is kept for backwards compatibility -- it was named
// after the only supported test endpoint at the time it was introduced.
// The new /file/test endpoint is backend-agnostic but reuses the same
// permission to avoid a role migration.
if !c.App.SessionHasPermissionTo(*c.AppContext.Session(), model.PermissionTestS3) {
c.SetPermissionError(model.PermissionTestS3)
return
}
appErr := c.App.CheckMandatoryS3Fields(&cfg.FileSettings)
if appErr != nil {
c.Err = appErr
if checkHasNilFields(&cfg.FileSettings) {
c.Err = model.NewAppError("testFileStore", "api.file.test_connection_settings_nil.app_error", nil, "", http.StatusBadRequest)
return
}
if *cfg.FileSettings.AmazonS3SecretAccessKey == model.FakeSetting {
cfg.FileSettings.AmazonS3SecretAccessKey = c.App.Config().FileSettings.AmazonS3SecretAccessKey
// Validate mandatory fields per driver. TestFileStoreConnectionWithConfig
// will catch missing fields by failing to construct the backend, but a
// dedicated validation step lets us surface a clearer error.
driver := ""
if cfg.FileSettings.DriverName != nil {
driver = *cfg.FileSettings.DriverName
}
switch driver {
case model.ImageDriverLocal:
// Local driver has no mandatory fields beyond the directory, which has a default.
case model.ImageDriverS3:
if appErr := c.App.CheckMandatoryS3Fields(&cfg.FileSettings); appErr != nil {
c.Err = appErr
return
}
case model.ImageDriverAzure:
if appErr := c.App.CheckMandatoryAzureFields(&cfg.FileSettings); appErr != nil {
c.Err = appErr
return
}
default:
c.Err = model.NewAppError("testFileStore", "api.file.test_connection_unsupported_driver.app_error", map[string]any{"Driver": driver}, "", http.StatusBadRequest)
return
}
appErr = c.App.TestFileStoreConnectionWithConfig(&cfg.FileSettings)
if appErr != nil {
// A client editing the admin UI and clicking Test Connection without
// re-entering the secret would send the FakeSetting placeholder back, so we
// need to desanitize this first.
config.Desanitize(c.App.Config(), cfg)
if appErr := c.App.TestFileStoreConnectionWithConfig(&cfg.FileSettings); appErr != nil {
c.Err = appErr
return
}
+97 -1
View File
@@ -725,9 +725,105 @@ func TestS3TestConnection(t *testing.T) {
config.FileSettings = model.FileSettings{}
resp, err := th.SystemAdminClient.TestS3Connection(context.Background(), &config)
require.Error(t, err)
CheckErrorID(t, err, "api.file.test_connection_s3_settings_nil.app_error")
CheckErrorID(t, err, "api.file.test_connection_settings_nil.app_error")
CheckBadRequestStatus(t, resp)
})
t.Run("desanitizes FakeSetting using running config", func(t *testing.T) {
// Seed the running config with valid Minio credentials so the
// running config's AmazonS3SecretAccessKey is the real secret.
th.App.UpdateConfig(func(c *model.Config) {
c.FileSettings.DriverName = model.NewPointer(model.ImageDriverS3)
c.FileSettings.AmazonS3AccessKeyId = model.NewPointer(model.MinioAccessKey)
c.FileSettings.AmazonS3SecretAccessKey = model.NewPointer(model.MinioSecretKey)
c.FileSettings.AmazonS3Bucket = model.NewPointer(model.MinioBucket)
c.FileSettings.AmazonS3Endpoint = model.NewPointer(s3Endpoint)
c.FileSettings.AmazonS3Region = model.NewPointer("us-east-1")
c.FileSettings.AmazonS3PathPrefix = model.NewPointer("")
c.FileSettings.AmazonS3SSL = model.NewPointer(false)
})
// Build a request body that mirrors what the System Console sends
// after the admin clicks Test Connection without re-entering the
// secret: every field present, but the secret slot is the
// FakeSetting placeholder.
body := model.Config{FileSettings: model.FileSettings{}}
body.FileSettings.SetDefaults(false)
body.FileSettings.DriverName = model.NewPointer(model.ImageDriverS3)
body.FileSettings.AmazonS3AccessKeyId = model.NewPointer(model.MinioAccessKey)
body.FileSettings.AmazonS3SecretAccessKey = model.NewPointer(model.FakeSetting)
body.FileSettings.AmazonS3Bucket = model.NewPointer(model.MinioBucket)
body.FileSettings.AmazonS3Endpoint = model.NewPointer(s3Endpoint)
body.FileSettings.AmazonS3Region = model.NewPointer("us-east-1")
body.FileSettings.AmazonS3PathPrefix = model.NewPointer("")
body.FileSettings.AmazonS3SSL = model.NewPointer(false)
// If desanitize is not running, the server tests with the literal
// "********" string as the secret and Minio returns a 403 auth
// error. A 200 here proves the placeholder was swapped for the
// real running-config value before the connection test.
resp, err := th.SystemAdminClient.TestS3Connection(context.Background(), &body)
require.NoError(t, err)
CheckOKStatus(t, resp)
})
t.Run("unsupported driver", func(t *testing.T) {
unsupported := model.FileSettings{}
unsupported.SetDefaults(false)
unsupported.DriverName = model.NewPointer("bogus")
resp, err := th.SystemAdminClient.TestS3Connection(context.Background(), &model.Config{FileSettings: unsupported})
require.Error(t, err)
CheckErrorID(t, err, "api.file.test_connection_unsupported_driver.app_error")
CheckBadRequestStatus(t, resp)
})
t.Run("empty driver name", func(t *testing.T) {
empty := model.FileSettings{}
empty.SetDefaults(false)
empty.DriverName = model.NewPointer("")
resp, err := th.SystemAdminClient.TestS3Connection(context.Background(), &model.Config{FileSettings: empty})
require.Error(t, err)
CheckErrorID(t, err, "api.file.test_connection_unsupported_driver.app_error")
CheckBadRequestStatus(t, resp)
})
t.Run("azure missing mandatory fields", func(t *testing.T) {
// CheckMandatoryAzureFields rejects requests that don't carry an
// Azure storage account, container, and access key. Each missing
// field path must produce the same 400 with the dedicated error
// ID so admins get a clear signal in the System Console toast.
base := model.FileSettings{}
base.SetDefaults(false)
base.DriverName = model.NewPointer(model.ImageDriverAzure)
cases := []struct {
name string
mut func(*model.FileSettings)
}{
{"missing storage account", func(fs *model.FileSettings) {
fs.AzureContainer = model.NewPointer("mattermost")
fs.AzureAccessKey = model.NewPointer("secret")
}},
{"missing container", func(fs *model.FileSettings) {
fs.AzureStorageAccount = model.NewPointer("acmemattermost")
fs.AzureAccessKey = model.NewPointer("secret")
}},
{"missing access key", func(fs *model.FileSettings) {
fs.AzureStorageAccount = model.NewPointer("acmemattermost")
fs.AzureContainer = model.NewPointer("mattermost")
}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
fs := base
tc.mut(&fs)
resp, err := th.SystemAdminClient.TestS3Connection(context.Background(), &model.Config{FileSettings: fs})
require.Error(t, err)
CheckErrorID(t, err, "api.admin.test_azure.missing_azure_field")
CheckBadRequestStatus(t, resp)
})
}
})
}
func TestSupportedTimezones(t *testing.T) {
+24 -7
View File
@@ -60,16 +60,33 @@ func (a *App) ExportFileBackend() filestore.FileBackend {
}
func (a *App) CheckMandatoryS3Fields(settings *model.FileSettings) *model.AppError {
var fileBackendSettings filestore.FileBackendSettings
bucket := settings.AmazonS3Bucket
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)
bucket = settings.ExportAmazonS3Bucket
}
if bucket == nil || *bucket == "" {
return model.NewAppError("CheckMandatoryS3Fields", "api.admin.test_s3.missing_s3_bucket", nil, "", http.StatusBadRequest)
}
return nil
}
err := fileBackendSettings.CheckMandatoryS3Fields()
if err != nil {
return model.NewAppError("CheckMandatoryS3Fields", "api.admin.test_s3.missing_s3_bucket", nil, "", http.StatusBadRequest).Wrap(err)
func (a *App) CheckMandatoryAzureFields(settings *model.FileSettings) *model.AppError {
storageAccount := settings.AzureStorageAccount
accessKey := settings.AzureAccessKey
container := settings.AzureContainer
if a.License().IsCloud() && a.Config().FeatureFlags.CloudDedicatedExportUI && a.Config().FileSettings.DedicatedExportStore != nil && *a.Config().FileSettings.DedicatedExportStore {
storageAccount = settings.ExportAzureStorageAccount
accessKey = settings.ExportAzureAccessKey
container = settings.ExportAzureContainer
}
if storageAccount == nil || *storageAccount == "" {
return model.NewAppError("CheckMandatoryAzureFields", "api.admin.test_azure.missing_azure_field", nil, "missing azure storage account setting", http.StatusBadRequest)
}
if container == nil || *container == "" {
return model.NewAppError("CheckMandatoryAzureFields", "api.admin.test_azure.missing_azure_field", nil, "missing azure container setting", http.StatusBadRequest)
}
if accessKey == nil || *accessKey == "" {
return model.NewAppError("CheckMandatoryAzureFields", "api.admin.test_azure.missing_azure_field", nil, "missing azure access key setting", http.StatusBadRequest)
}
return nil
}
+1 -1
View File
@@ -182,7 +182,7 @@ func (s *Store) Set(newCfg *model.Config) (*model.Config, *model.Config, error)
// Sometimes the config is received with "fake" data in sensitive fields. Apply the real
// data from the existing config as necessary.
desanitize(oldCfg, newCfg)
Desanitize(oldCfg, newCfg)
// We apply back environment overrides since the input config may or
// may not have them applied.
+21 -15
View File
@@ -21,33 +21,35 @@ func marshalConfig(cfg *model.Config) ([]byte, error) {
return json.MarshalIndent(cfg, "", " ")
}
// desanitize replaces fake settings with their actual values.
func desanitize(actual, target *model.Config) {
if target.LdapSettings.BindPassword != nil && *target.LdapSettings.BindPassword == model.FakeSetting {
// Desanitize replaces fake settings with their actual values. Safe to call on
// partial configs: every pointer dereference is nil-guarded so callers do not
// need to run SetDefaults() first.
func Desanitize(actual, target *model.Config) {
if target.LdapSettings.BindPassword != nil && *target.LdapSettings.BindPassword == model.FakeSetting && actual.LdapSettings.BindPassword != nil {
*target.LdapSettings.BindPassword = *actual.LdapSettings.BindPassword
}
if *target.FileSettings.PublicLinkSalt == model.FakeSetting {
if target.FileSettings.PublicLinkSalt != nil && *target.FileSettings.PublicLinkSalt == model.FakeSetting && actual.FileSettings.PublicLinkSalt != nil {
*target.FileSettings.PublicLinkSalt = *actual.FileSettings.PublicLinkSalt
}
if *target.FileSettings.AmazonS3SecretAccessKey == model.FakeSetting {
if target.FileSettings.AmazonS3SecretAccessKey != nil && *target.FileSettings.AmazonS3SecretAccessKey == model.FakeSetting && actual.FileSettings.AmazonS3SecretAccessKey != nil {
target.FileSettings.AmazonS3SecretAccessKey = actual.FileSettings.AmazonS3SecretAccessKey
}
if target.FileSettings.ExportAmazonS3SecretAccessKey != nil && *target.FileSettings.ExportAmazonS3SecretAccessKey == model.FakeSetting {
if target.FileSettings.ExportAmazonS3SecretAccessKey != nil && *target.FileSettings.ExportAmazonS3SecretAccessKey == model.FakeSetting && actual.FileSettings.ExportAmazonS3SecretAccessKey != nil {
target.FileSettings.ExportAmazonS3SecretAccessKey = actual.FileSettings.ExportAmazonS3SecretAccessKey
}
if target.FileSettings.AzureAccessKey != nil && *target.FileSettings.AzureAccessKey == model.FakeSetting {
if target.FileSettings.AzureAccessKey != nil && *target.FileSettings.AzureAccessKey == model.FakeSetting && actual.FileSettings.AzureAccessKey != nil {
target.FileSettings.AzureAccessKey = actual.FileSettings.AzureAccessKey
}
if target.FileSettings.ExportAzureAccessKey != nil && *target.FileSettings.ExportAzureAccessKey == model.FakeSetting {
if target.FileSettings.ExportAzureAccessKey != nil && *target.FileSettings.ExportAzureAccessKey == model.FakeSetting && actual.FileSettings.ExportAzureAccessKey != nil {
target.FileSettings.ExportAzureAccessKey = actual.FileSettings.ExportAzureAccessKey
}
if *target.EmailSettings.SMTPPassword == model.FakeSetting {
if target.EmailSettings.SMTPPassword != nil && *target.EmailSettings.SMTPPassword == model.FakeSetting {
target.EmailSettings.SMTPPassword = actual.EmailSettings.SMTPPassword
}
if *target.GitLabSettings.Secret == model.FakeSetting {
if target.GitLabSettings.Secret != nil && *target.GitLabSettings.Secret == model.FakeSetting {
target.GitLabSettings.Secret = actual.GitLabSettings.Secret
}
@@ -63,14 +65,14 @@ func desanitize(actual, target *model.Config) {
target.OpenIdSettings.Secret = actual.OpenIdSettings.Secret
}
if *target.SqlSettings.DataSource == model.FakeSetting {
if target.SqlSettings.DataSource != nil && *target.SqlSettings.DataSource == model.FakeSetting && actual.SqlSettings.DataSource != nil {
*target.SqlSettings.DataSource = *actual.SqlSettings.DataSource
}
if *target.SqlSettings.AtRestEncryptKey == model.FakeSetting {
if target.SqlSettings.AtRestEncryptKey != nil && *target.SqlSettings.AtRestEncryptKey == model.FakeSetting {
target.SqlSettings.AtRestEncryptKey = actual.SqlSettings.AtRestEncryptKey
}
if *target.ElasticsearchSettings.Password == model.FakeSetting {
if target.ElasticsearchSettings.Password != nil && *target.ElasticsearchSettings.Password == model.FakeSetting && actual.ElasticsearchSettings.Password != nil {
*target.ElasticsearchSettings.Password = *actual.ElasticsearchSettings.Password
}
@@ -90,11 +92,15 @@ func desanitize(actual, target *model.Config) {
}
}
if *target.MessageExportSettings.GlobalRelaySettings.SMTPPassword == model.FakeSetting {
if target.MessageExportSettings.GlobalRelaySettings != nil &&
target.MessageExportSettings.GlobalRelaySettings.SMTPPassword != nil &&
*target.MessageExportSettings.GlobalRelaySettings.SMTPPassword == model.FakeSetting &&
actual.MessageExportSettings.GlobalRelaySettings != nil &&
actual.MessageExportSettings.GlobalRelaySettings.SMTPPassword != nil {
*target.MessageExportSettings.GlobalRelaySettings.SMTPPassword = *actual.MessageExportSettings.GlobalRelaySettings.SMTPPassword
}
if *target.ServiceSettings.SplitKey == model.FakeSetting {
if target.ServiceSettings.SplitKey != nil && *target.ServiceSettings.SplitKey == model.FakeSetting && actual.ServiceSettings.SplitKey != nil {
*target.ServiceSettings.SplitKey = *actual.ServiceSettings.SplitKey
}
+8 -2
View File
@@ -28,6 +28,8 @@ func TestDesanitize(t *testing.T) {
actual.FileSettings.PublicLinkSalt = new("public_link_salt")
actual.FileSettings.AmazonS3SecretAccessKey = new("amazon_s3_secret_access_key")
actual.FileSettings.ExportAmazonS3SecretAccessKey = new("export_amazon_s3_secret_access_key")
actual.FileSettings.AzureAccessKey = new("azure_access_key")
actual.FileSettings.ExportAzureAccessKey = new("export_azure_access_key")
actual.EmailSettings.SMTPPassword = new("smtp_password")
actual.GitLabSettings.Secret = new("secret")
actual.OpenIdSettings.Secret = new("secret")
@@ -59,6 +61,8 @@ func TestDesanitize(t *testing.T) {
target.FileSettings.PublicLinkSalt = model.NewPointer(model.FakeSetting)
target.FileSettings.AmazonS3SecretAccessKey = model.NewPointer(model.FakeSetting)
target.FileSettings.ExportAmazonS3SecretAccessKey = model.NewPointer(model.FakeSetting)
target.FileSettings.AzureAccessKey = model.NewPointer(model.FakeSetting)
target.FileSettings.ExportAzureAccessKey = model.NewPointer(model.FakeSetting)
target.EmailSettings.SMTPPassword = model.NewPointer(model.FakeSetting)
target.GitLabSettings.Secret = model.NewPointer(model.FakeSetting)
target.OpenIdSettings.Secret = model.NewPointer(model.FakeSetting)
@@ -77,7 +81,7 @@ func TestDesanitize(t *testing.T) {
}
actualClone := actual.Clone()
desanitize(actual, target)
Desanitize(actual, target)
assert.Equal(t, actualClone, actual, "actual should not have been changed")
// Verify the settings that should have been left untouched in target
@@ -89,6 +93,8 @@ func TestDesanitize(t *testing.T) {
assert.Equal(t, *actual.FileSettings.PublicLinkSalt, *target.FileSettings.PublicLinkSalt)
assert.Equal(t, *actual.FileSettings.AmazonS3SecretAccessKey, *target.FileSettings.AmazonS3SecretAccessKey)
assert.Equal(t, *actual.FileSettings.ExportAmazonS3SecretAccessKey, *target.FileSettings.ExportAmazonS3SecretAccessKey)
assert.Equal(t, *actual.FileSettings.AzureAccessKey, *target.FileSettings.AzureAccessKey)
assert.Equal(t, *actual.FileSettings.ExportAzureAccessKey, *target.FileSettings.ExportAzureAccessKey)
assert.Equal(t, *actual.EmailSettings.SMTPPassword, *target.EmailSettings.SMTPPassword)
assert.Equal(t, *actual.GitLabSettings.Secret, *target.GitLabSettings.Secret)
assert.Equal(t, *actual.OpenIdSettings.Secret, *target.OpenIdSettings.Secret)
@@ -116,7 +122,7 @@ func TestDesanitizeRemovesAllFakeSettings(t *testing.T) {
sanitized := actual.Clone()
sanitized.Sanitize(nil, nil)
desanitize(actual, sanitized)
Desanitize(actual, sanitized)
assertNoFakeSettings(t, reflect.ValueOf(*sanitized), "Config")
}
+10 -2
View File
@@ -187,6 +187,10 @@
"id": "api.admin.syncables_error",
"translation": "failed to add user to group-teams and group-channels"
},
{
"id": "api.admin.test_azure.missing_azure_field",
"translation": "An Azure Blob Storage setting is missing or invalid."
},
{
"id": "api.admin.test_email.body",
"translation": "It appears your Mattermost email is setup correctly!"
@@ -2445,8 +2449,12 @@
"translation": "The configured bucket or container does not exist. Verify your file storage configuration and permissions."
},
{
"id": "api.file.test_connection_s3_settings_nil.app_error",
"translation": "File storage settings has unset values."
"id": "api.file.test_connection_settings_nil.app_error",
"translation": "File storage settings have unset values."
},
{
"id": "api.file.test_connection_unsupported_driver.app_error",
"translation": "Unsupported file storage driver \"{{.Driver}}\". Supported values are \"local\", \"amazons3\", and \"azureblob\"."
},
{
"id": "api.file.upload_file.abac_denied.app_error",
+20
View File
@@ -385,6 +385,10 @@ func (c *Client4) testS3Route() clientRoute {
return newClientRoute("file").Join("s3_test")
}
func (c *Client4) testFileStoreRoute() clientRoute {
return newClientRoute("file").Join("test")
}
func (c *Client4) databaseRoute() clientRoute {
return newClientRoute("database")
}
@@ -4405,6 +4409,10 @@ func (c *Client4) TestSiteURL(ctx context.Context, siteURL string) (*Response, e
}
// TestS3Connection will attempt to connect to the AWS S3.
//
// Deprecated: use TestFileStoreConnection instead. The underlying endpoint
// is kept for backwards compatibility but now routes through the same
// backend-agnostic handler as TestFileStoreConnection.
func (c *Client4) TestS3Connection(ctx context.Context, config *Config) (*Response, error) {
r, err := c.doAPIPostJSON(ctx, c.testS3Route(), config)
if err != nil {
@@ -4414,6 +4422,18 @@ func (c *Client4) TestS3Connection(ctx context.Context, config *Config) (*Respon
return BuildResponse(r), nil
}
// TestFileStoreConnection attempts to connect to the configured file storage
// backend (Amazon S3, Azure Blob Storage, or local), based on the FileSettings
// in the supplied config.
func (c *Client4) TestFileStoreConnection(ctx context.Context, config *Config) (*Response, error) {
r, err := c.doAPIPostJSON(ctx, c.testFileStoreRoute(), config)
if err != nil {
return BuildResponse(r), err
}
defer closeBody(r)
return BuildResponse(r), nil
}
// GetConfig will retrieve the server config with some sanitized items.
func (c *Client4) GetConfig(ctx context.Context) (*Config, *Response, error) {
r, err := c.doAPIGet(ctx, c.configRoute(), "")
@@ -366,8 +366,8 @@ export async function elasticsearchTest(config, success, error) {
}
}
export async function testS3Connection(success, error) {
const {data, error: err} = await dispatch(AdminActions.testS3Connection());
export async function testFileStoreConnection(success, error) {
const {data, error: err} = await dispatch(AdminActions.testFileStoreConnection());
if (data && success) {
success(data);
} else if (err && error) {
@@ -22,7 +22,7 @@ import {
removePrivateSamlCertificate,
removePublicSamlCertificate,
setSamlIdpCertificateFromMetadata,
testS3Connection,
testFileStoreConnection,
testSiteURL,
testSmtp,
uploadIdpSamlCertificate,
@@ -137,6 +137,7 @@ export {it};
const FILE_STORAGE_DRIVER_LOCAL = 'local';
const FILE_STORAGE_DRIVER_S3 = 'amazons3';
const FILE_STORAGE_DRIVER_AZURE = 'azureblob';
const MEBIBYTE = Math.pow(1024, 2);
const SAML_SETTINGS_SIGNATURE_ALGORITHM_SHA1 = 'RSAwithSHA1';
@@ -1129,7 +1130,7 @@ const AdminDefinition: AdminDefinitionType = {
type: 'dropdown',
key: 'FileSettings.DriverName',
label: defineMessage({id: 'admin.image.storeTitle', defaultMessage: 'File Storage System:'}),
help_text: defineMessage({id: 'admin.image.storeDescription', defaultMessage: 'Storage system where files and image attachments are saved.\n \nSelecting "Amazon S3" enables fields to enter your Amazon credentials and bucket details.\n \nSelecting "Local File System" enables the field to specify a local file directory.'}), // eslint-disable-line formatjs/no-multiple-whitespaces
help_text: defineMessage({id: 'admin.image.storeDescription', defaultMessage: 'Storage system where files and image attachments are saved.\n \nSelecting "Amazon S3" enables fields to enter your Amazon credentials and bucket details.\n \nSelecting "Azure Blob Storage" enables fields to enter your Azure Storage account credentials and container details.\n \nSelecting "Local File System" enables the field to specify a local file directory.'}), // eslint-disable-line formatjs/no-multiple-whitespaces
help_text_markdown: true,
options: [
{
@@ -1140,6 +1141,10 @@ const AdminDefinition: AdminDefinitionType = {
value: FILE_STORAGE_DRIVER_S3,
display_name: defineMessage({id: 'admin.image.storeAmazonS3', defaultMessage: 'Amazon S3'}),
},
{
value: FILE_STORAGE_DRIVER_AZURE,
display_name: defineMessage({id: 'admin.image.storeAzureBlob', defaultMessage: 'Azure Blob Storage'}),
},
],
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
},
@@ -1324,15 +1329,101 @@ const AdminDefinition: AdminDefinitionType = {
it.not(it.stateEquals('FileSettings.DriverName', FILE_STORAGE_DRIVER_S3)),
),
},
{
type: 'text',
key: 'FileSettings.AzureStorageAccount',
label: defineMessage({id: 'admin.image.azureStorageAccountTitle', defaultMessage: 'Azure Storage Account:'}),
help_text: defineMessage({id: 'admin.image.azureStorageAccountDescription', defaultMessage: 'The name of your Azure Storage account.'}),
placeholder: defineMessage({id: 'admin.image.azureStorageAccountExample', defaultMessage: 'E.g.: "mattermoststorage"'}),
isDisabled: it.any(
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
it.not(it.stateEquals('FileSettings.DriverName', FILE_STORAGE_DRIVER_AZURE)),
),
isHidden: it.not(it.stateEquals('FileSettings.DriverName', FILE_STORAGE_DRIVER_AZURE)),
},
{
type: 'text',
key: 'FileSettings.AzureContainer',
label: defineMessage({id: 'admin.image.azureContainerTitle', defaultMessage: 'Azure Container:'}),
help_text: defineMessage({id: 'admin.image.azureContainerDescription', defaultMessage: 'Name of the container in your Azure Storage account.'}),
placeholder: defineMessage({id: 'admin.image.azureContainerExample', defaultMessage: 'E.g.: "mattermost-media"'}),
isDisabled: it.any(
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
it.not(it.stateEquals('FileSettings.DriverName', FILE_STORAGE_DRIVER_AZURE)),
),
isHidden: it.not(it.stateEquals('FileSettings.DriverName', FILE_STORAGE_DRIVER_AZURE)),
},
{
type: 'text',
key: 'FileSettings.AzurePathPrefix',
label: defineMessage({id: 'admin.image.azurePathPrefixTitle', defaultMessage: 'Azure Path Prefix:'}),
help_text: defineMessage({id: 'admin.image.azurePathPrefixDescription', defaultMessage: 'Optional path prefix to use for blobs in your Azure container.'}),
placeholder: defineMessage({id: 'admin.image.azurePathPrefixExample', defaultMessage: 'E.g.: "files/" or leave empty'}),
isDisabled: it.any(
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
it.not(it.stateEquals('FileSettings.DriverName', FILE_STORAGE_DRIVER_AZURE)),
),
isHidden: it.not(it.stateEquals('FileSettings.DriverName', FILE_STORAGE_DRIVER_AZURE)),
},
{
type: 'text',
key: 'FileSettings.AzureAccessKey',
label: defineMessage({id: 'admin.image.azureAccessKeyTitle', defaultMessage: 'Azure Storage Account Key:'}),
help_text: defineMessage({id: 'admin.image.azureAccessKeyDescription', defaultMessage: 'The shared key for your Azure Storage account.'}),
placeholder: defineMessage({id: 'admin.image.azureAccessKeyExample', defaultMessage: 'E.g.: "9MZbtYgfq18PJ8PbRaJ5u91IH8izHvReTbcuQzMl+So="'}),
isDisabled: it.any(
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
it.not(it.stateEquals('FileSettings.DriverName', FILE_STORAGE_DRIVER_AZURE)),
),
isHidden: it.not(it.stateEquals('FileSettings.DriverName', FILE_STORAGE_DRIVER_AZURE)),
},
{
type: 'text',
key: 'FileSettings.AzureEndpoint',
label: defineMessage({id: 'admin.image.azureEndpointTitle', defaultMessage: 'Azure Endpoint:'}),
help_text: defineMessage({id: 'admin.image.azureEndpointDescription', defaultMessage: 'Optional host[:port] override for non-default endpoints such as Azurite, Azure Government, or other sovereign clouds. Leave empty to use the default "\'{account}\'.blob.core.windows.net" host.'}),
placeholder: defineMessage({id: 'admin.image.azureEndpointExample', defaultMessage: 'E.g.: "azurite:10000" or leave empty'}),
isDisabled: it.any(
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
it.not(it.stateEquals('FileSettings.DriverName', FILE_STORAGE_DRIVER_AZURE)),
),
isHidden: it.not(it.stateEquals('FileSettings.DriverName', FILE_STORAGE_DRIVER_AZURE)),
},
{
type: 'bool',
key: 'FileSettings.AzureSSL',
label: defineMessage({id: 'admin.image.azureSSLTitle', defaultMessage: 'Enable Secure Azure Blob Storage Connections:'}),
help_text: defineMessage({id: 'admin.image.azureSSLDescription', defaultMessage: 'When false, allow insecure connections to Azure Blob Storage. Defaults to secure connections only.'}),
isDisabled: it.any(
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
it.not(it.stateEquals('FileSettings.DriverName', FILE_STORAGE_DRIVER_AZURE)),
),
isHidden: it.not(it.stateEquals('FileSettings.DriverName', FILE_STORAGE_DRIVER_AZURE)),
},
{
type: 'number',
key: 'FileSettings.AzureRequestTimeoutMilliseconds',
label: defineMessage({id: 'admin.image.azureRequestTimeoutTitle', defaultMessage: 'Azure Request Timeout (Milliseconds):'}),
help_text: defineMessage({id: 'admin.image.azureRequestTimeoutDescription', defaultMessage: 'Number of milliseconds to wait for a response from Azure Blob Storage before timing out.'}),
placeholder: defineMessage({id: 'admin.image.azureRequestTimeoutExample', defaultMessage: 'E.g.: "30000"'}),
isDisabled: it.any(
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
it.not(it.stateEquals('FileSettings.DriverName', FILE_STORAGE_DRIVER_AZURE)),
),
isHidden: it.not(it.stateEquals('FileSettings.DriverName', FILE_STORAGE_DRIVER_AZURE)),
},
{
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}'}), // eslint-disable-line formatjs/enforce-placeholders -- error provided at runtime
success_message: defineMessage({id: 'admin.s3.s3Success', defaultMessage: 'Connection was successful'}),
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
action: testFileStoreConnection,
key: 'TestFileStoreConnection',
label: defineMessage({id: 'admin.filestore.connectionTest', defaultMessage: 'Test Connection'}),
loading: defineMessage({id: 'admin.filestore.testing', defaultMessage: 'Testing...'}),
error_message: defineMessage({id: 'admin.filestore.testFail', defaultMessage: 'Connection unsuccessful: {error}'}), // eslint-disable-line formatjs/enforce-placeholders -- error provided at runtime
success_message: defineMessage({id: 'admin.filestore.testSuccess', defaultMessage: 'Connection was successful'}),
isDisabled: it.any(
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
it.stateEquals('FileSettings.DriverName', FILE_STORAGE_DRIVER_LOCAL),
),
},
],
},
@@ -1360,13 +1451,17 @@ const AdminDefinition: AdminDefinitionType = {
type: 'dropdown',
key: 'FileSettings.ExportDriverName',
label: defineMessage({id: 'admin.exportStorage.exportDriverName', defaultMessage: 'Export Storage Driver:'}),
isDisabled: true,
isDisabled: it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
isHidden: it.stateEquals('FileSettings.DedicatedExportStore', false),
options: [
{
value: FILE_STORAGE_DRIVER_S3,
display_name: defineMessage({id: 'admin.image.storeAmazonS3', defaultMessage: 'Amazon S3'}),
},
{
value: FILE_STORAGE_DRIVER_AZURE,
display_name: defineMessage({id: 'admin.image.storeAzureBlob', defaultMessage: 'Azure Blob Storage'}),
},
],
},
{
@@ -1379,7 +1474,7 @@ const AdminDefinition: AdminDefinitionType = {
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)),
isHidden: it.stateEquals('FileSettings.DedicatedExportStore', false),
},
{
type: 'text',
@@ -1402,7 +1497,7 @@ const AdminDefinition: AdminDefinitionType = {
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)),
isHidden: it.any(it.not(it.stateEquals('FileSettings.ExportDriverName', FILE_STORAGE_DRIVER_S3)), it.stateEquals('FileSettings.DedicatedExportStore', false)),
},
{
type: 'text',
@@ -1414,7 +1509,7 @@ const AdminDefinition: AdminDefinitionType = {
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)),
isHidden: it.any(it.not(it.stateEquals('FileSettings.ExportDriverName', FILE_STORAGE_DRIVER_S3)), it.stateEquals('FileSettings.DedicatedExportStore', false)),
},
{
type: 'text',
@@ -1426,7 +1521,7 @@ const AdminDefinition: AdminDefinitionType = {
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)),
isHidden: it.any(it.not(it.stateEquals('FileSettings.ExportDriverName', FILE_STORAGE_DRIVER_S3)), it.stateEquals('FileSettings.DedicatedExportStore', false)),
},
{
type: 'text',
@@ -1438,7 +1533,7 @@ const AdminDefinition: AdminDefinitionType = {
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)),
isHidden: it.any(it.not(it.stateEquals('FileSettings.ExportDriverName', FILE_STORAGE_DRIVER_S3)), it.stateEquals('FileSettings.DedicatedExportStore', false)),
},
{
type: 'text',
@@ -1450,7 +1545,7 @@ const AdminDefinition: AdminDefinitionType = {
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)),
isHidden: it.any(it.not(it.stateEquals('FileSettings.ExportDriverName', FILE_STORAGE_DRIVER_S3)), it.stateEquals('FileSettings.DedicatedExportStore', false)),
},
{
type: 'text',
@@ -1462,7 +1557,7 @@ const AdminDefinition: AdminDefinitionType = {
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)),
isHidden: it.any(it.not(it.stateEquals('FileSettings.ExportDriverName', FILE_STORAGE_DRIVER_S3)), it.stateEquals('FileSettings.DedicatedExportStore', false)),
},
{
type: 'bool',
@@ -1473,7 +1568,7 @@ const AdminDefinition: AdminDefinitionType = {
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)),
isHidden: it.any(it.not(it.stateEquals('FileSettings.ExportDriverName', FILE_STORAGE_DRIVER_S3)), it.stateEquals('FileSettings.DedicatedExportStore', false)),
},
{
type: 'bool',
@@ -1484,7 +1579,7 @@ const AdminDefinition: AdminDefinitionType = {
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)),
isHidden: it.any(it.not(it.stateEquals('FileSettings.ExportDriverName', FILE_STORAGE_DRIVER_S3)), it.stateEquals('FileSettings.DedicatedExportStore', false)),
},
{
type: 'bool',
@@ -1502,7 +1597,7 @@ const AdminDefinition: AdminDefinitionType = {
),
},
help_text_markdown: false,
isHidden: it.any(it.stateEquals('FileSettings.ExportDriverName', 'NONE'), it.stateEquals('FileSettings.DedicatedExportStore', false)),
isHidden: it.any(it.not(it.stateEquals('FileSettings.ExportDriverName', FILE_STORAGE_DRIVER_S3)), it.stateEquals('FileSettings.DedicatedExportStore', false)),
isDisabled: it.any(
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
it.stateEquals('FileSettings.DedicatedExportStore', false),
@@ -1516,19 +1611,103 @@ const AdminDefinition: AdminDefinitionType = {
placeholder: defineMessage({id: 'admin.image.amazonS3StorageClassExample', defaultMessage: 'E.g.: "STANDARD" or "STANDARD_IA"'}),
isDisabled: it.any(
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
it.not(it.stateEquals('FileSettings.DriverName', FILE_STORAGE_DRIVER_S3)),
it.stateEquals('FileSettings.DedicatedExportStore', false),
),
isHidden: it.any(it.not(it.stateEquals('FileSettings.ExportDriverName', FILE_STORAGE_DRIVER_S3)), it.stateEquals('FileSettings.DedicatedExportStore', false)),
},
{
type: 'text',
key: 'FileSettings.ExportAzureStorageAccount',
label: defineMessage({id: 'admin.image.azureStorageAccountTitle', defaultMessage: 'Azure Storage Account:'}),
help_text: defineMessage({id: 'admin.image.azureStorageAccountDescription', defaultMessage: 'The name of your Azure Storage account.'}),
placeholder: defineMessage({id: 'admin.image.azureStorageAccountExample', defaultMessage: 'E.g.: "mattermoststorage"'}),
isDisabled: it.any(
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
it.stateEquals('FileSettings.DedicatedExportStore', false),
),
isHidden: it.any(it.not(it.stateEquals('FileSettings.ExportDriverName', FILE_STORAGE_DRIVER_AZURE)), it.stateEquals('FileSettings.DedicatedExportStore', false)),
},
{
type: 'text',
key: 'FileSettings.ExportAzureContainer',
label: defineMessage({id: 'admin.image.azureContainerTitle', defaultMessage: 'Azure Container:'}),
help_text: defineMessage({id: 'admin.image.azureContainerExportDescription', defaultMessage: 'Name of the container in your Azure Storage account.'}),
placeholder: defineMessage({id: 'admin.image.azureContainerExportExample', 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.not(it.stateEquals('FileSettings.ExportDriverName', FILE_STORAGE_DRIVER_AZURE)), it.stateEquals('FileSettings.DedicatedExportStore', false)),
},
{
type: 'text',
key: 'FileSettings.ExportAzurePathPrefix',
label: defineMessage({id: 'admin.image.azurePathPrefixTitle', defaultMessage: 'Azure Path Prefix:'}),
help_text: defineMessage({id: 'admin.image.azurePathPrefixDescription', defaultMessage: 'Optional path prefix to use for blobs in your Azure container.'}),
placeholder: defineMessage({id: 'admin.image.azurePathPrefixExample', defaultMessage: 'E.g.: "files/" or leave empty'}),
isDisabled: it.any(
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
it.stateEquals('FileSettings.DedicatedExportStore', false),
),
isHidden: it.any(it.not(it.stateEquals('FileSettings.ExportDriverName', FILE_STORAGE_DRIVER_AZURE)), it.stateEquals('FileSettings.DedicatedExportStore', false)),
},
{
type: 'text',
key: 'FileSettings.ExportAzureAccessKey',
label: defineMessage({id: 'admin.image.azureAccessKeyTitle', defaultMessage: 'Azure Storage Account Key:'}),
help_text: defineMessage({id: 'admin.image.azureAccessKeyDescription', defaultMessage: 'The shared key for your Azure Storage account.'}),
placeholder: defineMessage({id: 'admin.image.azureAccessKeyExample', defaultMessage: 'E.g.: "9MZbtYgfq18PJ8PbRaJ5u91IH8izHvReTbcuQzMl+So="'}),
isDisabled: it.any(
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
it.stateEquals('FileSettings.DedicatedExportStore', false),
),
isHidden: it.any(it.not(it.stateEquals('FileSettings.ExportDriverName', FILE_STORAGE_DRIVER_AZURE)), it.stateEquals('FileSettings.DedicatedExportStore', false)),
},
{
type: 'text',
key: 'FileSettings.ExportAzureEndpoint',
label: defineMessage({id: 'admin.image.azureEndpointTitle', defaultMessage: 'Azure Endpoint:'}),
help_text: defineMessage({id: 'admin.image.azureEndpointDescription', defaultMessage: 'Optional host[:port] override for non-default endpoints such as Azurite, Azure Government, or other sovereign clouds. Leave empty to use the default "\'{account}\'.blob.core.windows.net" host.'}),
placeholder: defineMessage({id: 'admin.image.azureEndpointExample', defaultMessage: 'E.g.: "azurite:10000" or leave empty'}),
isDisabled: it.any(
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
it.stateEquals('FileSettings.DedicatedExportStore', false),
),
isHidden: it.any(it.not(it.stateEquals('FileSettings.ExportDriverName', FILE_STORAGE_DRIVER_AZURE)), it.stateEquals('FileSettings.DedicatedExportStore', false)),
},
{
type: 'bool',
key: 'FileSettings.ExportAzureSSL',
label: defineMessage({id: 'admin.image.azureSSLTitle', defaultMessage: 'Enable Secure Azure Blob Storage Connections:'}),
help_text: defineMessage({id: 'admin.image.azureSSLDescription', defaultMessage: 'When false, allow insecure connections to Azure Blob Storage. 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.not(it.stateEquals('FileSettings.ExportDriverName', FILE_STORAGE_DRIVER_AZURE)), it.stateEquals('FileSettings.DedicatedExportStore', false)),
},
{
type: 'number',
key: 'FileSettings.ExportAzureRequestTimeoutMilliseconds',
label: defineMessage({id: 'admin.image.azureRequestTimeoutTitle', defaultMessage: 'Azure Request Timeout (Milliseconds):'}),
help_text: defineMessage({id: 'admin.image.azureRequestTimeoutDescription', defaultMessage: 'Number of milliseconds to wait for a response from Azure Blob Storage before timing out.'}),
placeholder: defineMessage({id: 'admin.image.azureRequestTimeoutExample', defaultMessage: 'E.g.: "30000"'}),
isDisabled: it.any(
it.not(it.userHasWritePermissionOnResource(RESOURCE_KEYS.ENVIRONMENT.FILE_STORAGE)),
it.stateEquals('FileSettings.DedicatedExportStore', false),
),
isHidden: it.any(it.not(it.stateEquals('FileSettings.ExportDriverName', FILE_STORAGE_DRIVER_AZURE)), 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}'}), // eslint-disable-line formatjs/enforce-placeholders -- error provided at runtime
success_message: defineMessage({id: 'admin.s3.s3Success', defaultMessage: 'Connection was successful'}),
action: testFileStoreConnection,
key: 'TestFileStoreConnection',
label: defineMessage({id: 'admin.filestore.connectionTest', defaultMessage: 'Test Connection'}),
loading: defineMessage({id: 'admin.filestore.testing', defaultMessage: 'Testing...'}),
error_message: defineMessage({id: 'admin.filestore.testFail', defaultMessage: 'Connection unsuccessful: {error}'}), // eslint-disable-line formatjs/enforce-placeholders -- error provided at runtime
success_message: defineMessage({id: 'admin.filestore.testSuccess', 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)),
isHidden: it.stateEquals('FileSettings.DedicatedExportStore', false),
},
],
},
+28 -5
View File
@@ -1342,6 +1342,10 @@
"admin.file.enableMobileDownloadTitle": "Allow File Downloads on Mobile:",
"admin.file.enableMobileUploadDesc": "When false, disables file uploads on mobile apps. If Allow File Sharing is set to true, users can still upload files from a mobile web browser.",
"admin.file.enableMobileUploadTitle": "Allow File Uploads on Mobile:",
"admin.filestore.connectionTest": "Test Connection",
"admin.filestore.testFail": "Connection unsuccessful: {error}",
"admin.filestore.testing": "Testing...",
"admin.filestore.testSuccess": "Connection was successful",
"admin.filter.apply": "Apply",
"admin.filter.filters": "Filters",
"admin.filter.reset": "Reset filters",
@@ -1500,6 +1504,28 @@
"admin.image.amazonS3TraceTitle": "Enable Amazon S3 Debugging:",
"admin.image.archiveRecursionDescription": "When enabled, content of documents within ZIP files will be returned in search results. This may have an impact on server performance for large files.",
"admin.image.archiveRecursionTitle": "Enable searching content of documents within ZIP files:",
"admin.image.azureAccessKeyDescription": "The shared key for your Azure Storage account.",
"admin.image.azureAccessKeyExample": "E.g.: \"9MZbtYgfq18PJ8PbRaJ5u91IH8izHvReTbcuQzMl+So=\"",
"admin.image.azureAccessKeyTitle": "Azure Storage Account Key:",
"admin.image.azureContainerDescription": "Name of the container in your Azure Storage account.",
"admin.image.azureContainerExample": "E.g.: \"mattermost-media\"",
"admin.image.azureContainerExportDescription": "Name of the container in your Azure Storage account.",
"admin.image.azureContainerExportExample": "E.g.: \"mattermost-export\"",
"admin.image.azureContainerTitle": "Azure Container:",
"admin.image.azureEndpointDescription": "Optional host[:port] override for non-default endpoints such as Azurite, Azure Government, or other sovereign clouds. Leave empty to use the default \"'{account}'.blob.core.windows.net\" host.",
"admin.image.azureEndpointExample": "E.g.: \"azurite:10000\" or leave empty",
"admin.image.azureEndpointTitle": "Azure Endpoint:",
"admin.image.azurePathPrefixDescription": "Optional path prefix to use for blobs in your Azure container.",
"admin.image.azurePathPrefixExample": "E.g.: \"files/\" or leave empty",
"admin.image.azurePathPrefixTitle": "Azure Path Prefix:",
"admin.image.azureRequestTimeoutDescription": "Number of milliseconds to wait for a response from Azure Blob Storage before timing out.",
"admin.image.azureRequestTimeoutExample": "E.g.: \"30000\"",
"admin.image.azureRequestTimeoutTitle": "Azure Request Timeout (Milliseconds):",
"admin.image.azureSSLDescription": "When false, allow insecure connections to Azure Blob Storage. Defaults to secure connections only.",
"admin.image.azureSSLTitle": "Enable Secure Azure Blob Storage Connections:",
"admin.image.azureStorageAccountDescription": "The name of your Azure Storage account.",
"admin.image.azureStorageAccountExample": "E.g.: \"mattermoststorage\"",
"admin.image.azureStorageAccountTitle": "Azure Storage Account:",
"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/.",
@@ -1522,7 +1548,8 @@
"admin.image.shareDescription": "Allow users to share public links to files and images.",
"admin.image.shareTitle": "Enable Public File Links: ",
"admin.image.storeAmazonS3": "Amazon S3",
"admin.image.storeDescription": "Storage system where files and image attachments are saved.\n \nSelecting \"Amazon S3\" enables fields to enter your Amazon credentials and bucket details.\n \nSelecting \"Local File System\" enables the field to specify a local file directory.",
"admin.image.storeAzureBlob": "Azure Blob Storage",
"admin.image.storeDescription": "Storage system where files and image attachments are saved.\n \nSelecting \"Amazon S3\" enables fields to enter your Amazon credentials and bucket details.\n \nSelecting \"Azure Blob Storage\" enables fields to enter your Azure Storage account credentials and container details.\n \nSelecting \"Local File System\" enables the field to specify a local file directory.",
"admin.image.storeLocal": "Local File System",
"admin.image.storeTitle": "File Storage System:",
"admin.info_banner.restart_required.desc": "Changing properties in this section will require a server restart before taking effect.",
@@ -2750,10 +2777,6 @@
"admin.reset_password.titleResetFor": "Reset password for {name}",
"admin.reset_password.titleSwitchFor": "Switch account to Email/Password for {name}",
"admin.revoke_token_button.delete": "Delete",
"admin.s3.connectionS3Test": "Test Connection",
"admin.s3.s3Fail": "Connection unsuccessful: {error}",
"admin.s3.s3Success": "Connection was successful",
"admin.s3.testing": "Testing...",
"admin.saml_feature_discovery.copy": "When you connect Mattermost with your organization's single sign-on provider, users can access Mattermost without having to re-enter their credentials.",
"admin.saml_feature_discovery.title": "Integrate SAML 2.0 with Mattermost Professional",
"admin.saml.adminAttrDesc": "(Optional) The attribute in the SAML Assertion for designating System Admins. The users selected by the query will have access to your Mattermost server as System Admins. By default, System Admins have complete access to the Mattermost System Console.\n \nExisting members that are identified by this attribute will be promoted from member to System Admin upon next login. The next login is based upon Session lengths set in **System Console > Session Lengths**. It is highly recommend to manually demote users to members in **System Console > User Management** to ensure access is restricted immediately.\n \nNote: If this filter is removed/changed, System Admins that were promoted via this filter will be demoted to members and will not retain access to the System Console. When this filter is not in use, System Admins can be manually promoted/demoted in **System Console > User Management**.",
@@ -121,6 +121,11 @@ export function testSiteURL(siteURL: string) {
});
}
/**
* @deprecated Use testFileStoreConnection instead. The /file/s3_test
* endpoint is kept for backwards compatibility but is no longer
* backend-specific.
*/
export function testS3Connection(config?: AdminConfig) {
return bindClientFunc({
clientFunc: Client4.testS3Connection,
@@ -130,6 +135,15 @@ export function testS3Connection(config?: AdminConfig) {
});
}
export function testFileStoreConnection(config?: AdminConfig) {
return bindClientFunc({
clientFunc: Client4.testFileStoreConnection,
params: [
config,
],
});
}
export function invalidateCaches() {
return bindClientFunc({
clientFunc: Client4.invalidateCaches,
+12
View File
@@ -3625,6 +3625,11 @@ export default class Client4 {
);
};
/**
* @deprecated Use testFileStoreConnection instead. The /file/s3_test
* endpoint is kept for backwards compatibility but now routes through
* the same backend-agnostic handler.
*/
testS3Connection = (config?: AdminConfig) => {
return this.doFetch<StatusOK>(
`${this.getBaseRoute()}/file/s3_test`,
@@ -3632,6 +3637,13 @@ export default class Client4 {
);
};
testFileStoreConnection = (config?: AdminConfig) => {
return this.doFetch<StatusOK>(
`${this.getBaseRoute()}/file/test`,
{method: 'post', body: JSON.stringify(config)},
);
};
invalidateCaches = () => {
return this.doFetch<StatusOK>(
`${this.getBaseRoute()}/caches/invalidate`,