diff options
-rw-r--r-- | api4/user.go | 76 | ||||
-rw-r--r-- | api4/user_test.go | 98 | ||||
-rw-r--r-- | app/session.go | 38 | ||||
-rw-r--r-- | model/client4.go | 26 | ||||
-rw-r--r-- | model/user_access_token.go | 2 | ||||
-rw-r--r-- | store/sqlstore/upgrade.go | 12 | ||||
-rw-r--r-- | store/sqlstore/user_access_token_store.go | 62 | ||||
-rw-r--r-- | store/store.go | 2 | ||||
-rw-r--r-- | store/storetest/user_access_token_store.go | 37 |
9 files changed, 348 insertions, 5 deletions
diff --git a/api4/user.go b/api4/user.go index d17591afa..889681b54 100644 --- a/api4/user.go +++ b/api4/user.go @@ -61,6 +61,8 @@ func (api *API) InitUser() { api.BaseRoutes.User.Handle("/tokens", api.ApiSessionRequired(getUserAccessTokens)).Methods("GET") api.BaseRoutes.Users.Handle("/tokens/{token_id:[A-Za-z0-9]+}", api.ApiSessionRequired(getUserAccessToken)).Methods("GET") api.BaseRoutes.Users.Handle("/tokens/revoke", api.ApiSessionRequired(revokeUserAccessToken)).Methods("POST") + api.BaseRoutes.Users.Handle("/tokens/disable", api.ApiSessionRequired(disableUserAccessToken)).Methods("POST") + api.BaseRoutes.Users.Handle("/tokens/enable", api.ApiSessionRequired(enableUserAccessToken)).Methods("POST") } func createUser(c *Context, w http.ResponseWriter, r *http.Request) { @@ -1290,3 +1292,77 @@ func revokeUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAudit("success - token_id=" + accessToken.Id) ReturnStatusOK(w) } + +func disableUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + tokenId := props["token_id"] + + if tokenId == "" { + c.SetInvalidParam("token_id") + } + + c.LogAudit("") + + // No separate permission for this action for now + if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_REVOKE_USER_ACCESS_TOKEN) { + c.SetPermissionError(model.PERMISSION_REVOKE_USER_ACCESS_TOKEN) + return + } + + accessToken, err := c.App.GetUserAccessToken(tokenId, false) + if err != nil { + c.Err = err + return + } + + if !app.SessionHasPermissionToUser(c.Session, accessToken.UserId) { + c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) + return + } + + err = c.App.DisableUserAccessToken(accessToken) + if err != nil { + c.Err = err + return + } + + c.LogAudit("success - token_id=" + accessToken.Id) + ReturnStatusOK(w) +} + +func enableUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + tokenId := props["token_id"] + + if tokenId == "" { + c.SetInvalidParam("token_id") + } + + c.LogAudit("") + + // No separate permission for this action for now + if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_CREATE_USER_ACCESS_TOKEN) { + c.SetPermissionError(model.PERMISSION_CREATE_USER_ACCESS_TOKEN) + return + } + + accessToken, err := c.App.GetUserAccessToken(tokenId, false) + if err != nil { + c.Err = err + return + } + + if !app.SessionHasPermissionToUser(c.Session, accessToken.UserId) { + c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) + return + } + + err = c.App.EnableUserAccessToken(accessToken) + if err != nil { + c.Err = err + return + } + + c.LogAudit("success - token_id=" + accessToken.Id) + ReturnStatusOK(w) +} diff --git a/api4/user_test.go b/api4/user_test.go index ceaf3f038..1f408048e 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -2302,6 +2302,8 @@ func TestCreateUserAccessToken(t *testing.T) { t.Fatal("id should not be empty") } else if rtoken.Description != testDescription { t.Fatal("description did not match") + } else if !rtoken.IsActive { + t.Fatal("token should be active") } oldSessionToken := Client.AuthToken @@ -2445,7 +2447,7 @@ func TestRevokeUserAccessToken(t *testing.T) { if !ok { t.Fatal("should have passed") } - + oldSessionToken = Client.AuthToken Client.AuthToken = token.Token _, resp = Client.GetMe("") @@ -2463,6 +2465,100 @@ func TestRevokeUserAccessToken(t *testing.T) { } } +func TestDisableUserAccessToken(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer th.TearDown() + Client := th.Client + AdminClient := th.SystemAdminClient + + testDescription := "test token" + + enableUserAccessTokens := *utils.Cfg.ServiceSettings.EnableUserAccessTokens + defer func() { + *utils.Cfg.ServiceSettings.EnableUserAccessTokens = enableUserAccessTokens + }() + *utils.Cfg.ServiceSettings.EnableUserAccessTokens = true + + th.App.UpdateUserRoles(th.BasicUser.Id, model.ROLE_SYSTEM_USER.Id+" "+model.ROLE_SYSTEM_USER_ACCESS_TOKEN.Id) + token, resp := Client.CreateUserAccessToken(th.BasicUser.Id, testDescription) + CheckNoError(t, resp) + + oldSessionToken := Client.AuthToken + Client.AuthToken = token.Token + _, resp = Client.GetMe("") + CheckNoError(t, resp) + Client.AuthToken = oldSessionToken + + ok, resp := Client.DisableUserAccessToken(token.Id) + CheckNoError(t, resp) + + if !ok { + t.Fatal("should have passed") + } + + oldSessionToken = Client.AuthToken + Client.AuthToken = token.Token + _, resp = Client.GetMe("") + CheckUnauthorizedStatus(t, resp) + Client.AuthToken = oldSessionToken + + token, resp = AdminClient.CreateUserAccessToken(th.BasicUser2.Id, testDescription) + CheckNoError(t, resp) + + ok, resp = Client.DisableUserAccessToken(token.Id) + CheckForbiddenStatus(t, resp) + + if ok { + t.Fatal("should have failed") + } +} + +func TestEnableUserAccessToken(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer th.TearDown() + Client := th.Client + + testDescription := "test token" + + enableUserAccessTokens := *utils.Cfg.ServiceSettings.EnableUserAccessTokens + defer func() { + *utils.Cfg.ServiceSettings.EnableUserAccessTokens = enableUserAccessTokens + }() + *utils.Cfg.ServiceSettings.EnableUserAccessTokens = true + + th.App.UpdateUserRoles(th.BasicUser.Id, model.ROLE_SYSTEM_USER.Id+" "+model.ROLE_SYSTEM_USER_ACCESS_TOKEN.Id) + token, resp := Client.CreateUserAccessToken(th.BasicUser.Id, testDescription) + CheckNoError(t, resp) + + oldSessionToken := Client.AuthToken + Client.AuthToken = token.Token + _, resp = Client.GetMe("") + CheckNoError(t, resp) + Client.AuthToken = oldSessionToken + + _, resp = Client.DisableUserAccessToken(token.Id) + CheckNoError(t, resp) + + oldSessionToken = Client.AuthToken + Client.AuthToken = token.Token + _, resp = Client.GetMe("") + CheckUnauthorizedStatus(t, resp) + Client.AuthToken = oldSessionToken + + ok, resp := Client.EnableUserAccessToken(token.Id) + CheckNoError(t, resp) + + if !ok { + t.Fatal("should have passed") + } + + oldSessionToken = Client.AuthToken + Client.AuthToken = token.Token + _, resp = Client.GetMe("") + CheckNoError(t, resp) + Client.AuthToken = oldSessionToken +} + func TestUserAccessTokenInactiveUser(t *testing.T) { th := Setup().InitBasic().InitSystemAdmin() defer th.TearDown() diff --git a/app/session.go b/app/session.go index f8b931043..7492a7c37 100644 --- a/app/session.go +++ b/app/session.go @@ -268,6 +268,10 @@ func (a *App) createSessionForUserAccessToken(tokenString string) (*model.Sessio return nil, model.NewAppError("createSessionForUserAccessToken", "app.user_access_token.invalid_or_missing", nil, result.Err.Error(), http.StatusUnauthorized) } else { token = result.Data.(*model.UserAccessToken) + + if token.IsActive == false { + return nil, model.NewAppError("createSessionForUserAccessToken", "app.user_access_token.invalid_or_missing", nil, "inactive_token", http.StatusUnauthorized) + } } var user *model.User @@ -320,6 +324,40 @@ func (a *App) RevokeUserAccessToken(token *model.UserAccessToken) *model.AppErro return a.RevokeSession(session) } +func (a *App) DisableUserAccessToken(token *model.UserAccessToken) *model.AppError { + var session *model.Session + if result := <-a.Srv.Store.Session().Get(token.Token); result.Err == nil { + session = result.Data.(*model.Session) + } + + if result := <-a.Srv.Store.UserAccessToken().UpdateTokenDisable(token.Id); result.Err != nil { + return result.Err + } + + if session == nil { + return nil + } + + return a.RevokeSession(session) +} + +func (a *App) EnableUserAccessToken(token *model.UserAccessToken) *model.AppError { + var session *model.Session + if result := <-a.Srv.Store.Session().Get(token.Token); result.Err == nil { + session = result.Data.(*model.Session) + } + + if result := <-a.Srv.Store.UserAccessToken().UpdateTokenEnable(token.Id); result.Err != nil { + return result.Err + } + + if session == nil { + return nil + } + + return nil +} + func (a *App) GetUserAccessTokensForUser(userId string, page, perPage int) ([]*model.UserAccessToken, *model.AppError) { if result := <-a.Srv.Store.UserAccessToken().GetByUser(userId, page*perPage, perPage); result.Err != nil { return nil, result.Err diff --git a/model/client4.go b/model/client4.go index 5703c4143..dc5a25bec 100644 --- a/model/client4.go +++ b/model/client4.go @@ -1065,6 +1065,32 @@ func (c *Client4) RevokeUserAccessToken(tokenId string) (bool, *Response) { } } +// DisableUserAccessToken will disable a user access token by id. Must have the +// 'revoke_user_access_token' permission and if disabling for another user, must have the +// 'edit_other_users' permission. +func (c *Client4) DisableUserAccessToken(tokenId string) (bool, *Response) { + requestBody := map[string]string{"token_id": tokenId} + if r, err := c.DoApiPost(c.GetUsersRoute()+"/tokens/disable", MapToJson(requestBody)); err != nil { + return false, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) + } +} + +// EnableUserAccessToken will enable a user access token by id. Must have the +// 'create_user_access_token' permission and if enabling for another user, must have the +// 'edit_other_users' permission. +func (c *Client4) EnableUserAccessToken(tokenId string) (bool, *Response) { + requestBody := map[string]string{"token_id": tokenId} + if r, err := c.DoApiPost(c.GetUsersRoute()+"/tokens/enable", MapToJson(requestBody)); err != nil { + return false, BuildErrorResponse(r, err) + } else { + defer closeBody(r) + return CheckStatusOK(r), BuildResponse(r) + } +} + // Team Section // CreateTeam creates a team in the system based on the provided team struct. diff --git a/model/user_access_token.go b/model/user_access_token.go index 090780fd0..e189ec233 100644 --- a/model/user_access_token.go +++ b/model/user_access_token.go @@ -14,6 +14,7 @@ type UserAccessToken struct { Token string `json:"token,omitempty"` UserId string `json:"user_id"` Description string `json:"description"` + IsActive bool `json:"is_active"` } func (t *UserAccessToken) IsValid() *AppError { @@ -38,6 +39,7 @@ func (t *UserAccessToken) IsValid() *AppError { func (t *UserAccessToken) PreSave() { t.Id = NewId() + t.IsActive = true } func (t *UserAccessToken) ToJson() string { diff --git a/store/sqlstore/upgrade.go b/store/sqlstore/upgrade.go index 5f466cf51..a6c1ecc43 100644 --- a/store/sqlstore/upgrade.go +++ b/store/sqlstore/upgrade.go @@ -312,8 +312,12 @@ func UpgradeDatabaseToVersion43(sqlStore SqlStore) { } func UpgradeDatabaseToVersion44(sqlStore SqlStore) { - // TODO: Uncomment following when version 4.4.0 is released - //if shouldPerformUpgrade(sqlStore, VERSION_4_3_0, VERSION_4_4_0) { - // saveSchemaVersion(sqlStore, VERSION_4_4_0) - //} + // TODO: Uncomment following condition when version 4.4.0 is released + // if shouldPerformUpgrade(sqlStore, VERSION_4_3_0, VERSION_4_4_0) { + + // Add the IsActive column to UserAccessToken. + sqlStore.CreateColumnIfNotExists("UserAccessTokens", "IsActive", "boolean", "boolean", "1") + + // saveSchemaVersion(sqlStore, VERSION_4_4_0) + // } } diff --git a/store/sqlstore/user_access_token_store.go b/store/sqlstore/user_access_token_store.go index 2535943c7..530ba8d16 100644 --- a/store/sqlstore/user_access_token_store.go +++ b/store/sqlstore/user_access_token_store.go @@ -198,3 +198,65 @@ func (s SqlUserAccessTokenStore) GetByUser(userId string, offset, limit int) sto result.Data = tokens }) } + +func (s SqlUserAccessTokenStore) UpdateTokenEnable(tokenId string) store.StoreChannel { + return store.Do(func(result *store.StoreResult) { + if _, err := s.GetMaster().Exec("UPDATE UserAccessTokens SET IsActive = TRUE WHERE Id = :Id", map[string]interface{}{"Id": tokenId}); err != nil { + result.Err = model.NewAppError("SqlUserAccessTokenStore.UpdateTokenEnable", "store.sql_user_access_token.update_token_enable.app_error", nil, "id="+tokenId+", "+err.Error(), http.StatusInternalServerError) + } else { + result.Data = tokenId + } + }) +} + +func (s SqlUserAccessTokenStore) UpdateTokenDisable(tokenId string) store.StoreChannel { + return store.Do(func(result *store.StoreResult) { + transaction, err := s.GetMaster().Begin() + if err != nil { + result.Err = model.NewAppError("SqlUserAccessTokenStore.UpdateTokenDisable", "store.sql_user_access_token.update_token_disble.app_error", nil, err.Error(), http.StatusInternalServerError) + } else { + if extrasResult := s.deleteSessionsAndDisableToken(transaction, tokenId); extrasResult.Err != nil { + *result = extrasResult + } + + if result.Err == nil { + if err := transaction.Commit(); err != nil { + // don't need to rollback here since the transaction is already closed + result.Err = model.NewAppError("SqlUserAccessTokenStore.UpdateTokenDisable", "store.sql_user_access_token.update_token_disable.app_error", nil, err.Error(), http.StatusInternalServerError) + } + } else { + if err := transaction.Rollback(); err != nil { + result.Err = model.NewAppError("SqlUserAccessTokenStore.UpdateTokenDisable", "store.sql_user_access_token.update_token_disable.app_error", nil, err.Error(), http.StatusInternalServerError) + } + } + } + }) +} + +func (s SqlUserAccessTokenStore) deleteSessionsAndDisableToken(transaction *gorp.Transaction, tokenId string) store.StoreResult { + result := store.StoreResult{} + + query := "" + if s.DriverName() == model.DATABASE_DRIVER_POSTGRES { + query = "DELETE FROM Sessions s USING UserAccessTokens o WHERE o.Token = s.Token AND o.Id = :Id" + } else if s.DriverName() == model.DATABASE_DRIVER_MYSQL { + query = "DELETE s.* FROM Sessions s INNER JOIN UserAccessTokens o ON o.Token = s.Token WHERE o.Id = :Id" + } + + if _, err := transaction.Exec(query, map[string]interface{}{"Id": tokenId}); err != nil { + result.Err = model.NewAppError("SqlUserAccessTokenStore.deleteSessionsAndDisableToken", "store.sql_user_access_token.update_token_disable.app_error", nil, "id="+tokenId+", err="+err.Error(), http.StatusInternalServerError) + return result + } + + return s.updateTokenDisable(transaction, tokenId) +} + +func (s SqlUserAccessTokenStore) updateTokenDisable(transaction *gorp.Transaction, tokenId string) store.StoreResult { + result := store.StoreResult{} + + if _, err := transaction.Exec("UPDATE UserAccessTokens SET IsActive = FALSE WHERE Id = :Id", map[string]interface{}{"Id": tokenId}); err != nil { + result.Err = model.NewAppError("SqlUserAccessTokenStore.updateTokenDisable", "store.sql_user_access_token.update_token_disable.app_error", nil, "", http.StatusInternalServerError) + } + + return result +} diff --git a/store/store.go b/store/store.go index 120778e84..7616ee7eb 100644 --- a/store/store.go +++ b/store/store.go @@ -436,4 +436,6 @@ type UserAccessTokenStore interface { Get(tokenId string) StoreChannel GetByToken(tokenString string) StoreChannel GetByUser(userId string, page, perPage int) StoreChannel + UpdateTokenEnable(tokenId string) StoreChannel + UpdateTokenDisable(tokenId string) StoreChannel } diff --git a/store/storetest/user_access_token_store.go b/store/storetest/user_access_token_store.go index 292929419..661c969da 100644 --- a/store/storetest/user_access_token_store.go +++ b/store/storetest/user_access_token_store.go @@ -12,6 +12,7 @@ import ( func TestUserAccessTokenStore(t *testing.T, ss store.Store) { t.Run("UserAccessTokenSaveGetDelete", func(t *testing.T) { testUserAccessTokenSaveGetDelete(t, ss) }) + t.Run("UserAccessTokenDisableEnable", func(t *testing.T) { testUserAccessTokenDisableEnable(t, ss) }) } func testUserAccessTokenSaveGetDelete(t *testing.T, ss store.Store) { @@ -87,3 +88,39 @@ func testUserAccessTokenSaveGetDelete(t *testing.T, ss store.Store) { t.Fatal("should error - access token should be deleted") } } + +func testUserAccessTokenDisableEnable(t *testing.T, ss store.Store) { + uat := &model.UserAccessToken{ + Token: model.NewId(), + UserId: model.NewId(), + Description: "testtoken", + } + + s1 := model.Session{} + s1.UserId = uat.UserId + s1.Token = uat.Token + + store.Must(ss.Session().Save(&s1)) + + if result := <-ss.UserAccessToken().Save(uat); result.Err != nil { + t.Fatal(result.Err) + } + + if err := (<-ss.UserAccessToken().UpdateTokenDisable(uat.Id)).Err; err != nil { + t.Fatal(err) + } + + if err := (<-ss.Session().Get(s1.Token)).Err; err == nil { + t.Fatal("should error - session should be deleted") + } + + s2 := model.Session{} + s2.UserId = uat.UserId + s2.Token = uat.Token + + store.Must(ss.Session().Save(&s2)) + + if err := (<-ss.UserAccessToken().UpdateTokenEnable(uat.Id)).Err; err != nil { + t.Fatal(err) + } +} |