feat: List users by metadata (#10415)

# Which Problems Are Solved

Some users have reported the need of retrieving users given a metadata
key, metadata value or both. This change introduces metadata search
filter on the `ListUsers()` endpoint to allow Zitadel users to search
for user records by metadata.

The changes affect only v2 APIs.

# How the Problems Are Solved

- Add new search filter to `ListUserRequest`: `MetaKey` and `MetaValue`
  - Add SQL indices on metadata key and metadata value
  - Update query to left join `user_metadata` table

# Additional Context

  - Closes #9053
  - Depends on https://github.com/zitadel/zitadel/pull/10567

---------

Co-authored-by: Silvan <27845747+adlerhurst@users.noreply.github.com>
Co-authored-by: Stefan Benz <46600784+stebenz@users.noreply.github.com>

(cherry picked from commit 8df402fb4f)
This commit is contained in:
Marco A.
2025-09-01 18:12:36 +02:00
committed by Livio Spring
parent d7f202d20f
commit 8cf623d5b5
21 changed files with 674 additions and 139 deletions

View File

@@ -366,7 +366,7 @@ var (
"password_set",
"count",
}
usersQuery = `SELECT projections.users14.id,` +
usersQuery = `SELECT DISTINCT projections.users14.id,` +
` projections.users14.creation_date,` +
` projections.users14.change_date,` +
` projections.users14.resource_owner,` +
@@ -390,6 +390,7 @@ var (
` projections.users14_humans.is_phone_verified,` +
` projections.users14_humans.password_change_required,` +
` projections.users14_humans.password_changed,` +
` projections.users14_humans.mfa_init_skipped,` +
` projections.users14_machines.user_id,` +
` projections.users14_machines.name,` +
` projections.users14_machines.description,` +
@@ -399,6 +400,7 @@ var (
` 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` +
` LEFT JOIN projections.users14_machines ON projections.users14.id = projections.users14_machines.user_id AND projections.users14.instance_id = projections.users14_machines.instance_id` +
` LEFT JOIN projections.user_metadata5 ON projections.users14.id = projections.user_metadata5.user_id AND projections.users14.instance_id = projections.user_metadata5.instance_id` +
` LEFT JOIN LATERAL (SELECT ARRAY_AGG(ln.login_name ORDER BY ln.login_name) AS login_names, MAX(CASE WHEN ln.is_primary THEN ln.login_name ELSE NULL END) AS preferred_login_name FROM projections.login_names3 AS ln WHERE ln.user_id = projections.users14.id AND ln.instance_id = projections.users14.instance_id) AS login_names ON TRUE`
usersCols = []string{
"id",
@@ -426,6 +428,7 @@ var (
"is_phone_verified",
"password_change_required",
"password_changed",
"mfa_init_skipped",
// machine
"user_id",
"name",
@@ -992,6 +995,7 @@ func Test_UserPrepares(t *testing.T) {
true,
true,
testNow,
testNow,
// machine
nil,
nil,
@@ -1032,6 +1036,7 @@ func Test_UserPrepares(t *testing.T) {
IsPhoneVerified: true,
PasswordChangeRequired: true,
PasswordChanged: testNow,
MFAInitSkipped: testNow,
},
},
},
@@ -1071,6 +1076,7 @@ func Test_UserPrepares(t *testing.T) {
true,
true,
testNow,
testNow,
// machine
nil,
nil,
@@ -1104,6 +1110,7 @@ func Test_UserPrepares(t *testing.T) {
nil,
nil,
nil,
nil,
// machine
"id",
"name",
@@ -1144,6 +1151,7 @@ func Test_UserPrepares(t *testing.T) {
IsPhoneVerified: true,
PasswordChangeRequired: true,
PasswordChanged: testNow,
MFAInitSkipped: testNow,
},
},
{