Commit dad894f1 by Alexander Zobnin Committed by GitHub

API: get list of users with additional auth info (#17305)

* batch disable users

* batch revoke users tokens

* split batch disable user and revoke token

* API: get users with auth info and isExternal flag

* fix tests for batch disable users

* Users: refactor /api/users/search endpoint

* Users: use alias for "user" table

* Chore: add BatchDisableUsers() to the bus

* Users: order user list by id explicitly

* Users: return AuthModule from /api/users/:id endpoint

* Users: do not return unused fields

* Users: fix SearchUsers method after last changes

* User: return auth module as array for future purposes

* User: tests for SearchUsers()

* User: return only latest auth module in SearchUsers()

* User: fix JOIN, get only most recent auth module
parent 40708bef
...@@ -28,6 +28,11 @@ func getUserUserProfile(userID int64) Response { ...@@ -28,6 +28,11 @@ func getUserUserProfile(userID int64) Response {
return Error(500, "Failed to get user", err) return Error(500, "Failed to get user", err)
} }
getAuthQuery := m.GetAuthInfoQuery{UserId: userID}
if err := bus.Dispatch(&getAuthQuery); err == nil {
query.Result.AuthModule = []string{getAuthQuery.Result.AuthModule}
}
return JSON(200, query.Result) return JSON(200, query.Result)
} }
......
...@@ -216,6 +216,7 @@ type UserProfileDTO struct { ...@@ -216,6 +216,7 @@ type UserProfileDTO struct {
OrgId int64 `json:"orgId"` OrgId int64 `json:"orgId"`
IsGrafanaAdmin bool `json:"isGrafanaAdmin"` IsGrafanaAdmin bool `json:"isGrafanaAdmin"`
IsDisabled bool `json:"isDisabled"` IsDisabled bool `json:"isDisabled"`
AuthModule []string `json:"authModule"`
} }
type UserSearchHitDTO struct { type UserSearchHitDTO struct {
...@@ -228,9 +229,24 @@ type UserSearchHitDTO struct { ...@@ -228,9 +229,24 @@ type UserSearchHitDTO struct {
IsDisabled bool `json:"isDisabled"` IsDisabled bool `json:"isDisabled"`
LastSeenAt time.Time `json:"lastSeenAt"` LastSeenAt time.Time `json:"lastSeenAt"`
LastSeenAtAge string `json:"lastSeenAtAge"` LastSeenAtAge string `json:"lastSeenAtAge"`
AuthModule AuthModuleConversion `json:"authModule"`
} }
type UserIdDTO struct { type UserIdDTO struct {
Id int64 `json:"id"` Id int64 `json:"id"`
Message string `json:"message"` Message string `json:"message"`
} }
// implement Conversion interface to define custom field mapping (xorm feature)
type AuthModuleConversion []string
func (auth *AuthModuleConversion) FromDB(data []byte) error {
auth_module := string(data)
*auth = []string{auth_module}
return nil
}
// Just a stub, we don't wanna write to database
func (auth *AuthModuleConversion) ToDB() ([]byte, error) {
return []byte{}, nil
}
...@@ -435,7 +435,15 @@ func SearchUsers(query *models.SearchUsersQuery) error { ...@@ -435,7 +435,15 @@ func SearchUsers(query *models.SearchUsersQuery) error {
whereConditions := make([]string, 0) whereConditions := make([]string, 0)
whereParams := make([]interface{}, 0) whereParams := make([]interface{}, 0)
sess := x.Table("user") sess := x.Table("user").Alias("u")
// Join with only most recent auth module
joinCondition := `(
SELECT id from user_auth
WHERE user_auth.user_id = u.id
ORDER BY user_auth.created DESC `
joinCondition = "user_auth.id=" + joinCondition + dialect.Limit(1) + ")"
sess.Join("LEFT", "user_auth", joinCondition)
if query.OrgId > 0 { if query.OrgId > 0 {
whereConditions = append(whereConditions, "org_id = ?") whereConditions = append(whereConditions, "org_id = ?")
...@@ -450,7 +458,7 @@ func SearchUsers(query *models.SearchUsersQuery) error { ...@@ -450,7 +458,7 @@ func SearchUsers(query *models.SearchUsersQuery) error {
if query.AuthModule != "" { if query.AuthModule != "" {
whereConditions = append( whereConditions = append(
whereConditions, whereConditions,
`id IN (SELECT user_id `u.id IN (SELECT user_id
FROM user_auth FROM user_auth
WHERE auth_module=?)`, WHERE auth_module=?)`,
) )
...@@ -464,14 +472,15 @@ func SearchUsers(query *models.SearchUsersQuery) error { ...@@ -464,14 +472,15 @@ func SearchUsers(query *models.SearchUsersQuery) error {
offset := query.Limit * (query.Page - 1) offset := query.Limit * (query.Page - 1)
sess.Limit(query.Limit, offset) sess.Limit(query.Limit, offset)
sess.Cols("id", "email", "name", "login", "is_admin", "is_disabled", "last_seen_at") sess.Cols("u.id", "u.email", "u.name", "u.login", "u.is_admin", "u.is_disabled", "u.last_seen_at", "user_auth.auth_module")
sess.OrderBy("u.id")
if err := sess.Find(&query.Result.Users); err != nil { if err := sess.Find(&query.Result.Users); err != nil {
return err return err
} }
// get total // get total
user := models.User{} user := models.User{}
countSess := x.Table("user") countSess := x.Table("user").Alias("u")
if len(whereConditions) > 0 { if len(whereConditions) > 0 {
countSess.Where(strings.Join(whereConditions, " AND "), whereParams...) countSess.Where(strings.Join(whereConditions, " AND "), whereParams...)
......
...@@ -4,6 +4,7 @@ import ( ...@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"testing" "testing"
"time"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
...@@ -253,6 +254,61 @@ func TestUserDataAccess(t *testing.T) { ...@@ -253,6 +254,61 @@ func TestUserDataAccess(t *testing.T) {
} }
}) })
}) })
Convey("When searching users", func() {
// Find a user to set tokens on
login := "loginuser0"
// Calling GetUserByAuthInfoQuery on an existing user will populate an entry in the user_auth table
// Make the first log-in during the past
getTime = func() time.Time { return time.Now().AddDate(0, 0, -2) }
query := &models.GetUserByAuthInfoQuery{Login: login, AuthModule: "test1", AuthId: "test1"}
err = GetUserByAuthInfo(query)
getTime = time.Now
So(err, ShouldBeNil)
So(query.Result.Login, ShouldEqual, login)
// Add a second auth module for this user
// Have this module's last log-in be more recent
getTime = func() time.Time { return time.Now().AddDate(0, 0, -1) }
query = &models.GetUserByAuthInfoQuery{Login: login, AuthModule: "test2", AuthId: "test2"}
err = GetUserByAuthInfo(query)
getTime = time.Now
So(err, ShouldBeNil)
So(query.Result.Login, ShouldEqual, login)
Convey("Should return the only most recently used auth_module", func() {
searchUserQuery := &models.SearchUsersQuery{}
err = SearchUsers(searchUserQuery)
So(err, ShouldBeNil)
So(searchUserQuery.Result.Users, ShouldHaveLength, 5)
for _, user := range searchUserQuery.Result.Users {
if user.Login == login {
So(user.AuthModule, ShouldHaveLength, 1)
So(user.AuthModule[0], ShouldEqual, "test2")
}
}
// "log in" again with the first auth module
updateAuthCmd := &models.UpdateAuthInfoCommand{UserId: query.Result.Id, AuthModule: "test1", AuthId: "test1"}
err = UpdateAuthInfo(updateAuthCmd)
So(err, ShouldBeNil)
searchUserQuery = &models.SearchUsersQuery{}
err = SearchUsers(searchUserQuery)
So(err, ShouldBeNil)
for _, user := range searchUserQuery.Result.Users {
if user.Login == login {
So(user.AuthModule, ShouldHaveLength, 1)
So(user.AuthModule[0], ShouldEqual, "test1")
}
}
})
})
}) })
Convey("Given one grafana admin user", func() { Convey("Given one grafana admin user", func() {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment