mirror of
https://github.com/coder/coder.git
synced 2026-09-01 14:53:15 +08:00
a005e5cd22
## Summary User search can now resolve exact `email:` and `username:` terms through `GET /api/v2/users` instead of only supporting fuzzy free-text matches. The database query already had exact email and username filters; this wires the public search parser and API handler to those filters so clients can ask for a single user by email without fetching every user or depending on substring matching. This is the API half of coder/terraform-provider-coderd#403: that provider PR adds `data.coderd_user.email`, and this PR gives it an efficient exact lookup path. ## Testing - `go test ./coderd/searchquery -run '^TestSearchUsers$' -count=1` - `go test ./coderd -run '^TestGetUsersFilter$' -count=1` - Live API test: - Built local enterprise Coder from this branch. - Started Coder on `http://127.0.0.1:39991` against a clean Postgres database. - Created `lookup-target@example.com`. - Verified `GET /api/v2/users?q=email:LOOKUP-TARGET@EXAMPLE.COM&limit=2` returned exactly one user: ```json { "count": 1, "users": [ { "id": "efc6f909-ce0a-4731-bd2f-6e4df417aaa7", "username": "lookup-target", "email": "lookup-target@example.com" } ] } ``` ---   --------- Co-authored-by: Ethan Dickson <ethanndickson@gmail.com>
215 lines
5.6 KiB
SQL
215 lines
5.6 KiB
SQL
-- name: GetGroupMembers :many
|
|
SELECT * FROM group_members_expanded
|
|
WHERE CASE
|
|
WHEN @include_system::bool THEN TRUE
|
|
ELSE
|
|
user_is_system = false
|
|
END;
|
|
|
|
-- name: GetGroupMembersByGroupID :many
|
|
SELECT *
|
|
FROM group_members_expanded
|
|
WHERE group_id = @group_id
|
|
-- Filter by system type
|
|
AND CASE
|
|
WHEN @include_system::bool THEN TRUE
|
|
ELSE
|
|
user_is_system = false
|
|
END;
|
|
|
|
-- name: GetGroupMembersByGroupIDPaginated :many
|
|
SELECT
|
|
*, COUNT(*) OVER() AS count
|
|
FROM
|
|
group_members_expanded
|
|
WHERE
|
|
group_members_expanded.group_id = @group_id
|
|
AND CASE
|
|
-- This allows using the last element on a page as effectively a cursor.
|
|
-- This is an important option for scripts that need to paginate without
|
|
-- duplicating or missing data.
|
|
WHEN @after_id :: uuid != '00000000-0000-0000-0000-000000000000'::uuid THEN (
|
|
-- The pagination cursor is the last ID of the previous page.
|
|
-- The query is ordered by the username field, so select all
|
|
-- rows after the cursor.
|
|
(LOWER(user_username)) > (
|
|
SELECT
|
|
LOWER(user_username)
|
|
FROM
|
|
group_members_expanded
|
|
WHERE
|
|
group_id = @group_id
|
|
AND user_id = @after_id
|
|
)
|
|
)
|
|
ELSE true
|
|
END
|
|
-- Start filters
|
|
-- Filter by email, username, or name (display name)
|
|
AND CASE
|
|
WHEN @search :: text != '' THEN (
|
|
user_email ILIKE concat('%', @search, '%')
|
|
OR user_username ILIKE concat('%', @search, '%')
|
|
OR user_name ILIKE concat('%', @search, '%')
|
|
)
|
|
ELSE true
|
|
END
|
|
-- Filter by name (display name)
|
|
AND CASE
|
|
WHEN @name :: text != '' THEN
|
|
user_name ILIKE concat('%', @name, '%')
|
|
ELSE true
|
|
END
|
|
-- Filter by exact username
|
|
AND CASE
|
|
WHEN @exact_username :: text != '' THEN
|
|
lower(user_username) = lower(@exact_username)
|
|
ELSE true
|
|
END
|
|
-- Filter by exact email
|
|
AND CASE
|
|
WHEN @exact_email :: text != '' THEN
|
|
lower(user_email) = lower(@exact_email)
|
|
ELSE true
|
|
END
|
|
-- Filter by status
|
|
AND CASE
|
|
-- @status needs to be a text because it can be empty, If it was
|
|
-- user_status enum, it would not.
|
|
WHEN cardinality(@status :: user_status[]) > 0 THEN
|
|
user_status = ANY(@status :: user_status[])
|
|
ELSE true
|
|
END
|
|
-- Filter by rbac_roles
|
|
AND CASE
|
|
-- @rbac_role allows filtering by rbac roles. If 'member' is included, show everyone, as
|
|
-- everyone is a member.
|
|
WHEN cardinality(@rbac_role :: text[]) > 0 AND 'member' != ANY(@rbac_role :: text[]) THEN
|
|
user_rbac_roles && @rbac_role :: text[]
|
|
ELSE true
|
|
END
|
|
-- Filter by last_seen
|
|
AND CASE
|
|
WHEN @last_seen_before :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
|
|
user_last_seen_at <= @last_seen_before
|
|
ELSE true
|
|
END
|
|
AND CASE
|
|
WHEN @last_seen_after :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
|
|
user_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
|
|
user_created_at <= @created_before
|
|
ELSE true
|
|
END
|
|
AND CASE
|
|
WHEN @created_after :: timestamp with time zone != '0001-01-01 00:00:00Z' THEN
|
|
user_created_at >= @created_after
|
|
ELSE true
|
|
END
|
|
-- Filter by system type
|
|
AND CASE
|
|
WHEN @include_system::bool THEN TRUE
|
|
ELSE user_is_system = false
|
|
END
|
|
-- Filter by github.com user ID
|
|
AND CASE
|
|
WHEN @github_com_user_id :: bigint != 0 THEN
|
|
user_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
|
|
user_login_type = ANY(@login_type :: login_type[])
|
|
ELSE true
|
|
END
|
|
-- Filter by service account.
|
|
AND CASE
|
|
WHEN sqlc.narg('is_service_account') :: boolean IS NOT NULL THEN
|
|
user_is_service_account = sqlc.narg('is_service_account') :: boolean
|
|
ELSE true
|
|
END
|
|
-- End of filters
|
|
ORDER BY
|
|
-- Deterministic and consistent ordering of all users. This is to ensure consistent pagination.
|
|
LOWER(user_username) ASC OFFSET @offset_opt
|
|
LIMIT
|
|
-- A null limit means "no limit", so 0 means return all
|
|
NULLIF(@limit_opt :: int, 0);
|
|
|
|
-- name: GetGroupMembersCountByGroupID :one
|
|
-- Returns the total count of members in a group. Shows the total
|
|
-- count even if the caller does not have read access to ResourceGroupMember.
|
|
-- They only need ResourceGroup read access.
|
|
SELECT COUNT(*)
|
|
FROM group_members_expanded
|
|
WHERE group_id = @group_id
|
|
-- Filter by system type
|
|
AND CASE
|
|
WHEN @include_system::bool THEN TRUE
|
|
ELSE
|
|
user_is_system = false
|
|
END;
|
|
|
|
-- name: GetGroupMembersCountByGroupIDs :many
|
|
-- Returns the total member count for each of the given group IDs in a
|
|
-- single query. Used to avoid N+1 lookups when listing many groups. Like
|
|
-- GetGroupMembersCountByGroupID, the count is returned even when the
|
|
-- caller does not have read access to individual group members.
|
|
SELECT
|
|
group_id,
|
|
COUNT(*) AS member_count
|
|
FROM group_members_expanded
|
|
WHERE group_id = ANY(@group_ids :: uuid[])
|
|
AND CASE
|
|
WHEN @include_system::bool THEN TRUE
|
|
ELSE user_is_system = false
|
|
END
|
|
GROUP BY group_id;
|
|
|
|
-- InsertUserGroupsByID adds a user to all provided groups, if they exist.
|
|
-- name: InsertUserGroupsByID :many
|
|
WITH groups AS (
|
|
SELECT
|
|
id
|
|
FROM
|
|
groups
|
|
WHERE
|
|
groups.id = ANY(@group_ids :: uuid [])
|
|
)
|
|
INSERT INTO
|
|
group_members (user_id, group_id)
|
|
SELECT
|
|
@user_id,
|
|
groups.id
|
|
FROM
|
|
groups
|
|
-- If there is a conflict, the user is already a member
|
|
ON CONFLICT DO NOTHING
|
|
RETURNING group_id;
|
|
|
|
-- name: RemoveUserFromGroups :many
|
|
DELETE FROM
|
|
group_members
|
|
WHERE
|
|
user_id = @user_id AND
|
|
group_id = ANY(@group_ids :: uuid [])
|
|
RETURNING group_id;
|
|
|
|
-- name: InsertGroupMember :exec
|
|
INSERT INTO
|
|
group_members (user_id, group_id)
|
|
VALUES
|
|
($1, $2);
|
|
|
|
-- name: DeleteGroupMemberFromGroup :exec
|
|
DELETE FROM
|
|
group_members
|
|
WHERE
|
|
user_id = $1 AND
|
|
group_id = $2;
|