feat: support filtering users table by login type (#17238)

#15896 Mentions ability to add support for filtering by login type

The issue mentions that backend API support exists but the backend did
not seem to have the support for this filter. So I have added the
ability to filter it.

I also added a corresponding update to readme file to make sure the docs
will correctly showcase this feature
This commit is contained in:
Utsavkumar Lal
2025-04-09 13:59:41 +10:00
committed by GitHub
parent f2d24bc3f4
commit 0e658219b2
10 changed files with 226 additions and 24 deletions
+12
View File
@@ -6824,6 +6824,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 {
+1
View File
@@ -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,
)
+10 -2
View File
@@ -12410,16 +12410,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 {
@@ -12433,6 +12439,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"`
}
@@ -12472,6 +12479,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,
)
+6
View File
@@ -260,6 +260,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
+1
View File
@@ -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
+67 -21
View File
@@ -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,
},
},
},
+1
View File
@@ -307,6 +307,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
+120
View File
@@ -1902,6 +1902,126 @@ func TestGetUsers(t *testing.T) {
require.Len(t, res.Users, 1)
require.Equal(t, res.Users[0].ID, first.UserID)
})
t.Run("LoginTypeNoneFilter", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, nil)
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.LoginTypeNone,
})
require.NoError(t, err)
res, err := client.Users(ctx, codersdk.UsersRequest{
LoginType: []codersdk.LoginType{codersdk.LoginTypeNone},
})
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 := testutil.Context(t, testutil.WaitLong)
filtered := make([]codersdk.User, 0)
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)
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)
res, err := client.Users(ctx, codersdk.UsersRequest{
LoginType: []codersdk.LoginType{codersdk.LoginTypeNone, codersdk.LoginTypeGithub},
})
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 := testutil.Context(t, testutil.WaitLong)
_, 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)
res, err := client.Users(ctx, codersdk.UsersRequest{
Status: codersdk.UserStatusSuspended,
LoginType: []codersdk.LoginType{codersdk.LoginTypeNone, codersdk.LoginTypeGithub},
})
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)
})
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) {
+5 -1
View File
@@ -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
@@ -755,6 +756,9 @@ func (c *Client) Users(ctx context.Context, req UsersRequest) (GetUsersResponse,
if req.SearchQuery != "" {
params = append(params, req.SearchQuery)
}
for _, lt := range req.LoginType {
params = append(params, "login_type:"+string(lt))
}
q.Set("q", strings.Join(params, " "))
r.URL.RawQuery = q.Encode()
},
+3
View File
@@ -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 login using Github:
`login_type:github`
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 to the [LoginType documentation](https://pkg.go.dev/github.com/coder/coder/v2/codersdk#LoginType) for a list of supported values