From d0e840a34abcb81804a833ad5bbf87477b74dd62 Mon Sep 17 00:00:00 2001 From: Utsav Lal Date: Thu, 3 Apr 2025 00:53:03 -0400 Subject: [PATCH 01/12] ability to filter users based on login type --- coderd/database/modelqueries.go | 1 + coderd/database/queries.sql.go | 12 ++++++++++-- coderd/database/queries/users.sql | 6 ++++++ coderd/searchquery/search.go | 1 + coderd/users.go | 1 + 5 files changed, 19 insertions(+), 2 deletions(-) diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 3c437cde293d3..1bf37ce0c09e6 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -395,6 +395,7 @@ func (q *sqlQuerier) GetAuthorizedUsers(ctx context.Context, arg GetUsersParams, arg.CreatedAfter, arg.IncludeSystem, arg.GithubComUserID, + pq.Array(arg.LoginType), arg.OffsetOpt, arg.LimitOpt, ) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index ebc4a0da439c0..5edc20ccd93b2 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -11928,16 +11928,22 @@ WHERE github_com_user_id = $10 ELSE true END + -- Filter by login_type + AND CASE + WHEN cardinality($11 :: login_type[]) > 0 THEN + login_type = ANY($11 :: login_type[]) + ELSE true + END -- End of filters -- Authorize Filter clause will be injected below in GetAuthorizedUsers -- @authorize_filter ORDER BY -- Deterministic and consistent ordering of all users. This is to ensure consistent pagination. - LOWER(username) ASC OFFSET $11 + LOWER(username) ASC OFFSET $12 LIMIT -- A null limit means "no limit", so 0 means return all - NULLIF($12 :: int, 0) + NULLIF($13 :: int, 0) ` type GetUsersParams struct { @@ -11951,6 +11957,7 @@ type GetUsersParams struct { CreatedAfter time.Time `db:"created_after" json:"created_after"` IncludeSystem bool `db:"include_system" json:"include_system"` GithubComUserID int64 `db:"github_com_user_id" json:"github_com_user_id"` + LoginType []LoginType `db:"login_type" json:"login_type"` OffsetOpt int32 `db:"offset_opt" json:"offset_opt"` LimitOpt int32 `db:"limit_opt" json:"limit_opt"` } @@ -11990,6 +11997,7 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUse arg.CreatedAfter, arg.IncludeSystem, arg.GithubComUserID, + pq.Array(arg.LoginType), arg.OffsetOpt, arg.LimitOpt, ) diff --git a/coderd/database/queries/users.sql b/coderd/database/queries/users.sql index c4304cfc3e60e..649f5ae41622e 100644 --- a/coderd/database/queries/users.sql +++ b/coderd/database/queries/users.sql @@ -237,6 +237,12 @@ WHERE github_com_user_id = @github_com_user_id ELSE true END + -- Filter by login_type + AND CASE + WHEN cardinality(@login_type :: login_type[]) > 0 THEN + login_type = ANY(@login_type :: login_type[]) + ELSE true + END -- End of filters -- Authorize Filter clause will be injected below in GetAuthorizedUsers diff --git a/coderd/searchquery/search.go b/coderd/searchquery/search.go index 938f725330cd0..6f4a1c337c535 100644 --- a/coderd/searchquery/search.go +++ b/coderd/searchquery/search.go @@ -88,6 +88,7 @@ func Users(query string) (database.GetUsersParams, []codersdk.ValidationError) { CreatedAfter: parser.Time3339Nano(values, time.Time{}, "created_after"), CreatedBefore: parser.Time3339Nano(values, time.Time{}, "created_before"), GithubComUserID: parser.Int64(values, 0, "github_com_user_id"), + LoginType: httpapi.ParseCustomList(parser, values, []database.LoginType{}, "login_type", httpapi.ParseEnum[database.LoginType]), } parser.ErrorExcessParams(values) return filter, parser.Errors diff --git a/coderd/users.go b/coderd/users.go index 069e1fc240302..eef609a40f607 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -306,6 +306,7 @@ func (api *API) GetUsers(rw http.ResponseWriter, r *http.Request) ([]database.Us CreatedAfter: params.CreatedAfter, CreatedBefore: params.CreatedBefore, GithubComUserID: params.GithubComUserID, + LoginType: params.LoginType, // #nosec G115 - Pagination offsets are small and fit in int32 OffsetOpt: int32(paginationParams.Offset), // #nosec G115 - Pagination limits are small and fit in int32 From 3016e401f183c21f276e2ffa7481167be5a682ce Mon Sep 17 00:00:00 2001 From: Utsav Lal Date: Thu, 3 Apr 2025 00:53:13 -0400 Subject: [PATCH 02/12] update documentation also --- docs/admin/users/index.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/admin/users/index.md b/docs/admin/users/index.md index ed7fbdebd4c5f..cdc22e5877455 100644 --- a/docs/admin/users/index.md +++ b/docs/admin/users/index.md @@ -190,6 +190,8 @@ to use the Coder's filter query: `status:active last_seen_before:"2023-07-01T00:00:00Z"` - To find users who were created between January 1 and January 18, 2023: `created_before:"2023-01-18T00:00:00Z" created_after:"2023-01-01T23:59:59Z"` +- To find users who have login type as github and is a member: + `login_type:github role:member` The following filters are supported: @@ -203,3 +205,4 @@ The following filters are supported: the RFC3339Nano format. - `created_before` and `created_after` - The time a user was created. Uses the RFC3339Nano format. +- `login_type` - Represents the login type of the user. Refer here for all the roles [LoginType documentation](https://pkg.go.dev/github.com/coder/coder/v2/codersdk#LoginType) From de02f49b9ee88ca6c0b53cf26bf63f039f186a49 Mon Sep 17 00:00:00 2001 From: Utsav Lal Date: Thu, 3 Apr 2025 13:00:11 -0400 Subject: [PATCH 03/12] add tests --- coderd/searchquery/search_test.go | 88 +++++++++++++++++++++++-------- coderd/users.go | 2 +- coderd/users_test.go | 25 +++++++++ codersdk/users.go | 6 ++- 4 files changed, 98 insertions(+), 23 deletions(-) diff --git a/coderd/searchquery/search_test.go b/coderd/searchquery/search_test.go index 0a8e08e3d45fe..065937f389e4a 100644 --- a/coderd/searchquery/search_test.go +++ b/coderd/searchquery/search_test.go @@ -386,62 +386,69 @@ func TestSearchUsers(t *testing.T) { Name: "Empty", Query: "", Expected: database.GetUsersParams{ - Status: []database.UserStatus{}, - RbacRole: []string{}, + Status: []database.UserStatus{}, + RbacRole: []string{}, + LoginType: []database.LoginType{}, }, }, { Name: "Username", Query: "user-name", Expected: database.GetUsersParams{ - Search: "user-name", - Status: []database.UserStatus{}, - RbacRole: []string{}, + Search: "user-name", + Status: []database.UserStatus{}, + RbacRole: []string{}, + LoginType: []database.LoginType{}, }, }, { Name: "UsernameWithSpaces", Query: " user-name ", Expected: database.GetUsersParams{ - Search: "user-name", - Status: []database.UserStatus{}, - RbacRole: []string{}, + Search: "user-name", + Status: []database.UserStatus{}, + RbacRole: []string{}, + LoginType: []database.LoginType{}, }, }, { Name: "Username+Param", Query: "usEr-name stAtus:actiVe", Expected: database.GetUsersParams{ - Search: "user-name", - Status: []database.UserStatus{database.UserStatusActive}, - RbacRole: []string{}, + Search: "user-name", + Status: []database.UserStatus{database.UserStatusActive}, + RbacRole: []string{}, + LoginType: []database.LoginType{}, }, }, { Name: "OnlyParams", Query: "status:acTIve sEArch:User-Name role:Owner", Expected: database.GetUsersParams{ - Search: "user-name", - Status: []database.UserStatus{database.UserStatusActive}, - RbacRole: []string{codersdk.RoleOwner}, + Search: "user-name", + Status: []database.UserStatus{database.UserStatusActive}, + RbacRole: []string{codersdk.RoleOwner}, + LoginType: []database.LoginType{}, }, }, { Name: "QuotedParam", Query: `status:SuSpenDeD sEArch:"User Name" role:meMber`, Expected: database.GetUsersParams{ - Search: "user name", - Status: []database.UserStatus{database.UserStatusSuspended}, - RbacRole: []string{codersdk.RoleMember}, + Search: "user name", + Status: []database.UserStatus{database.UserStatusSuspended}, + RbacRole: []string{codersdk.RoleMember}, + LoginType: []database.LoginType{}, }, }, { Name: "QuotedKey", Query: `"status":acTIve "sEArch":User-Name "role":Owner`, Expected: database.GetUsersParams{ - Search: "user-name", - Status: []database.UserStatus{database.UserStatusActive}, - RbacRole: []string{codersdk.RoleOwner}, + Search: "user-name", + Status: []database.UserStatus{database.UserStatusActive}, + RbacRole: []string{codersdk.RoleOwner}, + LoginType: []database.LoginType{}, }, }, { @@ -449,9 +456,48 @@ func TestSearchUsers(t *testing.T) { Name: "QuotedSpecial", Query: `search:"user:name"`, Expected: database.GetUsersParams{ - Search: "user:name", + Search: "user:name", + Status: []database.UserStatus{}, + RbacRole: []string{}, + LoginType: []database.LoginType{}, + }, + }, + { + Name: "LoginType", + Query: "login_type:github", + Expected: database.GetUsersParams{ + Search: "", + Status: []database.UserStatus{}, + RbacRole: []string{}, + LoginType: []database.LoginType{database.LoginTypeGithub}, + }, + }, + { + Name: "MultipleLoginTypesWithSpaces", + Query: "login_type:github login_type:password", + Expected: database.GetUsersParams{ + Search: "", Status: []database.UserStatus{}, RbacRole: []string{}, + LoginType: []database.LoginType{ + database.LoginTypeGithub, + database.LoginTypePassword, + }, + }, + }, + { + Name: "MultipleLoginTypesWithCommas", + Query: "login_type:github,password,none,oidc", + Expected: database.GetUsersParams{ + Search: "", + Status: []database.UserStatus{}, + RbacRole: []string{}, + LoginType: []database.LoginType{ + database.LoginTypeGithub, + database.LoginTypePassword, + database.LoginTypeNone, + database.LoginTypeOIDC, + }, }, }, diff --git a/coderd/users.go b/coderd/users.go index eef609a40f607..3d63888bed65a 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -306,7 +306,7 @@ func (api *API) GetUsers(rw http.ResponseWriter, r *http.Request) ([]database.Us CreatedAfter: params.CreatedAfter, CreatedBefore: params.CreatedBefore, GithubComUserID: params.GithubComUserID, - LoginType: params.LoginType, + LoginType: params.LoginType, // #nosec G115 - Pagination offsets are small and fit in int32 OffsetOpt: int32(paginationParams.Offset), // #nosec G115 - Pagination limits are small and fit in int32 diff --git a/coderd/users_test.go b/coderd/users_test.go index c21eca85a5ee7..d2ada56123fc5 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -1902,6 +1902,31 @@ func TestGetUsers(t *testing.T) { require.Len(t, res.Users, 1) require.Equal(t, res.Users[0].ID, first.UserID) }) + t.Run("LoginType", func(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + client, db := coderdtest.NewWithDatabase(t, nil) + first := coderdtest.CreateFirstUser(t, client) + _ = dbgen.User(t, db, database.User{ + Email: "test2@coder.com", + Username: "test2", + }) + // nolint:gocritic // Unit test + _, err := db.UpdateUserLoginType(dbauthz.AsSystemRestricted(ctx), database.UpdateUserLoginTypeParams{ + UserID: first.UserID, + NewLoginType: database.LoginTypeNone, + }) + require.NoError(t, err) + res, err := client.Users(ctx, codersdk.UsersRequest{ + LoginType: codersdk.LoginTypeNone, + }) + require.NoError(t, err) + require.Len(t, res.Users, 1) + require.Equal(t, res.Users[0].ID, first.UserID) + require.Equal(t, res.Users[0].LoginType, codersdk.LoginTypeGithub) + }) } func TestGetUsersPagination(t *testing.T) { diff --git a/codersdk/users.go b/codersdk/users.go index 31854731a0ae1..14428e15b4d21 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -28,7 +28,8 @@ type UsersRequest struct { // Filter users by status. Status UserStatus `json:"status,omitempty" typescript:"-"` // Filter users that have the given role. - Role string `json:"role,omitempty" typescript:"-"` + Role string `json:"role,omitempty" typescript:"-"` + LoginType LoginType `json:"login_type,omitempty" typescript:"-"` SearchQuery string `json:"q,omitempty"` Pagination @@ -723,6 +724,9 @@ func (c *Client) Users(ctx context.Context, req UsersRequest) (GetUsersResponse, if req.SearchQuery != "" { params = append(params, req.SearchQuery) } + if req.LoginType != "" { + params = append(params, "login_type:"+string(req.LoginType)) + } q.Set("q", strings.Join(params, " ")) r.URL.RawQuery = q.Encode() }, From 241c898a2034bbca5b0c54c22620737ee2cab877 Mon Sep 17 00:00:00 2001 From: Utsav Lal Date: Thu, 3 Apr 2025 23:41:06 -0400 Subject: [PATCH 04/12] change test case --- coderd/users_test.go | 58 ++++++++++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 23 deletions(-) diff --git a/coderd/users_test.go b/coderd/users_test.go index d2ada56123fc5..faf1567a5f5a4 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -1902,31 +1902,43 @@ func TestGetUsers(t *testing.T) { require.Len(t, res.Users, 1) require.Equal(t, res.Users[0].ID, first.UserID) }) - t.Run("LoginType", func(t *testing.T) { - t.Parallel() - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() +} - client, db := coderdtest.NewWithDatabase(t, nil) - first := coderdtest.CreateFirstUser(t, client) - _ = dbgen.User(t, db, database.User{ - Email: "test2@coder.com", - Username: "test2", - }) - // nolint:gocritic // Unit test - _, err := db.UpdateUserLoginType(dbauthz.AsSystemRestricted(ctx), database.UpdateUserLoginTypeParams{ - UserID: first.UserID, - NewLoginType: database.LoginTypeNone, - }) - require.NoError(t, err) - res, err := client.Users(ctx, codersdk.UsersRequest{ - LoginType: codersdk.LoginTypeNone, - }) - require.NoError(t, err) - require.Len(t, res.Users, 1) - require.Equal(t, res.Users[0].ID, first.UserID) - require.Equal(t, res.Users[0].LoginType, codersdk.LoginTypeGithub) +func TestGetUsersFilters(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + first := coderdtest.CreateFirstUser(t, client) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + // Create a user with a specific role + _, err := client.User(ctx, first.UserID.String()) + require.NoError(t, err, "") + + _, err = client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ + Email: "alice@email.com", + Username: "alice", + Password: "MySecurePassword!", + OrganizationIDs: []uuid.UUID{first.OrganizationID}, + UserLoginType: codersdk.LoginTypePassword, + }) + require.NoError(t, err) + + _, err = client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ + Email: "alice123@email.com", + Username: "alice123", + OrganizationIDs: []uuid.UUID{first.OrganizationID}, + UserLoginType: codersdk.LoginTypeGithub, + }) + require.NoError(t, err) + + // Test filtering by role + res, err := client.Users(ctx, codersdk.UsersRequest{ + LoginType: "password", // Ensure we're filtering by login type }) + require.NoError(t, err, "should not error when filtering by role") + require.Len(t, res.Users, 1, "should find one user with the member role") + require.Equal(t, res.Users[0].Username, "alice", "should return the correct user with member role") } func TestGetUsersPagination(t *testing.T) { From 5ddc42af755e722d731f9c88eee4eb3651fd99aa Mon Sep 17 00:00:00 2001 From: Utsav Lal Date: Mon, 7 Apr 2025 01:06:02 -0400 Subject: [PATCH 05/12] filter by multiple login types --- codersdk/users.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/codersdk/users.go b/codersdk/users.go index 14428e15b4d21..7cbaaa49518d3 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -28,8 +28,8 @@ type UsersRequest struct { // Filter users by status. Status UserStatus `json:"status,omitempty" typescript:"-"` // Filter users that have the given role. - Role string `json:"role,omitempty" typescript:"-"` - LoginType LoginType `json:"login_type,omitempty" typescript:"-"` + Role string `json:"role,omitempty" typescript:"-"` + LoginType []LoginType `json:"login_type,omitempty" typescript:"-"` SearchQuery string `json:"q,omitempty"` Pagination @@ -724,8 +724,10 @@ func (c *Client) Users(ctx context.Context, req UsersRequest) (GetUsersResponse, if req.SearchQuery != "" { params = append(params, req.SearchQuery) } - if req.LoginType != "" { - params = append(params, "login_type:"+string(req.LoginType)) + if len(req.LoginType) > 0 { + for _, lt := range req.LoginType { + params = append(params, "login_type:"+string(lt)) + } } q.Set("q", strings.Join(params, " ")) r.URL.RawQuery = q.Encode() From f73e1f23c3816c8bc6a06a5d5eaad79d53d91277 Mon Sep 17 00:00:00 2001 From: Utsav Lal Date: Mon, 7 Apr 2025 01:06:28 -0400 Subject: [PATCH 06/12] add filter by logintype to mock db --- coderd/database/dbmem/dbmem.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index bfae69fa68b98..b82f3d03f1c27 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -6796,6 +6796,18 @@ func (q *FakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams users = usersFilteredByRole } + if len(params.LoginType) > 0 { + usersFilteredByLoginType := make([]database.User, 0, len(users)) + for i, user := range users { + if slice.ContainsCompare(params.LoginType, user.LoginType, func(a, b database.LoginType) bool { + return strings.EqualFold(string(a), string(b)) + }) { + usersFilteredByLoginType = append(usersFilteredByLoginType, users[i]) + } + } + users = usersFilteredByLoginType + } + if !params.CreatedBefore.IsZero() { usersFilteredByCreatedAt := make([]database.User, 0, len(users)) for i, user := range users { From b649181eedd044785050dd0a2f4c2c751b93e4af Mon Sep 17 00:00:00 2001 From: Utsav Lal Date: Mon, 7 Apr 2025 01:06:39 -0400 Subject: [PATCH 07/12] update tests --- coderd/users_test.go | 121 ++++++++++++++++++++++++++++++++----------- 1 file changed, 92 insertions(+), 29 deletions(-) diff --git a/coderd/users_test.go b/coderd/users_test.go index faf1567a5f5a4..0ac1e5cbb99bc 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -1902,43 +1902,106 @@ func TestGetUsers(t *testing.T) { require.Len(t, res.Users, 1) require.Equal(t, res.Users[0].ID, first.UserID) }) -} -func TestGetUsersFilters(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - first := coderdtest.CreateFirstUser(t, client) - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() + t.Run("LoginTypeNoneFilter", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + first := coderdtest.CreateFirstUser(t, client) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() - // Create a user with a specific role - _, err := client.User(ctx, first.UserID.String()) - require.NoError(t, err, "") + _, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ + Email: "bob@email.com", + Username: "bob", + OrganizationIDs: []uuid.UUID{first.OrganizationID}, + UserLoginType: codersdk.LoginTypeNone, + }) + require.NoError(t, err) - _, err = client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ - Email: "alice@email.com", - Username: "alice", - Password: "MySecurePassword!", - OrganizationIDs: []uuid.UUID{first.OrganizationID}, - UserLoginType: codersdk.LoginTypePassword, + // Test filtering by role + res, err := client.Users(ctx, codersdk.UsersRequest{ + LoginType: []codersdk.LoginType{codersdk.LoginTypeNone}, + }) + require.NoError(t, err, "should not error when filtering by role") + require.Len(t, res.Users, 1, "should find one user with the member role") + require.Equal(t, res.Users[0].Username, "bob", "should return the correct user with member role") }) - require.NoError(t, err) - _, err = client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ - Email: "alice123@email.com", - Username: "alice123", - OrganizationIDs: []uuid.UUID{first.OrganizationID}, - UserLoginType: codersdk.LoginTypeGithub, + t.Run("LoginTypeMultipleFilter", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + first := coderdtest.CreateFirstUser(t, client) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + _, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ + Email: "bob@email.com", + Username: "bob", + OrganizationIDs: []uuid.UUID{first.OrganizationID}, + UserLoginType: codersdk.LoginTypeNone, + }) + require.NoError(t, err) + + _, err = client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ + Email: "charlie@email.com", + Username: "charlie", + OrganizationIDs: []uuid.UUID{first.OrganizationID}, + UserLoginType: codersdk.LoginTypeGithub, + }) + require.NoError(t, err) + + // Test filtering by role + res, err := client.Users(ctx, codersdk.UsersRequest{ + LoginType: []codersdk.LoginType{codersdk.LoginTypeNone, codersdk.LoginTypeGithub}, // Ensure we're filtering by login type + }) + require.NoError(t, err, "should not error when filtering by role") + require.Len(t, res.Users, 2, "should find two users with the specified login types") + usernames := make(map[string]bool) + for _, user := range res.Users { + usernames[user.Username] = true + } + require.True(t, usernames["bob"], "should return the correct user with login type none") + require.True(t, usernames["charlie"], "should return the correct user with login type github") + // Ensure that the user with login type none is indeed in the result + for _, user := range res.Users { + if user.Username == "bob" { + require.Equal(t, user.LoginType, codersdk.LoginTypeNone, "bob should have login type none") + } + if user.Username == "charlie" { + require.Equal(t, user.LoginType, codersdk.LoginTypeGithub, "charlie should have login type github") + } + } }) - require.NoError(t, err) - // Test filtering by role - res, err := client.Users(ctx, codersdk.UsersRequest{ - LoginType: "password", // Ensure we're filtering by login type + t.Run("DormantUserWithLoginTypeNone", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + first := coderdtest.CreateFirstUser(t, client) + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) + defer cancel() + + _, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ + Email: "bob@email.com", + Username: "bob", + OrganizationIDs: []uuid.UUID{first.OrganizationID}, + UserLoginType: codersdk.LoginTypeNone, + }) + require.NoError(t, err) + + _, err = client.UpdateUserStatus(ctx, "bob", codersdk.UserStatusSuspended) + require.NoError(t, err, "should set bob's status to dormant") + + // Test filtering by role + res, err := client.Users(ctx, codersdk.UsersRequest{ + Status: codersdk.UserStatusSuspended, + LoginType: []codersdk.LoginType{codersdk.LoginTypeNone, codersdk.LoginTypeGithub}, + }) + require.NoError(t, err, "should not error when filtering by role") + require.Len(t, res.Users, 1, "should find one dormant user with the specified login type") + require.Equal(t, res.Users[0].Username, "bob", "should return the correct dormant user with login type none") + require.Equal(t, res.Users[0].Status, codersdk.UserStatusSuspended, "bob should be in dormant status") + require.Equal(t, res.Users[0].LoginType, codersdk.LoginTypeNone, "bob should have login type none") }) - require.NoError(t, err, "should not error when filtering by role") - require.Len(t, res.Users, 1, "should find one user with the member role") - require.Equal(t, res.Users[0].Username, "alice", "should return the correct user with member role") } func TestGetUsersPagination(t *testing.T) { From 15407e0d3b89d5982424b399f144816cd88056f1 Mon Sep 17 00:00:00 2001 From: Utsav Lal Date: Mon, 7 Apr 2025 10:18:23 -0400 Subject: [PATCH 08/12] cleanup tests --- coderd/users_test.go | 59 ++++++++++++++++---------------------------- 1 file changed, 21 insertions(+), 38 deletions(-) diff --git a/coderd/users_test.go b/coderd/users_test.go index 0ac1e5cbb99bc..e09f4402a808f 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -1907,8 +1907,7 @@ func TestGetUsers(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) first := coderdtest.CreateFirstUser(t, client) - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() + ctx := testutil.Context(t, testutil.WaitLong) _, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ Email: "bob@email.com", @@ -1918,67 +1917,52 @@ func TestGetUsers(t *testing.T) { }) require.NoError(t, err) - // Test filtering by role res, err := client.Users(ctx, codersdk.UsersRequest{ LoginType: []codersdk.LoginType{codersdk.LoginTypeNone}, }) - require.NoError(t, err, "should not error when filtering by role") - require.Len(t, res.Users, 1, "should find one user with the member role") - require.Equal(t, res.Users[0].Username, "bob", "should return the correct user with member role") + require.NoError(t, err) + require.Len(t, res.Users, 1) + require.Equal(t, res.Users[0].LoginType, codersdk.LoginTypeNone) }) t.Run("LoginTypeMultipleFilter", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) first := coderdtest.CreateFirstUser(t, client) - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() + ctx := testutil.Context(t, testutil.WaitLong) + filtered := make([]codersdk.User, 0) - _, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ + bob, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ Email: "bob@email.com", Username: "bob", OrganizationIDs: []uuid.UUID{first.OrganizationID}, UserLoginType: codersdk.LoginTypeNone, }) require.NoError(t, err) + filtered = append(filtered, bob) - _, err = client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ + charlie, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ Email: "charlie@email.com", Username: "charlie", OrganizationIDs: []uuid.UUID{first.OrganizationID}, UserLoginType: codersdk.LoginTypeGithub, }) require.NoError(t, err) + filtered = append(filtered, charlie) - // Test filtering by role res, err := client.Users(ctx, codersdk.UsersRequest{ - LoginType: []codersdk.LoginType{codersdk.LoginTypeNone, codersdk.LoginTypeGithub}, // Ensure we're filtering by login type + LoginType: []codersdk.LoginType{codersdk.LoginTypeNone, codersdk.LoginTypeGithub}, }) - require.NoError(t, err, "should not error when filtering by role") - require.Len(t, res.Users, 2, "should find two users with the specified login types") - usernames := make(map[string]bool) - for _, user := range res.Users { - usernames[user.Username] = true - } - require.True(t, usernames["bob"], "should return the correct user with login type none") - require.True(t, usernames["charlie"], "should return the correct user with login type github") - // Ensure that the user with login type none is indeed in the result - for _, user := range res.Users { - if user.Username == "bob" { - require.Equal(t, user.LoginType, codersdk.LoginTypeNone, "bob should have login type none") - } - if user.Username == "charlie" { - require.Equal(t, user.LoginType, codersdk.LoginTypeGithub, "charlie should have login type github") - } - } + require.NoError(t, err) + require.Len(t, res.Users, 2) + require.ElementsMatch(t, filtered, res.Users) }) t.Run("DormantUserWithLoginTypeNone", func(t *testing.T) { t.Parallel() client := coderdtest.New(t, nil) first := coderdtest.CreateFirstUser(t, client) - ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) - defer cancel() + ctx := testutil.Context(t, testutil.WaitLong) _, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ Email: "bob@email.com", @@ -1989,18 +1973,17 @@ func TestGetUsers(t *testing.T) { require.NoError(t, err) _, err = client.UpdateUserStatus(ctx, "bob", codersdk.UserStatusSuspended) - require.NoError(t, err, "should set bob's status to dormant") + require.NoError(t, err) - // Test filtering by role res, err := client.Users(ctx, codersdk.UsersRequest{ Status: codersdk.UserStatusSuspended, LoginType: []codersdk.LoginType{codersdk.LoginTypeNone, codersdk.LoginTypeGithub}, }) - require.NoError(t, err, "should not error when filtering by role") - require.Len(t, res.Users, 1, "should find one dormant user with the specified login type") - require.Equal(t, res.Users[0].Username, "bob", "should return the correct dormant user with login type none") - require.Equal(t, res.Users[0].Status, codersdk.UserStatusSuspended, "bob should be in dormant status") - require.Equal(t, res.Users[0].LoginType, codersdk.LoginTypeNone, "bob should have login type none") + require.NoError(t, err) + require.Len(t, res.Users, 1) + require.Equal(t, res.Users[0].Username, "bob") + require.Equal(t, res.Users[0].Status, codersdk.UserStatusSuspended) + require.Equal(t, res.Users[0].LoginType, codersdk.LoginTypeNone) }) } From 38825d8fc2987bcff714c1b5922a41585a75faba Mon Sep 17 00:00:00 2001 From: Utsav Lal Date: Tue, 8 Apr 2025 00:24:34 -0400 Subject: [PATCH 09/12] remove redundant len check --- codersdk/users.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/codersdk/users.go b/codersdk/users.go index 3e37c6c6b487b..e32978c5a839e 100644 --- a/codersdk/users.go +++ b/codersdk/users.go @@ -751,10 +751,8 @@ func (c *Client) Users(ctx context.Context, req UsersRequest) (GetUsersResponse, if req.SearchQuery != "" { params = append(params, req.SearchQuery) } - if len(req.LoginType) > 0 { - for _, lt := range req.LoginType { - params = append(params, "login_type:"+string(lt)) - } + for _, lt := range req.LoginType { + params = append(params, "login_type:"+string(lt)) } q.Set("q", strings.Join(params, " ")) r.URL.RawQuery = q.Encode() From 16e9e45aba2bfdb82e5de6b82f59ac0a9882f1e2 Mon Sep 17 00:00:00 2001 From: Utsav Lal Date: Tue, 8 Apr 2025 00:24:48 -0400 Subject: [PATCH 10/12] add test to filter 1 user among many --- coderd/users_test.go | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/coderd/users_test.go b/coderd/users_test.go index bb3583dd75c0e..e32b6d0c5b927 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -1985,6 +1985,43 @@ func TestGetUsers(t *testing.T) { require.Equal(t, res.Users[0].Status, codersdk.UserStatusSuspended) require.Equal(t, res.Users[0].LoginType, codersdk.LoginTypeNone) }) + + t.Run("LoginTypeOidcFromMultipleUser", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{ + OIDCConfig: &coderd.OIDCConfig{ + AllowSignups: true, + }, + }) + first := coderdtest.CreateFirstUser(t, client) + ctx := testutil.Context(t, testutil.WaitLong) + + _, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ + Email: "bob@email.com", + Username: "bob", + OrganizationIDs: []uuid.UUID{first.OrganizationID}, + UserLoginType: codersdk.LoginTypeOIDC, + }) + require.NoError(t, err) + + for i := range 5 { + _, err := client.CreateUserWithOrgs(ctx, codersdk.CreateUserRequestWithOrgs{ + Email: fmt.Sprintf("%d@coder.com", i), + Username: fmt.Sprintf("user%d", i), + OrganizationIDs: []uuid.UUID{first.OrganizationID}, + UserLoginType: codersdk.LoginTypeNone, + }) + require.NoError(t, err) + } + + res, err := client.Users(ctx, codersdk.UsersRequest{ + LoginType: []codersdk.LoginType{codersdk.LoginTypeOIDC}, + }) + require.NoError(t, err) + require.Len(t, res.Users, 1) + require.Equal(t, res.Users[0].Username, "bob") + require.Equal(t, res.Users[0].LoginType, codersdk.LoginTypeOIDC) + }) } func TestGetUsersPagination(t *testing.T) { From f524144a1a660aa54bad825692d9bb9a15f67b53 Mon Sep 17 00:00:00 2001 From: Utsav Lal Date: Tue, 8 Apr 2025 09:17:45 -0400 Subject: [PATCH 11/12] doc update --- docs/admin/users/index.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/admin/users/index.md b/docs/admin/users/index.md index cdc22e5877455..01b53edbca6d3 100644 --- a/docs/admin/users/index.md +++ b/docs/admin/users/index.md @@ -190,8 +190,8 @@ to use the Coder's filter query: `status:active last_seen_before:"2023-07-01T00:00:00Z"` - To find users who were created between January 1 and January 18, 2023: `created_before:"2023-01-18T00:00:00Z" created_after:"2023-01-01T23:59:59Z"` -- To find users who have login type as github and is a member: - `login_type:github role:member` +- To find users who login using github: + `login_type:github` The following filters are supported: @@ -205,4 +205,4 @@ The following filters are supported: the RFC3339Nano format. - `created_before` and `created_after` - The time a user was created. Uses the RFC3339Nano format. -- `login_type` - Represents the login type of the user. Refer here for all the roles [LoginType documentation](https://pkg.go.dev/github.com/coder/coder/v2/codersdk#LoginType) +- `login_type` - Represents the login type of the user. Refer to the [LoginType documentation](https://pkg.go.dev/github.com/coder/coder/v2/codersdk#LoginType) for a list of supported values From 6f2fc597fe115c95dffe61d212cf42b787e175c5 Mon Sep 17 00:00:00 2001 From: Utsav Lal Date: Tue, 8 Apr 2025 09:58:34 -0400 Subject: [PATCH 12/12] capitalize correctly --- docs/admin/users/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/admin/users/index.md b/docs/admin/users/index.md index 01b53edbca6d3..af26f4bb62a2b 100644 --- a/docs/admin/users/index.md +++ b/docs/admin/users/index.md @@ -190,7 +190,7 @@ to use the Coder's filter query: `status:active last_seen_before:"2023-07-01T00:00:00Z"` - To find users who were created between January 1 and January 18, 2023: `created_before:"2023-01-18T00:00:00Z" created_after:"2023-01-01T23:59:59Z"` -- To find users who login using github: +- To find users who login using Github: `login_type:github` The following filters are supported: