fix(api): sorting on list users endpoints (#10750)

# Which Problems Are Solved

#10415 added the possibility to filter users based on metadata. To
prevent duplicate results an sql `DISTINCT` was added. This resulted in
issues if the list was sorted on string columns like `username` or
`displayname`, since they are sorted using `lower`. Using `DISTINCT`
requires the `order by` column to be part of the `SELECT` statement.

# How the Problems Are Solved

Added the order by column to the statement.

# Additional Changes

None

# Additional Context

- relates to #10415
- backport to v4.x

---------

Co-authored-by: Silvan <27845747+adlerhurst@users.noreply.github.com>
(cherry picked from commit 2c0ee0008f)
This commit is contained in:
Livio Spring
2025-09-18 12:17:23 +02:00
parent 3667e0dac9
commit a3c0d53c79
2 changed files with 27 additions and 10 deletions

View File

@@ -634,7 +634,7 @@ func (q *Queries) searchUsers(ctx context.Context, queries *UserSearchQueries, p
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
query, scan := prepareUsersQuery()
query, scan := prepareUsersQuery(queries.SortingColumn)
query = userPermissionCheckV2(ctx, query, permissionCheckV2, queries.Queries)
stmt, args, err := queries.toQuery(query).Where(sq.Eq{
UserInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),
@@ -1270,7 +1270,7 @@ func prepareUserUniqueQuery() (sq.SelectBuilder, func(*sql.Row) (bool, error)) {
}
}
func prepareUsersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) {
func prepareUsersQuery(orderBy Column) (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) {
return sq.Select(
UserIDCol.identifier(),
UserCreationDateCol.identifier(),
@@ -1302,6 +1302,7 @@ func prepareUsersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) {
MachineDescriptionCol.identifier(),
MachineSecretCol.identifier(),
MachineAccessTokenTypeCol.identifier(),
orderBy.orderBy(),
countColumn.identifier()).
Distinct().
From(userTable.identifier()).
@@ -1319,6 +1320,7 @@ func prepareUsersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) {
preferredLoginName := sql.NullString{}
human, machine := sqlHuman{}, sqlMachine{}
var orderByValue any
err := rows.Scan(
&u.ID,
@@ -1354,6 +1356,7 @@ func prepareUsersQuery() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) {
&machine.encodedSecret,
&machine.accessTokenType,
&orderByValue,
&count,
)
if err != nil {

View File

@@ -9,6 +9,7 @@ import (
"regexp"
"testing"
sq "github.com/Masterminds/squirrel"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/text/language"
@@ -396,6 +397,7 @@ var (
` projections.users14_machines.description,` +
` projections.users14_machines.secret,` +
` projections.users14_machines.access_token_type,` +
` projections.users14.id,` +
` COUNT(*) OVER ()` +
` FROM projections.users14` +
` LEFT JOIN projections.users14_humans ON projections.users14.id = projections.users14_humans.user_id AND projections.users14.instance_id = projections.users14_humans.instance_id` +
@@ -435,6 +437,7 @@ var (
"description",
"secret",
"access_token_type",
"id",
"count",
}
countUsersQuery = "SELECT COUNT(*) OVER () FROM projections.users14"
@@ -944,8 +947,10 @@ func Test_UserPrepares(t *testing.T) {
object: (*NotifyUser)(nil),
},
{
name: "prepareUsersQuery no result",
prepare: prepareUsersQuery,
name: "prepareUsersQuery no result",
prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) {
return prepareUsersQuery(UserIDCol)
},
want: want{
sqlExpectations: mockQuery(
regexp.QuoteMeta(usersQuery),
@@ -962,8 +967,10 @@ func Test_UserPrepares(t *testing.T) {
object: &Users{Users: []*User{}},
},
{
name: "prepareUsersQuery one result",
prepare: prepareUsersQuery,
name: "prepareUsersQuery one result",
prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) {
return prepareUsersQuery(UserIDCol)
},
want: want{
sqlExpectations: mockQueries(
regexp.QuoteMeta(usersQuery),
@@ -1002,6 +1009,7 @@ func Test_UserPrepares(t *testing.T) {
nil,
nil,
nil,
"id", // orderBy col
},
},
),
@@ -1043,8 +1051,10 @@ func Test_UserPrepares(t *testing.T) {
},
},
{
name: "prepareUsersQuery multiple results",
prepare: prepareUsersQuery,
name: "prepareUsersQuery multiple results",
prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) {
return prepareUsersQuery(UserIDCol)
},
want: want{
sqlExpectations: mockQueries(
regexp.QuoteMeta(usersQuery),
@@ -1083,6 +1093,7 @@ func Test_UserPrepares(t *testing.T) {
nil,
nil,
nil,
"id", // orderBy col
},
{
"id",
@@ -1117,6 +1128,7 @@ func Test_UserPrepares(t *testing.T) {
"description",
"secret",
domain.OIDCTokenTypeBearer,
"id", // orderBy col
},
},
),
@@ -1176,8 +1188,10 @@ func Test_UserPrepares(t *testing.T) {
},
},
{
name: "prepareUsersQuery sql err",
prepare: prepareUsersQuery,
name: "prepareUsersQuery sql err",
prepare: func() (sq.SelectBuilder, func(*sql.Rows) (*Users, error)) {
return prepareUsersQuery(UserIDCol)
},
want: want{
sqlExpectations: mockQueryErr(
regexp.QuoteMeta(usersQuery),