[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
Этот коммит содержится в:
коммит произвёл
Miguel de la Cruz
родитель
0d5020e566
Коммит
b664291f21
15
api4/user.go
15
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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Ссылка в новой задаче
Block a user