diff --git a/pkg/api/user.go b/pkg/api/user.go index cde5b7b92fd..2d82b60ba33 100644 --- a/pkg/api/user.go +++ b/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) } diff --git a/pkg/models/user.go b/pkg/models/user.go index 58c2336f5a4..a9032f1a8d8 100644 --- a/pkg/models/user.go +++ b/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 +} diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 6b40bd3c307..e571c002f8b 100644 --- a/pkg/services/sqlstore/user.go +++ b/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...) diff --git a/pkg/services/sqlstore/user_test.go b/pkg/services/sqlstore/user_test.go index 5968101163e..227e90f86c2 100644 --- a/pkg/services/sqlstore/user_test.go +++ b/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() {