[PLT-7793] Add /users/tokens/search endpoint (#8088)

* Add /users/tokens/search endpoint + tests

* Fix check-style

* Unnecessary deletion
Этот коммит содержится в:
Adrian Carolli
2018-01-11 16:30:55 -05:00
коммит произвёл Joram Wilander
родитель 1d9efd0e39
Коммит b1d13a2d89
11 изменённых файлов: 231 добавлений и 0 удалений

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

@@ -60,6 +60,7 @@ func (api *API) InitUser() {
api.BaseRoutes.User.Handle("/tokens", api.ApiSessionRequired(createUserAccessToken)).Methods("POST") api.BaseRoutes.User.Handle("/tokens", api.ApiSessionRequired(createUserAccessToken)).Methods("POST")
api.BaseRoutes.User.Handle("/tokens", api.ApiSessionRequired(getUserAccessTokensForUser)).Methods("GET") api.BaseRoutes.User.Handle("/tokens", api.ApiSessionRequired(getUserAccessTokensForUser)).Methods("GET")
api.BaseRoutes.Users.Handle("/tokens", api.ApiSessionRequired(getUserAccessTokens)).Methods("GET") api.BaseRoutes.Users.Handle("/tokens", api.ApiSessionRequired(getUserAccessTokens)).Methods("GET")
api.BaseRoutes.Users.Handle("/tokens/search", api.ApiSessionRequired(searchUserAccessTokens)).Methods("POST")
api.BaseRoutes.Users.Handle("/tokens/{token_id:[A-Za-z0-9]+}", api.ApiSessionRequired(getUserAccessToken)).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/revoke", api.ApiSessionRequired(revokeUserAccessToken)).Methods("POST")
api.BaseRoutes.Users.Handle("/tokens/disable", api.ApiSessionRequired(disableUserAccessToken)).Methods("POST") api.BaseRoutes.Users.Handle("/tokens/disable", api.ApiSessionRequired(disableUserAccessToken)).Methods("POST")
@@ -1241,6 +1242,30 @@ func createUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(accessToken.ToJson())) w.Write([]byte(accessToken.ToJson()))
} }
func searchUserAccessTokens(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
return
}
props := model.UserAccessTokenSearchFromJson(r.Body)
if props == nil {
c.SetInvalidParam("user_access_token_search")
return
}
if len(props.Term) == 0 {
c.SetInvalidParam("term")
return
}
accessTokens, err := c.App.SearchUserAccessTokens(props.Term)
if err != nil {
c.Err = err
return
}
w.Write([]byte(model.UserAccessTokenListToJson(accessTokens)))
}
func getUserAccessTokens(c *Context, w http.ResponseWriter, r *http.Request) { func getUserAccessTokens(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) { if !c.App.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM) c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)

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

@@ -2469,6 +2469,52 @@ func TestGetUserAccessToken(t *testing.T) {
} }
} }
func TestSearchUserAccessToken(t *testing.T) {
th := Setup().InitBasic().InitSystemAdmin()
defer th.TearDown()
Client := th.Client
AdminClient := th.SystemAdminClient
testDescription := "test token"
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.EnableUserAccessTokens = true })
th.App.UpdateUserRoles(th.BasicUser.Id, model.SYSTEM_USER_ROLE_ID+" "+model.SYSTEM_USER_ACCESS_TOKEN_ROLE_ID, false)
token, resp := Client.CreateUserAccessToken(th.BasicUser.Id, testDescription)
CheckNoError(t, resp)
_, resp = Client.SearchUserAccessTokens(&model.UserAccessTokenSearch{Term: token.Id})
CheckForbiddenStatus(t, resp)
rtokens, resp := AdminClient.SearchUserAccessTokens(&model.UserAccessTokenSearch{Term: th.BasicUser.Id})
CheckNoError(t, resp)
if len(rtokens) != 1 {
t.Fatal("should have 1 tokens")
}
rtokens, resp = AdminClient.SearchUserAccessTokens(&model.UserAccessTokenSearch{Term: token.Id})
CheckNoError(t, resp)
if len(rtokens) != 1 {
t.Fatal("should have 1 tokens")
}
rtokens, resp = AdminClient.SearchUserAccessTokens(&model.UserAccessTokenSearch{Term: th.BasicUser.Username})
CheckNoError(t, resp)
if len(rtokens) != 1 {
t.Fatal("should have 1 tokens")
}
rtokens, resp = AdminClient.SearchUserAccessTokens(&model.UserAccessTokenSearch{Term: "not found"})
CheckNoError(t, resp)
if len(rtokens) != 0 {
t.Fatal("should have 0 tokens")
}
}
func TestRevokeUserAccessToken(t *testing.T) { func TestRevokeUserAccessToken(t *testing.T) {
th := Setup().InitBasic().InitSystemAdmin() th := Setup().InitBasic().InitSystemAdmin()
defer th.TearDown() defer th.TearDown()

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

@@ -393,3 +393,15 @@ func (a *App) GetUserAccessToken(tokenId string, sanitize bool) (*model.UserAcce
return token, nil return token, nil
} }
} }
func (a *App) SearchUserAccessTokens(term string) ([]*model.UserAccessToken, *model.AppError) {
if result := <-a.Srv.Store.UserAccessToken().Search(term); result.Err != nil {
return nil, result.Err
} else {
tokens := result.Data.([]*model.UserAccessToken)
for _, token := range tokens {
token.Token = ""
}
return tokens, nil
}
}

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

