fix(proxy-extras): hand libpq a root cert, not Prisma's sslcert, when the migration job builds indexes (#44203)

The migration job runs DatabaseURLSettings.apply_to_env(), which rewrites
DATABASE_SSLMODE=verify-full plus DATABASE_SSLROOTCERT into Prisma's TLS
dialect: sslmode=require&sslcert=<CA>&sslaccept=strict. The request-log
index build then hands that same URL to psycopg, and libpq reads sslcert
as a client certificate, failing with "certificate present, but not
private key file" on every verify-full deployment since #43948.

_strip_prisma_query_params now undoes the Prisma dialect before psycopg
sees the URL. sslaccept=strict (or any value Prisma treats as strict)
becomes sslrootcert=<CA> plus sslmode=verify-full whatever sslmode said,
since strict verifies chain and hostname and libpq only does that in
verify-full; sslmode=disable stays off. Without strict, Prisma verifies
nothing, so the CA is dropped and sslmode is kept as is. A URL that also
carries sslkey is libpq's own client-certificate form and is left alone.

Backport of #44203 to stable/1.103.x.
Cherry-picked from 4758fce91a (main).

Also on this line: the libpq URL keeps Prisma's host query param, so a
Unix-socket DATABASE_URL (?host=/cloudsql/...) builds the indexes on the
server Prisma migrated instead of failing over TCP to the URL's host.

Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
devin-ai-integration[bot] 2026-11-09 17:07:27 -08:00 • committed by mateo-berri
parent a5ad370892
commit 6a8685a3ee
2 changed files with 139 additions and 20 deletions

View file

