fix(proxy-extras): accept unencoded DB passwords in the v2 migration lock and redact its errors (#46592)

Partial backport of #46592 to stable/1.103.x: only the libpq URL userinfo encoding the index build connects with.
Cherry-picked hunk from 1ac9cb2a1d (main).

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
Yassin Kortam 2026-11-09 17:10:52 -08:00 • committed by mateo-berri
parent ee94046d86
commit cf6ab076eb
2 changed files with 82 additions and 3 deletions

View file

@ -708,16 +708,23 @@ class ProxyExtrasDBManager:
(connection_limit, pool_timeout, schema, pgbouncer, sslaccept, ...) and
translate Prisma's TLS params back, since libpq reads ``sslcert`` as a
client certificate where Prisma reads it as the CA."""
from urllib.parse import parse_qsl, quote, urlencode, urlparse, urlunparse
from urllib.parse import parse_qsl, quote, unquote, urlencode, urlparse, urlunparse
parsed: Final = urlparse(url)
netloc: Final = parsed.netloc
userinfo, separator, hostport = netloc.rpartition("@")
user, password_separator, password = userinfo.partition(":")
canonical_userinfo: Final = quote(unquote(user), safe="") + (
password_separator + quote(unquote(password), safe="") if password_separator else ""
)
canonical_netloc: Final = f"{canonical_userinfo}{separator}{hostport}" if separator else netloc
if not parsed.query:
return url
return url if canonical_netloc == netloc else urlunparse(parsed._replace(netloc=canonical_netloc))
pairs: Final = tuple(parse_qsl(parsed.query))
kept: Final = tuple((k, v) for k, v in pairs if k in LIBPQ_URL_PARAMS)
sslaccept: Final = next((v for k, v in pairs if k == "sslaccept"), None)
libpq_pairs: Final = ProxyExtrasDBManager._libpq_tls_params(kept, sslaccept)
return urlunparse(parsed._replace(query=urlencode(libpq_pairs, quote_via=quote)))
return urlunparse(parsed._replace(netloc=canonical_netloc, query=urlencode(libpq_pairs, quote_via=quote)))
@staticmethod
def _libpq_tls_params(

View file

@ -1006,6 +1006,78 @@ class TestStripPrismaQueryParams:
return dict(parse_qsl(urlparse(url).query))
@pytest.mark.parametrize(
"url,expected_user,expected_password,expected_host,expected_port,expected_sslmode",
(
(
"postgresql://u:wr ong'pw9@writer:5433/litellm",
"u",
"wr ong'pw9",
"writer",
"5433",
None,
),
(
"postgresql://u:wr ong'pw9@writer:5433/litellm?schema=x&sslmode=require",
"u",
"wr ong'pw9",
"writer",
"5433",
"require",
),
(
"postgresql://u:p%40ss%20w@writer:5433/litellm",
"u",
"p@ss w",
"writer",
"5433",
None,
),
(
"postgresql://u:p%z@writer:5433/litellm",
"u",
"p%z",
"writer",
"5433",
None,
),
(
"postgresql://u:wr ong'pw9@[::1]:5433/litellm",
"u",
"wr ong'pw9",
"::1",
"5433",
None,
),
),
)
def test_userinfo_is_encoded_for_libpq(
self,
url: str,
expected_user: str,
expected_password: str,
expected_host: str,
expected_port: str,
expected_sslmode: str | None,
) -> None:
from psycopg.conninfo import conninfo_to_dict
result: Final = ProxyExtrasDBManager._strip_prisma_query_params(url)
conninfo: Final = conninfo_to_dict(result)
assert conninfo["user"] == expected_user
assert conninfo["password"] == expected_password
assert conninfo["host"] == expected_host
assert conninfo["port"] == expected_port
assert conninfo["dbname"] == "litellm"
assert conninfo.get("sslmode") == expected_sslmode
assert "schema" not in conninfo
def test_empty_password_userinfo_keeps_its_separator(self) -> None:
url: Final = "postgresql://u:@writer/litellm"
assert ProxyExtrasDBManager._strip_prisma_query_params(url) == url
def test_prisma_ca_sslcert_becomes_sslrootcert_with_verify_full(self):
url = "postgresql://u:p@writer:5432/db?schema=public&sslmode=require&sslcert=/tmp/pinned.pem&sslaccept=strict"