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:
332
server/channels/app/platform/session_test.go
Normal file
332
server/channels/app/platform/session_test.go
Normal file
@@ -0,0 +1,332 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package platform
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/mattermost/mattermost/server/public/model"
|
||||
)
|
||||
|
||||
const (
|
||||
dayInMillis = 86400000
|
||||
grace = 5 * 1000
|
||||
thirtyDays = dayInMillis * 30
|
||||
)
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t)
|
||||
|
||||
session := &model.Session{
|
||||
Id: model.NewId(),
|
||||
Token: model.NewId(),
|
||||
UserId: model.NewId(),
|
||||
}
|
||||
|
||||
session2 := &model.Session{
|
||||
Id: model.NewId(),
|
||||
Token: model.NewId(),
|
||||
UserId: model.NewId(),
|
||||
}
|
||||
|
||||
err := th.Service.sessionCache.SetWithExpiry(session.Token, session, 5*time.Minute)
|
||||
require.NoError(t, err)
|
||||
err = th.Service.sessionCache.SetWithExpiry(session2.Token, session2, 5*time.Minute)
|
||||
require.NoError(t, err)
|
||||
|
||||
var keys []string
|
||||
err = th.Service.sessionCache.Scan(func(in []string) error {
|
||||
keys = append(keys, in...)
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, keys)
|
||||
|
||||
th.Service.ClearUserSessionCache(session.UserId)
|
||||
|
||||
var rkeys []string
|
||||
err = th.Service.sessionCache.Scan(func(in []string) error {
|
||||
rkeys = append(rkeys, in...)
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Lenf(t, rkeys, len(keys)-1, "should have one less: %d - %d != 1", len(keys), len(rkeys))
|
||||
require.NotEmpty(t, rkeys)
|
||||
clear(rkeys)
|
||||
rkeys = []string{}
|
||||
|
||||
err = th.Service.ClearAllUsersSessionCache()
|
||||
require.NoError(t, err)
|
||||
err = th.Service.sessionCache.Scan(func(in []string) error {
|
||||
rkeys = append(rkeys, in...)
|
||||
return nil
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, rkeys, 0)
|
||||
}
|
||||
|
||||
func TestSetSessionExpireInHours(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t)
|
||||
|
||||
now := model.GetMillis()
|
||||
createAt := now - (dayInMillis * 20)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
extend bool
|
||||
create bool
|
||||
days int
|
||||
want int64
|
||||
}{
|
||||
{name: "zero days, extend", extend: true, create: true, days: 0, want: now},
|
||||
{name: "zero days, extend", extend: true, create: false, days: 0, want: now},
|
||||
{name: "zero days, no extend", extend: false, create: true, days: 0, want: createAt},
|
||||
{name: "zero days, no extend", extend: false, create: false, days: 0, want: now},
|
||||
{name: "thirty days, extend", extend: true, create: true, days: 30, want: now + thirtyDays},
|
||||
{name: "thirty days, extend", extend: true, create: false, days: 30, want: now + thirtyDays},
|
||||
{name: "thirty days, no extend", extend: false, create: true, days: 30, want: createAt + thirtyDays},
|
||||
{name: "thirty days, no extend", extend: false, create: false, days: 30, want: now + thirtyDays},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
th.Service.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.ServiceSettings.ExtendSessionLengthWithActivity = tt.extend
|
||||
})
|
||||
var create int64
|
||||
if tt.create {
|
||||
create = createAt
|
||||
}
|
||||
|
||||
session := &model.Session{
|
||||
CreateAt: create,
|
||||
ExpiresAt: model.GetMillis() + dayInMillis,
|
||||
}
|
||||
th.Service.SetSessionExpireInHours(session, tt.days*24)
|
||||
|
||||
// must be within 5 seconds of expected time.
|
||||
require.GreaterOrEqual(t, session.ExpiresAt, tt.want-grace)
|
||||
require.LessOrEqual(t, session.ExpiresAt, tt.want+grace)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestOAuthRevokeAccessToken(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t)
|
||||
|
||||
err := th.Service.RevokeAccessToken(th.Context, model.NewRandomString(16))
|
||||
require.Error(t, err, "Should have failed due to an incorrect token")
|
||||
|
||||
session := &model.Session{}
|
||||
session.CreateAt = model.GetMillis()
|
||||
session.UserId = model.NewId()
|
||||
session.Token = model.NewId()
|
||||
session.Roles = model.SystemUserRoleId
|
||||
th.Service.SetSessionExpireInHours(session, 24)
|
||||
|
||||
session, _ = th.Service.CreateSession(th.Context, session)
|
||||
err = th.Service.RevokeAccessToken(th.Context, session.Token)
|
||||
require.Error(t, err, "Should have failed does not have an access token")
|
||||
|
||||
accessData := &model.AccessData{}
|
||||
accessData.Token = session.Token
|
||||
accessData.UserId = session.UserId
|
||||
accessData.RedirectUri = "http://example.com"
|
||||
accessData.ClientId = model.NewId()
|
||||
accessData.ExpiresAt = session.ExpiresAt
|
||||
|
||||
_, nErr := th.Service.Store.OAuth().SaveAccessData(accessData)
|
||||
require.NoError(t, nErr)
|
||||
|
||||
err = th.Service.RevokeAccessToken(th.Context, accessData.Token)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestUpdateSessionsIsGuest(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t)
|
||||
|
||||
t.Run("Test session is demoted", func(t *testing.T) {
|
||||
user := th.CreateUserOrGuest(t, false)
|
||||
|
||||
session := &model.Session{}
|
||||
session.CreateAt = model.GetMillis()
|
||||
session.UserId = user.Id
|
||||
session.Token = model.NewId()
|
||||
session.Roles = "fake_role"
|
||||
th.Service.SetSessionExpireInHours(session, 24)
|
||||
|
||||
session, _ = th.Service.CreateSession(th.Context, session)
|
||||
|
||||
demotedUser, err := th.Service.Store.User().DemoteUserToGuest(th.Context, user.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, model.SystemGuestRoleId, demotedUser.Roles)
|
||||
|
||||
err = th.Service.UpdateSessionsIsGuest(th.Context, demotedUser, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
session, err = th.Service.GetSession(th.Context, session.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, model.SystemGuestRoleId, session.Roles)
|
||||
require.Equal(t, "true", session.Props[model.SessionPropIsGuest])
|
||||
})
|
||||
|
||||
t.Run("Test session is promoted", func(t *testing.T) {
|
||||
user := th.CreateUserOrGuest(t, true)
|
||||
|
||||
session := &model.Session{}
|
||||
session.CreateAt = model.GetMillis()
|
||||
session.UserId = user.Id
|
||||
session.Token = model.NewId()
|
||||
session.Roles = "fake_role"
|
||||
th.Service.SetSessionExpireInHours(session, 24)
|
||||
|
||||
session, _ = th.Service.CreateSession(th.Context, session)
|
||||
|
||||
err := th.Service.Store.User().PromoteGuestToUser(th.Context, user.Id)
|
||||
require.NoError(t, err)
|
||||
|
||||
promotedUser, err := th.Service.Store.User().Get(th.Context, user.Id)
|
||||
require.NoError(t, err)
|
||||
err = th.Service.UpdateSessionsIsGuest(th.Context, promotedUser, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
session, err = th.Service.GetSession(th.Context, session.Id)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, model.SystemUserRoleId, session.Roles)
|
||||
require.Equal(t, "false", session.Props[model.SessionPropIsGuest])
|
||||
})
|
||||
}
|
||||
|
||||
func TestRevokeSessionsForDeviceTokens(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t).InitBasic(t)
|
||||
|
||||
// Both revoke variants are structurally identical (one matches DeviceId,
|
||||
// the other VoIPDeviceId), and the invariants they must hold are the
|
||||
// same. Drive them through a single table.
|
||||
type variant struct {
|
||||
name string
|
||||
setToken func(s *model.Session, token string)
|
||||
revoke func(userID, token, currentSessionID string) error
|
||||
emptyTokenErr error
|
||||
}
|
||||
|
||||
variants := []variant{
|
||||
{
|
||||
name: "RevokeOtherSessionsForDeviceId",
|
||||
setToken: func(s *model.Session, token string) { s.DeviceId = token },
|
||||
revoke: func(userID, token, currentSessionID string) error {
|
||||
return th.Service.RevokeOtherSessionsForDeviceId(th.Context, userID, token, currentSessionID)
|
||||
},
|
||||
emptyTokenErr: ErrEmptyDeviceId,
|
||||
},
|
||||
{
|
||||
name: "RevokeOtherSessionsForVoIPDeviceId",
|
||||
setToken: func(s *model.Session, token string) { s.VoIPDeviceId = token },
|
||||
revoke: func(userID, token, currentSessionID string) error {
|
||||
return th.Service.RevokeOtherSessionsForVoIPDeviceId(th.Context, userID, token, currentSessionID)
|
||||
},
|
||||
emptyTokenErr: ErrEmptyVoIPDeviceId,
|
||||
},
|
||||
}
|
||||
|
||||
createSession := func(t *testing.T, v variant, userID, token string) *model.Session {
|
||||
t.Helper()
|
||||
s := &model.Session{UserId: userID, Roles: model.SystemUserRoleId}
|
||||
v.setToken(s, token)
|
||||
saved, err := th.Service.CreateSession(th.Context, s)
|
||||
require.NoError(t, err)
|
||||
return saved
|
||||
}
|
||||
|
||||
sessionExists := func(t *testing.T, id string) bool {
|
||||
t.Helper()
|
||||
_, err := th.Service.GetSessionByID(th.Context, id)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
for _, v := range variants {
|
||||
t.Run(v.name, func(t *testing.T) {
|
||||
t.Run("revokes the matching session", func(t *testing.T) {
|
||||
token := "apple_rn:revokes-matching-" + v.name
|
||||
target := createSession(t, v, th.BasicUser.Id, token)
|
||||
|
||||
require.NoError(t, v.revoke(th.BasicUser.Id, token, ""))
|
||||
assert.False(t, sessionExists(t, target.Id), "matching session must be revoked")
|
||||
})
|
||||
|
||||
t.Run("does not revoke the session passed as currentSessionId", func(t *testing.T) {
|
||||
// Self-skip: a session re-attaching its own token must not
|
||||
// revoke itself.
|
||||
token := "apple_rn:self-skip-" + v.name
|
||||
self := createSession(t, v, th.BasicUser.Id, token)
|
||||
|
||||
require.NoError(t, v.revoke(th.BasicUser.Id, token, self.Id))
|
||||
assert.True(t, sessionExists(t, self.Id), "currentSessionId must be exempt from revocation")
|
||||
})
|
||||
|
||||
t.Run("does not revoke sessions belonging to other users", func(t *testing.T) {
|
||||
// The function scopes by userID — a session with a matching
|
||||
// token on a different user must stay alive.
|
||||
token := "apple_rn:cross-user-" + v.name
|
||||
otherUserSession := createSession(t, v, th.BasicUser2.Id, token)
|
||||
|
||||
require.NoError(t, v.revoke(th.BasicUser.Id, token, ""))
|
||||
assert.True(t, sessionExists(t, otherUserSession.Id),
|
||||
"session on a different user must not be revoked")
|
||||
})
|
||||
|
||||
t.Run("empty token returns an error and revokes nothing", func(t *testing.T) {
|
||||
webSession := createSession(t, v, th.BasicUser.Id, "")
|
||||
|
||||
err := v.revoke(th.BasicUser.Id, "", "")
|
||||
require.ErrorIs(t, err, v.emptyTokenErr)
|
||||
assert.True(t, sessionExists(t, webSession.Id),
|
||||
"empty-token revoke must not match sessions with no token in this slot")
|
||||
})
|
||||
|
||||
t.Run("leaves siblings of the same user with different tokens alone", func(t *testing.T) {
|
||||
targetToken := "apple_rn:target-" + v.name
|
||||
siblingToken := "apple_rn:sibling-" + v.name
|
||||
|
||||
target := createSession(t, v, th.BasicUser.Id, targetToken)
|
||||
sibling := createSession(t, v, th.BasicUser.Id, siblingToken)
|
||||
|
||||
require.NoError(t, v.revoke(th.BasicUser.Id, targetToken, ""))
|
||||
assert.False(t, sessionExists(t, target.Id))
|
||||
assert.True(t, sessionExists(t, sibling.Id),
|
||||
"session with a different token must stay alive")
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevokeAllSessionsReturnsRevokedSessions(t *testing.T) {
|
||||
mainHelper.Parallel(t)
|
||||
th := Setup(t)
|
||||
|
||||
userId := model.NewId()
|
||||
|
||||
s1 := &model.Session{UserId: userId, ExpiresAt: model.GetMillis() + dayInMillis, DeviceId: model.NewId()}
|
||||
s1, _ = th.Service.CreateSession(th.Context, s1)
|
||||
|
||||
s2 := &model.Session{UserId: userId, ExpiresAt: model.GetMillis() + dayInMillis}
|
||||
s2, _ = th.Service.CreateSession(th.Context, s2)
|
||||
|
||||
sessions, err := th.Service.RevokeAllSessions(th.Context, userId)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, sessions, 2)
|
||||
require.ElementsMatch(t, []string{s1.Id, s2.Id}, []string{sessions[0].Id, sessions[1].Id})
|
||||
|
||||
remaining, err := th.Service.Store.Session().GetSessions(th.Context, userId)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, remaining)
|
||||
}
|
||||
Reference in New Issue
Block a user