Browse Source

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
Alexander Zobnin 6 years ago
parent
commit
dad894f1cc
4 changed files with 107 additions and 21 deletions
  1. 5 0
      pkg/api/user.go
  2. 33 17
      pkg/models/user.go
  3. 13 4
      pkg/services/sqlstore/user.go
  4. 56 0
      pkg/services/sqlstore/user_test.go

+ 5 - 0
pkg/api/user.go

@@ -28,6 +28,11 @@ func getUserUserProfile(userID int64) Response {
 		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)
 }
 

+ 33 - 17
pkg/models/user.go

@@ -208,29 +208,45 @@ func (user *SignedInUser) HasRole(role RoleType) bool {
 }
 
 type UserProfileDTO struct {
-	Id             int64  `json:"id"`
-	Email          string `json:"email"`
-	Name           string `json:"name"`
-	Login          string `json:"login"`
-	Theme          string `json:"theme"`
-	OrgId          int64  `json:"orgId"`
-	IsGrafanaAdmin bool   `json:"isGrafanaAdmin"`
-	IsDisabled     bool   `json:"isDisabled"`
+	Id             int64    `json:"id"`
+	Email          string   `json:"email"`
+	Name           string   `json:"name"`
+	Login          string   `json:"login"`
+	Theme          string   `json:"theme"`
+	OrgId          int64    `json:"orgId"`
+	IsGrafanaAdmin bool     `json:"isGrafanaAdmin"`
+	IsDisabled     bool     `json:"isDisabled"`
+	AuthModule     []string `json:"authModule"`
 }
 
 type UserSearchHitDTO struct {
-	Id            int64     `json:"id"`
-	Name          string    `json:"name"`
-	Login         string    `json:"login"`
-	Email         string    `json:"email"`
-	AvatarUrl     string    `json:"avatarUrl"`
-	IsAdmin       bool      `json:"isAdmin"`
-	IsDisabled    bool      `json:"isDisabled"`
-	LastSeenAt    time.Time `json:"lastSeenAt"`
-	LastSeenAtAge string    `json:"lastSeenAtAge"`
+	Id            int64                `json:"id"`
+	Name          string               `json:"name"`
+	Login         string               `json:"login"`
+	Email         string               `json:"email"`
+	AvatarUrl     string               `json:"avatarUrl"`
+	IsAdmin       bool                 `json:"isAdmin"`
+	IsDisabled    bool                 `json:"isDisabled"`
+	LastSeenAt    time.Time            `json:"lastSeenAt"`
+	LastSeenAtAge string               `json:"lastSeenAtAge"`
+	AuthModule    AuthModuleConversion `json:"authModule"`
 }
 
 type UserIdDTO struct {
 	Id      int64  `json:"id"`
 	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
+}

+ 13 - 4
pkg/services/sqlstore/user.go

@@ -435,7 +435,15 @@ func SearchUsers(query *models.SearchUsersQuery) error {
 
 	whereConditions := make([]string, 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 {
 		whereConditions = append(whereConditions, "org_id = ?")
@@ -450,7 +458,7 @@ func SearchUsers(query *models.SearchUsersQuery) error {
 	if query.AuthModule != "" {
 		whereConditions = append(
 			whereConditions,
-			`id IN (SELECT user_id
+			`u.id IN (SELECT user_id
 			FROM user_auth
 			WHERE auth_module=?)`,
 		)
@@ -464,14 +472,15 @@ func SearchUsers(query *models.SearchUsersQuery) error {
 
 	offset := query.Limit * (query.Page - 1)
 	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 {
 		return err
 	}
 
 	// get total
 	user := models.User{}
-	countSess := x.Table("user")
+	countSess := x.Table("user").Alias("u")
 
 	if len(whereConditions) > 0 {
 		countSess.Where(strings.Join(whereConditions, " AND "), whereParams...)

+ 56 - 0
pkg/services/sqlstore/user_test.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"fmt"
 	"testing"
+	"time"
 
 	. "github.com/smartystreets/goconvey/convey"
 
@@ -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() {