fix(proxy): cap /key/list user_email lookup at 1000 users

A broad substring like @ matches every user, and the unbounded lookup
materialized the full user table in proxy memory before building the
key filter. Bound it with take so the resolved id list and the IN
clause stay a fixed size
This commit is contained in:
ryan-crabbe-berri 2026-07-25 15:59:13 -07:00
parent 139dc60e5b
commit 74cb382932
3 changed files with 8 additions and 3 deletions

View file

@ -5176,6 +5176,7 @@ async def get_member_team_ids(
VALID_EXPIRES_FILTER_VALUES = frozenset({"active", "expired"})
USER_EMAIL_KEY_FILTER_MAX_USERS = 1000
@router.get(
@ -5195,7 +5196,7 @@ async def list_keys(
),
user_email: str | None = Query(
None,
description="Filter keys by the owning user's email. Case-insensitive substring match against the user table; only keys whose user_id belongs to a matching user are returned.",
description="Filter keys by the owning user's email. Case-insensitive substring match against the user table, capped at the first 1000 matching users; only keys whose user_id belongs to a matching user are returned.",
),
team_id: Optional[str] = Query(None, description="Filter keys by team ID"),
organization_id: Optional[str] = Query(None, description="Filter keys by organization ID"),
@ -5329,7 +5330,8 @@ async def list_keys(
[
user.user_id
for user in await UserRepository(prisma_client).table.find_many(
where={"user_email": {"contains": user_email, "mode": "insensitive"}}
where={"user_email": {"contains": user_email, "mode": "insensitive"}},
take=USER_EMAIL_KEY_FILTER_MAX_USERS,
)
]
if user_email and isinstance(user_email, str)

View file

@ -6354,6 +6354,9 @@ async def test_list_keys_user_email_resolves_to_user_ids():
assert mock_find_many.call_args.kwargs["where"] == {
"user_email": {"contains": "alias@example.com", "mode": "insensitive"}
}
assert mock_find_many.call_args.kwargs["take"] == 1000, (
"user lookup must be bounded; a broad substring like '@' matches every user"
)
assert mock_list_key_helper.call_args.kwargs["user_ids_for_email"] == ["user-1", "user-2"]
mock_find_many.return_value = []

View file

@ -42813,7 +42813,7 @@ export interface operations {
size?: number;
/** @description Filter keys by user ID. Exact match by default; set substring_matching=true (admin only) for case-insensitive substring matching. */
user_id?: string | null;
/** @description Filter keys by the owning user's email. Case-insensitive substring match against the user table; only keys whose user_id belongs to a matching user are returned. */
/** @description Filter keys by the owning user's email. Case-insensitive substring match against the user table, capped at the first 1000 matching users; only keys whose user_id belongs to a matching user are returned. */
user_email?: string | null;
/** @description Filter keys by team ID */
team_id?: string | null;