Skip to content

Fix Paimon DLF request signing: sign every request, cache only credentials#111611

Open
alesapin wants to merge 2 commits into
masterfrom
paimon-dlf-request-signing
Open

Fix Paimon DLF request signing: sign every request, cache only credentials#111611
alesapin wants to merge 2 commits into
masterfrom
paimon-dlf-request-signing

Conversation

@alesapin

@alesapin alesapin commented Jul 23, 2026

Copy link
Copy Markdown
Member

The DLF v4 Authorization header for the Paimon REST catalog was computed once for the first request (GET /v1/paimon/config at CREATE DATABASE time), cached in PaimonToken::dlf_generated_authorization, and reused for every subsequent request. The DLF v4 signature covers the canonical request (method, resource path, query parameters and signed headers), so a cached signature is invalid for any other request, and the server answers 401 to e.g. GET /v1/paimon/databases?maxResults=100. A retry that was supposed to recover never fired because it compared HTTPException::code (the ClickHouse error code, 86) with the HTTP status 401. The bug stayed invisible because table listing swallows catalog errors, so SHOW TABLES on a DLF-authenticated catalog silently returned an empty list.

This follows the reference implementation (DLFAuthSignature in Apache Paimon), which computes the signature for every request anew and caches only the credentials — in our case already stored in PaimonToken. The signature cache is removed (it was also a data race: a mutable string written without synchronization from concurrent listing threads) together with the dead retry.

The same code-vs-HTTP-status confusion is fixed in existsTable and tryGetTableMetadata: HTTP 404 there was meant to map to "table does not exist" but was rethrown instead, because e.code() is never 404.

test_paimon_rest_catalog is extended to create a database and a table on the DLF mock server through its REST API (signing the requests with a Python port of DLFAuthSignature from Apache Paimon 1.1.1, the version the mock server is built with) and to assert that listing, DESC and SELECT work through the DLF-authenticated catalog. The mock servers keep their catalogs in memory, so the DLF server needs its own content; without the fix, the listing silently returns an empty result and the metadata requests fail with HTTP 401.

Related: #92011
Related: #111379

Changelog category (leave one):

  • Bug Fix (user-visible misbehavior in an official stable release)

Changelog entry (a user-readable short description of the changes that goes into CHANGELOG.md):

Fix DLF authentication in the Paimon REST catalog: the request signature was computed once and reused, so every catalog request after the first one failed with HTTP 401 and SHOW TABLES returned an empty list. Also fix HTTP 404 detection in table existence checks.

🤖 Generated with Claude Code

…tials

The DLF v4 `Authorization` header was computed once for the first request
(`GET /v1/paimon/config` at `CREATE DATABASE` time), cached in
`PaimonToken::dlf_generated_authorization`, and reused for every
subsequent request. The signature covers the canonical request (method,
resource path, query parameters and signed headers), so a cached value is
invalid for any other request, and the DLF server answers 401 to e.g.
`GET /v1/paimon/databases?maxResults=100`. The retry that was supposed to
recover never fired because it compared `HTTPException::code` (the
ClickHouse error code, 86) with the HTTP status 401. The bug stayed
invisible because table listing swallows catalog errors, so `SHOW TABLES`
just returned an empty list.

Follow the reference implementation (`DLFDefaultSigner` in Apache Paimon):
compute the signature for every request anew and cache only the
credentials, which are already stored in `PaimonToken`. Drop the cache
(which was also a data race: a `mutable` string written without
synchronization from concurrent listing threads) and the dead retry.

Also fix the same `code`-vs-HTTP-status confusion in `existsTable` and
`tryGetTableMetadata`: HTTP 404 there was meant to map to "table does
not exist" but was rethrown instead, because `e.code()` is never 404.

Extend `test_paimon_rest_catalog` to assert that listing, `DESC` and
`SELECT` work through the DLF-authenticated catalog: the DLF mock server
shares the warehouse with the bearer one, so the results must match.
Without the fix the listing silently returns an empty result.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@clickhouse-gh

