From 15b361094a64fe024db6d3eaf9539143cee73ce4 Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Thu, 28 Sep 2017 09:04:52 -0400 Subject: PLT-7633 (E20) Add session idle timeout config setting (#7524) * Add session idle timeout config setting * Modify config setting name to SessionIdleTimeoutInMinutes * Small re-org of if statement * Merge with latest master --- api4/context.go | 1 + app/session.go | 12 ++++++ app/session_test.go | 119 ++++++++++++++++++++++++++++++++++++++++++++++++++++ config/default.json | 1 + model/config.go | 6 +++ 5 files changed, 139 insertions(+) diff --git a/api4/context.go b/api4/context.go index c526f38bb..9af15d72b 100644 --- a/api4/context.go +++ b/api4/context.go @@ -157,6 +157,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { c.Err = model.NewAppError("ServeHTTP", "api.context.token_provided.app_error", nil, "token="+token, http.StatusUnauthorized) } else { c.Session = *session + c.App.UpdateLastActivityAtIfNeeded(*session) } } diff --git a/app/session.go b/app/session.go index f0245acba..0e7701135 100644 --- a/app/session.go +++ b/app/session.go @@ -71,6 +71,18 @@ func (a *App) GetSession(token string) (*model.Session, *model.AppError) { return nil, model.NewAppError("GetSession", "api.context.invalid_token.error", map[string]interface{}{"Token": token}, "", http.StatusUnauthorized) } + if *utils.Cfg.ServiceSettings.SessionIdleTimeoutInMinutes > 0 && + utils.IsLicensed() && *utils.License().Features.Compliance && + session != nil && !session.IsOAuth && !session.IsMobileApp() && + session.Props[model.SESSION_PROP_TYPE] != model.SESSION_TYPE_USER_ACCESS_TOKEN { + + timeout := int64(*utils.Cfg.ServiceSettings.SessionIdleTimeoutInMinutes) * 1000 * 60 + if model.GetMillis()-session.LastActivityAt > timeout { + a.RevokeSessionById(session.Id) + return nil, model.NewAppError("GetSession", "api.context.invalid_token.error", map[string]interface{}{"Token": token}, "idle timeout", http.StatusUnauthorized) + } + } + return session, nil } diff --git a/app/session_test.go b/app/session_test.go index e91132a8a..c001655db 100644 --- a/app/session_test.go +++ b/app/session_test.go @@ -7,6 +7,10 @@ import ( "testing" "github.com/mattermost/mattermost-server/model" + "github.com/mattermost/mattermost-server/utils" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestCache(t *testing.T) { @@ -32,3 +36,118 @@ func TestCache(t *testing.T) { t.Fatal("should have one less") } } + +func TestGetSessionIdleTimeoutInMinutes(t *testing.T) { + th := Setup().InitBasic() + + session := &model.Session{ + UserId: model.NewId(), + } + + session, _ = th.App.CreateSession(session) + + isLicensed := utils.IsLicensed() + license := utils.License() + timeout := *utils.Cfg.ServiceSettings.SessionIdleTimeoutInMinutes + defer func() { + utils.SetIsLicensed(isLicensed) + utils.SetLicense(license) + *utils.Cfg.ServiceSettings.SessionIdleTimeoutInMinutes = timeout + }() + utils.SetIsLicensed(true) + utils.SetLicense(&model.License{Features: &model.Features{}}) + utils.License().Features.SetDefaults() + *utils.License().Features.Compliance = true + *utils.Cfg.ServiceSettings.SessionIdleTimeoutInMinutes = 5 + + rsession, err := th.App.GetSession(session.Token) + require.Nil(t, err) + assert.Equal(t, rsession.Id, session.Id) + + rsession, err = th.App.GetSession(session.Token) + + // Test regular session, should timeout + time := session.LastActivityAt - (1000 * 60 * 6) + <-th.App.Srv.Store.Session().UpdateLastActivityAt(session.Id, time) + th.App.ClearSessionCacheForUserSkipClusterSend(session.UserId) + + rsession, err = th.App.GetSession(session.Token) + require.NotNil(t, err) + assert.Equal(t, "api.context.invalid_token.error", err.Id) + assert.Equal(t, "idle timeout", err.DetailedError) + assert.Nil(t, rsession) + + // Test mobile session, should not timeout + session = &model.Session{ + UserId: model.NewId(), + DeviceId: "android:" + model.NewId(), + } + + session, _ = th.App.CreateSession(session) + time = session.LastActivityAt - (1000 * 60 * 6) + <-th.App.Srv.Store.Session().UpdateLastActivityAt(session.Id, time) + th.App.ClearSessionCacheForUserSkipClusterSend(session.UserId) + + _, err = th.App.GetSession(session.Token) + assert.Nil(t, err) + + // Test oauth session, should not timeout + session = &model.Session{ + UserId: model.NewId(), + IsOAuth: true, + } + + session, _ = th.App.CreateSession(session) + time = session.LastActivityAt - (1000 * 60 * 6) + <-th.App.Srv.Store.Session().UpdateLastActivityAt(session.Id, time) + th.App.ClearSessionCacheForUserSkipClusterSend(session.UserId) + + _, err = th.App.GetSession(session.Token) + assert.Nil(t, err) + + // Test personal access token session, should not timeout + session = &model.Session{ + UserId: model.NewId(), + } + session.AddProp(model.SESSION_PROP_TYPE, model.SESSION_TYPE_USER_ACCESS_TOKEN) + + session, _ = th.App.CreateSession(session) + time = session.LastActivityAt - (1000 * 60 * 6) + <-th.App.Srv.Store.Session().UpdateLastActivityAt(session.Id, time) + th.App.ClearSessionCacheForUserSkipClusterSend(session.UserId) + + _, err = th.App.GetSession(session.Token) + assert.Nil(t, err) + + // Test regular session with license off, should not timeout + *utils.License().Features.Compliance = false + + session = &model.Session{ + UserId: model.NewId(), + } + + session, _ = th.App.CreateSession(session) + time = session.LastActivityAt - (1000 * 60 * 6) + <-th.App.Srv.Store.Session().UpdateLastActivityAt(session.Id, time) + th.App.ClearSessionCacheForUserSkipClusterSend(session.UserId) + + _, err = th.App.GetSession(session.Token) + assert.Nil(t, err) + + *utils.License().Features.Compliance = true + + // Test regular session with timeout set to 0, should not timeout + *utils.Cfg.ServiceSettings.SessionIdleTimeoutInMinutes = 0 + + session = &model.Session{ + UserId: model.NewId(), + } + + session, _ = th.App.CreateSession(session) + time = session.LastActivityAt - (1000 * 60 * 6) + <-th.App.Srv.Store.Session().UpdateLastActivityAt(session.Id, time) + th.App.ClearSessionCacheForUserSkipClusterSend(session.UserId) + + _, err = th.App.GetSession(session.Token) + assert.Nil(t, err) +} diff --git a/config/default.json b/config/default.json index 34fdb97a4..f9c5647e7 100644 --- a/config/default.json +++ b/config/default.json @@ -36,6 +36,7 @@ "SessionLengthMobileInDays": 30, "SessionLengthSSOInDays": 30, "SessionCacheInMinutes": 10, + "SessionIdleTimeout": 0, "WebsocketSecurePort": 443, "WebsocketPort": 80, "WebserverMode": "gzip", diff --git a/model/config.go b/model/config.go index c55324ea7..8b6aad0e5 100644 --- a/model/config.go +++ b/model/config.go @@ -182,6 +182,7 @@ type ServiceSettings struct { SessionLengthMobileInDays *int SessionLengthSSOInDays *int SessionCacheInMinutes *int + SessionIdleTimeoutInMinutes *int WebsocketSecurePort *int WebsocketPort *int WebserverMode *string @@ -1155,6 +1156,11 @@ func (o *Config) SetDefaults() { *o.ServiceSettings.SessionCacheInMinutes = 10 } + if o.ServiceSettings.SessionIdleTimeoutInMinutes == nil { + o.ServiceSettings.SessionIdleTimeoutInMinutes = new(int) + *o.ServiceSettings.SessionIdleTimeoutInMinutes = 0 + } + if o.ServiceSettings.EnableCommands == nil { o.ServiceSettings.EnableCommands = new(bool) *o.ServiceSettings.EnableCommands = false -- cgit v1.2.3-1-g7c22