mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
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:
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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()
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user