From b664291f21ad11e04fa208284b5d94013f1a6a97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guillermo=20Vay=C3=A1?= Date: Mon, 1 Jul 2019 23:28:46 +0200 Subject: [PATCH] [Mm-7854] [Backend] Add an endpoint to revoke sessions from all users (#11200) * first steps towards revoke all sessions endpoint * route added * change permission into a more restrictive one * fix url * add store code * testing & mocking * fixing what merge broke * remove sessions without retrieving them * flush sessions from cache * stop going through sessions to revoke caches, not needed anymore * add test, fix func name * fix tests * remove unneeded code * [MM-7854]remove access tokens, move to users * fix docstring * [MM-7854] improve readability by using require * [MM-7854] fix tests * [MM-7854]fix comment * [MM-7854]improve testing logic --- api4/user.go | 15 +++++++++++ api4/user_test.go | 40 +++++++++++++++++++++++++++++ app/cluster_handlers.go | 5 ++++ app/session.go | 34 ++++++++++++++++++++++++ app/session_test.go | 21 ++++++++++----- model/client4.go | 10 ++++++++ model/cluster_message.go | 1 + store/sqlstore/oauth_store.go | 7 +++++ store/store.go | 1 + store/storetest/mocks/OAuthStore.go | 16 ++++++++++++ store/storetest/oauth_store.go | 17 ++++++++++++ 11 files changed, 161 insertions(+), 6 deletions(-) diff --git a/api4/user.go b/api4/user.go index 633a4d10ad..e4aa289eea 100644 --- a/api4/user.go +++ b/api4/user.go @@ -63,6 +63,7 @@ func (api *API) InitUser() { api.BaseRoutes.User.Handle("/sessions", api.ApiSessionRequired(getSessions)).Methods("GET") api.BaseRoutes.User.Handle("/sessions/revoke", api.ApiSessionRequired(revokeSession)).Methods("POST") api.BaseRoutes.User.Handle("/sessions/revoke/all", api.ApiSessionRequired(revokeAllSessionsForUser)).Methods("POST") + api.BaseRoutes.Users.Handle("/sessions/revoke/all", api.ApiSessionRequired(revokeAllSessionsAllUsers)).Methods("POST") api.BaseRoutes.Users.Handle("/sessions/device", api.ApiSessionRequired(attachDeviceId)).Methods("PUT") api.BaseRoutes.User.Handle("/audits", api.ApiSessionRequired(getUserAudits)).Methods("GET") @@ -1500,6 +1501,20 @@ func revokeAllSessionsForUser(c *Context, w http.ResponseWriter, r *http.Request ReturnStatusOK(w) } +func revokeAllSessionsAllUsers(c *Context, w http.ResponseWriter, r *http.Request) { + if !c.App.SessionHasPermissionTo(c.App.Session, model.PERMISSION_MANAGE_SYSTEM) { + c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM) + return + } + + if err := c.App.RevokeSessionsFromAllUsers(); err != nil { + c.Err = err + return + } + + ReturnStatusOK(w) +} + func attachDeviceId(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) diff --git a/api4/user_test.go b/api4/user_test.go index 62a952037a..c8f73fb1ba 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -2490,6 +2490,46 @@ func TestRevokeAllSessions(t *testing.T) { CheckUnauthorizedStatus(t, resp) } +func TestRevokeSessionsFromAllUsers(t *testing.T) { + th := Setup().InitBasic() + defer th.TearDown() + + user := th.BasicUser + th.Client.Login(user.Email, user.Password) + _, resp := th.Client.RevokeSessionsFromAllUsers() + CheckForbiddenStatus(t, resp) + + th.Client.Logout() + _, resp = th.Client.RevokeSessionsFromAllUsers() + CheckUnauthorizedStatus(t, resp) + + th.Client.Login(user.Email, user.Password) + admin := th.SystemAdminUser + th.Client.Login(admin.Email, admin.Password) + sessions, err := th.Server.Store.Session().GetSessions(user.Id) + require.NotEmpty(t, sessions) + require.Nil(t, err) + sessions, err = th.Server.Store.Session().GetSessions(admin.Id) + require.NotEmpty(t, sessions) + require.Nil(t, err) + _, resp = th.Client.RevokeSessionsFromAllUsers() + CheckNoError(t, resp) + + // All sessions were revoked, so making the same call + // again will fail due to lack of a session. + _, resp = th.Client.RevokeSessionsFromAllUsers() + CheckUnauthorizedStatus(t, resp) + + sessions, err = th.Server.Store.Session().GetSessions(user.Id) + require.Empty(t, sessions) + require.Nil(t, err) + + sessions, err = th.Server.Store.Session().GetSessions(admin.Id) + require.Empty(t, sessions) + require.Nil(t, err) + +} + func TestAttachDeviceId(t *testing.T) { th := Setup().InitBasic() defer th.TearDown() diff --git a/app/cluster_handlers.go b/app/cluster_handlers.go index 5222099b1f..71ee028b55 100644 --- a/app/cluster_handlers.go +++ b/app/cluster_handlers.go @@ -22,6 +22,7 @@ func (a *App) RegisterAllClusterMessageHandlers() { a.Cluster.RegisterClusterMessageHandler(model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_USER, a.ClusterInvalidateCacheForUserHandler) a.Cluster.RegisterClusterMessageHandler(model.CLUSTER_EVENT_INVALIDATE_CACHE_FOR_USER_TEAMS, a.ClusterInvalidateCacheForUserTeamsHandler) a.Cluster.RegisterClusterMessageHandler(model.CLUSTER_EVENT_CLEAR_SESSION_CACHE_FOR_USER, a.ClusterClearSessionCacheForUserHandler) + a.Cluster.RegisterClusterMessageHandler(model.CLUSTER_EVENT_CLEAR_SESSION_CACHE_FOR_ALL_USERS, a.ClusterClearSessionCacheForAllUsersHandler) } func (a *App) ClusterPublishHandler(msg *model.ClusterMessage) { @@ -73,3 +74,7 @@ func (a *App) ClusterInvalidateCacheForUserTeamsHandler(msg *model.ClusterMessag func (a *App) ClusterClearSessionCacheForUserHandler(msg *model.ClusterMessage) { a.ClearSessionCacheForUserSkipClusterSend(msg.Data) } + +func (a *App) ClusterClearSessionCacheForAllUsersHandler(msg *model.ClusterMessage) { + a.ClearSessionCacheForAllUsersSkipClusterSend() +} diff --git a/app/session.go b/app/session.go index 418fe0f6c9..1f3f38457c 100644 --- a/app/session.go +++ b/app/session.go @@ -113,6 +113,23 @@ func (a *App) RevokeAllSessions(userId string) *model.AppError { return nil } +// RevokeSessionsFromAllUsers will go through all the sessions active +// in the server and revoke them +func (a *App) RevokeSessionsFromAllUsers() *model.AppError { + // revoke tokens before sessions so they can't be used to relogin + tErr := a.Srv.Store.OAuth().RemoveAllAccessData() + if tErr != nil { + return tErr + } + err := a.Srv.Store.Session().RemoveAllSessions() + if err != nil { + return err + } + a.ClearSessionCacheForAllUsers() + + return nil +} + func (a *App) ClearSessionCacheForUser(userId string) { a.ClearSessionCacheForUserSkipClusterSend(userId) @@ -126,6 +143,18 @@ func (a *App) ClearSessionCacheForUser(userId string) { } } +func (a *App) ClearSessionCacheForAllUsers() { + a.ClearSessionCacheForAllUsersSkipClusterSend() + + if a.Cluster != nil { + msg := &model.ClusterMessage{ + Event: model.CLUSTER_EVENT_CLEAR_SESSION_CACHE_FOR_ALL_USERS, + SendType: model.CLUSTER_SEND_RELIABLE, + } + a.Cluster.SendClusterMessage(msg) + } +} + func (a *App) ClearSessionCacheForUserSkipClusterSend(userId string) { keys := a.Srv.sessionCache.Keys() @@ -144,6 +173,11 @@ func (a *App) ClearSessionCacheForUserSkipClusterSend(userId string) { a.InvalidateWebConnSessionCacheForUser(userId) } +func (a *App) ClearSessionCacheForAllUsersSkipClusterSend() { + mlog.Info("Purging sessions cache") + a.Srv.sessionCache.Purge() +} + func (a *App) AddSessionToCache(session *model.Session) { a.Srv.sessionCache.AddWithExpiresInSecs(session.Token, session, int64(*a.Config().ServiceSettings.SessionCacheInMinutes*60)) } diff --git a/app/session_test.go b/app/session_test.go index 0516fe289c..bcb7b0f0c2 100644 --- a/app/session_test.go +++ b/app/session_test.go @@ -22,19 +22,28 @@ func TestCache(t *testing.T) { UserId: model.NewId(), } + session2 := &model.Session{ + Id: model.NewId(), + Token: model.NewId(), + UserId: model.NewId(), + } + th.App.Srv.sessionCache.AddWithExpiresInSecs(session.Token, session, 5*60) + th.App.Srv.sessionCache.AddWithExpiresInSecs(session2.Token, session2, 5*60) keys := th.App.Srv.sessionCache.Keys() - if len(keys) <= 0 { - t.Fatal("should have items") - } + require.NotEmpty(t, keys) th.App.ClearSessionCacheForUser(session.UserId) rkeys := th.App.Srv.sessionCache.Keys() - if len(rkeys) != len(keys)-1 { - t.Fatal("should have one less") - } + require.Lenf(t, rkeys, len(keys)-1, "should have one less: %d - %d != 1", len(keys), len(rkeys)) + require.NotEmpty(t, rkeys) + + th.App.ClearSessionCacheForAllUsers() + + rkeys = th.App.Srv.sessionCache.Keys() + require.Empty(t, rkeys) } func TestGetSessionIdleTimeoutInMinutes(t *testing.T) { diff --git a/model/client4.go b/model/client4.go index 2fe67c93c9..ff30781187 100644 --- a/model/client4.go +++ b/model/client4.go @@ -1166,6 +1166,16 @@ func (c *Client4) RevokeAllSessions(userId string) (bool, *Response) { return CheckStatusOK(r), BuildResponse(r) } +// RevokeAllSessions revokes all sessions for all the users. +func (c *Client4) RevokeSessionsFromAllUsers() (bool, *Response) { + r, err := c.DoApiPost(c.GetUsersRoute()+"/sessions/revoke/all", "") + if err != nil { + return false, BuildErrorResponse(r, err) + } + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) +} + // AttachDeviceId attaches a mobile device ID to the current session. func (c *Client4) AttachDeviceId(deviceId string) (bool, *Response) { requestBody := map[string]string{"device_id": deviceId} diff --git a/model/cluster_message.go b/model/cluster_message.go index 575d6a0bc5..82e0151ea3 100644 --- a/model/cluster_message.go +++ b/model/cluster_message.go @@ -24,6 +24,7 @@ const ( CLUSTER_EVENT_CLEAR_SESSION_CACHE_FOR_USER = "clear_session_user" CLUSTER_EVENT_INVALIDATE_CACHE_FOR_ROLES = "inv_roles" CLUSTER_EVENT_INVALIDATE_CACHE_FOR_SCHEMES = "inv_schemes" + CLUSTER_EVENT_CLEAR_SESSION_CACHE_FOR_ALL_USERS = "inv_all_user_sessions" CLUSTER_SEND_BEST_EFFORT = "best_effort" CLUSTER_SEND_RELIABLE = "reliable" diff --git a/store/sqlstore/oauth_store.go b/store/sqlstore/oauth_store.go index 1458b618ae..222804460d 100644 --- a/store/sqlstore/oauth_store.go +++ b/store/sqlstore/oauth_store.go @@ -239,6 +239,13 @@ func (as SqlOAuthStore) RemoveAccessData(token string) *model.AppError { return nil } +func (as SqlOAuthStore) RemoveAllAccessData() *model.AppError { + if _, err := as.GetMaster().Exec("DELETE FROM OAuthAccessData", map[string]interface{}{}); err != nil { + return model.NewAppError("SqlOAuthStore.RemoveAccessData", "store.sql_oauth.remove_access_data.app_error", nil, "err="+err.Error(), http.StatusInternalServerError) + } + return nil +} + func (as SqlOAuthStore) SaveAuthData(authData *model.AuthData) (*model.AuthData, *model.AppError) { authData.PreSave() if err := authData.IsValid(); err != nil { diff --git a/store/store.go b/store/store.go index 00c3776c9c..db366804db 100644 --- a/store/store.go +++ b/store/store.go @@ -373,6 +373,7 @@ type OAuthStore interface { GetAccessDataByRefreshToken(token string) (*model.AccessData, *model.AppError) GetPreviousAccessData(userId, clientId string) (*model.AccessData, *model.AppError) RemoveAccessData(token string) *model.AppError + RemoveAllAccessData() *model.AppError } type SystemStore interface { diff --git a/store/storetest/mocks/OAuthStore.go b/store/storetest/mocks/OAuthStore.go index aabb877bce..ab8f97b9ce 100644 --- a/store/storetest/mocks/OAuthStore.go +++ b/store/storetest/mocks/OAuthStore.go @@ -285,6 +285,22 @@ func (_m *OAuthStore) RemoveAccessData(token string) *model.AppError { return r0 } +// RemoveAllAccessData provides a mock function with given fields: +func (_m *OAuthStore) RemoveAllAccessData() *model.AppError { + ret := _m.Called() + + var r0 *model.AppError + if rf, ok := ret.Get(0).(func() *model.AppError); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.AppError) + } + } + + return r0 +} + // RemoveAuthData provides a mock function with given fields: code func (_m *OAuthStore) RemoveAuthData(code string) *model.AppError { ret := _m.Called(code) diff --git a/store/storetest/oauth_store.go b/store/storetest/oauth_store.go index ba1b8f6439..c754a2c277 100644 --- a/store/storetest/oauth_store.go +++ b/store/storetest/oauth_store.go @@ -213,6 +213,23 @@ func testOAuthStoreRemoveAccessData(t *testing.T, ss store.Store) { require.Nil(t, result, "did not delete access token") } +func testOAuthStoreRemoveAllAccessData(t *testing.T, ss store.Store) { + a1 := model.AccessData{} + a1.ClientId = model.NewId() + a1.UserId = model.NewId() + a1.Token = model.NewId() + a1.RefreshToken = model.NewId() + a1.RedirectUri = "http://example.com" + _, err := ss.OAuth().SaveAccessData(&a1) + require.Nil(t, err) + + err = ss.OAuth().RemoveAllAccessData() + require.Nil(t, err) + + result, _ := ss.OAuth().GetPreviousAccessData(a1.UserId, a1.ClientId) + require.Nil(t, result, "did not delete access token") +} + func testOAuthStoreSaveAuthData(t *testing.T, ss store.Store) { a1 := model.AuthData{} a1.ClientId = model.NewId()