snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
This commit is contained in:
466
server/platform/shared/filestore/azurestore_test.go
Normal file
466
server/platform/shared/filestore/azurestore_test.go
Normal file
@@ -0,0 +1,466 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package filestore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
func TestBuildAzureServiceURL(t *testing.T) {
|
||||
const account = "acmemattermost"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
cloud string
|
||||
scheme string
|
||||
account string // defaults to the package-level account const when empty
|
||||
endpoint string
|
||||
expected string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "commercial cloud, default scheme",
|
||||
cloud: model.AzureCloudCommercial,
|
||||
scheme: "https",
|
||||
expected: "https://acmemattermost.blob.core.windows.net/",
|
||||
},
|
||||
{
|
||||
name: "commercial cloud rejects an account name with a hash character",
|
||||
cloud: model.AzureCloudCommercial,
|
||||
scheme: "https",
|
||||
account: "account#",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "commercial cloud rejects an account name with a slash character",
|
||||
cloud: model.AzureCloudCommercial,
|
||||
scheme: "https",
|
||||
account: "account/",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "government cloud rejects a malformed account name",
|
||||
cloud: model.AzureCloudGovernment,
|
||||
scheme: "https",
|
||||
account: "account#",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "empty cloud falls back to commercial for legacy configs",
|
||||
cloud: "",
|
||||
scheme: "https",
|
||||
expected: "https://acmemattermost.blob.core.windows.net/",
|
||||
},
|
||||
{
|
||||
name: "government cloud uses the usgovcloudapi suffix",
|
||||
cloud: model.AzureCloudGovernment,
|
||||
scheme: "https",
|
||||
expected: "https://acmemattermost.blob.core.usgovcloudapi.net/",
|
||||
},
|
||||
{
|
||||
name: "custom cloud returns the endpoint verbatim (vhost-style)",
|
||||
cloud: model.AzureCloudCustom,
|
||||
scheme: "https",
|
||||
endpoint: "https://acmemattermost.blob.core.windows.net/",
|
||||
expected: "https://acmemattermost.blob.core.windows.net/",
|
||||
},
|
||||
{
|
||||
name: "custom cloud returns the endpoint verbatim (Azurite path-style)",
|
||||
cloud: model.AzureCloudCustom,
|
||||
scheme: "http",
|
||||
endpoint: "http://localhost:10000/devstoreaccount1/",
|
||||
expected: "http://localhost:10000/devstoreaccount1/",
|
||||
},
|
||||
{
|
||||
name: "custom cloud preserves arbitrary paths the admin provides",
|
||||
cloud: model.AzureCloudCustom,
|
||||
scheme: "https",
|
||||
endpoint: "https://blob-proxy.internal.example.com/some/prefix/account/",
|
||||
expected: "https://blob-proxy.internal.example.com/some/prefix/account/",
|
||||
},
|
||||
{
|
||||
name: "custom cloud rejects an empty endpoint",
|
||||
cloud: model.AzureCloudCustom,
|
||||
scheme: "https",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "custom cloud rejects an endpoint missing the scheme",
|
||||
cloud: model.AzureCloudCustom,
|
||||
scheme: "https",
|
||||
endpoint: "acmemattermost.blob.core.windows.net/",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "custom cloud rejects an endpoint with no host",
|
||||
cloud: model.AzureCloudCustom,
|
||||
scheme: "https",
|
||||
endpoint: "https:///acmemattermost/",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "custom cloud rejects a non-HTTP scheme",
|
||||
cloud: model.AzureCloudCustom,
|
||||
scheme: "https",
|
||||
endpoint: "ftp://blob.example.com/",
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "unknown cloud value is rejected",
|
||||
cloud: "azuregermany",
|
||||
scheme: "https",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
acct := account
|
||||
if tt.account != "" {
|
||||
acct = tt.account
|
||||
}
|
||||
got, err := buildAzureServiceURL(tt.cloud, tt.scheme, acct, tt.endpoint)
|
||||
if tt.wantErr {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.expected, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAzureFileBackendPrefix(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
prefix string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{name: "no prefix, plain path", prefix: "", input: "team/channel/file", expected: "team/channel/file"},
|
||||
{name: "no prefix, with dot-dot", prefix: "", input: "../escape", expected: "../escape"},
|
||||
{name: "prefix, plain path", prefix: "mattermost", input: "team/channel/file", expected: "mattermost/team/channel/file"},
|
||||
{name: "prefix, exact root", prefix: "mattermost", input: "", expected: "mattermost"},
|
||||
{name: "prefix, dot-dot escapes", prefix: "mattermost", input: "../escape", expected: "mattermost/escape"},
|
||||
{name: "prefix, nested dot-dot escapes", prefix: "mattermost", input: "sub/../../escape", expected: "mattermost/escape"},
|
||||
{name: "prefix, dot-dot in middle stays inside", prefix: "mattermost", input: "a/../b", expected: "mattermost/b"},
|
||||
{name: "prefix with trailing slash, dot-dot escapes", prefix: "mattermost/", input: "../escape", expected: "mattermost/escape"},
|
||||
{name: "prefix boundary collision must not escape", prefix: "mattermost", input: "../mattermost-evil/file", expected: "mattermost/file"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
b := &AzureFileBackend{pathPrefix: tt.prefix}
|
||||
require.Equal(t, tt.expected, b.prefix(tt.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// azuriteWellKnownAccount and azuriteWellKnownKey are Azurite's published
|
||||
// development credentials. They are not secrets - they are documented in the
|
||||
// Azurite README and ship hardcoded in every Azurite distribution.
|
||||
const (
|
||||
azuriteWellKnownAccount = "devstoreaccount1"
|
||||
azuriteWellKnownKey = "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw=="
|
||||
)
|
||||
|
||||
// TestAzureFileBackendAppendRefusesNonBlockBlob exercises the safety
|
||||
// check in AppendFile: when a blob exists with content but no committed
|
||||
// block list (i.e. it was uploaded via Put Blob by another tool), the
|
||||
// backend must refuse the append rather than silently destroy the
|
||||
// existing content.
|
||||
func TestAzureFileBackendAppendRefusesNonBlockBlob(t *testing.T) {
|
||||
be := newAzuriteBackend(t)
|
||||
|
||||
path := "append-refusal-test.bin"
|
||||
t.Cleanup(func() { _ = be.RemoveFile(path) })
|
||||
|
||||
// Write the blob via the high-level Upload helper, which calls the
|
||||
// Put Blob REST endpoint and leaves the committed-block list empty.
|
||||
original := []byte("planted-by-another-tool")
|
||||
bb := be.newBlockBlobClient(path)
|
||||
_, err := bb.Upload(context.Background(), nopReadSeekCloser{bytes.NewReader(original)}, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = be.AppendFile(bytes.NewReader([]byte("would-overwrite")), path)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "no committed block list")
|
||||
|
||||
// The original content must still be intact.
|
||||
got, err := be.ReadFile(path)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, original, got)
|
||||
}
|
||||
|
||||
// TestAzureFileBackendMakeContainerIdempotent ensures that calling
|
||||
// MakeContainer twice on the same backend is a no-op the second time.
|
||||
// Two nodes can race through TestConnection plus MakeContainer at boot;
|
||||
// the loser must converge instead of returning an error.
|
||||
func TestAzureFileBackendMakeContainerIdempotent(t *testing.T) {
|
||||
be := newAzuriteBackend(t)
|
||||
|
||||
require.NoError(t, be.MakeContainer())
|
||||
require.NoError(t, be.MakeContainer())
|
||||
}
|
||||
|
||||
type nopReadSeekCloser struct {
|
||||
*bytes.Reader
|
||||
}
|
||||
|
||||
func (nopReadSeekCloser) Close() error { return nil }
|
||||
|
||||
// newAzuriteBackend builds an Azure backend pointed at the Azurite emulator
|
||||
// and ensures the container exists. Standalone Azure tests should use this
|
||||
// instead of calling NewAzureFileBackend + TestConnection directly; the
|
||||
// shared FileBackendTestSuite handles provisioning itself in SetupTest.
|
||||
func newAzuriteBackend(t *testing.T) *AzureFileBackend {
|
||||
t.Helper()
|
||||
be, err := NewAzureFileBackend(azuriteSettings(t))
|
||||
require.NoError(t, err)
|
||||
|
||||
var noBucket *FileBackendNoBucketError
|
||||
if err := be.TestConnection(); errors.As(err, &noBucket) {
|
||||
require.NoError(t, be.MakeContainer())
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
return be
|
||||
}
|
||||
|
||||
func azuriteSettings(t *testing.T) FileBackendSettings {
|
||||
t.Helper()
|
||||
host := os.Getenv("CI_AZURITE_HOST")
|
||||
if host == "" {
|
||||
host = "localhost"
|
||||
}
|
||||
port := os.Getenv("CI_AZURITE_PORT")
|
||||
if port == "" {
|
||||
port = "10000"
|
||||
}
|
||||
return FileBackendSettings{
|
||||
DriverName: driverAzure,
|
||||
AzureStorageAccount: azuriteWellKnownAccount,
|
||||
AzureAuthMode: model.AzureAuthModeSharedKey,
|
||||
AzureAccessKey: azuriteWellKnownKey,
|
||||
AzureContainer: "mattermost-test",
|
||||
AzureCloud: model.AzureCloudCustom,
|
||||
AzureEndpoint: "http://" + net.JoinHostPort(host, port) + "/" + azuriteWellKnownAccount + "/",
|
||||
// The emulator runs on an internal address, so its host must be allowed.
|
||||
AllowedUntrustedInternalConnections: host,
|
||||
AzureRequestTimeoutMilliseconds: 30000,
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewAzureFileBackendAuthMode(t *testing.T) {
|
||||
base := FileBackendSettings{
|
||||
DriverName: driverAzure,
|
||||
AzureStorageAccount: "anaccount",
|
||||
AzureContainer: "acontainer",
|
||||
AzureEndpoint: "localhost:10000",
|
||||
AzureSSL: false,
|
||||
AzureRequestTimeoutMilliseconds: 30000,
|
||||
}
|
||||
|
||||
t.Run("shared_key constructs a client", func(t *testing.T) {
|
||||
s := base
|
||||
s.AzureAuthMode = model.AzureAuthModeSharedKey
|
||||
s.AzureAccessKey = azuriteWellKnownKey
|
||||
|
||||
be, err := NewAzureFileBackend(s)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, be.client)
|
||||
})
|
||||
|
||||
t.Run("default_credential constructs a client without an access key", func(t *testing.T) {
|
||||
s := base
|
||||
s.AzureAuthMode = model.AzureAuthModeDefaultCredential
|
||||
// Intentionally no AzureAccessKey - default credential reads
|
||||
// identity from the host environment, not config.
|
||||
|
||||
be, err := NewAzureFileBackend(s)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, be.client)
|
||||
})
|
||||
|
||||
t.Run("empty AuthMode is rejected", func(t *testing.T) {
|
||||
s := base
|
||||
s.AzureAuthMode = ""
|
||||
s.AzureAccessKey = azuriteWellKnownKey
|
||||
|
||||
_, err := NewAzureFileBackend(s)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "unknown azure auth mode")
|
||||
})
|
||||
|
||||
t.Run("unknown AuthMode is rejected", func(t *testing.T) {
|
||||
s := base
|
||||
s.AzureAuthMode = "oauth2"
|
||||
s.AzureAccessKey = azuriteWellKnownKey
|
||||
|
||||
_, err := NewAzureFileBackend(s)
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "unknown azure auth mode")
|
||||
})
|
||||
}
|
||||
|
||||
func TestCheckMandatoryAzureFieldsAuthMode(t *testing.T) {
|
||||
base := FileBackendSettings{
|
||||
AzureStorageAccount: "anaccount",
|
||||
AzureContainer: "acontainer",
|
||||
}
|
||||
|
||||
t.Run("shared_key requires access key", func(t *testing.T) {
|
||||
s := base
|
||||
s.AzureAuthMode = model.AzureAuthModeSharedKey
|
||||
s.AzureAccessKey = ""
|
||||
require.Error(t, s.CheckMandatoryAzureFields())
|
||||
|
||||
s.AzureAccessKey = "somekey"
|
||||
require.NoError(t, s.CheckMandatoryAzureFields())
|
||||
})
|
||||
|
||||
t.Run("default_credential does not require access key", func(t *testing.T) {
|
||||
s := base
|
||||
s.AzureAuthMode = model.AzureAuthModeDefaultCredential
|
||||
s.AzureAccessKey = ""
|
||||
require.NoError(t, s.CheckMandatoryAzureFields())
|
||||
})
|
||||
}
|
||||
|
||||
func TestAzureFileBackendTestSuite(t *testing.T) {
|
||||
suite.Run(t, &FileBackendTestSuite{settings: azuriteSettings(t)})
|
||||
}
|
||||
|
||||
// TestAzureGeneratePublicLink exercises the Service SAS path against Azurite.
|
||||
// The user-delegation SAS path requires Entra ID and cannot run in CI; it is
|
||||
// covered by the manual verification recipe in the PR description.
|
||||
func TestAzureGeneratePublicLink(t *testing.T) {
|
||||
t.Run("refuses to issue a link when presign expiration is unset", func(t *testing.T) {
|
||||
settings := azuriteSettings(t)
|
||||
// Deliberately leave AzurePresignExpiresSeconds at zero - the
|
||||
// shape an admin would hit if they enabled the export-direct-
|
||||
// download feature without ever configuring the field.
|
||||
be, err := NewAzureFileBackend(settings)
|
||||
require.NoError(t, err)
|
||||
|
||||
_, _, err = be.GeneratePublicLink("any/path")
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "presign expiration is not configured")
|
||||
})
|
||||
|
||||
t.Run("shared-key SAS round-trips through Azurite", func(t *testing.T) {
|
||||
settings := azuriteSettings(t)
|
||||
settings.AzurePresignExpiresSeconds = 300
|
||||
be, err := NewAzureFileBackend(settings)
|
||||
require.NoError(t, err)
|
||||
|
||||
var noBucket *FileBackendNoBucketError
|
||||
if connErr := be.TestConnection(); errors.As(connErr, &noBucket) {
|
||||
require.NoError(t, be.MakeContainer())
|
||||
} else {
|
||||
require.NoError(t, connErr)
|
||||
}
|
||||
|
||||
path := "tests/presign-" + model.NewId() + ".bin"
|
||||
payload := bytes.Repeat([]byte("mattermost"), 4096) // 40 KiB
|
||||
_, writeErr := be.WriteFile(bytes.NewReader(payload), path)
|
||||
require.NoError(t, writeErr)
|
||||
t.Cleanup(func() { _ = be.RemoveFile(path) })
|
||||
|
||||
link, expiry, err := be.GeneratePublicLink(path)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, link)
|
||||
require.Equal(t, 300*time.Second, expiry)
|
||||
|
||||
// SAS query parameters Azurite expects to see.
|
||||
u, err := url.Parse(link)
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, u.Query().Get("sig"))
|
||||
require.Equal(t, "r", u.Query().Get("sp"))
|
||||
require.Equal(t, "attachment", u.Query().Get("rscd"))
|
||||
|
||||
resp, err := http.Get(link)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = resp.Body.Close() })
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
require.Equal(t, "attachment", resp.Header.Get("Content-Disposition"))
|
||||
|
||||
got, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, payload, got)
|
||||
})
|
||||
|
||||
t.Run("tampered SAS is rejected by the storage service", func(t *testing.T) {
|
||||
settings := azuriteSettings(t)
|
||||
settings.AzurePresignExpiresSeconds = 300
|
||||
be, err := NewAzureFileBackend(settings)
|
||||
require.NoError(t, err)
|
||||
|
||||
var noBucket *FileBackendNoBucketError
|
||||
if connErr := be.TestConnection(); errors.As(connErr, &noBucket) {
|
||||
require.NoError(t, be.MakeContainer())
|
||||
} else {
|
||||
require.NoError(t, connErr)
|
||||
}
|
||||
|
||||
path := "tests/tamper-" + model.NewId() + ".bin"
|
||||
_, writeErr := be.WriteFile(bytes.NewReader([]byte("contents")), path)
|
||||
require.NoError(t, writeErr)
|
||||
t.Cleanup(func() { _ = be.RemoveFile(path) })
|
||||
|
||||
link, _, err := be.GeneratePublicLink(path)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Flip the last character of the signature so the link no longer
|
||||
// matches what Azurite signed.
|
||||
u, err := url.Parse(link)
|
||||
require.NoError(t, err)
|
||||
q := u.Query()
|
||||
sig := q.Get("sig")
|
||||
require.NotEmpty(t, sig)
|
||||
swapped := sig[:len(sig)-1] + flipBase64Char(sig[len(sig)-1])
|
||||
q.Set("sig", swapped)
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
resp, err := http.Get(u.String())
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = resp.Body.Close() })
|
||||
require.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("unknown auth mode is rejected", func(t *testing.T) {
|
||||
// Build a backend by hand so we can exercise the default-branch
|
||||
// guard. Going through NewAzureFileBackend would be rejected by
|
||||
// CheckMandatoryAzureFields first.
|
||||
be := &AzureFileBackend{
|
||||
authMode: "totally-not-real",
|
||||
presignExpires: time.Minute,
|
||||
}
|
||||
_, _, err := be.GeneratePublicLink("any/path")
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "unknown azure auth mode")
|
||||
})
|
||||
}
|
||||
|
||||
// flipBase64Char swaps a base64 character for another in the same alphabet
|
||||
// to produce a still-valid-but-wrong signature character. Used only by the
|
||||
// tamper test above.
|
||||
func flipBase64Char(c byte) string {
|
||||
if c == 'A' {
|
||||
return "B"
|
||||
}
|
||||
return "A"
|
||||
}
|
||||
Reference in New Issue
Block a user