@@ -6842,6 +6842,10 @@
"id": "store.sql_user_access_token.get_by_user.app_error", "id": "store.sql_user_access_token.get_by_user.app_error",
"translation": "We couldn't get the personal access tokens by user" "translation": "We couldn't get the personal access tokens by user"
}, },
{
"id": "store.sql_user_access_token.search.app_error",
"translation": "We encountered an error searching user access tokens"
},
{ {
"id": "store.sql_user_access_token.save.app_error", "id": "store.sql_user_access_token.save.app_error",
"translation": "We couldn't save the personal access token" "translation": "We couldn't save the personal access token"

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

@@ -1092,6 +1092,16 @@ func (c *Client4) RevokeUserAccessToken(tokenId string) (bool, *Response) {
} }
} }
// SearchUserAccessTokens returns user access tokens matching the provided search term.
func (c *Client4) SearchUserAccessTokens(search *UserAccessTokenSearch) ([]*UserAccessToken, *Response) {
if r, err := c.DoApiPost(c.GetUsersRoute()+"/tokens/search", search.ToJson()); err != nil {
return nil, BuildErrorResponse(r, err)
} else {
defer closeBody(r)
return UserAccessTokenListFromJson(r.Body), BuildResponse(r)
}
}
// DisableUserAccessToken will disable a user access token by id. Must have the // 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 // 'revoke_user_access_token' permission and if disabling for another user, must have the
// 'edit_other_users' permission. // 'edit_other_users' permission.

35
model/user_access_token_search.go Обычный файл
Просмотреть файл

@@ -0,0 +1,35 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
type UserAccessTokenSearch struct {
Term string `json:"term"`
}
// ToJson convert a UserAccessTokenSearch to json string
func (c *UserAccessTokenSearch) ToJson() string {
b, err := json.Marshal(c)
if err != nil {
return ""
}
return string(b)
}
// UserAccessTokenSearchJson decodes the input and returns a UserAccessTokenSearch
func UserAccessTokenSearchFromJson(data io.Reader) *UserAccessTokenSearch {
decoder := json.NewDecoder(data)
var cs UserAccessTokenSearch
err := decoder.Decode(&cs)
if err == nil {
return &cs
}
return nil
}

19
model/user_access_token_search_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,19 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"strings"
"testing"
)
func TestUserAccessTokenSearchJson(t *testing.T) {
userAccessTokenSearch := UserAccessTokenSearch{Term: NewId()}
json := userAccessTokenSearch.ToJson()
ruserAccessTokenSearch := UserAccessTokenSearchFromJson(strings.NewReader(json))
if userAccessTokenSearch.Term != ruserAccessTokenSearch.Term {
t.Fatal("Terms do not match")
}
}

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

