This is an automated email from the ASF dual-hosted git repository.
fanng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 0221fef05e [#8312] feat(iceberg-rest):Support endpoint to retrieve
valid credentials for a given table (#8592)
0221fef05e is described below
commit 0221fef05e00f0ea1ad3bd431661e9aa8381779b
Author: Xiaojian Sun <[email protected]>
AuthorDate: Mon Sep 22 17:00:09 2025 +0800
[#8312] feat(iceberg-rest):Support endpoint to retrieve valid credentials
for a given table (#8592)
### What changes were proposed in this pull request?
Support endpoint to retrieve valid credentials for a given table.
### Why are the changes needed?
Fix: #8312
### Does this PR introduce _any_ user-facing change?
N/A
### How was this patch tested?
org.apache.gravitino.iceberg.service.rest.TestIcebergTableOperations#testGetTableCredentials
---
.../listener/api/event/OperationType.java | 1 +
docs/iceberg-rest-service.md | 1 -
.../iceberg/service/CatalogWrapperForREST.java | 66 +++++++++++++++++-----
.../dispatcher/IcebergTableEventDispatcher.java | 33 +++++++++++
.../IcebergTableOperationDispatcher.java | 11 ++++
.../dispatcher/IcebergTableOperationExecutor.java | 9 +++
.../service/rest/IcebergTableOperations.java | 36 ++++++++++++
.../api/event/IcebergLoadTableCredentialEvent.java | 37 ++++++++++++
.../IcebergLoadTableCredentialFailureEvent.java | 37 ++++++++++++
.../event/IcebergLoadTableCredentialPreEvent.java | 37 ++++++++++++
.../service/rest/TestIcebergTableOperations.java | 25 ++++++++
11 files changed, 279 insertions(+), 14 deletions(-)
diff --git
a/core/src/main/java/org/apache/gravitino/listener/api/event/OperationType.java
b/core/src/main/java/org/apache/gravitino/listener/api/event/OperationType.java
index 3c7b1439b1..c9b760d901 100644
---
a/core/src/main/java/org/apache/gravitino/listener/api/event/OperationType.java
+++
b/core/src/main/java/org/apache/gravitino/listener/api/event/OperationType.java
@@ -25,6 +25,7 @@ public enum OperationType {
DROP_TABLE,
PURGE_TABLE,
LOAD_TABLE,
+ LOAD_TABLE_CREDENTIAL,
LIST_TABLE,
ALTER_TABLE,
RENAME_TABLE,
diff --git a/docs/iceberg-rest-service.md b/docs/iceberg-rest-service.md
index 5241681500..93240bd1c0 100644
--- a/docs/iceberg-rest-service.md
+++ b/docs/iceberg-rest-service.md
@@ -23,7 +23,6 @@ There are some key difference between Gravitino Iceberg REST
server and Gravitin
- multi table transaction
- pagination
- scan planning
- - load table credentials
- Works as a catalog proxy, supporting `Hive` and `JDBC` as catalog backend.
- Supports credential vending for `S3`、`GCS`、`OSS` and `ADLS`.
- Supports different storages like `S3`, `HDFS`, `OSS`, `GCS`, `ADLS` and
provides the capability to support other storages.
diff --git
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/CatalogWrapperForREST.java
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/CatalogWrapperForREST.java
index ffe38661cd..df9192ebd7 100644
---
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/CatalogWrapperForREST.java
+++
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/CatalogWrapperForREST.java
@@ -45,6 +45,8 @@ import org.apache.iceberg.catalog.Namespace;
import org.apache.iceberg.catalog.TableIdentifier;
import org.apache.iceberg.exceptions.ServiceUnavailableException;
import org.apache.iceberg.rest.requests.CreateTableRequest;
+import org.apache.iceberg.rest.responses.ImmutableLoadCredentialsResponse;
+import org.apache.iceberg.rest.responses.LoadCredentialsResponse;
import org.apache.iceberg.rest.responses.LoadTableResponse;
/** Process Iceberg REST specific operations, like credential vending. */
@@ -100,6 +102,39 @@ public class CatalogWrapperForREST extends
IcebergCatalogWrapper {
return loadTableResponse;
}
+ /**
+ * Get table credentials.
+ *
+ * @param identifier The table identifier for which to load credentials
+ * @return A {@link
org.apache.iceberg.rest.responses.LoadCredentialsResponse} object containing
+ * the credentials.
+ */
+ public LoadCredentialsResponse getTableCredentials(TableIdentifier
identifier) {
+ try {
+ LoadTableResponse loadTableResponse = super.loadTable(identifier);
+ Credential credential = getCredential(loadTableResponse);
+ org.apache.iceberg.rest.credentials.Credential icebergCredential =
+ new org.apache.iceberg.rest.credentials.Credential() {
+ @Override
+ public String prefix() {
+ return "";
+ }
+
+ @Override
+ public Map<String, String> config() {
+ return CredentialPropertyUtils.toIcebergProperties(credential);
+ }
+
+ @Override
+ public void validate() {}
+ };
+ return
ImmutableLoadCredentialsResponse.builder().addCredentials(icebergCredential).build();
+ } catch (ServiceUnavailableException e) {
+ LOG.warn("Service unavailable when loading table credentials for table:
{}", identifier, e);
+ return ImmutableLoadCredentialsResponse.builder().build();
+ }
+ }
+
@Override
public void close() {
if (catalogCredentialManager != null) {
@@ -113,6 +148,23 @@ public class CatalogWrapperForREST extends
IcebergCatalogWrapper {
private LoadTableResponse injectCredentialConfig(
TableIdentifier tableIdentifier, LoadTableResponse loadTableResponse) {
+ final Credential credential = getCredential(loadTableResponse);
+
+ LOG.info(
+ "Generate credential: {} for Iceberg table: {}",
+ credential.credentialType(),
+ tableIdentifier);
+
+ Map<String, String> credentialConfig =
CredentialPropertyUtils.toIcebergProperties(credential);
+ return LoadTableResponse.builder()
+ .withTableMetadata(loadTableResponse.tableMetadata())
+ .addAllConfig(loadTableResponse.config())
+ .addAllConfig(getCatalogConfigToClient())
+ .addAllConfig(credentialConfig)
+ .build();
+ }
+
+ private Credential getCredential(LoadTableResponse loadTableResponse) {
TableMetadata tableMetadata = loadTableResponse.tableMetadata();
String[] path =
Stream.of(
@@ -129,19 +181,7 @@ public class CatalogWrapperForREST extends
IcebergCatalogWrapper {
if (credential == null) {
throw new ServiceUnavailableException("Couldn't generate credential,
%s", context);
}
-
- LOG.info(
- "Generate credential: {} for Iceberg table: {}",
- credential.credentialType(),
- tableIdentifier);
-
- Map<String, String> credentialConfig =
CredentialPropertyUtils.toIcebergProperties(credential);
- return LoadTableResponse.builder()
- .withTableMetadata(loadTableResponse.tableMetadata())
- .addAllConfig(loadTableResponse.config())
- .addAllConfig(getCatalogConfigToClient())
- .addAllConfig(credentialConfig)
- .build();
+ return credential;
}
@VisibleForTesting
diff --git
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergTableEventDispatcher.java
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergTableEventDispatcher.java
index 1dddce8904..0bcf8a3c5f 100644
---
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergTableEventDispatcher.java
+++
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergTableEventDispatcher.java
@@ -33,6 +33,9 @@ import
org.apache.gravitino.listener.api.event.IcebergDropTablePreEvent;
import org.apache.gravitino.listener.api.event.IcebergListTableEvent;
import org.apache.gravitino.listener.api.event.IcebergListTableFailureEvent;
import org.apache.gravitino.listener.api.event.IcebergListTablePreEvent;
+import org.apache.gravitino.listener.api.event.IcebergLoadTableCredentialEvent;
+import
org.apache.gravitino.listener.api.event.IcebergLoadTableCredentialFailureEvent;
+import
org.apache.gravitino.listener.api.event.IcebergLoadTableCredentialPreEvent;
import org.apache.gravitino.listener.api.event.IcebergLoadTableEvent;
import org.apache.gravitino.listener.api.event.IcebergLoadTableFailureEvent;
import org.apache.gravitino.listener.api.event.IcebergLoadTablePreEvent;
@@ -52,6 +55,7 @@ import org.apache.iceberg.rest.requests.CreateTableRequest;
import org.apache.iceberg.rest.requests.RenameTableRequest;
import org.apache.iceberg.rest.requests.UpdateTableRequest;
import org.apache.iceberg.rest.responses.ListTablesResponse;
+import org.apache.iceberg.rest.responses.LoadCredentialsResponse;
import org.apache.iceberg.rest.responses.LoadTableResponse;
/**
@@ -230,4 +234,33 @@ public class IcebergTableEventDispatcher implements
IcebergTableOperationDispatc
eventBus.dispatchEvent(
new IcebergRenameTableEvent(context, gravitinoNameIdentifier,
renameTableRequest));
}
+
+ /**
+ * Get credentials for an Iceberg table.
+ *
+ * @param context Iceberg REST request context information.
+ * @param tableIdentifier The Iceberg table identifier.
+ * @return A {@link
org.apache.iceberg.rest.responses.LoadCredentialsResponse} object containing
+ * the credentials.
+ */
+ @Override
+ public LoadCredentialsResponse getTableCredentials(
+ IcebergRequestContext context, TableIdentifier tableIdentifier) {
+ NameIdentifier gravitinoNameIdentifier =
+ IcebergRestUtils.getGravitinoNameIdentifier(
+ metalakeName, context.catalogName(), tableIdentifier);
+ eventBus.dispatchEvent(
+ new IcebergLoadTableCredentialPreEvent(context,
gravitinoNameIdentifier));
+ LoadCredentialsResponse loadCredentialsResponse;
+ try {
+ loadCredentialsResponse =
+ icebergTableOperationDispatcher.getTableCredentials(context,
tableIdentifier);
+ } catch (Exception e) {
+ eventBus.dispatchEvent(
+ new IcebergLoadTableCredentialFailureEvent(context,
gravitinoNameIdentifier, e));
+ throw e;
+ }
+ eventBus.dispatchEvent(new IcebergLoadTableCredentialEvent(context,
gravitinoNameIdentifier));
+ return loadCredentialsResponse;
+ }
}
diff --git
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergTableOperationDispatcher.java
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergTableOperationDispatcher.java
index 34c0a71741..75762b42e8 100644
---
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergTableOperationDispatcher.java
+++
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergTableOperationDispatcher.java
@@ -26,6 +26,7 @@ import org.apache.iceberg.rest.requests.CreateTableRequest;
import org.apache.iceberg.rest.requests.RenameTableRequest;
import org.apache.iceberg.rest.requests.UpdateTableRequest;
import org.apache.iceberg.rest.responses.ListTablesResponse;
+import org.apache.iceberg.rest.responses.LoadCredentialsResponse;
import org.apache.iceberg.rest.responses.LoadTableResponse;
/**
@@ -102,4 +103,14 @@ public interface IcebergTableOperationDispatcher {
* @param renameTableRequest Rename table request information.
*/
void renameTable(IcebergRequestContext context, RenameTableRequest
renameTableRequest);
+
+ /**
+ * Get credentials for an Iceberg table.
+ *
+ * @param context Iceberg REST request context information.
+ * @param tableIdentifier The Iceberg table identifier.
+ * @return A {@link LoadCredentialsResponse} object containing the
credentials.
+ */
+ LoadCredentialsResponse getTableCredentials(
+ IcebergRequestContext context, TableIdentifier tableIdentifier);
}
diff --git
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergTableOperationExecutor.java
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergTableOperationExecutor.java
index 31e94ab9f6..cec833136a 100644
---
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergTableOperationExecutor.java
+++
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/dispatcher/IcebergTableOperationExecutor.java
@@ -27,6 +27,7 @@ import org.apache.iceberg.rest.requests.CreateTableRequest;
import org.apache.iceberg.rest.requests.RenameTableRequest;
import org.apache.iceberg.rest.requests.UpdateTableRequest;
import org.apache.iceberg.rest.responses.ListTablesResponse;
+import org.apache.iceberg.rest.responses.LoadCredentialsResponse;
import org.apache.iceberg.rest.responses.LoadTableResponse;
public class IcebergTableOperationExecutor implements
IcebergTableOperationDispatcher {
@@ -97,4 +98,12 @@ public class IcebergTableOperationExecutor implements
IcebergTableOperationDispa
.getCatalogWrapper(context.catalogName())
.renameTable(renameTableRequest);
}
+
+ @Override
+ public LoadCredentialsResponse getTableCredentials(
+ IcebergRequestContext context, TableIdentifier tableIdentifier) {
+ return icebergCatalogWrapperManager
+ .getCatalogWrapper(context.catalogName())
+ .getTableCredentials(tableIdentifier);
+ }
}
diff --git
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergTableOperations.java
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergTableOperations.java
index f6a9070100..68d8c20af2 100644
---
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergTableOperations.java
+++
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergTableOperations.java
@@ -56,6 +56,7 @@ import org.apache.iceberg.rest.requests.CreateTableRequest;
import org.apache.iceberg.rest.requests.ReportMetricsRequest;
import org.apache.iceberg.rest.requests.UpdateTableRequest;
import org.apache.iceberg.rest.responses.ListTablesResponse;
+import org.apache.iceberg.rest.responses.LoadCredentialsResponse;
import org.apache.iceberg.rest.responses.LoadTableResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -332,6 +333,41 @@ public class IcebergTableOperations {
}
}
+ @GET
+ @Path("{table}/credentials")
+ @Produces(MediaType.APPLICATION_JSON)
+ @Timed(name = "get-table-credentials." + MetricNames.HTTP_PROCESS_DURATION,
absolute = true)
+ @ResponseMetered(name = "get-table-credentials", absolute = true)
+ public Response getTableCredentials(
+ @PathParam("prefix") String prefix,
+ @Encoded() @PathParam("namespace") String namespace,
+ @PathParam("table") String table) {
+ String catalogName = IcebergRestUtils.getCatalogName(prefix);
+ Namespace icebergNS = RESTUtil.decodeNamespace(namespace);
+ LOG.info(
+ "Get Iceberg table credentials, catalog: {}, namespace: {}, table: {}",
+ catalogName,
+ icebergNS,
+ table);
+ try {
+ return Utils.doAs(
+ httpRequest,
+ () -> {
+ // Convert Iceberg table identifier to Gravitino NameIdentifier
+ TableIdentifier tableIdentifier = TableIdentifier.of(icebergNS,
table);
+ // First check if the table exists
+ IcebergRequestContext context =
+ new IcebergRequestContext(httpServletRequest(), catalogName);
+ // Get credentials using the table operation dispatcher
+ LoadCredentialsResponse credentialsResponse =
+ tableOperationDispatcher.getTableCredentials(context,
tableIdentifier);
+ return IcebergRestUtils.ok(credentialsResponse);
+ });
+ } catch (Exception e) {
+ return IcebergExceptionMapper.toRESTResponse(e);
+ }
+ }
+
private boolean isCredentialVending(String accessDelegation) {
if (StringUtils.isBlank(accessDelegation)) {
return false;
diff --git
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/listener/api/event/IcebergLoadTableCredentialEvent.java
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/listener/api/event/IcebergLoadTableCredentialEvent.java
new file mode 100644
index 0000000000..cafadb0c11
--- /dev/null
+++
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/listener/api/event/IcebergLoadTableCredentialEvent.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.listener.api.event;
+
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+
+/** Represent an event after loading Iceberg table credential successfully. */
+@DeveloperApi
+public class IcebergLoadTableCredentialEvent extends IcebergTableEvent {
+ public IcebergLoadTableCredentialEvent(
+ IcebergRequestContext icebergRequestContext, NameIdentifier
resourceIdentifier) {
+ super(icebergRequestContext, resourceIdentifier);
+ }
+
+ @Override
+ public OperationType operationType() {
+ return OperationType.LOAD_TABLE_CREDENTIAL;
+ }
+}
diff --git
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/listener/api/event/IcebergLoadTableCredentialFailureEvent.java
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/listener/api/event/IcebergLoadTableCredentialFailureEvent.java
new file mode 100644
index 0000000000..a21800e942
--- /dev/null
+++
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/listener/api/event/IcebergLoadTableCredentialFailureEvent.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.listener.api.event;
+
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+
+/** Represent a failure event when loading Iceberg table credential failed. */
+@DeveloperApi
+public class IcebergLoadTableCredentialFailureEvent extends
IcebergTableFailureEvent {
+ public IcebergLoadTableCredentialFailureEvent(
+ IcebergRequestContext icebergRequestContext, NameIdentifier
nameIdentifier, Exception e) {
+ super(icebergRequestContext, nameIdentifier, e);
+ }
+
+ @Override
+ public OperationType operationType() {
+ return OperationType.LOAD_TABLE_CREDENTIAL;
+ }
+}
diff --git
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/listener/api/event/IcebergLoadTableCredentialPreEvent.java
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/listener/api/event/IcebergLoadTableCredentialPreEvent.java
new file mode 100644
index 0000000000..f536424c23
--- /dev/null
+++
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/listener/api/event/IcebergLoadTableCredentialPreEvent.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.gravitino.listener.api.event;
+
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.annotation.DeveloperApi;
+
+/** Represent a pre event before loading Iceberg table credential. */
+@DeveloperApi
+public class IcebergLoadTableCredentialPreEvent extends IcebergTablePreEvent {
+ public IcebergLoadTableCredentialPreEvent(
+ IcebergRequestContext icebergRequestContext, NameIdentifier
tableIdentifier) {
+ super(icebergRequestContext, tableIdentifier);
+ }
+
+ @Override
+ public OperationType operationType() {
+ return OperationType.LOAD_TABLE_CREDENTIAL;
+ }
+}
diff --git
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergTableOperations.java
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergTableOperations.java
index b076df1bdd..28d45d4019 100644
---
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergTableOperations.java
+++
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergTableOperations.java
@@ -471,4 +471,29 @@ public class TestIcebergTableOperations extends
IcebergNamespaceTestBase {
Response response = doCreateTable(ns, name);
Assertions.assertEquals(status, response.getStatus());
}
+
+ @ParameterizedTest
+
@MethodSource("org.apache.gravitino.iceberg.service.rest.IcebergRestTestUtil#testNamespaces")
+ public void testGetTableCredentials(Namespace ns) {
+ String tableName = "test_table_credentials";
+
+ // First create the namespace
+ verifyCreateNamespaceSucc(ns);
+
+ // Then create a table
+ verifyCreateTableSucc(ns, tableName);
+
+ // Then test getting credentials
+ Response response = doGetTableCredentials(ns, tableName);
+ Assertions.assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+ // Verify the response contains credentials (even if empty)
+ String responseBody = response.readEntity(String.class);
+ Assertions.assertNotNull(responseBody);
+ Assertions.assertTrue(responseBody.contains("credentials"));
+ }
+
+ private Response doGetTableCredentials(Namespace ns, String tableName) {
+ return getTableClientBuilder(ns, Optional.of(tableName +
"/credentials")).get();
+ }
}