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 8381f2aec5 [#9936] fix(iceberg): URL decodes the table name in iceberg
rest server paths (#9955)
8381f2aec5 is described below
commit 8381f2aec5a305c79d8c55217e2e6ae9b94c4aa9
Author: Xinyi Lu <[email protected]>
AuthorDate: Thu Feb 12 18:45:03 2026 -0800
[#9936] fix(iceberg): URL decodes the table name in iceberg rest server
paths (#9955)
### What changes were proposed in this pull request?
This change decodes table names in the IcebergTableOperations path
through Iceberg RestUtil.decodeString.
### Why are the changes needed?
Fix: #9936
### Does this PR introduce _any_ user-facing change?
No
### How was this patch tested?
Through added unittest.
---
.../service/rest/IcebergTableOperations.java | 53 ++++++++++++++--------
.../service/rest/IcebergViewOperations.java | 34 +++++++++-----
.../service/rest/TestIcebergTableOperations.java | 35 ++++++++++++++
.../service/rest/TestIcebergViewOperations.java | 35 ++++++++++++++
4 files changed, 125 insertions(+), 32 deletions(-)
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 e499dd8c62..e8af54b985 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
@@ -195,10 +195,12 @@ public class IcebergTableOperations {
@AuthorizationMetadata(type = Entity.EntityType.CATALOG)
@PathParam("prefix") String prefix,
@AuthorizationMetadata(type = EntityType.SCHEMA) @Encoded()
@PathParam("namespace")
String namespace,
- @AuthorizationMetadata(type = Entity.EntityType.TABLE)
@PathParam("table") String table,
+ @AuthorizationMetadata(type = Entity.EntityType.TABLE) @Encoded()
@PathParam("table")
+ String table,
UpdateTableRequest updateTableRequest) {
String catalogName = IcebergRESTUtils.getCatalogName(prefix);
Namespace icebergNS = RESTUtil.decodeNamespace(namespace);
+ String tableName = RESTUtil.decodeString(table);
if (LOG.isInfoEnabled()) {
LOG.info(
"Update Iceberg table, catalog: {}, namespace: {}, table: {},
updateTableRequest: {}",
@@ -213,7 +215,7 @@ public class IcebergTableOperations {
() -> {
IcebergRequestContext context =
new IcebergRequestContext(httpServletRequest(), catalogName);
- TableIdentifier tableIdentifier = TableIdentifier.of(icebergNS,
table);
+ TableIdentifier tableIdentifier = TableIdentifier.of(icebergNS,
tableName);
LoadTableResponse loadTableResponse =
tableOperationDispatcher.updateTable(context, tableIdentifier,
updateTableRequest);
return IcebergRESTUtils.ok(loadTableResponse);
@@ -238,21 +240,23 @@ public class IcebergTableOperations {
@AuthorizationMetadata(type = Entity.EntityType.CATALOG)
@PathParam("prefix") String prefix,
@AuthorizationMetadata(type = EntityType.SCHEMA) @Encoded()
@PathParam("namespace")
String namespace,
- @AuthorizationMetadata(type = Entity.EntityType.TABLE)
@PathParam("table") String table,
+ @AuthorizationMetadata(type = Entity.EntityType.TABLE) @Encoded()
@PathParam("table")
+ String table,
@DefaultValue("false") @QueryParam("purgeRequested") boolean
purgeRequested) {
String catalogName = IcebergRESTUtils.getCatalogName(prefix);
Namespace icebergNS = RESTUtil.decodeNamespace(namespace);
+ String tableName = RESTUtil.decodeString(table);
LOG.info(
"Drop Iceberg table, catalog: {}, namespace: {}, table: {},
purgeRequested: {}",
catalogName,
icebergNS,
- table,
+ tableName,
purgeRequested);
try {
return Utils.doAs(
httpRequest,
() -> {
- TableIdentifier tableIdentifier = TableIdentifier.of(icebergNS,
table);
+ TableIdentifier tableIdentifier = TableIdentifier.of(icebergNS,
tableName);
IcebergRequestContext context =
new IcebergRequestContext(httpServletRequest(), catalogName);
tableOperationDispatcher.dropTable(context, tableIdentifier,
purgeRequested);
@@ -278,18 +282,20 @@ public class IcebergTableOperations {
@AuthorizationMetadata(type = Entity.EntityType.CATALOG)
@PathParam("prefix") String prefix,
@AuthorizationMetadata(type = EntityType.SCHEMA) @Encoded()
@PathParam("namespace")
String namespace,
- @IcebergAuthorizationMetadata(type = RequestType.LOAD_TABLE)
@PathParam("table") String table,
+ @IcebergAuthorizationMetadata(type = RequestType.LOAD_TABLE) @Encoded()
@PathParam("table")
+ String table,
@DefaultValue("all") @QueryParam("snapshots") String snapshots,
@HeaderParam(X_ICEBERG_ACCESS_DELEGATION) String accessDelegation) {
String catalogName = IcebergRESTUtils.getCatalogName(prefix);
Namespace icebergNS = RESTUtil.decodeNamespace(namespace);
+ String tableName = RESTUtil.decodeString(table);
boolean isCredentialVending = isCredentialVending(accessDelegation);
LOG.info(
"Load Iceberg table, catalog: {}, namespace: {}, table: {}, access
delegation: {}, "
+ "credential vending: {}",
catalogName,
icebergNS,
- table,
+ tableName,
accessDelegation,
isCredentialVending);
// todo support snapshots
@@ -297,7 +303,7 @@ public class IcebergTableOperations {
return Utils.doAs(
httpRequest,
() -> {
- TableIdentifier tableIdentifier = TableIdentifier.of(icebergNS,
table);
+ TableIdentifier tableIdentifier = TableIdentifier.of(icebergNS,
tableName);
IcebergRequestContext context =
new IcebergRequestContext(httpServletRequest(), catalogName,
isCredentialVending);
LoadTableResponse loadTableResponse =
@@ -324,21 +330,23 @@ public class IcebergTableOperations {
@AuthorizationMetadata(type = Entity.EntityType.CATALOG)
@PathParam("prefix") String prefix,
@AuthorizationMetadata(type = EntityType.SCHEMA) @Encoded()
@PathParam("namespace")
String namespace,
- @AuthorizationMetadata(type = Entity.EntityType.TABLE)
@PathParam("table") String table) {
+ @AuthorizationMetadata(type = Entity.EntityType.TABLE) @Encoded()
@PathParam("table")
+ String table) {
String catalogName = IcebergRESTUtils.getCatalogName(prefix);
Namespace icebergNS = RESTUtil.decodeNamespace(namespace);
+ String tableName = RESTUtil.decodeString(table);
LOG.info(
"Check Iceberg table exists, catalog: {}, namespace: {}, table: {}",
catalogName,
icebergNS,
- table);
+ tableName);
try {
return Utils.doAs(
httpRequest,
() -> {
IcebergRequestContext context =
new IcebergRequestContext(httpServletRequest(), catalogName);
- TableIdentifier tableIdentifier = TableIdentifier.of(icebergNS,
table);
+ TableIdentifier tableIdentifier = TableIdentifier.of(icebergNS,
tableName);
boolean exists = tableOperationDispatcher.tableExists(context,
tableIdentifier);
if (exists) {
return IcebergRESTUtils.noContent();
@@ -359,15 +367,16 @@ public class IcebergTableOperations {
public Response reportTableMetrics(
@PathParam("prefix") String prefix,
@Encoded() @PathParam("namespace") String namespace,
- @PathParam("table") String table,
+ @Encoded() @PathParam("table") String table,
ReportMetricsRequest request) {
String catalogName = IcebergRESTUtils.getCatalogName(prefix);
Namespace icebergNS = RESTUtil.decodeNamespace(namespace);
+ String tableName = RESTUtil.decodeString(table);
LOG.info(
"Report Iceberg table metrics, catalog: {}, namespace: {}, table: {}",
catalogName,
icebergNS,
- table);
+ tableName);
try {
return Utils.doAs(
httpRequest,
@@ -417,20 +426,21 @@ public class IcebergTableOperations {
@AuthorizationMetadata(type = Entity.EntityType.CATALOG)
@PathParam("prefix") String prefix,
@AuthorizationMetadata(type = EntityType.SCHEMA) @Encoded()
@PathParam("namespace")
String namespace,
- @AuthorizationMetadata(type = EntityType.TABLE) @PathParam("table")
String table) {
+ @AuthorizationMetadata(type = EntityType.TABLE) @Encoded()
@PathParam("table") String table) {
String catalogName = IcebergRESTUtils.getCatalogName(prefix);
Namespace icebergNS = RESTUtil.decodeNamespace(namespace);
+ String tableName = RESTUtil.decodeString(table);
LOG.info(
"Get Iceberg table credentials, catalog: {}, namespace: {}, table: {}",
catalogName,
icebergNS,
- table);
+ tableName);
try {
return Utils.doAs(
httpRequest,
() -> {
// Convert Iceberg table identifier to Gravitino NameIdentifier
- TableIdentifier tableIdentifier = TableIdentifier.of(icebergNS,
table);
+ TableIdentifier tableIdentifier = TableIdentifier.of(icebergNS,
tableName);
// First check if the table exists
IcebergRequestContext context =
new IcebergRequestContext(httpServletRequest(), catalogName);
@@ -470,19 +480,22 @@ public class IcebergTableOperations {
@PathParam("prefix") @AuthorizationMetadata(type = EntityType.CATALOG)
String prefix,
@Encoded() @PathParam("namespace") @AuthorizationMetadata(type =
EntityType.SCHEMA)
String namespace,
- @PathParam("table") @AuthorizationMetadata(type = EntityType.TABLE)
String table,
+ @Encoded() @PathParam("table") @AuthorizationMetadata(type =
EntityType.TABLE) String table,
PlanTableScanRequest scanRequest) {
String catalogName = IcebergRESTUtils.getCatalogName(prefix);
Namespace icebergNS = RESTUtil.decodeNamespace(namespace);
-
+ String tableName = RESTUtil.decodeString(table);
LOG.info(
- "Plan table scan, catalog: {}, namespace: {}, table: {}", catalogName,
icebergNS, table);
+ "Plan table scan, catalog: {}, namespace: {}, table: {}",
+ catalogName,
+ icebergNS,
+ tableName);
try {
return Utils.doAs(
httpRequest,
() -> {
- TableIdentifier tableIdentifier = TableIdentifier.of(icebergNS,
table);
+ TableIdentifier tableIdentifier = TableIdentifier.of(icebergNS,
tableName);
IcebergRequestContext context =
new IcebergRequestContext(httpServletRequest(), catalogName);
diff --git
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergViewOperations.java
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergViewOperations.java
index 728a1a2ceb..bcbc77ac38 100644
---
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergViewOperations.java
+++
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergViewOperations.java
@@ -134,16 +134,20 @@ public class IcebergViewOperations {
public Response loadView(
@PathParam("prefix") String prefix,
@Encoded() @PathParam("namespace") String namespace,
- @PathParam("view") String view) {
+ @Encoded() @PathParam("view") String view) {
String catalogName = IcebergRESTUtils.getCatalogName(prefix);
Namespace icebergNS = RESTUtil.decodeNamespace(namespace);
+ String viewName = RESTUtil.decodeString(view);
LOG.info(
- "Load Iceberg view, catalog: {}, namespace: {}, view: {}",
catalogName, icebergNS, view);
+ "Load Iceberg view, catalog: {}, namespace: {}, view: {}",
+ catalogName,
+ icebergNS,
+ viewName);
try {
return Utils.doAs(
httpRequest,
() -> {
- TableIdentifier viewIdentifier = TableIdentifier.of(icebergNS,
view);
+ TableIdentifier viewIdentifier = TableIdentifier.of(icebergNS,
viewName);
IcebergRequestContext context =
new IcebergRequestContext(httpServletRequest(), catalogName);
LoadViewResponse loadViewResponse =
@@ -163,15 +167,16 @@ public class IcebergViewOperations {
public Response replaceView(
@PathParam("prefix") String prefix,
@Encoded() @PathParam("namespace") String namespace,
- @PathParam("view") String view,
+ @Encoded() @PathParam("view") String view,
UpdateTableRequest replaceViewRequest) {
String catalogName = IcebergRESTUtils.getCatalogName(prefix);
Namespace icebergNS = RESTUtil.decodeNamespace(namespace);
+ String viewName = RESTUtil.decodeString(view);
LOG.info(
"Replace Iceberg view, catalog: {}, namespace: {}, view: {},
replaceViewRequest: {}",
catalogName,
icebergNS,
- view,
+ viewName,
SerializeReplaceViewRequest(replaceViewRequest));
try {
return Utils.doAs(
@@ -179,7 +184,7 @@ public class IcebergViewOperations {
() -> {
IcebergRequestContext context =
new IcebergRequestContext(httpServletRequest(), catalogName);
- TableIdentifier viewIdentifier = TableIdentifier.of(icebergNS,
view);
+ TableIdentifier viewIdentifier = TableIdentifier.of(icebergNS,
viewName);
LoadViewResponse loadViewResponse =
viewOperationDispatcher.replaceView(context, viewIdentifier,
replaceViewRequest);
return IcebergRESTUtils.ok(loadViewResponse);
@@ -197,16 +202,20 @@ public class IcebergViewOperations {
public Response dropView(
@PathParam("prefix") String prefix,
@Encoded() @PathParam("namespace") String namespace,
- @PathParam("view") String view) {
+ @Encoded() @PathParam("view") String view) {
String catalogName = IcebergRESTUtils.getCatalogName(prefix);
Namespace icebergNS = RESTUtil.decodeNamespace(namespace);
+ String viewName = RESTUtil.decodeString(view);
LOG.info(
- "Drop Iceberg view, catalog: {}, namespace: {}, view: {}",
catalogName, icebergNS, view);
+ "Drop Iceberg view, catalog: {}, namespace: {}, view: {}",
+ catalogName,
+ icebergNS,
+ viewName);
try {
return Utils.doAs(
httpRequest,
() -> {
- TableIdentifier viewIdentifier = TableIdentifier.of(icebergNS,
view);
+ TableIdentifier viewIdentifier = TableIdentifier.of(icebergNS,
viewName);
IcebergRequestContext context =
new IcebergRequestContext(httpServletRequest(), catalogName);
viewOperationDispatcher.dropView(context, viewIdentifier);
@@ -225,21 +234,22 @@ public class IcebergViewOperations {
public Response viewExists(
@PathParam("prefix") String prefix,
@Encoded() @PathParam("namespace") String namespace,
- @PathParam("view") String view) {
+ @Encoded() @PathParam("view") String view) {
String catalogName = IcebergRESTUtils.getCatalogName(prefix);
Namespace icebergNS = RESTUtil.decodeNamespace(namespace);
+ String viewName = RESTUtil.decodeString(view);
LOG.info(
"Check Iceberg view exists, catalog: {}, namespace: {}, view: {}",
catalogName,
icebergNS,
- view);
+ viewName);
try {
return Utils.doAs(
httpRequest,
() -> {
IcebergRequestContext context =
new IcebergRequestContext(httpServletRequest(), catalogName);
- TableIdentifier viewIdentifier = TableIdentifier.of(icebergNS,
view);
+ TableIdentifier viewIdentifier = TableIdentifier.of(icebergNS,
viewName);
boolean exists = viewOperationDispatcher.viewExists(context,
viewIdentifier);
if (exists) {
return IcebergRESTUtils.noContent();
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 f4a9a2b602..606367ecaa 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
@@ -74,6 +74,7 @@ import org.apache.iceberg.metrics.CommitReport;
import org.apache.iceberg.metrics.ImmutableCommitMetricsResult;
import org.apache.iceberg.metrics.ImmutableCommitReport;
import org.apache.iceberg.rest.PlanStatus;
+import org.apache.iceberg.rest.RESTUtil;
import org.apache.iceberg.rest.requests.CreateTableRequest;
import org.apache.iceberg.rest.requests.PlanTableScanRequest;
import org.apache.iceberg.rest.requests.RenameTableRequest;
@@ -692,6 +693,40 @@ public class TestIcebergTableOperations extends
IcebergNamespaceTestBase {
"Error message should mention remote signing: " + errorBody);
}
+ @ParameterizedTest
+
@MethodSource("org.apache.gravitino.iceberg.service.rest.IcebergRestTestUtil#testNamespaces")
+ void testTableOperationsWithEncodedName(Namespace namespace) {
+ // Table names with special characters that require percent-encoding in
URL paths.
+ // RESTUtil.encodeString encodes them for the URL, and the server should
decode
+ // them back via RESTUtil.decodeString thanks to @Encoded on the path
parameter.
+ String[] specialNames = {"table.with.dots", "table@special"};
+
+ verifyCreateNamespaceSucc(namespace);
+
+ for (String originalName : specialNames) {
+ String encodedName = RESTUtil.encodeString(originalName);
+
+ // Create uses the original name in the request body, not in the URL path
+ verifyCreateTableSucc(namespace, originalName);
+
+ // Load, exists use the encoded name in the URL path
+ verifyLoadTableSucc(namespace, encodedName);
+ verifyTableExistsStatusCode(namespace, encodedName, 204);
+
+ // Update: load metadata with encoded path, then update with encoded path
+ TableMetadata metadata = getTableMeta(namespace, encodedName);
+ verifyUpdateSucc(namespace, encodedName, metadata);
+ }
+
+ // Verify list returns the original (decoded) table names
+ verifyListTableSucc(namespace, ImmutableSet.copyOf(specialNames));
+
+ for (String originalName : specialNames) {
+ verifyDropTableSucc(namespace, RESTUtil.encodeString(originalName));
+ }
+ verifyDropNamespaceSucc(namespace);
+ }
+
@ParameterizedTest
@MethodSource("org.apache.gravitino.iceberg.service.rest.IcebergRestTestUtil#testNamespaces")
void testInvalidAccessDelegation(Namespace namespace) {
diff --git
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergViewOperations.java
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergViewOperations.java
index a319638fa9..79df5de0b0 100644
---
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergViewOperations.java
+++
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/iceberg/service/rest/TestIcebergViewOperations.java
@@ -54,6 +54,7 @@ import org.apache.iceberg.Schema;
import org.apache.iceberg.UpdateRequirements;
import org.apache.iceberg.catalog.Namespace;
import org.apache.iceberg.catalog.TableIdentifier;
+import org.apache.iceberg.rest.RESTUtil;
import org.apache.iceberg.rest.requests.CreateViewRequest;
import org.apache.iceberg.rest.requests.ImmutableCreateViewRequest;
import org.apache.iceberg.rest.requests.RenameTableRequest;
@@ -260,6 +261,40 @@ public class TestIcebergViewOperations extends
IcebergNamespaceTestBase {
verifyRenameViewFail(namespace, "rename_foo2", "rename_foo3", 409);
}
+ @ParameterizedTest
+
@MethodSource("org.apache.gravitino.iceberg.service.rest.IcebergRestTestUtil#testNamespaces")
+ void testViewOperationsWithEncodedName(Namespace namespace) {
+ // View names with special characters that require percent-encoding in URL
paths.
+ // RESTUtil.encodeString encodes them for the URL, and the server should
decode
+ // them back via RESTUtil.decodeString thanks to @Encoded on the path
parameter.
+ String[] specialNames = {"view.with.dots", "view@special"};
+
+ verifyCreateNamespaceSucc(namespace);
+
+ for (String originalName : specialNames) {
+ String encodedName = RESTUtil.encodeString(originalName);
+
+ // Create uses the original name in the request body, not in the URL path
+ verifyCreateViewSucc(namespace, originalName);
+
+ // Load, exists use the encoded name in the URL path
+ verifyLoadViewSucc(namespace, encodedName);
+ verifyViewExistsStatusCode(namespace, encodedName, 204);
+
+ // Replace: load metadata with encoded path, then replace with encoded
path
+ ViewMetadata metadata = getViewMeta(namespace, encodedName);
+ verifyReplaceSucc(namespace, encodedName, metadata);
+ }
+
+ // Verify list returns the original (decoded) view names
+ verifyLisViewSucc(namespace, ImmutableSet.copyOf(specialNames));
+
+ for (String originalName : specialNames) {
+ verifyDropViewSucc(namespace, RESTUtil.encodeString(originalName));
+ }
+ verifyDropNamespaceSucc(namespace);
+ }
+
private Response doCreateView(Namespace ns, String name) {
CreateViewRequest createViewRequest =
ImmutableCreateViewRequest.builder()