query: implement SearchUsersByMetadata + test

This commit is contained in:
Marco Ardizzone
2025-08-06 20:44:07 +02:00
parent 984fe58601
commit bb094c27ef
2 changed files with 1057 additions and 0 deletions

View File

@@ -0,0 +1,210 @@
package query
import (
"context"
"database/sql"
"slices"
sq "github.com/Masterminds/squirrel"
"github.com/zitadel/logging"
"golang.org/x/text/language"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/telemetry/tracing"
"github.com/zitadel/zitadel/internal/zerrors"
)
type UsersByMetadataSearchQueries struct {
SearchRequest
Queries []SearchQuery
}
type UserByMetadata struct {
ResourceOwner string `json:"resource_owner,omitempty"`
Key string `json:"key,omitempty"`
Value []byte `json:"value,omitempty"`
User *User `json:"user,omitempty"`
}
type UsersByMetadata struct {
SearchResponse
UsersByMeta []*UserByMetadata
}
func (q *Queries) SearchUsersByMetadata(ctx context.Context, queries *UsersByMetadataSearchQueries, permissionCheck domain.PermissionCheck) (usersByMeta *UsersByMetadata, err error) {
ctx, span := tracing.NewSpan(ctx)
defer func() { span.EndWithError(err) }()
permissionCheckV2 := PermissionV2(ctx, permissionCheck)
query, scan := prepareUserByMetadataListQuery()
query = userPermissionCheckV2(ctx, query, permissionCheckV2, queries.Queries)
for _, q := range queries.Queries {
query = q.toQuery(query)
}
eq := sq.Eq{
UserMetadataInstanceIDCol.identifier(): authz.GetInstance(ctx).InstanceID(),
}
stmt, args, err := queries.toQuery(query).Where(eq).ToSql()
if err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-J1CvYA", "Errors.Query.SQLStatement")
}
err = q.client.QueryContext(ctx, func(rows *sql.Rows) error {
usersByMeta, err = scan(rows)
return err
}, stmt, args...)
if err != nil {
logging.Errorf("Got error: %v", err)
return nil, err
}
if permissionCheck != nil && !authz.GetFeatures(ctx).PermissionCheckV2 {
usersByMetadataCheckPermission(ctx, usersByMeta, permissionCheck)
}
return usersByMeta, nil
}
func prepareUserByMetadataListQuery() (sq.SelectBuilder, func(*sql.Rows) (*UsersByMetadata, error)) {
return sq.Select(
UserMetadataKeyCol.identifier(),
UserMetadataValueCol.identifier(),
UserIDCol.identifier(),
UserStateCol.identifier(),
UserUsernameCol.identifier(),
UserTypeCol.identifier(),
HumanUserIDCol.identifier(),
HumanFirstNameCol.identifier(),
HumanLastNameCol.identifier(),
HumanNickNameCol.identifier(),
HumanDisplayNameCol.identifier(),
HumanPreferredLanguageCol.identifier(),
HumanGenderCol.identifier(),
HumanAvatarURLCol.identifier(),
HumanEmailCol.identifier(),
HumanIsEmailVerifiedCol.identifier(),
HumanPhoneCol.identifier(),
HumanIsPhoneVerifiedCol.identifier(),
HumanPasswordChangeRequiredCol.identifier(),
HumanPasswordChangedCol.identifier(),
HumanMFAInitSkippedCol.identifier(),
MachineUserIDCol.identifier(),
MachineNameCol.identifier(),
MachineDescriptionCol.identifier(),
MachineSecretCol.identifier(),
MachineAccessTokenTypeCol.identifier(),
countColumn.identifier()).
From(userMetadataTable.identifier()).
Join(join(UserIDCol, UserMetadataUserIDCol)).
LeftJoin(join(HumanUserIDCol, UserIDCol)).
LeftJoin(join(MachineUserIDCol, UserIDCol)).
PlaceholderFormat(sq.Dollar),
func(rows *sql.Rows) (*UsersByMetadata, error) {
usersByMeta := make([]*UserByMetadata, 0)
var count uint64
for rows.Next() {
userByMeta := new(UserByMetadata)
userByMeta.User = new(User)
userByMeta.User.Human = new(Human)
userByMeta.User.Machine = new(Machine)
human, machine := sqlHuman{}, sqlMachine{}
err := rows.Scan(
&userByMeta.Key,
&userByMeta.Value,
&userByMeta.User.ID,
&userByMeta.User.State,
&userByMeta.User.Username,
&userByMeta.User.Type,
&human.humanID,
&human.firstName,
&human.lastName,
&human.nickName,
&human.displayName,
&human.preferredLanguage,
&human.gender,
&human.avatarKey,
&human.email,
&human.isEmailVerified,
&human.phone,
&human.isPhoneVerified,
&human.passwordChangeRequired,
&human.passwordChanged,
&human.mfaInitSkipped,
&machine.machineID,
&machine.name,
&machine.description,
&machine.encodedSecret,
&machine.accessTokenType,
&count,
)
if err != nil {
return nil, err
}
if human.humanID.Valid {
userByMeta.User.Human.FirstName = human.firstName.String
userByMeta.User.Human.LastName = human.lastName.String
userByMeta.User.Human.NickName = human.nickName.String
userByMeta.User.Human.DisplayName = human.displayName.String
userByMeta.User.Human.PreferredLanguage, err = language.Parse(human.preferredLanguage.String)
if err != nil {
return nil, err
}
userByMeta.User.Human.Gender = domain.Gender(human.gender.Int32)
userByMeta.User.Human.AvatarKey = human.avatarKey.String
userByMeta.User.Human.Email = domain.EmailAddress(human.email.String)
userByMeta.User.Human.IsEmailVerified = human.isEmailVerified.Bool
userByMeta.User.Human.Phone = domain.PhoneNumber(human.phone.String)
userByMeta.User.Human.IsPhoneVerified = human.isPhoneVerified.Bool
userByMeta.User.Human.PasswordChangeRequired = human.passwordChangeRequired.Bool
userByMeta.User.Human.PasswordChanged = human.passwordChanged.Time
userByMeta.User.Human.MFAInitSkipped = human.mfaInitSkipped.Time
}
if machine.machineID.Valid {
userByMeta.User.Machine.Name = machine.name.String
userByMeta.User.Machine.Description = machine.description.String
userByMeta.User.Machine.EncodedSecret = machine.encodedSecret.String
userByMeta.User.Machine.AccessTokenType = domain.OIDCTokenType(machine.accessTokenType.Int32)
}
usersByMeta = append(usersByMeta, userByMeta)
}
if err := rows.Close(); err != nil {
return nil, zerrors.ThrowInternal(err, "QUERY-hwTK0J", "Errors.Query.CloseRows")
}
return &UsersByMetadata{
UsersByMeta: usersByMeta,
SearchResponse: SearchResponse{
Count: count,
},
}, nil
}
}
func usersByMetadataCheckPermission(ctx context.Context, users *UsersByMetadata, permissionCheck domain.PermissionCheck) {
users.UsersByMeta = slices.DeleteFunc(users.UsersByMeta,
func(user *UserByMetadata) bool {
return userCheckPermission(ctx, user.ResourceOwner, user.User.ID, permissionCheck) != nil
},
)
}