-
Notifications
You must be signed in to change notification settings - Fork 906
[#10684] feat(iceberg-rest): Support vended credentials on registerTable endpoint #10699
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -80,6 +80,7 @@ | |
| import org.apache.iceberg.rest.RESTCatalog; | ||
| import org.apache.iceberg.rest.requests.CreateTableRequest; | ||
| import org.apache.iceberg.rest.requests.PlanTableScanRequest; | ||
| import org.apache.iceberg.rest.requests.RegisterTableRequest; | ||
| import org.apache.iceberg.rest.requests.UpdateTableRequest; | ||
| import org.apache.iceberg.rest.responses.ImmutableLoadCredentialsResponse; | ||
| import org.apache.iceberg.rest.responses.LoadCredentialsResponse; | ||
|
|
@@ -156,6 +157,21 @@ public LoadTableResponse loadTable( | |
| return loadTableResponse; | ||
| } | ||
|
|
||
| public LoadTableResponse registerTable( | ||
| Namespace namespace, RegisterTableRequest request, boolean requestCredential) { | ||
| LoadTableResponse loadTableResponse = super.registerTable(namespace, request); | ||
| if (shouldGenerateCredential(loadTableResponse, requestCredential)) { | ||
| // Vend WRITE credentials: the registering user becomes the table owner | ||
| // (IcebergNamespaceHookDispatcher.setTableOwner runs after this call | ||
| // returns), consistent with createTable which also vends WRITE. | ||
| return injectCredentialConfig( | ||
| TableIdentifier.of(namespace, request.name()), | ||
| loadTableResponse, | ||
| CredentialPrivilege.WRITE); | ||
| } | ||
| return loadTableResponse; | ||
| } | ||
|
|
||
| @Override | ||
| public LoadTableResponse updateTable( | ||
| TableIdentifier tableIdentifier, UpdateTableRequest updateTableRequest) { | ||
|
|
@@ -286,7 +302,8 @@ protected boolean useDifferentClassLoader() { | |
| return false; | ||
| } | ||
|
|
||
| private LoadTableResponse injectCredentialConfig( | ||
| @VisibleForTesting | ||
| protected LoadTableResponse injectCredentialConfig( | ||
| TableIdentifier tableIdentifier, | ||
| LoadTableResponse loadTableResponse, | ||
| CredentialPrivilege privilege) { | ||
|
|
@@ -336,7 +353,8 @@ private Credential getCredential( | |
| return credential; | ||
| } | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same as above
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same JavaDoc rationale added on |
||
| private boolean shouldGenerateCredential( | ||
| @VisibleForTesting | ||
| protected boolean shouldGenerateCredential( | ||
| LoadTableResponse loadTableResponse, boolean requestCredential) { | ||
| if (!requestCredential) { | ||
| return false; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -603,7 +603,13 @@ private static boolean etagMatches(String ifNoneMatch, EntityTag etag) { | |
| return etag.getValue().equals(clientEtag); | ||
| } | ||
|
|
||
| private boolean isCredentialVending(String accessDelegation) { | ||
| /** | ||
| * Parses the {@code X-Iceberg-Access-Delegation} header value and returns whether the client is | ||
| * requesting credential vending. Package-private and static so that {@link | ||
| * IcebergNamespaceOperations#registerTable} can reuse the same parsing logic from the same | ||
| * package. | ||
| */ | ||
| static boolean isCredentialVending(String accessDelegation) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We are making this package-private
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch on the visibility change being undocumented, but the change is actually for production reuse rather than testing — |
||
| if (StringUtils.isBlank(accessDelegation)) { | ||
| return false; | ||
| } | ||
|
|
||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The PR's new 3-arg However, if the credential injection logic ever changes in // Cannot delegate to super.registerTable(3-arg) because the in-memory catalog
// does not support registerTable; must inline credential injection here.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added an inline comment at the call site explaining the duplication: the in-memory test catalog cannot perform |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,10 +21,15 @@ | |
| import java.util.Arrays; | ||
| import java.util.Optional; | ||
| import javax.servlet.http.HttpServletRequest; | ||
| import javax.ws.rs.client.Entity; | ||
| import javax.ws.rs.core.Application; | ||
| import javax.ws.rs.core.EntityTag; | ||
| import javax.ws.rs.core.MediaType; | ||
| import javax.ws.rs.core.Response; | ||
| import javax.ws.rs.core.Response.Status; | ||
| import org.apache.gravitino.credential.Credential; | ||
| import org.apache.gravitino.iceberg.service.IcebergRESTUtils; | ||
| import org.apache.gravitino.iceberg.service.extension.DummyCredentialProvider; | ||
| import org.apache.gravitino.listener.api.event.Event; | ||
| import org.apache.gravitino.listener.api.event.IcebergCreateNamespaceEvent; | ||
| import org.apache.gravitino.listener.api.event.IcebergCreateNamespaceFailureEvent; | ||
|
|
@@ -44,6 +49,9 @@ | |
| import org.apache.gravitino.listener.api.event.IcebergUpdateNamespaceFailureEvent; | ||
| import org.apache.gravitino.listener.api.event.IcebergUpdateNamespacePreEvent; | ||
| import org.apache.iceberg.catalog.Namespace; | ||
| import org.apache.iceberg.rest.requests.ImmutableRegisterTableRequest; | ||
| import org.apache.iceberg.rest.requests.RegisterTableRequest; | ||
| import org.apache.iceberg.rest.responses.LoadTableResponse; | ||
| import org.glassfish.jersey.internal.inject.AbstractBinder; | ||
| import org.glassfish.jersey.server.ResourceConfig; | ||
| import org.junit.jupiter.api.Assertions; | ||
|
|
@@ -257,4 +265,68 @@ void testUpdateNamespace() { | |
| dummyEventListener.clearEvent(); | ||
| verifyUpdateNamespaceSucc(Namespace.of("update_foo3", "a")); | ||
| } | ||
|
|
||
| @Test | ||
| void testRegisterTableWithCredentialVending() { | ||
| // register without credential vending -- no credentials in response | ||
| verifyRegisterTableSucc("register_cred_foo1", Namespace.of("register_cred_ns")); | ||
|
|
||
| // register with credential vending but local location -- should NOT vend | ||
| Response response = | ||
| doRegisterTableWithCredentialVending( | ||
| "register_cred_foo2", Namespace.of("register_cred_ns2"), "mock"); | ||
| Assertions.assertEquals(Status.OK.getStatusCode(), response.getStatus()); | ||
| LoadTableResponse loadTableResponse = response.readEntity(LoadTableResponse.class); | ||
| Assertions.assertFalse(loadTableResponse.config().containsKey(Credential.CREDENTIAL_TYPE)); | ||
|
|
||
| // register with credential vending and S3 location -- SHOULD vend | ||
| String s3Location = "s3://dummy-bucket/register_cred_foo3"; | ||
| response = | ||
| doRegisterTableWithCredentialVending( | ||
| "register_cred_foo3", Namespace.of("register_cred_ns3"), s3Location); | ||
| Assertions.assertEquals(Status.OK.getStatusCode(), response.getStatus()); | ||
| loadTableResponse = response.readEntity(LoadTableResponse.class); | ||
| Assertions.assertEquals( | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The S3 vending case only checks for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added an additional assertion on |
||
| DummyCredentialProvider.DUMMY_CREDENTIAL_TYPE, | ||
| loadTableResponse.config().get(Credential.CREDENTIAL_TYPE)); | ||
| // DummyCredentialProvider.SimpleCredential is not one of the typed credentials handled | ||
| // in CredentialPropertyUtils#toIcebergProperties, so it falls through to | ||
| // Credential#toProperties, which always emits credential-type and expire-time-in-ms. | ||
| // Asserting both guards against partial-injection regressions. | ||
| Assertions.assertEquals("0", loadTableResponse.config().get(Credential.EXPIRE_TIME_IN_MS)); | ||
| } | ||
|
|
||
| @Test | ||
| void testRegisterTableRemoteSigningNotSupported() { | ||
| RegisterTableRequest request = | ||
| ImmutableRegisterTableRequest.builder() | ||
| .name("remote_signing_test") | ||
| .metadataLocation("mock") | ||
| .build(); | ||
| Response response = | ||
| getNamespaceClientBuilder( | ||
| Optional.of(Namespace.of("register_remote_ns")), | ||
| Optional.of("register"), | ||
| Optional.empty()) | ||
| .header(IcebergTableOperations.X_ICEBERG_ACCESS_DELEGATION, "remote-signing") | ||
| .post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE)); | ||
| Assertions.assertEquals(406, response.getStatus()); | ||
| } | ||
|
|
||
| @Test | ||
| void testRegisterTableInvalidAccessDelegation() { | ||
| RegisterTableRequest request = | ||
| ImmutableRegisterTableRequest.builder() | ||
| .name("invalid_delegation_test") | ||
| .metadataLocation("mock") | ||
| .build(); | ||
| Response response = | ||
| getNamespaceClientBuilder( | ||
| Optional.of(Namespace.of("register_invalid_ns")), | ||
| Optional.of("register"), | ||
| Optional.empty()) | ||
| .header(IcebergTableOperations.X_ICEBERG_ACCESS_DELEGATION, "invalid-value") | ||
| .post(Entity.entity(request, MediaType.APPLICATION_JSON_TYPE)); | ||
| Assertions.assertEquals(400, response.getStatus()); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These methods go from
private→protected, which makes them part of the subclass API contract. This is the cleanest option given the constraints but will need to be maintained as a stable interface going forward. A short JavaDoc note on their intended use (subclass credential injection in cases wheresupercan't be called) would clarify intent.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added JavaDoc explaining that the
protectedvisibility is for subclasses that synthesize aLoadTableResponsewithout delegating tosuper(test wrappers around in-memory catalogs that can't perform certain operations natively), so they can still apply the same vending and injection logic as production paths.