From 5e59b5f70c28f10c4428e138f14b12039977f8a0 Mon Sep 17 00:00:00 2001 From: Doug Lauder Date: Wed, 6 May 2020 15:41:10 -0400 Subject: [PATCH] MM-23935 extend session expiry on user activity (#14275) * MM-23935 extend session expiry on user activity - if user types anything before a session expires the session will be extended to now + session length - ensures new session expiries are not written to DB too frequently - new session store func for updating session ExpiresAt - session length defaults for mobile and web/ldap changed from 180 days to 30 days --- api4/channel.go | 1 + api4/post.go | 1 + app/app_iface.go | 7 ++ app/diagnostics.go | 1 + app/opentracing_layer.go | 34 +++++++ app/session.go | 71 +++++++++++++- app/session_test.go | 131 +++++++++++++++++++++++++- i18n/en.json | 4 + model/config.go | 18 +++- store/opentracing_layer.go | 18 ++++ store/sqlstore/session_store.go | 8 ++ store/store.go | 1 + store/storetest/mocks/SessionStore.go | 16 ++++ store/storetest/session_store.go | 16 ++++ store/timer_layer.go | 16 ++++ tests/test-config.json | 1 + web/context.go | 8 ++ wsapi/user.go | 2 + 18 files changed, 349 insertions(+), 5 deletions(-) diff --git a/api4/channel.go b/api4/channel.go index 882f2f0f4a..ab78f01082 100644 --- a/api4/channel.go +++ b/api4/channel.go @@ -1241,6 +1241,7 @@ func viewChannel(c *Context, w http.ResponseWriter, r *http.Request) { } c.App.UpdateLastActivityAtIfNeeded(*c.App.Session()) + c.ExtendSessionExpiryIfNeeded(w, r) // Returning {"status": "OK", ...} for backwards compatibility resp := &model.ChannelViewResponse{ diff --git a/api4/post.go b/api4/post.go index ebb81a2fd5..dc419aed52 100644 --- a/api4/post.go +++ b/api4/post.go @@ -90,6 +90,7 @@ func createPost(c *Context, w http.ResponseWriter, r *http.Request) { } c.App.UpdateLastActivityAtIfNeeded(*c.App.Session()) + c.ExtendSessionExpiryIfNeeded(w, r) w.WriteHeader(http.StatusCreated) diff --git a/app/app_iface.go b/app/app_iface.go index 1740a0a892..9de2a74a43 100644 --- a/app/app_iface.go +++ b/app/app_iface.go @@ -121,6 +121,10 @@ type AppIface interface { // attributes of the attachment structure. The Slack attachment structure is // documented here: https://api.slack.com/docs/attachments ProcessSlackAttachments(attachments []*model.SlackAttachment) []*model.SlackAttachment + // ExtendSessionExpiryIfNeeded extends Session.ExpiresAt based on session lengths in config. + // A new ExpiresAt is only written if enough time has elapsed since last update. + // Returns true only if the session was extended. + ExtendSessionExpiryIfNeeded(session *model.Session) bool // FillInPostProps should be invoked before saving posts to fill in properties such as // channel_mentions. // @@ -183,6 +187,9 @@ type AppIface interface { GetSanitizedConfig() *model.Config // GetSchemeRolesForChannel Checks if a channel or its team has an override scheme for channel roles and returns the scheme roles or default channel roles. GetSchemeRolesForChannel(channelId string) (guestRoleName string, userRoleName string, adminRoleName string, err *model.AppError) + // GetSessionLengthInMillis returns the session length, in milliseconds, + // based on the type of session (Mobile, SSO, Web/LDAP). + GetSessionLengthInMillis(session *model.Session) int64 // GetTeamGroupUsers returns the users who are associated to the team via GroupTeams and GroupMembers. GetTeamGroupUsers(teamID string) ([]*model.User, *model.AppError) // GetTeamSchemeChannelRoles Checks if a team has an override scheme and returns the scheme channel role names or default channel role names. diff --git a/app/diagnostics.go b/app/diagnostics.go index 1290e6af65..9c1f2cd544 100644 --- a/app/diagnostics.go +++ b/app/diagnostics.go @@ -306,6 +306,7 @@ func (a *App) trackConfig() { "uses_letsencrypt": *cfg.ServiceSettings.UseLetsEncrypt, "forward_80_to_443": *cfg.ServiceSettings.Forward80To443, "maximum_login_attempts": *cfg.ServiceSettings.MaximumLoginAttempts, + "extend_session_length_with_activity": *cfg.ServiceSettings.ExtendSessionLengthWithActivity, "session_length_web_in_days": *cfg.ServiceSettings.SessionLengthWebInDays, "session_length_mobile_in_days": *cfg.ServiceSettings.SessionLengthMobileInDays, "session_length_sso_in_days": *cfg.ServiceSettings.SessionLengthSSOInDays, diff --git a/app/opentracing_layer.go b/app/opentracing_layer.go index 8311eb1554..9deec66a65 100644 --- a/app/opentracing_layer.go +++ b/app/opentracing_layer.go @@ -3337,6 +3337,23 @@ func (a *OpenTracingAppLayer) ExportPermissions(w io.Writer) error { return resultVar0 } +func (a *OpenTracingAppLayer) ExtendSessionExpiryIfNeeded(session *model.Session) bool { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.ExtendSessionExpiryIfNeeded") + + a.ctx = newCtx + a.app.Srv().Store.SetContext(newCtx) + defer func() { + a.app.Srv().Store.SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0 := a.app.ExtendSessionExpiryIfNeeded(session) + + return resultVar0 +} + func (a *OpenTracingAppLayer) FetchSamlMetadataFromIdp(url string) ([]byte, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.FetchSamlMetadataFromIdp") @@ -7407,6 +7424,23 @@ func (a *OpenTracingAppLayer) GetSessionById(sessionId string) (*model.Session, return resultVar0, resultVar1 } +func (a *OpenTracingAppLayer) GetSessionLengthInMillis(session *model.Session) int64 { + origCtx := a.ctx + span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSessionLengthInMillis") + + a.ctx = newCtx + a.app.Srv().Store.SetContext(newCtx) + defer func() { + a.app.Srv().Store.SetContext(origCtx) + a.ctx = origCtx + }() + + defer span.Finish() + resultVar0 := a.app.GetSessionLengthInMillis(session) + + return resultVar0 +} + func (a *OpenTracingAppLayer) GetSessions(userId string) ([]*model.Session, *model.AppError) { origCtx := a.ctx span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetSessions") diff --git a/app/session.go b/app/session.go index 4d02a3705d..878ca6c4d5 100644 --- a/app/session.go +++ b/app/session.go @@ -4,8 +4,10 @@ package app import ( + "math" "net/http" + "github.com/mattermost/mattermost-server/v5/audit" "github.com/mattermost/mattermost-server/v5/mlog" "github.com/mattermost/mattermost-server/v5/model" ) @@ -74,7 +76,8 @@ func (a *App) GetSession(token string) (*model.Session, *model.AppError) { if *a.Config().ServiceSettings.SessionIdleTimeoutInMinutes > 0 && !session.IsOAuth && - session.Props[model.SESSION_PROP_TYPE] != model.SESSION_TYPE_USER_ACCESS_TOKEN { + session.Props[model.SESSION_PROP_TYPE] != model.SESSION_TYPE_USER_ACCESS_TOKEN && + !*a.Config().ServiceSettings.ExtendSessionLengthWithActivity { timeout := int64(*a.Config().ServiceSettings.SessionIdleTimeoutInMinutes) * 1000 * 60 if (model.GetMillis() - session.LastActivityAt) > timeout { @@ -284,6 +287,72 @@ func (a *App) UpdateLastActivityAtIfNeeded(session model.Session) { a.AddSessionToCache(&session) } +// ExtendSessionExpiryIfNeeded extends Session.ExpiresAt based on session lengths in config. +// A new ExpiresAt is only written if enough time has elapsed since last update. +// Returns true only if the session was extended. +func (a *App) ExtendSessionExpiryIfNeeded(session *model.Session) bool { + if session == nil || session.IsExpired() { + return false + } + + sessionLength := a.GetSessionLengthInMillis(session) + + // Only extend the expiry if the lessor of 1% or 1 day has elapsed within the + // current session duration. + threshold := int64(math.Min(float64(sessionLength)*0.01, float64(24*60*60*1000))) + // Minimum session length is 1 day as of this writing, therefore a minimum ~14 minutes threshold. + // However we'll add a sanity check here in case that changes. Minimum 5 minute threshold, + // meaning we won't write a new expiry more than every 5 minutes. + if threshold < 5*60*1000 { + threshold = 5 * 60 * 1000 + } + + now := model.GetMillis() + elapsed := now - (session.ExpiresAt - sessionLength) + if elapsed < threshold { + return false + } + + auditRec := a.MakeAuditRecord("extendSessionExpiry", audit.Fail) + defer a.LogAuditRec(auditRec, nil) + auditRec.AddMeta("session", session) + + newExpiry := now + sessionLength + if err := a.Srv().Store.Session().UpdateExpiresAt(session.Id, newExpiry); err != nil { + mlog.Error("Failed to update ExpiresAt", mlog.String("user_id", session.UserId), mlog.String("session_id", session.Id), mlog.Err(err)) + auditRec.AddMeta("err", err.Error()) + return false + } + + // Update local cache. No need to invalidate cache for cluster as the session cache timeout + // ensures each node will get an extended expiry within the next 10 minutes. + // Worst case is another node may generate a redundant expiry update. + session.ExpiresAt = newExpiry + a.AddSessionToCache(session) + + auditRec.Success() + auditRec.AddMeta("extended_session", session) + return true +} + +// GetSessionLengthInMillis returns the session length, in milliseconds, +// based on the type of session (Mobile, SSO, Web/LDAP). +func (a *App) GetSessionLengthInMillis(session *model.Session) int64 { + if session == nil { + return 0 + } + + var days int + if session.IsMobileApp() { + days = *a.Config().ServiceSettings.SessionLengthMobileInDays + } else if session.IsOAuth { + days = *a.Config().ServiceSettings.SessionLengthSSOInDays + } else { + days = *a.Config().ServiceSettings.SessionLengthWebInDays + } + return int64(days * 24 * 60 * 60 * 1000) +} + func (a *App) CreateUserAccessToken(token *model.UserAccessToken) (*model.UserAccessToken, *model.AppError) { user, err := a.Srv().Store.User().Get(token.UserId) diff --git a/app/session_test.go b/app/session_test.go index 62f129f1d2..42724118f1 100644 --- a/app/session_test.go +++ b/app/session_test.go @@ -4,12 +4,12 @@ package app import ( + "fmt" "testing" + "github.com/mattermost/mattermost-server/v5/model" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - - "github.com/mattermost/mattermost-server/v5/model" ) func TestCache(t *testing.T) { @@ -58,6 +58,7 @@ func TestGetSessionIdleTimeoutInMinutes(t *testing.T) { th.App.SetLicense(model.NewTestLicense("compliance")) th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SessionIdleTimeoutInMinutes = 5 }) + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ExtendSessionLengthWithActivity = false }) rsession, err := th.App.GetSession(session.Token) require.Nil(t, err) @@ -177,3 +178,129 @@ func TestUpdateSessionOnPromoteDemote(t *testing.T) { assert.Equal(t, "true", rsession.Props[model.SESSION_PROP_IS_GUEST]) }) } + +const hourMillis int64 = 60 * 60 * 1000 +const dayMillis int64 = 24 * hourMillis + +func TestApp_GetSessionLengthInMillis(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SessionLengthMobileInDays = 3 }) + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SessionLengthSSOInDays = 2 }) + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SessionLengthWebInDays = 1 }) + + t.Run("get session length mobile", func(t *testing.T) { + session := &model.Session{ + UserId: model.NewId(), + DeviceId: model.NewId(), + } + session, err := th.App.CreateSession(session) + require.Nil(t, err) + + sessionLength := th.App.GetSessionLengthInMillis(session) + require.Equal(t, dayMillis*3, sessionLength) + }) + + t.Run("get session length SSO", func(t *testing.T) { + session := &model.Session{ + UserId: model.NewId(), + IsOAuth: true, + } + session, err := th.App.CreateSession(session) + require.Nil(t, err) + + sessionLength := th.App.GetSessionLengthInMillis(session) + require.Equal(t, dayMillis*2, sessionLength) + }) + + t.Run("get session length web/LDAP", func(t *testing.T) { + session := &model.Session{ + UserId: model.NewId(), + } + session, err := th.App.CreateSession(session) + require.Nil(t, err) + + sessionLength := th.App.GetSessionLengthInMillis(session) + require.Equal(t, dayMillis*1, sessionLength) + }) +} + +func TestApp_ExtendExpiryIfNeeded(t *testing.T) { + th := Setup(t).InitBasic() + defer th.TearDown() + + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.ExtendSessionLengthWithActivity = true }) + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SessionLengthMobileInDays = 3 }) + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SessionLengthSSOInDays = 2 }) + th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.SessionLengthWebInDays = 1 }) + + t.Run("expired session should not be extended", func(t *testing.T) { + expires := model.GetMillis() - hourMillis + session := &model.Session{ + UserId: model.NewId(), + ExpiresAt: expires, + } + session, err := th.App.CreateSession(session) + require.Nil(t, err) + + ok := th.App.ExtendSessionExpiryIfNeeded(session) + + require.False(t, ok) + require.Equal(t, expires, session.ExpiresAt) + require.True(t, session.IsExpired()) + }) + + t.Run("session within threshold should not be extended", func(t *testing.T) { + session := &model.Session{ + UserId: model.NewId(), + } + session, err := th.App.CreateSession(session) + require.Nil(t, err) + + expires := model.GetMillis() + th.App.GetSessionLengthInMillis(session) + session.ExpiresAt = expires + + ok := th.App.ExtendSessionExpiryIfNeeded(session) + + require.False(t, ok) + require.Equal(t, expires, session.ExpiresAt) + require.False(t, session.IsExpired()) + }) + + var tests = []struct { + name string + session *model.Session + }{ + {name: "mobile", session: &model.Session{UserId: model.NewId(), DeviceId: model.NewId(), Token: model.NewId()}}, + {name: "SSO", session: &model.Session{UserId: model.NewId(), IsOAuth: true, Token: model.NewId()}}, + {name: "web/LDAP", session: &model.Session{UserId: model.NewId(), Token: model.NewId()}}, + } + for _, test := range tests { + t.Run(fmt.Sprintf("%s session beyond threshold should update ExpiresAt", test.name), func(t *testing.T) { + session, err := th.App.CreateSession(test.session) + require.Nil(t, err) + + expires := model.GetMillis() + th.App.GetSessionLengthInMillis(session) - hourMillis + session.ExpiresAt = expires + + ok := th.App.ExtendSessionExpiryIfNeeded(session) + + require.True(t, ok) + require.Greater(t, session.ExpiresAt, expires) + require.False(t, session.IsExpired()) + + // check cache was updated + ts, ok := th.App.Srv().sessionCache.Get(session.Token) + require.True(t, ok) + cachedSession := ts.(*model.Session) + require.Equal(t, session.ExpiresAt, cachedSession.ExpiresAt) + + // check database was updated. + storedSession, err := th.App.Srv().Store.Session().Get(session.Token) + require.Nil(t, err) + require.Equal(t, session.ExpiresAt, storedSession.ExpiresAt) + }) + } + +} diff --git a/i18n/en.json b/i18n/en.json index 650b9a5676..aa1efef07b 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -6910,6 +6910,10 @@ "id": "store.sql_session.update_device_id.app_error", "translation": "Unable to update the device id." }, + { + "id": "store.sql_session.update_expires_at.app_error", + "translation": "Unable to update expires_at." + }, { "id": "store.sql_session.update_last_activity.app_error", "translation": "Unable to update the last_activity_at." diff --git a/model/config.go b/model/config.go index 7b2b911aa3..8c2da57c7c 100644 --- a/model/config.go +++ b/model/config.go @@ -283,6 +283,7 @@ type ServiceSettings struct { CorsAllowCredentials *bool `restricted:"true"` CorsDebug *bool `restricted:"true"` AllowCookiesForSubdomains *bool `restricted:"true"` + ExtendSessionLengthWithActivity *bool `restricted:"true"` SessionLengthWebInDays *int `restricted:"true"` SessionLengthMobileInDays *int `restricted:"true"` SessionLengthSSOInDays *int `restricted:"true"` @@ -521,12 +522,25 @@ func (s *ServiceSettings) SetDefaults(isUpdate bool) { s.EnableTutorial = NewBool(true) } + // Must be manually enabled for existing installations. + if s.ExtendSessionLengthWithActivity == nil { + s.ExtendSessionLengthWithActivity = NewBool(!isUpdate) + } + if s.SessionLengthWebInDays == nil { - s.SessionLengthWebInDays = NewInt(180) + if isUpdate { + s.SessionLengthWebInDays = NewInt(180) + } else { + s.SessionLengthWebInDays = NewInt(30) + } } if s.SessionLengthMobileInDays == nil { - s.SessionLengthMobileInDays = NewInt(180) + if isUpdate { + s.SessionLengthMobileInDays = NewInt(180) + } else { + s.SessionLengthMobileInDays = NewInt(30) + } } if s.SessionLengthSSOInDays == nil { diff --git a/store/opentracing_layer.go b/store/opentracing_layer.go index 38a1b1584e..a6bb82adad 100644 --- a/store/opentracing_layer.go +++ b/store/opentracing_layer.go @@ -5840,6 +5840,24 @@ func (s *OpenTracingLayerSessionStore) UpdateDeviceId(id string, deviceId string return resultVar0, resultVar1 } +func (s *OpenTracingLayerSessionStore) UpdateExpiresAt(sessionId string, time int64) *model.AppError { + origCtx := s.Root.Store.Context() + span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.UpdateExpiresAt") + s.Root.Store.SetContext(newCtx) + defer func() { + s.Root.Store.SetContext(origCtx) + }() + + defer span.Finish() + resultVar0 := s.SessionStore.UpdateExpiresAt(sessionId, time) + if resultVar0 != nil { + span.LogFields(spanlog.Error(resultVar0)) + ext.Error.Set(span, true) + } + + return resultVar0 +} + func (s *OpenTracingLayerSessionStore) UpdateLastActivityAt(sessionId string, time int64) *model.AppError { origCtx := s.Root.Store.Context() span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "SessionStore.UpdateLastActivityAt") diff --git a/store/sqlstore/session_store.go b/store/sqlstore/session_store.go index 2cdb02d451..16784f0f83 100644 --- a/store/sqlstore/session_store.go +++ b/store/sqlstore/session_store.go @@ -160,6 +160,14 @@ func (me SqlSessionStore) PermanentDeleteSessionsByUser(userId string) *model.Ap return nil } +func (me SqlSessionStore) UpdateExpiresAt(sessionId string, time int64) *model.AppError { + _, err := me.GetMaster().Exec("UPDATE Sessions SET ExpiresAt = :ExpiresAt WHERE Id = :Id", map[string]interface{}{"ExpiresAt": time, "Id": sessionId}) + if err != nil { + return model.NewAppError("SqlSessionStore.UpdateExpiresAt", "store.sql_session.update_expires_at.app_error", nil, "sessionId="+sessionId, http.StatusInternalServerError) + } + return nil +} + func (me SqlSessionStore) UpdateLastActivityAt(sessionId string, time int64) *model.AppError { _, err := me.GetMaster().Exec("UPDATE Sessions SET LastActivityAt = :LastActivityAt WHERE Id = :Id", map[string]interface{}{"LastActivityAt": time, "Id": sessionId}) if err != nil { diff --git a/store/store.go b/store/store.go index f08986b1be..73e7298b3a 100644 --- a/store/store.go +++ b/store/store.go @@ -351,6 +351,7 @@ type SessionStore interface { Remove(sessionIdOrToken string) *model.AppError RemoveAllSessions() *model.AppError PermanentDeleteSessionsByUser(teamId string) *model.AppError + UpdateExpiresAt(sessionId string, time int64) *model.AppError UpdateLastActivityAt(sessionId string, time int64) *model.AppError UpdateRoles(userId string, roles string) (string, *model.AppError) UpdateDeviceId(id string, deviceId string, expiresAt int64) (string, *model.AppError) diff --git a/store/storetest/mocks/SessionStore.go b/store/storetest/mocks/SessionStore.go index d42934ff44..6cca745901 100644 --- a/store/storetest/mocks/SessionStore.go +++ b/store/storetest/mocks/SessionStore.go @@ -213,6 +213,22 @@ func (_m *SessionStore) UpdateDeviceId(id string, deviceId string, expiresAt int return r0, r1 } +// UpdateExpiresAt provides a mock function with given fields: sessionId, time +func (_m *SessionStore) UpdateExpiresAt(sessionId string, time int64) *model.AppError { + ret := _m.Called(sessionId, time) + + var r0 *model.AppError + if rf, ok := ret.Get(0).(func(string, int64) *model.AppError); ok { + r0 = rf(sessionId, time) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.AppError) + } + } + + return r0 +} + // UpdateLastActivityAt provides a mock function with given fields: sessionId, time func (_m *SessionStore) UpdateLastActivityAt(sessionId string, time int64) *model.AppError { ret := _m.Called(sessionId, time) diff --git a/store/storetest/session_store.go b/store/storetest/session_store.go index adc74af1f9..1f670bb409 100644 --- a/store/storetest/session_store.go +++ b/store/storetest/session_store.go @@ -26,6 +26,7 @@ func TestSessionStore(t *testing.T, ss store.Store) { t.Run("SessionRemoveToken", func(t *testing.T) { testSessionRemoveToken(t, ss) }) t.Run("SessionUpdateDeviceId", func(t *testing.T) { testSessionUpdateDeviceId(t, ss) }) t.Run("SessionUpdateDeviceId2", func(t *testing.T) { testSessionUpdateDeviceId2(t, ss) }) + t.Run("UpdateExpiresAt", func(t *testing.T) { testSessionStoreUpdateExpiresAt(t, ss) }) t.Run("UpdateLastActivityAt", func(t *testing.T) { testSessionStoreUpdateLastActivityAt(t, ss) }) t.Run("SessionCount", func(t *testing.T) { testSessionCount(t, ss) }) } @@ -212,6 +213,21 @@ func testSessionUpdateDeviceId2(t *testing.T, ss store.Store) { require.Nil(t, err) } +func testSessionStoreUpdateExpiresAt(t *testing.T, ss store.Store) { + s1 := &model.Session{} + s1.UserId = model.NewId() + + s1, err := ss.Session().Save(s1) + require.Nil(t, err) + + err = ss.Session().UpdateExpiresAt(s1.Id, 1234567890) + require.Nil(t, err) + + session, err := ss.Session().Get(s1.Id) + require.Nil(t, err) + require.EqualValues(t, session.ExpiresAt, 1234567890, "ExpiresAt not updated correctly") +} + func testSessionStoreUpdateLastActivityAt(t *testing.T, ss store.Store) { s1 := &model.Session{} s1.UserId = model.NewId() diff --git a/store/timer_layer.go b/store/timer_layer.go index b4341aa59d..aa0f2b7633 100644 --- a/store/timer_layer.go +++ b/store/timer_layer.go @@ -5291,6 +5291,22 @@ func (s *TimerLayerSessionStore) UpdateDeviceId(id string, deviceId string, expi return resultVar0, resultVar1 } +func (s *TimerLayerSessionStore) UpdateExpiresAt(sessionId string, time int64) *model.AppError { + start := timemodule.Now() + + resultVar0 := s.SessionStore.UpdateExpiresAt(sessionId, time) + + elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second) + if s.Root.Metrics != nil { + success := "false" + if resultVar0 == nil { + success = "true" + } + s.Root.Metrics.ObserveStoreMethodDuration("SessionStore.UpdateExpiresAt", success, elapsed) + } + return resultVar0 +} + func (s *TimerLayerSessionStore) UpdateLastActivityAt(sessionId string, time int64) *model.AppError { start := timemodule.Now() diff --git a/tests/test-config.json b/tests/test-config.json index 621bed8590..c2d304baba 100644 --- a/tests/test-config.json +++ b/tests/test-config.json @@ -33,6 +33,7 @@ "EnableUserAccessTokens": false, "AllowCorsFrom": "", "AllowCookiesForSubdomains": false, + "ExtendSessionLengthWithActivity": true, "SessionLengthWebInDays": 30, "SessionLengthMobileInDays": 30, "SessionLengthSSOInDays": 30, diff --git a/web/context.go b/web/context.go index c12e71defe..8e60af9983 100644 --- a/web/context.go +++ b/web/context.go @@ -182,6 +182,14 @@ func (c *Context) MfaRequired() { } } +// ExtendSessionExpiryIfNeeded will update Session.ExpiresAt based on session lengths in config. +// Session cookies will be resent to the client with updated max age. +func (c *Context) ExtendSessionExpiryIfNeeded(w http.ResponseWriter, r *http.Request) { + if ok := c.App.ExtendSessionExpiryIfNeeded(c.App.Session()); ok { + c.App.AttachSessionCookies(w, r) + } +} + func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) { subpath, _ := utils.GetSubpathFromConfig(c.App.Config()) diff --git a/wsapi/user.go b/wsapi/user.go index 45bd9a1302..b30314c4ee 100644 --- a/wsapi/user.go +++ b/wsapi/user.go @@ -13,6 +13,8 @@ func (api *API) InitUser() { } func (api *API) userTyping(req *model.WebSocketRequest) (map[string]interface{}, *model.AppError) { + api.App.ExtendSessionExpiryIfNeeded(&req.Session) + if api.App.Srv().Busy.IsBusy() { // this is considered a non-critical service and will be disabled when server busy. return nil, NewServerBusyWebSocketError(req.Action)