@@ -211,6 +211,26 @@ func (s SqlUserAccessTokenStore) GetByUser(userId string, offset, limit int) sto
}) })
} }
func (s SqlUserAccessTokenStore) Search(term string) store.StoreChannel {
return store.Do(func(result *store.StoreResult) {
tokens := []*model.UserAccessToken{}
params := map[string]interface{}{"Term": term + "%"}
query := `
SELECT
uat.*
FROM UserAccessTokens uat
INNER JOIN Users u
ON uat.UserId = u.Id
WHERE uat.Id LIKE :Term OR uat.UserId LIKE :Term OR u.Username LIKE :Term`
if _, err := s.GetReplica().Select(&tokens, query, params); err != nil {
result.Err = model.NewAppError("SqlUserAccessTokenStore.Search", "store.sql_user_access_token.search.app_error", nil, "term="+term+", "+err.Error(), http.StatusInternalServerError)
}
result.Data = tokens
})
}
func (s SqlUserAccessTokenStore) UpdateTokenEnable(tokenId string) store.StoreChannel { func (s SqlUserAccessTokenStore) UpdateTokenEnable(tokenId string) store.StoreChannel {
return store.Do(func(result *store.StoreResult) { 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 { if _, err := s.GetMaster().Exec("UPDATE UserAccessTokens SET IsActive = TRUE WHERE Id = :Id", map[string]interface{}{"Id": tokenId}); err != nil {

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

@@ -449,6 +449,7 @@ type UserAccessTokenStore interface {
GetAll(offset int, limit int) StoreChannel GetAll(offset int, limit int) StoreChannel
GetByToken(tokenString string) StoreChannel GetByToken(tokenString string) StoreChannel
GetByUser(userId string, page, perPage int) StoreChannel GetByUser(userId string, page, perPage int) StoreChannel
Search(term string) StoreChannel
UpdateTokenEnable(tokenId string) StoreChannel UpdateTokenEnable(tokenId string) StoreChannel
UpdateTokenDisable(tokenId string) StoreChannel UpdateTokenDisable(tokenId string) StoreChannel
} }

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

@@ -109,6 +109,22 @@ func (_m *UserAccessTokenStore) GetByUser(userId string, page int, perPage int)
return r0 return r0
} }
// Search provides a mock function with given fields:
func (_m *UserAccessTokenStore) Search(term string) store.StoreChannel {
ret := _m.Called(term)
var r0 store.StoreChannel
if rf, ok := ret.Get(0).(func(string) store.StoreChannel); ok {
r0 = rf(term)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(store.StoreChannel)
}
}
return r0
}
// Save provides a mock function with given fields: token // Save provides a mock function with given fields: token
func (_m *UserAccessTokenStore) Save(token *model.UserAccessToken) store.StoreChannel { func (_m *UserAccessTokenStore) Save(token *model.UserAccessToken) store.StoreChannel {
ret := _m.Called(token) ret := _m.Called(token)

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

@@ -13,6 +13,7 @@ import (
func TestUserAccessTokenStore(t *testing.T, ss store.Store) { func TestUserAccessTokenStore(t *testing.T, ss store.Store) {
t.Run("UserAccessTokenSaveGetDelete", func(t *testing.T) { testUserAccessTokenSaveGetDelete(t, ss) }) t.Run("UserAccessTokenSaveGetDelete", func(t *testing.T) { testUserAccessTokenSaveGetDelete(t, ss) })
t.Run("UserAccessTokenDisableEnable", func(t *testing.T) { testUserAccessTokenDisableEnable(t, ss) }) t.Run("UserAccessTokenDisableEnable", func(t *testing.T) { testUserAccessTokenDisableEnable(t, ss) })
t.Run("UserAccessTokenSearch", func(t *testing.T) { testUserAccessTokenSearch(t, ss) })
} }
func testUserAccessTokenSaveGetDelete(t *testing.T, ss store.Store) { func testUserAccessTokenSaveGetDelete(t *testing.T, ss store.Store) {
@@ -130,3 +131,45 @@ func testUserAccessTokenDisableEnable(t *testing.T, ss store.Store) {
t.Fatal(err) t.Fatal(err)
} }
} }
func testUserAccessTokenSearch(t *testing.T, ss store.Store) {
u1 := model.User{}
u1.Email = model.NewId()
u1.Username = model.NewId()
store.Must(ss.User().Save(&u1))
uat := &model.UserAccessToken{
Token: model.NewId(),
UserId: u1.Id,
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 result := <-ss.UserAccessToken().Search(uat.Id); result.Err != nil {
t.Fatal(result.Err)
} else if received := result.Data.([]*model.UserAccessToken); len(received) != 1 {
t.Fatal("received incorrect number of tokens after search")
}
if result := <-ss.UserAccessToken().Search(uat.UserId); result.Err != nil {
t.Fatal(result.Err)
} else if received := result.Data.([]*model.UserAccessToken); len(received) != 1 {
t.Fatal("received incorrect number of tokens after search")
}
if result := <-ss.UserAccessToken().Search(u1.Username); result.Err != nil {
t.Fatal(result.Err)
} else if received := result.Data.([]*model.UserAccessToken); len(received) != 1 {
t.Fatal("received incorrect number of tokens after search")
}
}