User: Support sort query param for user and org user, search endpoints (#75229)

* User: Add sort option to user search

* Switch to an approach that uses the dashboard search options

* Cable user sort on the org endpoint

* Alias user table with u in org store

* Add test and cover orgs/:orgID/users/search endpoint

* Add test to userimpl store

* Simplify the store_test with sortopts.ParseSortQueryParam

* Account for PR feedback

* Positive check

* Update docs

* Update docs

* Switch to ErrOrFallback

Co-authored-by: Karl Persson <kalle.persson@grafana.com>

---------

Co-authored-by: Karl Persson <kalle.persson@grafana.com>
This commit is contained in:
Gabriel MABILLE
2023-09-28 10:16:18 +02:00
committed by GitHub
parent 4563fc48af
commit 96cbe70b14
13 changed files with 279 additions and 78 deletions

View File

@@ -7,6 +7,7 @@ import (
"github.com/grafana/grafana/pkg/models/roletype"
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/search/model"
"github.com/grafana/grafana/pkg/util/errutil"
)
@@ -178,11 +179,12 @@ type GetOrgUsersQuery struct {
}
type SearchOrgUsersQuery struct {
UserID int64 `xorm:"user_id"`
OrgID int64 `xorm:"org_id"`
Query string
Page int
Limit int
UserID int64 `xorm:"user_id"`
OrgID int64 `xorm:"org_id"`
Query string
Page int
Limit int
SortOpts []model.SortOption
// Flag used to allow oss edition to query users without access control
DontEnforceAccessControl bool

View File

@@ -546,7 +546,7 @@ func (ss *sqlStore) SearchOrgUsers(ctx context.Context, query *org.SearchOrgUser
}
err := ss.db.WithDbSession(ctx, func(dbSession *db.Session) error {
sess := dbSession.Table("org_user")
sess.Join("INNER", ss.dialect.Quote("user"), fmt.Sprintf("org_user.user_id=%s.id", ss.dialect.Quote("user")))
sess.Join("INNER", []string{ss.dialect.Quote("user"), "u"}, "org_user.user_id=u.id")
whereConditions := make([]string, 0)
whereParams := make([]any, 0)
@@ -559,7 +559,7 @@ func (ss *sqlStore) SearchOrgUsers(ctx context.Context, query *org.SearchOrgUser
whereParams = append(whereParams, query.UserID)
}
whereConditions = append(whereConditions, fmt.Sprintf("%s.is_service_account = ?", ss.dialect.Quote("user")))
whereConditions = append(whereConditions, "u.is_service_account = ?")
whereParams = append(whereParams, ss.dialect.BooleanStr(false))
if query.User == nil {
@@ -593,16 +593,25 @@ func (ss *sqlStore) SearchOrgUsers(ctx context.Context, query *org.SearchOrgUser
sess.Cols(
"org_user.org_id",
"org_user.user_id",
"user.email",
"user.name",
"user.login",
"u.email",
"u.name",
"u.login",
"org_user.role",
"user.last_seen_at",
"user.created",
"user.updated",
"user.is_disabled",
"u.last_seen_at",
"u.created",
"u.updated",
"u.is_disabled",
)
sess.Asc("user.email", "user.login")
if len(query.SortOpts) > 0 {
for i := range query.SortOpts {
for j := range query.SortOpts[i].Filter {
sess.OrderBy(query.SortOpts[i].Filter[j].OrderBy())
}
}
} else {
sess.Asc("u.login", "u.email")
}
if err := sess.Find(&result.OrgUsers); err != nil {
return err
@@ -611,7 +620,7 @@ func (ss *sqlStore) SearchOrgUsers(ctx context.Context, query *org.SearchOrgUser
// get total count
orgUser := org.OrgUser{}
countSess := dbSession.Table("org_user").
Join("INNER", ss.dialect.Quote("user"), fmt.Sprintf("org_user.user_id=%s.id", ss.dialect.Quote("user")))
Join("INNER", []string{ss.dialect.Quote("user"), "u"}, "org_user.user_id=u.id")
if len(whereConditions) > 0 {
countSess.Where(strings.Join(whereConditions, " AND "), whereParams...)

View File

@@ -15,6 +15,7 @@ import (
"github.com/grafana/grafana/pkg/services/auth/identity"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/quota/quotaimpl"
"github.com/grafana/grafana/pkg/services/searchusers/sortopts"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest"
"github.com/grafana/grafana/pkg/services/user"
@@ -404,6 +405,42 @@ func TestIntegrationOrgUserDataAccess(t *testing.T) {
require.Equal(t, len(result.OrgUsers), 1)
require.Equal(t, result.OrgUsers[0].Email, ac1.Email)
})
t.Run("Can get organization users with custom ordering login-asc", func(t *testing.T) {
sortOpts, err := sortopts.ParseSortQueryParam("login-asc,email-asc")
require.NoError(t, err)
query := org.SearchOrgUsersQuery{
OrgID: ac1.OrgID,
SortOpts: sortOpts,
User: &user.SignedInUser{
OrgID: ac1.OrgID,
Permissions: map[int64]map[string][]string{ac1.OrgID: {accesscontrol.ActionOrgUsersRead: {accesscontrol.ScopeUsersAll}}},
},
}
result, err := orgUserStore.SearchOrgUsers(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, len(result.OrgUsers), 2)
require.Equal(t, result.OrgUsers[0].Email, ac1.Email)
require.Equal(t, result.OrgUsers[1].Email, ac2.Email)
})
t.Run("Can get organization users with custom ordering login-desc", func(t *testing.T) {
sortOpts, err := sortopts.ParseSortQueryParam("login-desc,email-asc")
require.NoError(t, err)
query := org.SearchOrgUsersQuery{
OrgID: ac1.OrgID,
SortOpts: sortOpts,
User: &user.SignedInUser{
OrgID: ac1.OrgID,
Permissions: map[int64]map[string][]string{ac1.OrgID: {accesscontrol.ActionOrgUsersRead: {accesscontrol.ScopeUsersAll}}},
},
}
result, err := orgUserStore.SearchOrgUsers(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, len(result.OrgUsers), 2)
require.Equal(t, result.OrgUsers[0].Email, ac2.Email)
require.Equal(t, result.OrgUsers[1].Email, ac1.Email)
})
t.Run("Cannot update role so no one is admin user", func(t *testing.T) {
remCmd := org.RemoveOrgUserCommand{OrgID: ac1.OrgID, UserID: ac2.ID, ShouldDeleteOrphanedUser: true}
err := orgUserStore.RemoveOrgUser(context.Background(), &remCmd)