@ -78,6 +78,24 @@ class _InvalidIndex:
table_size: str
MAX_MIGRATE_DEPLOY_ATTEMPTS = 4
LIBPQ_URL_PARAMS: Final = frozenset(
{
"sslmode",
"sslcert",
"sslkey",
"sslrootcert",
"sslpassword",
"application_name",
"connect_timeout",
"client_encoding",
"options",
"service",
"gssencmode",
"krbsrvname",
"target_session_attrs",
"host",
}
)
@dataclass(frozen=True)
@ -686,30 +704,43 @@ class ProxyExtrasDBManager:
@staticmethod
def _strip_prisma_query_params(url: str) -> str:
"""Remove Prisma-specific query params (connection_limit, pool_timeout,
schema, etc.) from DATABASE_URL so psycopg can parse it."""
"""Rewrite a Prisma-dialect URL for libpq: drop the Prisma-only params
(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
parsed = urlparse(url)
parsed: Final = urlparse(url)
if not parsed.query:
return url
libpq_params = {
"sslmode",
"sslcert",
"sslkey",
"sslrootcert",
"sslpassword",
"application_name",
"connect_timeout",
"client_encoding",
"options",
"service",
"gssencmode",
"krbsrvname",
"target_session_attrs",
}
kept = [(k, v) for k, v in parse_qsl(parsed.query) if k in libpq_params]
return urlunparse(parsed._replace(query=urlencode(kept, quote_via=quote)))
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)))
@staticmethod
def _libpq_tls_params(
pairs: "tuple[tuple[str, str], ...]", sslaccept: "str | None"
) -> "tuple[tuple[str, str], ...]":
"""Undo ``translate_libpq_ssl_params``. Prisma's ``sslcert`` is the CA and
``sslaccept=strict`` checks chain and hostname, which libpq only does in
``sslmode=verify-full``, so strict becomes ``sslrootcert`` plus
``verify-full`` whatever ``sslmode`` said (``disable`` stays off). Prisma
defaults an absent ``sslaccept`` to ``accept_invalid_certs`` and anything
else to strict. Without strict it checks nothing, so the CA is dropped and
``sslmode`` is kept as is: libpq only verifies when a root cert is present.
A URL that also carries ``sslkey`` is libpq's own client-certificate form
and is kept."""
keys: Final = frozenset(k for k, _ in pairs)
if "sslcert" not in keys or "sslkey" in keys:
return pairs
sslmode: Final = next((v for k, v in pairs if k == "sslmode"), None)
rest: Final = tuple((k, v) for k, v in pairs if k not in ("sslcert", "sslmode"))
if sslaccept in (None, "accept_invalid_certs") or sslmode == "disable":
return rest if sslmode is None else rest + (("sslmode", sslmode),)
root_cert: Final = tuple(("sslrootcert", v) for k, v in pairs if k == "sslcert" and "sslrootcert" not in keys)
return rest + root_cert + (("sslmode", "verify-full"),)
@staticmethod
def _warn_if_db_ahead_of_head(migrations_dir: str) -> None:

View file

@ -996,6 +996,94 @@ class TestJWTKeyMappingCascade:
class TestStripPrismaQueryParams:
"""The psycopg URL the job connects with is derived from the Prisma-dialect
DATABASE_URL, whose TLS params mean something else to libpq."""
@staticmethod
def _query(url: str) -> dict[str, str]:
from urllib.parse import parse_qsl, urlparse
return dict(parse_qsl(urlparse(url).query))
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"
cleaned = ProxyExtrasDBManager._strip_prisma_query_params(url)
assert self._query(cleaned) == {"sslmode": "verify-full", "sslrootcert": "/tmp/pinned.pem"}
assert cleaned.startswith("postgresql://u:p@writer:5432/db?")
@pytest.mark.parametrize("sslmode", ["prefer", "require"])
@pytest.mark.parametrize("sslaccept", ["strict", "unknown-mode-prisma-treats-as-strict"])
def test_strict_verifies_chain_and_hostname_whatever_sslmode_prisma_was_given(self, sslmode, sslaccept):
url = f"postgresql://writer/db?sslmode={sslmode}&sslcert=/certs/ca.pem&sslaccept={sslaccept}"
cleaned = ProxyExtrasDBManager._strip_prisma_query_params(url)
assert self._query(cleaned) == {"sslmode": "verify-full", "sslrootcert": "/certs/ca.pem"}
def test_strict_with_tls_disabled_stays_off(self):
url = "postgresql://writer/db?sslmode=disable&sslcert=/certs/ca.pem&sslaccept=strict"
cleaned = ProxyExtrasDBManager._strip_prisma_query_params(url)
assert self._query(cleaned) == {"sslmode": "disable"}
@pytest.mark.parametrize("sslaccept", ["&sslaccept=accept_invalid_certs", ""])
def test_without_strict_the_ca_is_dropped_so_libpq_checks_nothing_like_prisma(self, sslaccept):
url = f"postgresql://writer/db?sslmode=require&sslcert=/certs/ca.pem{sslaccept}"
cleaned = ProxyExtrasDBManager._strip_prisma_query_params(url)
assert self._query(cleaned) == {"sslmode": "require"}
def test_a_ca_alone_without_strict_or_sslmode_leaves_libpq_its_defaults(self):
cleaned = ProxyExtrasDBManager._strip_prisma_query_params("postgresql://writer/db?sslcert=/certs/ca.pem")
assert cleaned == "postgresql://writer/db"
def test_a_libpq_client_certificate_pair_is_left_alone(self):
url = "postgresql://writer/db?sslmode=verify-full&sslrootcert=/ca.pem&sslcert=/client.crt&sslkey=/client.key"
cleaned = ProxyExtrasDBManager._strip_prisma_query_params(url)
assert self._query(cleaned) == {
"sslmode": "verify-full",
"sslrootcert": "/ca.pem",
"sslcert": "/client.crt",
"sslkey": "/client.key",
}
def test_an_explicit_sslrootcert_wins_over_the_prisma_sslcert(self):
url = "postgresql://writer/db?sslmode=require&sslrootcert=/ca.pem&sslcert=/pinned.pem&sslaccept=strict"
cleaned = ProxyExtrasDBManager._strip_prisma_query_params(url)
assert self._query(cleaned) == {"sslmode": "verify-full", "sslrootcert": "/ca.pem"}
def test_a_prisma_socket_host_param_reaches_libpq(self) -> None:
from psycopg.conninfo import conninfo_to_dict
url: Final = "postgresql://u:p@localhost/litellm?host=/cloudsql/proj:us-central1:inst&schema=tenant"
conninfo: Final = conninfo_to_dict(ProxyExtrasDBManager._strip_prisma_query_params(url))
assert conninfo["host"] == "/cloudsql/proj:us-central1:inst"
assert "schema" not in conninfo
def test_prisma_only_params_are_dropped_and_plain_urls_pass_through(self):
url = "postgresql://u:p@pooler:6543/db?schema=tenant&pgbouncer=true&connection_limit=5&connect_timeout=3"
cleaned = ProxyExtrasDBManager._strip_prisma_query_params(url)
assert cleaned == "postgresql://u:p@pooler:6543/db?connect_timeout=3"
assert (
ProxyExtrasDBManager._strip_prisma_query_params("postgresql://u:p@writer/db")
== "postgresql://u:p@writer/db"
)
class TestBuildRequestLogIndexes:
"""The migration job hands the index build the direct database URL and the schema
the migrations target, waits for it, and reports its result."""