[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
Этот коммит содержится в:
Guillermo Vayá
2019-07-01 23:28:46 +02:00
коммит произвёл Miguel de la Cruz
родитель 0d5020e566
Коммит b664291f21
11 изменённых файлов: 161 добавлений и 6 удалений

Просмотреть файл

@@ -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)

Просмотреть файл

@@ -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()

Просмотреть файл

@@ -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()
}

Просмотреть файл

@@ -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))
}

Просмотреть файл

@@ -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) {

Просмотреть файл

@@ -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}

Просмотреть файл

@@ -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"

Просмотреть файл

@@ -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 {

Просмотреть файл

@@ -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 {

Просмотреть файл

@@ -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)

Просмотреть файл

@@ -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()