Skip to content

[#10685] feat(iceberg-rest): Support vended credentials on planTableScan endpoint#10800

Open
sachinnn99 wants to merge 2 commits into
apache:mainfrom
sachinnn99:feat/10685-plan-table-scan-credential-vending
Open

[#10685] feat(iceberg-rest): Support vended credentials on planTableScan endpoint#10800
sachinnn99 wants to merge 2 commits into
apache:mainfrom
sachinnn99:feat/10685-plan-table-scan-credential-vending

Conversation

@sachinnn99

@sachinnn99 sachinnn99 commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

Add X-Iceberg-Access-Delegation header support to the planTableScan endpoint and return storage credentials in the response when vended-credentials is requested. Mirrors the existing credential-vending flow from createTable/loadTable/registerTable (#10684).

Changes:

  • IcebergTableOperations.planTableScan: add @HeaderParam(X_ICEBERG_ACCESS_DELEGATION), compute isCredentialVending, build the 3-arg IcebergRequestContext, merge storage-credentials into the response JSON when eligible
  • IcebergTableOperations: inject IcebergCatalogWrapperManager (same pattern as IcebergConfigOperations) and add a private buildScanResponseWithCredentials helper that serializes the response to an ObjectNode and appends the storage-credentials array
  • CatalogWrapperForREST: add getCredentialsIfEligible(identifier, requestCredential, privilege) that wraps shouldGenerateCredential + getCredential and returns credentials separately, reusing the same Gravitino→Iceberg credential conversion pattern as getTableCredentials
  • MockIcebergTableOperations: update constructor to match the parent
  • TestIcebergTableOperations: add three tests — testPlanTableScanWithCredentialVending (no header / local / S3), testPlanTableScanRemoteSigningNotSupported, testPlanTableScanInvalidAccessDelegation

Note: PlanTableScanResponse in Iceberg 1.10.1 has no config/credentials field (credentials support was added in Iceberg PR #14518, targeting 1.11.0+). Credentials are therefore merged into the response JSON at the endpoint. When Gravitino upgrades to Iceberg 1.11.0+, the JSON-level merge can be replaced with a direct PlanTableScanResponse.Builder.withCredentials(...) call in CatalogWrapperForREST.planTableScan. Credential eligibility (shouldGenerateCredential) is still enforced at the wrapper level so local/HDFS tables correctly receive no credentials.

Why are the changes needed?

The Iceberg REST spec defines X-Iceberg-Access-Delegation as a valid header on planTableScan and CompletedPlanningResult includes a storage-credentials field. Currently, clients performing server-side scan planning must make a separate GET .../credentials call to obtain storage access credentials before reading the data files returned in the scan plan.

Fixes #10685

Does this PR introduce any user-facing change?

Yes. The planTableScan REST endpoint now accepts the X-Iceberg-Access-Delegation header and returns vended credentials in the response storage-credentials field when requested. Backward compatible — clients that do not send the header get existing behavior.

How was this patch tested?

Added unit tests in TestIcebergTableOperations:

  • testPlanTableScanWithCredentialVending — no vending without header, no vending for file:// location, vending present for s3:// location with expected DUMMY_CREDENTIAL_TYPE
  • testPlanTableScanRemoteSigningNotSupported — 406 response for remote-signing
  • testPlanTableScanInvalidAccessDelegation — 400 response for invalid header values

All existing TestIcebergTableOperations tests still pass (no regressions).

@roryqi

roryqi commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

@sachinnn99 Could u resolve the conflicts? @laserninja Could u help me review this pull request?

@laserninja laserninja left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for the PR! Posted some Inline comments

LoadCredentialsResponse credentialsResponse =
icebergCatalogWrapperManager
.getCatalogWrapper(catalogName)
.getCredentialsIfEligible(tableIdentifier, true, CredentialPrivilege.READ);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[HIGH] Bypasses the dispatcher/event-listener layer

All other credential operations go through tableOperationDispatcher (e.g. getTableCredentials), which triggers event listeners and authorization hooks. Here the endpoint accesses IcebergCatalogWrapperManager directly, so:

  • Credential vending on planTableScan won't emit events
  • Authorization hooks won't fire for the credential fetch

The existing loadTable/createTable endpoints delegate credential vending to CatalogWrapperForREST via the dispatcher. The same pattern should be followed here — e.g. add a planTableScan overload in CatalogWrapperForREST that accepts requestCredential, or route through a dispatcher method.

if (!credentialsResponse.credentials().isEmpty()) {
return buildScanResponseWithCredentials(scanResponse, credentialsResponse);
}
} catch (Exception e) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[HIGH] Overly broad catch (Exception e) silently swallows errors

This catches all exceptions including IllegalArgumentException, NullPointerException, authorization failures, etc. A client requesting vended-credentials that fails due to a misconfiguration would silently get no credentials with only a WARN log — very hard to debug.

The existing getTableCredentials method only catches ServiceUnavailableException. This should follow the same pattern:

} catch (ServiceUnavailableException e) {

Other exceptions (auth failures, illegal state, NPE) should propagate so they surface as proper error responses.

public LoadCredentialsResponse getCredentialsIfEligible(
TableIdentifier identifier, boolean requestCredential, CredentialPrivilege privilege) {
try {
LoadTableResponse loadTableResponse = super.loadTable(identifier);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[HIGH] Redundant loadTable call

planTableScan already loads and scans the table via tableOperationDispatcher.planTableScan(...). Then getCredentialsIfEligible calls super.loadTable(identifier) again — a second full table load just to check the location and generate credentials. This is wasteful for every credential-vended scan.

Consider either:

  1. Passing the table metadata/location obtained during planTableScan into the credential vending method, or
  2. Caching or reusing the already-loaded table within the same request

}
}

private Response buildScanResponseWithCredentials(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[MEDIUM] JSON-level merge is fragile — add a TODO with issue reference

Manually constructing JSON via ObjectNode/ArrayNode is fine as a temporary workaround for Iceberg 1.10.1's lack of PlanTableScanResponse.Builder.withCredentials(), but:

  1. Please add a // TODO(#XXXX): Replace with PlanTableScanResponse.Builder.withCredentials() after Iceberg 1.11.0 upgrade comment so this is tracked for cleanup.
  2. Verify the JSON structure matches the Iceberg REST spec's CompletedPlanningResult schema — specifically the storage-credentials array format.
  3. This also bypasses the standard IcebergRESTUtils.ok() serialization path. Any future changes to response serialization (e.g. custom serializers, response wrapping) won't apply here.

s3ScanJson.has("storage-credentials"), "S3 table should have storage-credentials");
JsonNode credentials = s3ScanJson.get("storage-credentials");
Assertions.assertTrue(credentials.isArray() && credentials.size() > 0);
JsonNode firstCred = credentials.get(0);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[MEDIUM] Tests don't verify prefix or refresh properties

The tests verify config contains the credential type, but don't verify:

  1. The prefix field is set correctly — currently getCredentialsIfEligible returns "" (empty string), but the existing getTableCredentials/injectCredentialConfig methods use the table's metadata location as the prefix. This discrepancy should be tested.
  2. Refresh properties (e.g. client.refresh-credentials-endpoint) are present — IcebergRESTUtils.toRESTCredential() adds these but this code path uses raw CredentialPropertyUtils.toIcebergProperties() which omits them.
  3. The overall credential structure matches what loadTable with vended-credentials returns for the same table, ensuring consistency.

Consider adding:

Assertions.assertFalse(firstCred.get("prefix").asText().isEmpty(),
    "Credential prefix should be the table location");

@sachinnn99 sachinnn99 force-pushed the feat/10685-plan-table-scan-credential-vending branch from c4aece4 to dac1df3 Compare June 16, 2026 15:14
@sachinnn99

sachinnn99 commented Jun 16, 2026

Copy link
Copy Markdown
Contributor Author

@laserninja Addressed everything in the latest push:

  • Dispatcher bypass: moved credential vending into the executor + wrapper, matching the loadTable pattern. Privilege is now determined via getCredentialPrivilege() instead of hardcoded READ.
  • Broad catch: eliminated entirely from the REST layer. Only ServiceUnavailableException is caught inside the wrapper now.
  • Redundant loadTable: planTableScan reuses the Table already loaded for scanning -- no second load. Added getCredentialFromTable() for this.
  • Added TODO for the builder workaround and prefix assertion in the test.

Conflicts with main are resolved.

@laserninja

laserninja commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Verification: All Requested Changes ✓

Excellent follow-up! The latest revision addresses all the HIGH and MEDIUM issues raised in the previous review:

Issues Fixed

[HIGH] Bypasses dispatcher layer

  • Credential vending now routes through IcebergTableOperationExecutor.planTableScan()
  • Calls getCredentialPrivilege() and passes parameters to CatalogWrapperForREST.planTableScan(...)
  • IcebergCatalogWrapperManager field removed from IcebergTableOperations
  • Events and authorization hooks will now fire correctly

[HIGH] Broad catch (Exception e)

  • Narrowed to only catch ServiceUnavailableException in injectScanCredentials()
  • Matches the pattern in getTableCredentials()
  • Auth failures, illegal args, and other errors now propagate properly

[HIGH] Redundant loadTable call

  • injectScanCredentials() receives the Table already loaded for scan planning
  • No additional loadTable invocation
  • Credential generation uses getCredentialFromTable() which extracts location/properties from the Table

[MEDIUM] JSON-level merge fragility

  • Replaced ad-hoc ObjectNode/ArrayNode JSON construction with PlanTableScanResponse.builder()...withCredentials(...)
  • TODO comment notes future cleanup when Iceberg 1.11.0+ is adopted
  • Much cleaner and more maintainable

[MEDIUM] Test coverage for prefix & refresh properties

  • Tests now assert prefix field is present and equals table location
  • IcebergRESTUtils.buildRefreshProps() and toRESTCredential() ensure refresh endpoints are included
  • Consistent with getTableCredentials pattern

📝 Minor Items

Duplicate Javadoc on no-arg planTableScan overload
The original method's full Javadoc still appears above the new /** Plan table scan without credential vending. */ comment, resulting in two stacked /** */ blocks. Consider removing the old one since it now applies only to the 4-arg overload (which has its own comprehensive Javadoc).

FederatedCatalogWrapper doesn't override planTableScan
FederatedCatalogWrapper still doesn't override the new 4-arg planTableScan() method. When federated catalogs call this, they'll use the local credential manager instead of proxying to the remote catalog. However:

  • The graceful fallback (ServiceUnavailableException → no credentials) prevents breakage
  • This could be addressed in a follow-up PR if federated credential vending on scans becomes important

LGTM with one minor cleanup (duplicate Javadoc).

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown

Code Coverage Report

Overall Project 67.1% +0.05% 🟢
Files changed 68.51% 🟢

Module Coverage
aliyun 1.72% 🔴
api 46.82% 🟢
authorization-common 85.96% 🟢
aws 3.66% -0.28% 🔴
azure 2.47% 🔴
catalog-common 10.4% 🔴
catalog-fileset 80.23% 🟢
catalog-glue 66.91% -0.54% 🟢
catalog-hive 79.44% +2.43% 🟢
catalog-jdbc-clickhouse 80.02% 🟢
catalog-jdbc-common 44.22% 🟢
catalog-jdbc-doris 80.28% 🟢
catalog-jdbc-hologres 54.03% 🟢
catalog-jdbc-mysql 79.23% 🟢
catalog-jdbc-oceanbase 78.38% +6.83% 🟢
catalog-jdbc-postgresql 82.29% 🟢
catalog-jdbc-starrocks 78.51% 🟢
catalog-kafka 77.01% 🟢
catalog-lakehouse-generic 58.53% 🟢
catalog-lakehouse-hudi 79.1% 🟢
catalog-lakehouse-iceberg 85.86% 🟢
catalog-lakehouse-paimon 82.14% 🟢
catalog-model 77.72% 🟢
cli 44.51% 🟢
client-java 78.01% 🟢
common 50.17% -0.04% 🟢
core 82.45% -0.37% 🟢
filesystem-hadoop3 77.27% -1.29% 🟢
flink 0.0% 🔴
flink-common 46.84% -0.33% 🟢
flink-runtime 0.0% 🔴
gcp 14.12% 🔴
hadoop-common 10.88% 🔴
hive-metastore-common 53.77% 🟢
iceberg-common 58.15% 🟢
iceberg-rest-server 74.05% +2.13% 🟢
idp-basic 86.18% -1.65% 🟢
integration-test-common 0.0% 🔴
jobs 66.17% 🟢
lance-common 20.83% -2.95% 🔴
lance-rest-server 60.13% 🟢
lineage 53.02% 🟢
optimizer 82.87% 🟢
optimizer-api 21.95% 🔴
server 85.96% 🟢
server-common 74.21% -0.64% 🟢
spark 28.57% 🔴
spark-common 41.66% +0.42% 🟢
trino-connector 40.13% +0.45% 🟢
Files
Module File Coverage
aws AwsIrsaCredentialGenerator.java 3.25% 🔴
catalog-glue GravitinoGlueCredentialsProvider.java 0.0% 🔴
catalog-hive HiveCatalogOperations.java 81.74% 🟢
catalog-jdbc-oceanbase OceanBaseTableOperations.java 83.33% 🟢
OceanBaseCatalog.java 0.0% 🔴
common AuthenticatorType.java 0.0% 🔴
core CaffeineEntityCache.java 81.87% 🟢
RelationalEntityStore.java 55.17% 🔴
IsolatedClassLoader.java 47.96% 🔴
NoOpsCache.java 26.32% 🔴
SupportsRelationEntityCache.java 0.0% 🔴
filesystem-hadoop3 GravitinoVirtualFileSystem.java 70.68% 🟢
flink-common GravitinoIcebergCatalogFactory.java 32.43% 🔴
iceberg-rest-server IcebergTableOperationExecutor.java 100.0% 🟢
FederatedCatalogWrapper.java 86.96% 🟢
IcebergTableOperations.java 81.28% 🟢
CatalogWrapperForREST.java 74.91% 🟢
idp-basic PasswordHasherFactory.java 100.0% 🟢
Argon2idPasswordHasher.java 94.62% 🟢
BasicAuthenticator.java 89.8% 🟢
IdpRESTFeature.java 22.22% 🔴
Argon2idDefaults.java 0.0% 🔴
lance-common JsonArrowSchemaConverter.java 0.0% 🔴
PageUtil.java 0.0% 🔴
server-common AuthenticatorFactory.java 0.0% 🔴
spark-common GravitinoGlueCredentialsProvider.java 100.0% 🟢
trino-connector IcebergCatalogPropertyConverter.java 64.29% 🟢

@sachinnn99 sachinnn99 force-pushed the feat/10685-plan-table-scan-credential-vending branch from 986ed6f to fe1a580 Compare June 17, 2026 07:37
@sachinnn99

Copy link
Copy Markdown
Contributor Author

@laserninja Thanks for the thorough verification! Removed the duplicate Javadoc on the no-arg overload in the latest push. Ready for approval when you get a chance.

@sachinnn99

Copy link
Copy Markdown
Contributor Author

Hey @roryqi, I've addressed all of laserninja's feedback and resolved the conflicts. Could you please take a look? Thanks!

@roryqi roryqi requested a review from laserninja June 29, 2026 02:39

@laserninja laserninja left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM!

@roryqi

roryqi commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

@lasdf1234 Could u help me review this pull request?

@lasdf1234

Copy link
Copy Markdown
Collaborator

@lasdf1234 Could u help me review this pull request?

Got.

Table table = getCatalog().loadTable(tableIdentifier);
Optional<PlanTableScanResponse> cachedResponse =
scanPlanCache.get(ScanPlanCacheKey.create(tableIdentifier, table, scanRequest));
if (cachedResponse.isPresent()) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

ScanPlanCacheKey does not include requestCredentialVending, and we cache the full PlanTableScanResponse (with or without credentials). This can cause:

1、Plan without header → plan with vended-credentials: cache hit returns a response without storage-credentials.
2、 Plan with header → plan without header: cache hit may return stale/extra credentials.
3、Cached credentials may expire while the scan plan remains valid.

Suggestion: Cache only credential-free scan plans. Inject credentials after cache lookup on every request (hit or miss), similar to how loadTable generates credentials per request:

@lasdf1234

Copy link
Copy Markdown
Collaborator

@sachinnn99 Another issue is that you need to consider the scenario of a federation IRC.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Improvement] planTableScan endpoint should support X-Iceberg-Access-Delegation and return storage credentials

4 participants