clickhouse-gh Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Workflow [PR], commit [f20f511]


AI Review

Summary

This PR fixes Paimon DLF request signing by recomputing the Authorization header for each REST call, removes the stale cached signature/data race, and switches the missing-table checks to HTTPException::getHTTPStatus(). The signing fix itself looks coherent, but one advertised behavior change is still not proven by the updated regression coverage: the PR also changes how missing tables are detected, and the new test never exercises those 404 paths.

Findings

⚠️ Majors

  • [tests/integration/test_paimon_rest_catalog/test.py:239] The new integration test proves successful DLF-authenticated listing, DESC, and SELECT, but it still never drives the other behavior changed in this PR: existsTable and tryGetTableMetadata now map HTTP 404 to “table does not exist” via getHTTPStatus() in src/Databases/DataLake/PaimonRestCatalog.cpp:466 and src/Databases/DataLake/PaimonRestCatalog.cpp:484. That means CI would still stay green if one of those call sites regressed back to e.code() later. Suggested fix: add a missing-table assertion through the user-visible surfaces backed by those methods, for example EXISTS TABLE test_dlf.missing = 0 plus a metadata lookup such as SHOW CREATE TABLE test_dlf.missing reporting that the table does not exist.
Final Verdict

⚠️ Needs changes before merge: the code fix is plausible, but the PR still lacks regression coverage for the missing-table 404 behavior it changes.

@clickhouse-gh clickhouse-gh Bot added the pr-bugfix Pull request with bugfix, not backported by default label Jul 23, 2026
# queries verify that signing works for requests other than the initial
# config one made at CREATE DATABASE time.
assert (
node.query("SHOW TABLES;", database="paimon_rest_db_dlf")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This regression test proves the per-request DLF signing fix on successful catalog calls, but it does not cover the second behavior changed in this PR: existsTable and tryGetTableMetadata now treat HTTP 404 as “table missing” via getHTTPStatus() instead of rethrowing. None of the new assertions hits that path, so CI would still stay green if one of those call sites regressed back to e.code() later.

Please add a missing-table assertion through the user-visible surfaces backed by those methods, for example EXISTS TABLE returning 0 and a metadata lookup such as SHOW CREATE TABLE reporting that the table does not exist.

@alesapin

alesapin commented Jul 23, 2026

Copy link
Copy Markdown
Member Author

I don't understand source logic in original PR. Basically it cached a checksum about some request-specific information like datetime or query parameters. It doesn't make sense to cache it, because next request will have different datetime and likely query params. So checksum will not match and we will issue request again in retry logic.

@alesapin

Copy link
Copy Markdown
Member Author

cc @JiaQiTang98

…alog

The previous version of the test asserted that the DLF-authenticated
database sees the table created through the bearer server. That was
wrong: the mock REST servers keep their catalogs in memory (only table
data lives on the shared warehouse volume), so the DLF server's catalog
is always empty and `SHOW TABLES` legitimately returned an empty list:
https://s3.amazonaws.com/clickhouse-test-reports/json.html?PR=111611&sha=0e8694b3be688800c4ae2f829510f51b2e807b81&name_0=PR&name_1=Integration%20tests%20%28amd_asan_ubsan%2C%20flaky%29

Create a database and a table on the DLF server through its REST API
instead, signing the requests with a Python port of `DLFAuthSignature`
from Apache Paimon 1.1.1 (the version the mock server is built with),
and assert that listing, `DESC` and `SELECT` through the DLF catalog
return the created content. The signer was validated against the C++
implementation by pointing a locally built server at a mock that
recomputes and compares the signature the same way the Java server
does: all requests matched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@alesapin

Copy link
Copy Markdown
Member Author

I don't understand test change :(

@kssenii kssenii self-assigned this Jul 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr-bugfix Pull request with bugfix, not backported by default

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants