diff --git a/internal/api/grpc/user/query.go b/internal/api/grpc/user/query.go index 81fc738e9..af06c4660 100644 --- a/internal/api/grpc/user/query.go +++ b/internal/api/grpc/user/query.go @@ -40,6 +40,8 @@ func UserQueryToQuery(query *user_pb.SearchQuery) (query.SearchQuery, error) { return LoginNameQueryToQuery(q.LoginNameQuery) case *user_pb.SearchQuery_ResourceOwner: return ResourceOwnerQueryToQuery(q.ResourceOwner) + case *user_pb.SearchQuery_InUserIdsQuery: + return InUserIdsQueryToQuery(q.InUserIdsQuery) default: return nil, errors.ThrowInvalidArgument(nil, "GRPC-vR9nC", "List.Query.Invalid") } @@ -84,3 +86,7 @@ func LoginNameQueryToQuery(q *user_pb.LoginNameQuery) (query.SearchQuery, error) func ResourceOwnerQueryToQuery(q *user_pb.ResourceOwnerQuery) (query.SearchQuery, error) { return query.NewUserResourceOwnerSearchQuery(q.OrgID, query.TextEquals) } + +func InUserIdsQueryToQuery(q *user_pb.InUserIDQuery) (query.SearchQuery, error) { + return query.NewUserInUserIdsSearchQuery(q.UserIds) +} diff --git a/internal/query/search_query.go b/internal/query/search_query.go index 16d37502d..63dd6739f 100644 --- a/internal/query/search_query.go +++ b/internal/query/search_query.go @@ -156,6 +156,10 @@ const ( columnCompareMax ) +type InTextQuery struct { + Column Column + Values []string +} type TextQuery struct { Column Column Text string @@ -167,8 +171,22 @@ var ( ErrInvalidCompare = errors.New("invalid compare") ErrMissingColumn = errors.New("missing column") ErrInvalidNumber = errors.New("value is no number") + ErrEmptyValues = errors.New("values array must not be empty") ) +func NewInTextQuery(col Column, values []string) (*InTextQuery, error) { + if len(values) == 0 { + return nil, ErrEmptyValues + } + if col.isZero() { + return nil, ErrMissingColumn + } + return &InTextQuery{ + Column: col, + Values: values, + }, nil +} + func NewTextQuery(col Column, value string, compare TextComparison) (*TextQuery, error) { if compare < 0 || compare >= textCompareMax { return nil, ErrInvalidCompare @@ -183,6 +201,15 @@ func NewTextQuery(col Column, value string, compare TextComparison) (*TextQuery, }, nil } +func (q *InTextQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder { + return query.Where(q.comp()) +} + +func (s *InTextQuery) comp() sq.Sqlizer { + // This translates to an IN query + return sq.Eq{s.Column.identifier(): s.Values} +} + func (q *TextQuery) toQuery(query sq.SelectBuilder) sq.SelectBuilder { return query.Where(q.comp()) } @@ -269,7 +296,7 @@ func NewNumberQuery(c Column, value interface{}, compare NumberComparison) (*Num } switch reflect.TypeOf(value).Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64: - //everything fine + // everything fine default: return nil, ErrInvalidNumber } diff --git a/internal/query/search_query_test.go b/internal/query/search_query_test.go index db74c7c04..531656565 100644 --- a/internal/query/search_query_test.go +++ b/internal/query/search_query_test.go @@ -1382,3 +1382,118 @@ func TestNumberComparisonFromMethod(t *testing.T) { }) } } + +func TestNewInTextQuery(t *testing.T) { + type args struct { + column Column + value []string + } + tests := []struct { + name string + args args + want *InTextQuery + wantErr func(error) bool + }{ + { + name: "empty values", + args: args{ + column: testCol, + value: []string{}, + }, + wantErr: func(err error) bool { + return errors.Is(err, ErrEmptyValues) + }, + }, + { + name: "no column", + args: args{ + column: Column{}, + value: []string{"adler", "hurst"}, + }, + wantErr: func(err error) bool { + return errors.Is(err, ErrMissingColumn) + }, + }, + { + name: "no column name", + args: args{ + column: testNoCol, + value: []string{"adler", "hurst"}, + }, + wantErr: func(err error) bool { + return errors.Is(err, ErrMissingColumn) + }, + }, + { + name: "correct", + args: args{ + column: testCol, + value: []string{"adler", "hurst"}, + }, + want: &InTextQuery{ + Column: testCol, + Values: []string{"adler", "hurst"}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := NewInTextQuery(tt.args.column, tt.args.value) + if err != nil && tt.wantErr == nil { + t.Errorf("NewTextQuery() no error expected got %v", err) + return + } else if tt.wantErr != nil && !tt.wantErr(err) { + t.Errorf("NewTextQuery() unexpeted error = %v", err) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewTextQuery() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestInTextQuery_comp(t *testing.T) { + type fields struct { + Column Column + Values []string + } + type want struct { + query interface{} + isNil bool + } + tests := []struct { + name string + fields fields + want want + }{ + { + name: "equals", + fields: fields{ + Column: testCol, + Values: []string{"Adler", "Hurst"}, + }, + want: want{ + query: sq.Eq{"test_table.test_col": []string{"Adler", "Hurst"}}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := &InTextQuery{ + Column: tt.fields.Column, + Values: tt.fields.Values, + } + query := s.comp() + if query == nil && tt.want.isNil { + return + } else if tt.want.isNil && query != nil { + t.Error("query should not be nil") + } + + if !reflect.DeepEqual(query, tt.want.query) { + t.Errorf("wrong query: want: %v, (%T), got: %v, (%T)", tt.want.query, tt.want.query, query, query) + } + }) + } +} diff --git a/internal/query/user.go b/internal/query/user.go index 46a683e67..c335129ed 100644 --- a/internal/query/user.go +++ b/internal/query/user.go @@ -605,6 +605,10 @@ func (r *UserSearchQueries) AppendMyResourceOwnerQuery(orgID string) error { return nil } +func NewUserInUserIdsSearchQuery(values []string) (SearchQuery, error) { + return NewInTextQuery(UserIDCol, values) +} + func NewUserResourceOwnerSearchQuery(value string, comparison TextComparison) (SearchQuery, error) { return NewTextQuery(UserResourceOwnerCol, value, comparison) } diff --git a/proto/zitadel/user.proto b/proto/zitadel/user.proto index bbe4f0be5..493db3aca 100644 --- a/proto/zitadel/user.proto +++ b/proto/zitadel/user.proto @@ -184,9 +184,19 @@ message SearchQuery { StateQuery state_query = 7; TypeQuery type_query = 8; LoginNameQuery login_name_query = 9; + InUserIDQuery in_user_ids_query = 10; } } +message InUserIDQuery { + repeated string user_ids = 1 [ + (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_field) = { + description: "the ids of the users to include" + example: "[\"69629023906488334\",\"69622366012355662\"]"; + } + ]; +} + message UserNameQuery { string user_name = 1 [ (validate.rules).string = {max_len: 200},