This is an automated email from the ASF dual-hosted git repository.
roryqi pushed a commit to branch branch-1.3
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/branch-1.3 by this push:
new bfe2876bd5 [Cherry-pick to branch-1.3] [#14447] fix(iceberg-rest):
authorize /v1/config and report non-Iceberg prefixes as client errors (#14449)
bfe2876bd5 is described below
commit bfe2876bd5802eea3073d63d568e1ee50c619c31
Author: Mark Hoerth <[email protected]>
AuthorDate: Sun Nov 1 19:44:30 2026 -0800
[Cherry-pick to branch-1.3] [#14447] fix(iceberg-rest): authorize
/v1/config and report non-Iceberg prefixes as client errors (#14449)
Backport to branch-1.3 of the fix for #14447 (same change as the PR to
main; it applies cleanly).
### What changes were proposed in this pull request?
- Intercept `IcebergConfigOperations` and authorize `getConfig` with
`LOAD_CATALOG_AUTHORIZATION_EXPRESSION` (the same expression as
`listNamespaces`), against the catalog named by the `warehouse`
parameter. The interceptor accepts a plain catalog name as well as a
path prefix. An empty warehouse now resolves through
`IcebergRESTUtils.getCatalogName`, so it uses the same default catalog
as an empty prefix, which is the catalog it is authorized against.
-
`IcebergMetadataAuthorizationMethodInterceptor.shouldSkipAuthorization`:
treat an `IllegalArgumentException` from the catalog lookup (a catalog
that is not an Iceberg catalog) like a missing catalog. The request is
then authorized normally: a caller without access gets 403 as for any
other name, and an authorized caller gets the operation's 400.
### Why are the changes needed?
Users without access to a catalog could read its configuration and probe
catalog names, and a client mistake was reported as an internal server
error.
Fixes #14447
### Does this PR introduce _any_ user-facing change?
Yes. With authorization enabled, `/v1/config` requires `USE_CATALOG` or
ownership on the catalog, so an engine's catalog initialization fails
with 403 for users who could not use the catalog anyway. A prefix naming
a non-Iceberg catalog returns 403 or 400 instead of 500.
### How was this patch tested?
`TestIcebergMetadataAuthorizationMethodInterceptor`:
- `testConfigEndpointIsAuthorizedOnTheWarehouseCatalog`: the config
resource is intercepted, carries the expression, and resolves a named
and an empty warehouse to the right catalog.
- `testNonIcebergCatalogDoesNotSkipAuthorization`.
-
`testInvokeShouldSkipAuthorizationWrapperLookupFailureReturnsAuthInternalError`
now uses an `IllegalStateException`, so it still checks that an
unexpected lookup failure is a 500.
`./gradlew :iceberg:iceberg-rest-server:test -PskipITs`
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01Eu6fb9pLfmrSJzGQDVLi1g
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
.../service/rest/IcebergConfigOperations.java | 19 +++++--
...bergMetadataAuthorizationMethodInterceptor.java | 12 ++++-
.../filter/IcebergRESTAuthInterceptionService.java | 2 +
...bergMetadataAuthorizationMethodInterceptor.java | 60 +++++++++++++++++++++-
4 files changed, 87 insertions(+), 6 deletions(-)
diff --git
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergConfigOperations.java
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergConfigOperations.java
index 719d75ce93..a96ee2b843 100644
---
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergConfigOperations.java
+++
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/iceberg/service/rest/IcebergConfigOperations.java
@@ -38,11 +38,15 @@ import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.apache.commons.lang3.StringUtils;
-import org.apache.gravitino.catalog.lakehouse.iceberg.IcebergConstants;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.MetadataObject;
import org.apache.gravitino.iceberg.service.CatalogWrapperForREST;
import org.apache.gravitino.iceberg.service.IcebergCatalogWrapperManager;
import org.apache.gravitino.iceberg.service.IcebergRESTUtils;
import org.apache.gravitino.metrics.MetricNames;
+import
org.apache.gravitino.server.authorization.annotations.AuthorizationExpression;
+import
org.apache.gravitino.server.authorization.annotations.AuthorizationMetadata;
+import
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants;
import org.apache.iceberg.rest.Endpoint;
import org.apache.iceberg.rest.responses.ConfigResponse;
@@ -98,7 +102,14 @@ public class IcebergConfigOperations {
@Produces(MediaType.APPLICATION_JSON)
@Timed(name = "config." + MetricNames.HTTP_PROCESS_DURATION, absolute = true)
@ResponseMetered(name = "config", absolute = true)
- public Response getConfig(@DefaultValue("") @QueryParam("warehouse") String
warehouse) {
+ @AuthorizationExpression(
+ expression =
AuthorizationExpressionConstants.LOAD_CATALOG_AUTHORIZATION_EXPRESSION,
+ accessMetadataType = MetadataObject.Type.CATALOG)
+ public Response getConfig(
+ @AuthorizationMetadata(type = Entity.EntityType.CATALOG)
+ @DefaultValue("")
+ @QueryParam("warehouse")
+ String warehouse) {
String catalogName = getCatalogName(warehouse);
boolean supportsView = supportsViewOperations(catalogName);
ConfigResponse.Builder builder = ConfigResponse.builder();
@@ -126,7 +137,9 @@ public class IcebergConfigOperations {
private String getCatalogName(String warehouse) {
if (StringUtils.isBlank(warehouse)) {
- return IcebergConstants.ICEBERG_REST_DEFAULT_CATALOG;
+ // The same default catalog an empty prefix resolves to, and that the
request is authorized
+ // against.
+ return IcebergRESTUtils.getCatalogName(warehouse);
} else {
return warehouse;
}
diff --git
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/IcebergMetadataAuthorizationMethodInterceptor.java
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/IcebergMetadataAuthorizationMethodInterceptor.java
index f7667247a5..baeb09901d 100644
---
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/IcebergMetadataAuthorizationMethodInterceptor.java
+++
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/IcebergMetadataAuthorizationMethodInterceptor.java
@@ -26,6 +26,7 @@ import java.util.Map;
import java.util.Optional;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
+import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.Entity;
import org.apache.gravitino.Entity.EntityType;
import org.apache.gravitino.NameIdentifier;
@@ -93,7 +94,11 @@ public class IcebergMetadataAuthorizationMethodInterceptor
String value = String.valueOf(args[i]);
switch (type) {
case CATALOG:
- catalog = IcebergRESTUtils.getCatalogName(value);
+ // A path prefix ends with '/'; the config endpoint's warehouse is
a plain catalog name.
+ catalog =
+ StringUtils.isBlank(value) || value.endsWith("/")
+ ? IcebergRESTUtils.getCatalogName(value)
+ : value;
nameIdentifierMap.put(
Entity.EntityType.CATALOG,
NameIdentifierUtil.ofCatalog(metalakeName, catalog));
break;
@@ -206,7 +211,10 @@ public class IcebergMetadataAuthorizationMethodInterceptor
IcebergCatalogWrapper catalogWrapper;
try {
catalogWrapper = wrapperManager.getCatalogWrapper(catalogId.name());
- } catch (NoSuchCatalogException e) {
+ } catch (NoSuchCatalogException | IllegalArgumentException e) {
+ // A missing catalog, or a catalog that is not an Iceberg catalog, is
not a REST backend:
+ // authorize the request normally. A caller without access gets 403
whatever the name
+ // refers to, and an authorized caller gets the operation's own error.
return false;
}
// When IRC2 is another Gravitino server, IRC1 acts as a proxy and does
not perform
diff --git
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/IcebergRESTAuthInterceptionService.java
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/IcebergRESTAuthInterceptionService.java
index b7ea32d820..9c2e14de79 100644
---
a/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/IcebergRESTAuthInterceptionService.java
+++
b/iceberg/iceberg-rest-server/src/main/java/org/apache/gravitino/server/web/filter/IcebergRESTAuthInterceptionService.java
@@ -23,6 +23,7 @@ import com.google.common.collect.ImmutableSet;
import java.lang.reflect.Method;
import java.util.List;
import org.aopalliance.intercept.MethodInterceptor;
+import org.apache.gravitino.iceberg.service.rest.IcebergConfigOperations;
import org.apache.gravitino.iceberg.service.rest.IcebergNamespaceOperations;
import org.apache.gravitino.iceberg.service.rest.IcebergTableOperations;
import org.apache.gravitino.iceberg.service.rest.IcebergTableRenameOperations;
@@ -41,6 +42,7 @@ public class IcebergRESTAuthInterceptionService extends
BaseInterceptionService
public Filter getDescriptorFilter() {
return new ClassListFilter(
ImmutableSet.of(
+ IcebergConfigOperations.class.getName(),
IcebergTableOperations.class.getName(),
IcebergTableRenameOperations.class.getName(),
IcebergNamespaceOperations.class.getName(),
diff --git
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/server/web/filter/TestIcebergMetadataAuthorizationMethodInterceptor.java
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/server/web/filter/TestIcebergMetadataAuthorizationMethodInterceptor.java
index abad7e0dd3..63ade84bd6 100644
---
a/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/server/web/filter/TestIcebergMetadataAuthorizationMethodInterceptor.java
+++
b/iceberg/iceberg-rest-server/src/test/java/org/apache/gravitino/server/web/filter/TestIcebergMetadataAuthorizationMethodInterceptor.java
@@ -43,9 +43,11 @@ import
org.apache.gravitino.iceberg.service.IcebergCatalogWrapperManager;
import org.apache.gravitino.iceberg.service.IcebergRESTUtils;
import
org.apache.gravitino.iceberg.service.authorization.IcebergRESTServerContext;
import org.apache.gravitino.iceberg.service.provider.IcebergConfigProvider;
+import org.apache.gravitino.iceberg.service.rest.IcebergConfigOperations;
import org.apache.gravitino.iceberg.service.rest.IcebergTableOperations;
import
org.apache.gravitino.server.authorization.annotations.AuthorizationExpression;
import
org.apache.gravitino.server.authorization.annotations.AuthorizationMetadata;
+import
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants;
import org.apache.gravitino.utils.NameIdentifierUtil;
import org.apache.iceberg.catalog.Catalog;
import org.apache.iceberg.catalog.Namespace;
@@ -54,6 +56,8 @@ import org.apache.iceberg.rest.RESTCatalog;
import org.apache.iceberg.rest.RESTUtil;
import org.apache.iceberg.rest.requests.ReportMetricsRequest;
import org.apache.iceberg.rest.responses.ErrorResponse;
+import org.glassfish.hk2.api.ActiveDescriptor;
+import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
@@ -297,6 +301,58 @@ public class
TestIcebergMetadataAuthorizationMethodInterceptor {
Mockito.verify(invocation, Mockito.times(1)).proceed();
}
+ @Test
+ public void testNonIcebergCatalogDoesNotSkipAuthorization() {
+ // A prefix naming a catalog of another kind is authorized normally
instead of failing with
+ // an internal error inside the authorization step.
+ IcebergCatalogWrapperManager wrapperManager =
Mockito.mock(IcebergCatalogWrapperManager.class);
+ Mockito.when(wrapperManager.getCatalogWrapper("glue_catalog"))
+ .thenThrow(new IllegalArgumentException("Catalog glue_catalog is not
an Iceberg catalog"));
+ resetContext(wrapperManager, true);
+
+ IcebergMetadataAuthorizationMethodInterceptor interceptor =
+ new IcebergMetadataAuthorizationMethodInterceptor();
+ assertFalse(
+ interceptor.shouldSkipAuthorization(
+ Map.of(Entity.EntityType.CATALOG, NameIdentifier.of(TEST_METALAKE,
"glue_catalog"))));
+ }
+
+ @Test
+ public void testConfigEndpointIsAuthorizedOnTheWarehouseCatalog() throws
Exception {
+ Assertions.assertTrue(
+ new IcebergRESTAuthInterceptionService()
+ .getDescriptorFilter()
+ .matches(descriptorFor(IcebergConfigOperations.class)));
+
+ Method getConfig = IcebergConfigOperations.class.getMethod("getConfig",
String.class);
+ AuthorizationExpression expression =
getConfig.getAnnotation(AuthorizationExpression.class);
+ assertNotNull(expression);
+ assertEquals(
+ AuthorizationExpressionConstants.LOAD_CATALOG_AUTHORIZATION_EXPRESSION,
+ expression.expression());
+
+ IcebergMetadataAuthorizationMethodInterceptor interceptor =
+ new IcebergMetadataAuthorizationMethodInterceptor();
+ // The warehouse names the catalog; an empty warehouse means the default
catalog.
+ assertEquals(
+ NameIdentifierUtil.ofCatalog(TEST_METALAKE, "other_catalog"),
+ interceptor
+ .extractNameIdentifierFromParameters(
+ getConfig.getParameters(), new Object[] {"other_catalog"})
+ .get(Entity.EntityType.CATALOG));
+ assertEquals(
+ NameIdentifierUtil.ofCatalog(TEST_METALAKE, TEST_CATALOG),
+ interceptor
+ .extractNameIdentifierFromParameters(getConfig.getParameters(),
new Object[] {""})
+ .get(Entity.EntityType.CATALOG));
+ }
+
+ private static ActiveDescriptor<?> descriptorFor(Class<?> clazz) {
+ ActiveDescriptor<?> descriptor = Mockito.mock(ActiveDescriptor.class);
+ Mockito.when(descriptor.getImplementation()).thenReturn(clazz.getName());
+ return descriptor;
+ }
+
@Test
public void testMetricsReportDoesNotSkipAuthorizationForRestCatalog() throws
Exception {
IcebergCatalogWrapperManager wrapperManager =
Mockito.mock(IcebergCatalogWrapperManager.class);
@@ -472,8 +528,10 @@ public class
TestIcebergMetadataAuthorizationMethodInterceptor {
public void
testInvokeShouldSkipAuthorizationWrapperLookupFailureReturnsAuthInternalError()
throws Throwable {
IcebergCatalogWrapperManager wrapperManager =
Mockito.mock(IcebergCatalogWrapperManager.class);
+ // An unexpected lookup failure is a server error. (An
IllegalArgumentException, such as a
+ // prefix naming a catalog that is not an Iceberg catalog, is authorized
normally instead.)
Mockito.when(wrapperManager.getCatalogWrapper(TEST_CATALOG))
- .thenThrow(new IllegalArgumentException("wrapper lookup failed"));
+ .thenThrow(new IllegalStateException("wrapper lookup failed"));
resetContext(wrapperManager, true);
Method method =