diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index 7f974aa1a76a2..7b19790a6d8ea 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -5800,6 +5800,26 @@ func (q *FakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams users = usersFilteredByRole } + if !params.CreatedBefore.IsZero() { + usersFilteredByCreatedAt := make([]database.User, 0, len(users)) + for i, user := range users { + if user.CreatedAt.Before(params.CreatedBefore) { + usersFilteredByCreatedAt = append(usersFilteredByCreatedAt, users[i]) + } + } + users = usersFilteredByCreatedAt + } + + if !params.CreatedAfter.IsZero() { + usersFilteredByCreatedAt := make([]database.User, 0, len(users)) + for i, user := range users { + if user.CreatedAt.After(params.CreatedAfter) { + usersFilteredByCreatedAt = append(usersFilteredByCreatedAt, users[i]) + } + } + users = usersFilteredByCreatedAt + } + if !params.LastSeenBefore.IsZero() { usersFilteredByLastSeen := make([]database.User, 0, len(users)) for i, user := range users { diff --git a/coderd/database/modelqueries.go b/coderd/database/modelqueries.go index 2a61f339398f2..78f6285e3c11a 100644 --- a/coderd/database/modelqueries.go +++ b/coderd/database/modelqueries.go @@ -391,6 +391,8 @@ func (q *sqlQuerier) GetAuthorizedUsers(ctx context.Context, arg GetUsersParams, pq.Array(arg.RbacRole), arg.LastSeenBefore, arg.LastSeenAfter, + arg.CreatedBefore, + arg.CreatedAfter, arg.OffsetOpt, arg.LimitOpt, ) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index fa448d35f0b8e..1a7911bc64b4d 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -10404,16 +10404,27 @@ WHERE last_seen_at >= $6 ELSE true END + -- Filter by created_at + AND CASE + WHEN $7 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN + created_at <= $7 + ELSE true + END + AND CASE + WHEN $8 :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN + created_at >= $8 + 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 $7 + LOWER(username) ASC OFFSET $9 LIMIT -- A null limit means "no limit", so 0 means return all - NULLIF($8 :: int, 0) + NULLIF($10 :: int, 0) ` type GetUsersParams struct { @@ -10423,6 +10434,8 @@ type GetUsersParams struct { RbacRole []string `db:"rbac_role" json:"rbac_role"` LastSeenBefore time.Time `db:"last_seen_before" json:"last_seen_before"` LastSeenAfter time.Time `db:"last_seen_after" json:"last_seen_after"` + CreatedBefore time.Time `db:"created_before" json:"created_before"` + CreatedAfter time.Time `db:"created_after" json:"created_after"` OffsetOpt int32 `db:"offset_opt" json:"offset_opt"` LimitOpt int32 `db:"limit_opt" json:"limit_opt"` } @@ -10458,6 +10471,8 @@ func (q *sqlQuerier) GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUse pq.Array(arg.RbacRole), arg.LastSeenBefore, arg.LastSeenAfter, + arg.CreatedBefore, + arg.CreatedAfter, arg.OffsetOpt, arg.LimitOpt, ) diff --git a/coderd/database/queries/users.sql b/coderd/database/queries/users.sql index a4f8844fd2db5..1f30a2c2c1d24 100644 --- a/coderd/database/queries/users.sql +++ b/coderd/database/queries/users.sql @@ -199,6 +199,17 @@ WHERE last_seen_at >= @last_seen_after ELSE true END + -- Filter by created_at + AND CASE + WHEN @created_before :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN + created_at <= @created_before + ELSE true + END + AND CASE + WHEN @created_after :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN + created_at >= @created_after + 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 707f32bfc7d32..a4fe5d4775d6c 100644 --- a/coderd/searchquery/search.go +++ b/coderd/searchquery/search.go @@ -70,6 +70,8 @@ func Users(query string) (database.GetUsersParams, []codersdk.ValidationError) { RbacRole: parser.Strings(values, []string{}, "role"), LastSeenAfter: parser.Time3339Nano(values, time.Time{}, "last_seen_after"), LastSeenBefore: parser.Time3339Nano(values, time.Time{}, "last_seen_before"), + CreatedAfter: parser.Time3339Nano(values, time.Time{}, "created_after"), + CreatedBefore: parser.Time3339Nano(values, time.Time{}, "created_before"), } parser.ErrorExcessParams(values) return filter, parser.Errors diff --git a/coderd/users.go b/coderd/users.go index 2fccef83f2013..56f295986859c 100644 --- a/coderd/users.go +++ b/coderd/users.go @@ -317,6 +317,8 @@ func (api *API) GetUsers(rw http.ResponseWriter, r *http.Request) ([]database.Us RbacRole: params.RbacRole, LastSeenBefore: params.LastSeenBefore, LastSeenAfter: params.LastSeenAfter, + CreatedAfter: params.CreatedAfter, + CreatedBefore: params.CreatedBefore, OffsetOpt: int32(paginationParams.Offset), LimitOpt: int32(paginationParams.Limit), }) diff --git a/coderd/users_test.go b/coderd/users_test.go index 617ab31259f91..1386d76f3e0bf 100644 --- a/coderd/users_test.go +++ b/coderd/users_test.go @@ -26,9 +26,11 @@ import ( "github.com/coder/coder/v2/coderd/audit" "github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/database" + "github.com/coder/coder/v2/coderd/database/db2sdk" "github.com/coder/coder/v2/coderd/database/dbauthz" "github.com/coder/coder/v2/coderd/database/dbfake" "github.com/coder/coder/v2/coderd/database/dbgen" + "github.com/coder/coder/v2/coderd/database/dbtestutil" "github.com/coder/coder/v2/coderd/database/dbtime" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/util/ptr" @@ -1515,6 +1517,73 @@ func TestUsersFilter(t *testing.T) { users = append(users, user) } + // Add users with different creation dates for testing date filters + for i := 0; i < 3; i++ { + // nolint:gocritic // Using system context is necessary to seed data in tests + user1, err := api.Database.InsertUser(dbauthz.AsSystemRestricted(ctx), database.InsertUserParams{ + ID: uuid.New(), + Email: fmt.Sprintf("before%d@coder.com", i), + Username: fmt.Sprintf("before%d", i), + LoginType: database.LoginTypeNone, + Status: string(codersdk.UserStatusActive), + RBACRoles: []string{codersdk.RoleMember}, + CreatedAt: dbtime.Time(time.Date(2022, 12, 15+i, 12, 0, 0, 0, time.UTC)), + }) + require.NoError(t, err) + + // The expected timestamps must be parsed from strings to compare equal during `ElementsMatch` + sdkUser1 := db2sdk.User(user1, nil) + sdkUser1.CreatedAt, err = time.Parse(time.RFC3339, sdkUser1.CreatedAt.Format(time.RFC3339)) + require.NoError(t, err) + sdkUser1.UpdatedAt, err = time.Parse(time.RFC3339, sdkUser1.UpdatedAt.Format(time.RFC3339)) + require.NoError(t, err) + sdkUser1.LastSeenAt, err = time.Parse(time.RFC3339, sdkUser1.LastSeenAt.Format(time.RFC3339)) + require.NoError(t, err) + users = append(users, sdkUser1) + + // nolint:gocritic //Using system context is necessary to seed data in tests + user2, err := api.Database.InsertUser(dbauthz.AsSystemRestricted(ctx), database.InsertUserParams{ + ID: uuid.New(), + Email: fmt.Sprintf("during%d@coder.com", i), + Username: fmt.Sprintf("during%d", i), + LoginType: database.LoginTypeNone, + Status: string(codersdk.UserStatusActive), + RBACRoles: []string{codersdk.RoleOwner}, + CreatedAt: dbtime.Time(time.Date(2023, 1, 15+i, 12, 0, 0, 0, time.UTC)), + }) + require.NoError(t, err) + + sdkUser2 := db2sdk.User(user2, nil) + sdkUser2.CreatedAt, err = time.Parse(time.RFC3339, sdkUser2.CreatedAt.Format(time.RFC3339)) + require.NoError(t, err) + sdkUser2.UpdatedAt, err = time.Parse(time.RFC3339, sdkUser2.UpdatedAt.Format(time.RFC3339)) + require.NoError(t, err) + sdkUser2.LastSeenAt, err = time.Parse(time.RFC3339, sdkUser2.LastSeenAt.Format(time.RFC3339)) + require.NoError(t, err) + users = append(users, sdkUser2) + + // nolint:gocritic // Using system context is necessary to seed data in tests + user3, err := api.Database.InsertUser(dbauthz.AsSystemRestricted(ctx), database.InsertUserParams{ + ID: uuid.New(), + Email: fmt.Sprintf("after%d@coder.com", i), + Username: fmt.Sprintf("after%d", i), + LoginType: database.LoginTypeNone, + Status: string(codersdk.UserStatusActive), + RBACRoles: []string{codersdk.RoleOwner}, + CreatedAt: dbtime.Time(time.Date(2023, 2, 15+i, 12, 0, 0, 0, time.UTC)), + }) + require.NoError(t, err) + + sdkUser3 := db2sdk.User(user3, nil) + sdkUser3.CreatedAt, err = time.Parse(time.RFC3339, sdkUser3.CreatedAt.Format(time.RFC3339)) + require.NoError(t, err) + sdkUser3.UpdatedAt, err = time.Parse(time.RFC3339, sdkUser3.UpdatedAt.Format(time.RFC3339)) + require.NoError(t, err) + sdkUser3.LastSeenAt, err = time.Parse(time.RFC3339, sdkUser3.LastSeenAt.Format(time.RFC3339)) + require.NoError(t, err) + users = append(users, sdkUser3) + } + // --- Setup done --- testCases := []struct { Name string @@ -1657,6 +1726,37 @@ func TestUsersFilter(t *testing.T) { return u.LastSeenAt.Before(end) && u.LastSeenAt.After(start) }, }, + { + Name: "CreatedAtBefore", + Filter: codersdk.UsersRequest{ + SearchQuery: `created_before:"2023-01-31T23:59:59Z"`, + }, + FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool { + end := time.Date(2023, 1, 31, 23, 59, 59, 0, time.UTC) + return u.CreatedAt.Before(end) + }, + }, + { + Name: "CreatedAtAfter", + Filter: codersdk.UsersRequest{ + SearchQuery: `created_after:"2023-01-01T00:00:00Z"`, + }, + FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool { + start := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) + return u.CreatedAt.After(start) + }, + }, + { + Name: "CreatedAtRange", + Filter: codersdk.UsersRequest{ + SearchQuery: `created_after:"2023-01-01T00:00:00Z" created_before:"2023-01-31T23:59:59Z"`, + }, + FilterF: func(_ codersdk.UsersRequest, u codersdk.User) bool { + start := time.Date(2023, 1, 1, 0, 0, 0, 0, time.UTC) + end := time.Date(2023, 1, 31, 23, 59, 59, 0, time.UTC) + return u.CreatedAt.After(start) && u.CreatedAt.Before(end) + }, + }, } for _, c := range testCases { @@ -1677,6 +1777,16 @@ func TestUsersFilter(t *testing.T) { exp = append(exp, made) } } + + // TODO: This can be removed with dbmem + if !dbtestutil.WillUsePostgres() { + for i := range matched.Users { + if len(matched.Users[i].OrganizationIDs) == 0 { + matched.Users[i].OrganizationIDs = nil + } + } + } + require.ElementsMatch(t, exp, matched.Users, "expected users returned") }) } diff --git a/docs/admin/users/index.md b/docs/admin/users/index.md index a00030a514f05..9dcdb237eb764 100644 --- a/docs/admin/users/index.md +++ b/docs/admin/users/index.md @@ -185,8 +185,10 @@ to use the Coder's filter query: - To find active users, use the filter `status:active`. - To find admin users, use the filter `role:admin`. -- To find users have not been active since July 2023: +- To find users who have not been active since July 2023: `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"` The following filters are supported: @@ -195,6 +197,8 @@ The following filters are supported: - `role` - Represents the role of the user. You can refer to the [TemplateRole documentation](https://pkg.go.dev/github.com/coder/coder/v2/codersdk#TemplateRole) for a list of supported user roles. -- `last_seen_before` and `last_seen_after` - The last time a used has used the +- `last_seen_before` and `last_seen_after` - The last time a user has used the platform (e.g. logging in, any API requests, connecting to workspaces). Uses the RFC3339Nano format. +- `created_before` and `created_after` - The time a user was created. Uses the + RFC3339Nano format. pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy