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:
486
server/channels/api4/ephemeral_mode_test.go
Normal file
486
server/channels/api4/ephemeral_mode_test.go
Normal file
@@ -0,0 +1,486 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package api4
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
const (
|
||||
wipeSignatureAckId = "ackid"
|
||||
wipeSignatureDeviceId = "testdevice"
|
||||
)
|
||||
|
||||
// signWipePush mints the signature a session wipe push carries for the given user, which the
|
||||
// client echoes back when confirming its wipe.
|
||||
func signWipePush(t *testing.T, th *TestHelper, userID string) string {
|
||||
t.Helper()
|
||||
|
||||
signature, err := jwt.NewWithClaims(jwt.SigningMethodES256, jwt.MapClaims{
|
||||
"ack_id": wipeSignatureAckId,
|
||||
"device_id": wipeSignatureDeviceId,
|
||||
"user_id": userID,
|
||||
}).SignedString(th.App.AsymmetricSigningKey())
|
||||
require.NoError(t, err)
|
||||
|
||||
return signature
|
||||
}
|
||||
|
||||
// setupEphemeralModeAuditTest starts a TestHelper with a file-based audit target so tests can
|
||||
// read back the audit entries the ephemeral mode endpoints log.
|
||||
func setupEphemeralModeAuditTest(t *testing.T) (*TestHelper, *os.File) {
|
||||
logFile, err := os.CreateTemp("", "ephemeral_mode_audit.log")
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, logFile.Close())
|
||||
require.NoError(t, os.Remove(logFile.Name()))
|
||||
})
|
||||
|
||||
th := SetupWithServerOptionsAndConfig(t, nil, func(cfg *model.Config) {
|
||||
cfg.ExperimentalAuditSettings.FileEnabled = new(true)
|
||||
cfg.ExperimentalAuditSettings.FileName = new(logFile.Name())
|
||||
}).InitBasic(t)
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
t.Cleanup(func() { th.RemoveLicense(t) })
|
||||
|
||||
return th, logFile
|
||||
}
|
||||
|
||||
func readAuditEntry(t *testing.T, th *TestHelper, logFile *os.File, eventName, userID string) *AuditEntry {
|
||||
require.NoError(t, th.Server.Audit.Flush())
|
||||
require.NoError(t, logFile.Sync())
|
||||
|
||||
data, err := io.ReadAll(logFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
entry := FindAuditEntry(string(data), eventName, userID)
|
||||
require.NotNil(t, entry, "should find a %s audit entry for user %s", eventName, userID)
|
||||
return entry
|
||||
}
|
||||
|
||||
func TestLogCleanup(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
|
||||
t.Run("logs success when deleted record counts and cleanup date are all provided", func(t *testing.T) {
|
||||
th, logFile := setupEphemeralModeAuditTest(t)
|
||||
|
||||
body := mustMarshal(t, model.CleanupReport{
|
||||
PostsDeleted: model.NewPointer(int64(10)),
|
||||
PlaybookRunsDeleted: model.NewPointer(int64(2)),
|
||||
CleanupAt: model.NewPointer(model.GetMillis()),
|
||||
})
|
||||
resp, err := th.Client.DoAPIPost(context.Background(), "/ephemeral_mode/cleanup", string(body))
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
entry := readAuditEntry(t, th, logFile, model.AuditEventAutoCacheCleanupRun, th.BasicUser.Id)
|
||||
require.Equal(t, model.AuditStatusSuccess, entry.Status)
|
||||
require.EqualValues(t, 10, entry.Parameters["posts_deleted"])
|
||||
require.EqualValues(t, 2, entry.Parameters["playbook_runs_deleted"])
|
||||
require.Contains(t, entry.Parameters, "cleanup_at")
|
||||
require.Contains(t, entry.Parameters, "server_ts")
|
||||
})
|
||||
|
||||
t.Run("succeeds when cleanup date is omitted but deleted record counts are provided", func(t *testing.T) {
|
||||
th, logFile := setupEphemeralModeAuditTest(t)
|
||||
|
||||
body := mustMarshal(t, model.CleanupReport{
|
||||
PostsDeleted: model.NewPointer(int64(5)),
|
||||
PlaybookRunsDeleted: model.NewPointer(int64(1)),
|
||||
})
|
||||
resp, err := th.Client.DoAPIPost(context.Background(), "/ephemeral_mode/cleanup", string(body))
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
entry := readAuditEntry(t, th, logFile, model.AuditEventAutoCacheCleanupRun, th.BasicUser.Id)
|
||||
require.Equal(t, model.AuditStatusSuccess, entry.Status)
|
||||
require.EqualValues(t, 5, entry.Parameters["posts_deleted"])
|
||||
require.EqualValues(t, 1, entry.Parameters["playbook_runs_deleted"])
|
||||
require.NotContains(t, entry.Parameters, "cleanup_at")
|
||||
})
|
||||
|
||||
t.Run("accepts a failure report with no deleted record counts and records no counts for it", func(t *testing.T) {
|
||||
th, logFile := setupEphemeralModeAuditTest(t)
|
||||
|
||||
body := mustMarshal(t, model.CleanupReport{
|
||||
CleanupAt: model.NewPointer(model.GetMillis()),
|
||||
ErrorReason: model.NewPointer("disk full during cleanup"),
|
||||
})
|
||||
resp, err := th.Client.DoAPIPost(context.Background(), "/ephemeral_mode/cleanup", string(body))
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
entry := readAuditEntry(t, th, logFile, model.AuditEventAutoCacheCleanupRun, th.BasicUser.Id)
|
||||
require.Equal(t, model.AuditStatusFail, entry.Status)
|
||||
require.NotContains(t, entry.Parameters, "posts_deleted")
|
||||
require.NotContains(t, entry.Parameters, "playbook_runs_deleted")
|
||||
errData, ok := entry.Raw["error"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "disk full during cleanup", errData["description"])
|
||||
})
|
||||
|
||||
t.Run("logs failure without a description when the client reports an empty error reason", func(t *testing.T) {
|
||||
th, logFile := setupEphemeralModeAuditTest(t)
|
||||
|
||||
body := mustMarshal(t, model.CleanupReport{
|
||||
ErrorReason: model.NewPointer(""),
|
||||
})
|
||||
resp, err := th.Client.DoAPIPost(context.Background(), "/ephemeral_mode/cleanup", string(body))
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
entry := readAuditEntry(t, th, logFile, model.AuditEventAutoCacheCleanupRun, th.BasicUser.Id)
|
||||
require.Equal(t, model.AuditStatusFail, entry.Status)
|
||||
errData, ok := entry.Raw["error"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
require.NotContains(t, errData, "description")
|
||||
})
|
||||
|
||||
t.Run("rejects a success report that provides only the cleanup date", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic(t)
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense(t)
|
||||
|
||||
body := mustMarshal(t, model.CleanupReport{
|
||||
CleanupAt: model.NewPointer(model.GetMillis()),
|
||||
})
|
||||
resp, err := th.Client.DoAPIPost(context.Background(), "/ephemeral_mode/cleanup", string(body))
|
||||
require.Error(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("rejects a success report that provides only the deleted post count", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic(t)
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense(t)
|
||||
|
||||
body := mustMarshal(t, model.CleanupReport{
|
||||
PostsDeleted: model.NewPointer(int64(3)),
|
||||
})
|
||||
resp, err := th.Client.DoAPIPost(context.Background(), "/ephemeral_mode/cleanup", string(body))
|
||||
require.Error(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("rejects request when license does not support ephemeral mode", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
body := mustMarshal(t, model.CleanupReport{
|
||||
CleanupAt: model.NewPointer(model.GetMillis()),
|
||||
})
|
||||
resp, err := th.Client.DoAPIPost(context.Background(), "/ephemeral_mode/cleanup", string(body))
|
||||
require.Error(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLogOfflinePurge(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
|
||||
t.Run("logs success when offline time and purge date are both provided", func(t *testing.T) {
|
||||
th, logFile := setupEphemeralModeAuditTest(t)
|
||||
|
||||
body := mustMarshal(t, model.OfflinePurgeReport{
|
||||
OfflineTimeMinutes: model.NewPointer(int64(90)),
|
||||
PurgeAt: model.NewPointer(model.GetMillis()),
|
||||
})
|
||||
resp, err := th.Client.DoAPIPost(context.Background(), "/ephemeral_mode/purge", string(body))
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
entry := readAuditEntry(t, th, logFile, model.AuditEventOfflinePurge, th.BasicUser.Id)
|
||||
require.Equal(t, model.AuditStatusSuccess, entry.Status)
|
||||
require.EqualValues(t, 90, entry.Parameters["offline_time_minutes"])
|
||||
require.Contains(t, entry.Parameters, "purge_at")
|
||||
require.Contains(t, entry.Parameters, "server_ts")
|
||||
})
|
||||
|
||||
t.Run("succeeds when purge date is omitted but offline time is provided", func(t *testing.T) {
|
||||
th, logFile := setupEphemeralModeAuditTest(t)
|
||||
|
||||
body := mustMarshal(t, model.OfflinePurgeReport{
|
||||
OfflineTimeMinutes: model.NewPointer(int64(45)),
|
||||
})
|
||||
resp, err := th.Client.DoAPIPost(context.Background(), "/ephemeral_mode/purge", string(body))
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
entry := readAuditEntry(t, th, logFile, model.AuditEventOfflinePurge, th.BasicUser.Id)
|
||||
require.Equal(t, model.AuditStatusSuccess, entry.Status)
|
||||
require.EqualValues(t, 45, entry.Parameters["offline_time_minutes"])
|
||||
require.NotContains(t, entry.Parameters, "purge_at")
|
||||
})
|
||||
|
||||
t.Run("logs failure with the reported reason when the client reports the offline purge failed", func(t *testing.T) {
|
||||
th, logFile := setupEphemeralModeAuditTest(t)
|
||||
|
||||
body := mustMarshal(t, model.OfflinePurgeReport{
|
||||
OfflineTimeMinutes: model.NewPointer(int64(90)),
|
||||
ErrorReason: model.NewPointer("could not reach storage"),
|
||||
})
|
||||
resp, err := th.Client.DoAPIPost(context.Background(), "/ephemeral_mode/purge", string(body))
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
entry := readAuditEntry(t, th, logFile, model.AuditEventOfflinePurge, th.BasicUser.Id)
|
||||
require.Equal(t, model.AuditStatusFail, entry.Status)
|
||||
errData, ok := entry.Raw["error"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "could not reach storage", errData["description"])
|
||||
})
|
||||
|
||||
t.Run("accepts a failure report with no offline time and records no offline time for it", func(t *testing.T) {
|
||||
th, logFile := setupEphemeralModeAuditTest(t)
|
||||
|
||||
body := mustMarshal(t, model.OfflinePurgeReport{
|
||||
ErrorReason: model.NewPointer("could not read the last connected timestamp"),
|
||||
})
|
||||
resp, err := th.Client.DoAPIPost(context.Background(), "/ephemeral_mode/purge", string(body))
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
entry := readAuditEntry(t, th, logFile, model.AuditEventOfflinePurge, th.BasicUser.Id)
|
||||
require.Equal(t, model.AuditStatusFail, entry.Status)
|
||||
require.NotContains(t, entry.Parameters, "offline_time_minutes")
|
||||
errData, ok := entry.Raw["error"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "could not read the last connected timestamp", errData["description"])
|
||||
})
|
||||
|
||||
t.Run("rejects request missing offline time even when purge date is provided", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic(t)
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense(t)
|
||||
|
||||
body := mustMarshal(t, model.OfflinePurgeReport{
|
||||
PurgeAt: model.NewPointer(model.GetMillis()),
|
||||
})
|
||||
resp, err := th.Client.DoAPIPost(context.Background(), "/ephemeral_mode/purge", string(body))
|
||||
require.Error(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("rejects request missing offline time and purge date", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic(t)
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense(t)
|
||||
|
||||
body := mustMarshal(t, model.OfflinePurgeReport{})
|
||||
resp, err := th.Client.DoAPIPost(context.Background(), "/ephemeral_mode/purge", string(body))
|
||||
require.Error(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("rejects request when license does not support ephemeral mode", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
body := mustMarshal(t, model.OfflinePurgeReport{
|
||||
OfflineTimeMinutes: model.NewPointer(int64(45)),
|
||||
})
|
||||
resp, err := th.Client.DoAPIPost(context.Background(), "/ephemeral_mode/purge", string(body))
|
||||
require.Error(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
func TestLogSessionWipe(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
|
||||
// The wipe confirmation is sent by a client whose session was already revoked server-side,
|
||||
// so it must succeed without any authentication. The signature the wipe push carried is
|
||||
// what attributes the record.
|
||||
t.Run("logs success without a session and takes the actor from the signature", func(t *testing.T) {
|
||||
th, logFile := setupEphemeralModeAuditTest(t)
|
||||
client := th.CreateClient()
|
||||
|
||||
body := mustMarshal(t, model.SessionWipeReport{
|
||||
Signature: signWipePush(t, th, th.BasicUser.Id),
|
||||
WipeAt: model.NewPointer(model.GetMillis()),
|
||||
})
|
||||
resp, err := client.DoAPIPost(context.Background(), "/ephemeral_mode/wipe", string(body))
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
entry := readAuditEntry(t, th, logFile, model.AuditEventSessionWipe, th.BasicUser.Id)
|
||||
require.Equal(t, model.AuditStatusSuccess, entry.Status)
|
||||
require.Equal(t, model.RedactDeviceId(wipeSignatureDeviceId), entry.Parameters["device_id"])
|
||||
require.Equal(t, wipeSignatureAckId, entry.Parameters["ack_id"])
|
||||
require.Contains(t, entry.Parameters, "wipe_at")
|
||||
require.Contains(t, entry.Parameters, "server_ts")
|
||||
})
|
||||
|
||||
t.Run("succeeds when the wipe date is omitted", func(t *testing.T) {
|
||||
th, logFile := setupEphemeralModeAuditTest(t)
|
||||
client := th.CreateClient()
|
||||
|
||||
body := mustMarshal(t, model.SessionWipeReport{
|
||||
Signature: signWipePush(t, th, th.BasicUser.Id),
|
||||
})
|
||||
resp, err := client.DoAPIPost(context.Background(), "/ephemeral_mode/wipe", string(body))
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
entry := readAuditEntry(t, th, logFile, model.AuditEventSessionWipe, th.BasicUser.Id)
|
||||
require.Equal(t, model.AuditStatusSuccess, entry.Status)
|
||||
require.NotContains(t, entry.Parameters, "wipe_at")
|
||||
})
|
||||
|
||||
t.Run("logs failure with the reported reason when the client reports the session wipe failed", func(t *testing.T) {
|
||||
th, logFile := setupEphemeralModeAuditTest(t)
|
||||
client := th.CreateClient()
|
||||
|
||||
body := mustMarshal(t, model.SessionWipeReport{
|
||||
Signature: signWipePush(t, th, th.BasicUser.Id),
|
||||
ErrorReason: model.NewPointer("local storage wipe threw an exception"),
|
||||
})
|
||||
resp, err := client.DoAPIPost(context.Background(), "/ephemeral_mode/wipe", string(body))
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
entry := readAuditEntry(t, th, logFile, model.AuditEventSessionWipe, th.BasicUser.Id)
|
||||
require.Equal(t, model.AuditStatusFail, entry.Status)
|
||||
errData, ok := entry.Raw["error"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, "local storage wipe threw an exception", errData["description"])
|
||||
})
|
||||
|
||||
// An error reason must not waive the signature requirement: this endpoint is
|
||||
// unauthenticated, so that would let anyone append unattributable audit records.
|
||||
t.Run("rejects a failure report that omits the signature", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic(t)
|
||||
th.App.Srv().SetLicense(model.NewTestLicenseSKU(model.LicenseShortSkuEnterpriseAdvanced))
|
||||
defer th.RemoveLicense(t)
|
||||
client := th.CreateClient()
|
||||
|
||||
body := mustMarshal(t, model.SessionWipeReport{
|
||||
ErrorReason: model.NewPointer("local storage was already unreadable"),
|
||||
})
|
||||
resp, err := client.DoAPIPost(context.Background(), "/ephemeral_mode/wipe", string(body))
|
||||
require.Error(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
// Rejecting the request is not enough on an unauthenticated endpoint: without a verified
|
||||
// signature there is no actor, so the attempt must leave no sessionWipe record behind at all.
|
||||
t.Run("rejects a report whose signature does not verify and records nothing for it", func(t *testing.T) {
|
||||
th, logFile := setupEphemeralModeAuditTest(t)
|
||||
client := th.CreateClient()
|
||||
|
||||
body := mustMarshal(t, model.SessionWipeReport{
|
||||
Signature: signWipePush(t, th, th.BasicUser.Id) + "tampered",
|
||||
})
|
||||
resp, err := client.DoAPIPost(context.Background(), "/ephemeral_mode/wipe", string(body))
|
||||
require.Error(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
|
||||
require.NoError(t, th.Server.Audit.Flush())
|
||||
data, err := io.ReadAll(logFile)
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, FindAuditEntry(string(data), model.AuditEventSessionWipe, ""))
|
||||
})
|
||||
|
||||
t.Run("rejects request when license does not support ephemeral mode", func(t *testing.T) {
|
||||
th := Setup(t).InitBasic(t)
|
||||
client := th.CreateClient()
|
||||
|
||||
body := mustMarshal(t, model.SessionWipeReport{
|
||||
Signature: signWipePush(t, th, th.BasicUser.Id),
|
||||
})
|
||||
resp, err := client.DoAPIPost(context.Background(), "/ephemeral_mode/wipe", string(body))
|
||||
require.Error(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusNotImplemented, resp.StatusCode)
|
||||
})
|
||||
}
|
||||
|
||||
// An over-long reason is truncated rather than rejected, so a client that reports a failure
|
||||
// verbosely still gets its audit record. The ellipsis keeps a cut reason distinguishable from
|
||||
// a complete one when the record is read back as evidence.
|
||||
func TestEphemeralModeTruncatesLongErrorReasons(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
|
||||
longReason := strings.Repeat("test", errorReasonMaxRunes)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
path string
|
||||
event string
|
||||
client func(th *TestHelper) *model.Client4
|
||||
body func(t *testing.T, th *TestHelper) any
|
||||
}{
|
||||
{
|
||||
name: "cleanup",
|
||||
path: "/ephemeral_mode/cleanup",
|
||||
event: model.AuditEventAutoCacheCleanupRun,
|
||||
client: func(th *TestHelper) *model.Client4 { return th.Client },
|
||||
body: func(_ *testing.T, _ *TestHelper) any {
|
||||
return model.CleanupReport{ErrorReason: model.NewPointer(longReason)}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "offline purge",
|
||||
path: "/ephemeral_mode/purge",
|
||||
event: model.AuditEventOfflinePurge,
|
||||
client: func(th *TestHelper) *model.Client4 { return th.Client },
|
||||
body: func(_ *testing.T, _ *TestHelper) any {
|
||||
return model.OfflinePurgeReport{ErrorReason: model.NewPointer(longReason)}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "session wipe",
|
||||
path: "/ephemeral_mode/wipe",
|
||||
event: model.AuditEventSessionWipe,
|
||||
client: func(th *TestHelper) *model.Client4 { return th.CreateClient() },
|
||||
body: func(t *testing.T, th *TestHelper) any {
|
||||
return model.SessionWipeReport{Signature: signWipePush(t, th, th.BasicUser.Id), ErrorReason: model.NewPointer(longReason)}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name+" records the error reason cut to the maximum length and marked as cut", func(t *testing.T) {
|
||||
th, logFile := setupEphemeralModeAuditTest(t)
|
||||
|
||||
body := mustMarshal(t, tc.body(t, th))
|
||||
resp, err := tc.client(th).DoAPIPost(context.Background(), tc.path, string(body))
|
||||
require.NoError(t, err)
|
||||
defer resp.Body.Close()
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
entry := readAuditEntry(t, th, logFile, tc.event, th.BasicUser.Id)
|
||||
errData, ok := entry.Raw["error"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, longReason[:errorReasonMaxRunes-3]+"...", errData["description"])
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user