Files
team/server/channels/app/platform/cluster_handlers_revoke_test.go
481a240a1e MM-68543 Invalidate active WebConn session cache on global session revocation (#36332)
* MM-68543 Invalidate active WebConn session cache on global session revocation

Mirrors the per-user revocation pattern (ClearUserSessionCache ->
ClearSessionCacheForUserSkipClusterSend -> hub fan-out) for the global
revocation path so that ClearAllUsersSessionCache invokes the same
local-side primitive on the originating node as the cluster handler
runs on remote nodes. Also covers single-node deployments where the
cluster broadcast was previously the only trigger of the WebConn
invalidation.

Adds a Hub.InvalidateAll fan-out primitive on the websocket hub and
two contract tests covering the SkipClusterSend variant and the
production RevokeSessionsFromAllUsers entry point.

Made-with: Cursor

* MM-68543 Restore error propagation on ClearAllUsersSessionCache

The previous commit moved the local-side work into
ClearSessionCacheForAllUsersSkipClusterSend, which returned no error,
so ClearAllUsersSessionCache started always returning nil even when the
underlying session-cache purge failed.

Make the helper return the cache-purge error and propagate it back
through ClearAllUsersSessionCache, restoring the historical error
contract for callers (RevokeSessionsFromAllUsers,
App.ClearSessionCacheForAllUsers, TestCache). The hub fan-out and the
cluster broadcast still run unconditionally so security invalidation
happens even on local-purge failure.

Made-with: Cursor

* MM-68543 Trim comments and fix unchecked errcheck on App wrapper

Address review feedback: trim verbose comments across the touched
files and check the error returned by ClearSessionCacheForAllUsersSkipClusterSend
in the App-level wrapper to fix the golangci-lint errcheck failure
introduced when the helper started returning an error.

Made-with: Cursor

* MM-68543 Wipe channel routing index instead of rebuilding it on global revoke

Rebuilding byChannelID per user via InvalidateCMCacheForUser issues a
GetAllChannelMembersForUser DB query for every user with a live conn
on the hub, which is wasted work when those conns have just been
invalidated. Replace it with a single clear() of byChannelID, hidden
behind a small clearChannels() helper. Index entries repopulate
naturally as conns re-handshake or fully reconnect.

---------

Co-authored-by: Hanzo Dev <[email protected]>
2026-05-07 09:01:36 -04:00

97 lines
2.7 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package platform
import (
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/mattermost/mattermost/server/public/model"
)
// TestRevokeSessionsFromAllUsersInvalidatesWebConnSession asserts that
// after the public RevokeSessionsFromAllUsers entry point returns,
// every live WebConn on this node — across multiple users (hashed to
// different hubs) and multiple connections per user — has its cached
// session reset to the authenticated-as-no-one state.
func TestRevokeSessionsFromAllUsersInvalidatesWebConnSession(t *testing.T) {
mainHelper.Parallel(t)
th := Setup(t).InitBasic(t)
s := httptest.NewServer(dummyWebsocketHandler(t))
defer s.Close()
// Spread connections across multiple hubs via GetHubForUserId's
// hash(userID) mod len(hubs) sharding, and use multiple conns per
// user to cover the multi-device case.
type userConns struct {
userID string
wcs []*WebConn
}
users := []*userConns{
{userID: th.BasicUser.Id},
{userID: th.BasicUser2.Id},
}
for range 4 {
users = append(users, &userConns{userID: model.NewId()})
}
for _, u := range users {
preWarmStatusOnline(th, u.userID)
}
const connsPerUser = 2
for _, u := range users {
for range connsPerUser {
session, err := th.Service.CreateSession(th.Context, &model.Session{
UserId: u.userID,
})
require.NoError(t, err)
session.ExpiresAt = model.GetMillis() + time.Hour.Milliseconds()
wc := registerDummyWebConn(t, th, s.Listener.Addr(), session)
t.Cleanup(func() { wc.Close() })
u.wcs = append(u.wcs, wc)
waitForWebConnRegistered(t, th, session)
}
}
for _, u := range users {
for _, wc := range u.wcs {
require.NotNil(t, wc.GetSession(),
"precondition: webconn for user %q must have a cached session before revoke", u.userID)
require.Greater(t, wc.GetSessionExpiresAt(), model.GetMillis(),
"precondition: cached expiry for user %q must be in the future before revoke", u.userID)
require.NotEmpty(t, wc.GetSessionToken(),
"precondition: webconn for user %q must have a cached session token before revoke", u.userID)
}
}
require.NoError(t, th.Service.RevokeSessionsFromAllUsers(),
"RevokeSessionsFromAllUsers should not error")
require.Eventually(t, func() bool {
for _, u := range users {
for _, wc := range u.wcs {
if wc.GetSession() != nil {
return false
}
if wc.GetSessionExpiresAt() != 0 {
return false
}
if wc.GetSessionToken() != "" {
return false
}
}
}
return true
}, 5*time.Second, 25*time.Millisecond,
"RevokeSessionsFromAllUsers did not invalidate every live WebConn across all hubs")
}