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 cfe3e0a296 [Cherry-pick to branch-1.3] [#12977] fix(server): Report
dotted metadata names clearly (#12980) (#12995)
cfe3e0a296 is described below
commit cfe3e0a296164ea832e1821b28ebacf85df936cc
Author: github-actions[bot]
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Wed Sep 9 10:00:46 2026 +0800
[Cherry-pick to branch-1.3] [#12977] fix(server): Report dotted metadata
names clearly (#12980) (#12995)
**Cherry-pick Information:**
- Original commit: 5aea36da5d7a9460a9b2e35e94e938c1161de4d9
- Target branch: `branch-1.3`
- Status: ✅ Conflicts resolved manually
**Resolution:**
- Preserved the `branch-1.3` list-authorization short-circuit and
applied the dotted metadata-name validation before it.
- Adapted the interception regression test to use the `branch-1.3`
`TableOperations` fixture instead of main-only test helpers.
- Removed all committed conflict markers.
**Validation:**
- `./gradlew :core:test --tests
org.apache.gravitino.utils.TestNameIdentifierUtil :server-common:test
--tests
org.apache.gravitino.server.authorization.TestMetadataAuthzHelper
:server:test --tests
org.apache.gravitino.server.web.filter.TestGravitinoInterceptionService
-PskipITs -PskipWeb=true`
- Conflict-marker scan and `git diff --check`
---------
Co-authored-by: roryqi <[email protected]>
Co-authored-by: roryqi <[email protected]>
---
.../apache/gravitino/utils/NameIdentifierUtil.java | 20 +++++++++
.../gravitino/utils/TestNameIdentifierUtil.java | 24 +++++++++++
.../server/authorization/MetadataAuthzHelper.java | 26 ++++++++----
.../authorization/TestMetadataAuthzHelper.java | 44 ++++++++++++++++++++
.../web/filter/GravitinoInterceptionService.java | 4 ++
.../filter/TestGravitinoInterceptionService.java | 47 ++++++++++++++++++++++
6 files changed, 158 insertions(+), 7 deletions(-)
diff --git
a/core/src/main/java/org/apache/gravitino/utils/NameIdentifierUtil.java
b/core/src/main/java/org/apache/gravitino/utils/NameIdentifierUtil.java
index b7bc9b742d..33747d8b44 100644
--- a/core/src/main/java/org/apache/gravitino/utils/NameIdentifierUtil.java
+++ b/core/src/main/java/org/apache/gravitino/utils/NameIdentifierUtil.java
@@ -593,6 +593,25 @@ public class NameIdentifierUtil {
NamespaceUtil.checkJobTemplate(ident.namespace());
}
+ /**
+ * Check whether the metadata object name can be represented in a qualified
metadata object name.
+ *
+ * @param ident The metadata object identifier to check
+ * @param entityType The metadata object entity type
+ * @throws IllegalNameIdentifierException If the object name contains the
qualified-name separator
+ */
+ public static void checkMetadataObjectName(NameIdentifier ident,
Entity.EntityType entityType) {
+ Preconditions.checkArgument(
+ ident != null && entityType != null, "The identifier and entity type
must not be null");
+
+ if (ident.name().contains(".")) {
+ throw new IllegalNameIdentifierException(
+ "The %s name '%s' is unsupported because '.' is reserved as the
qualified-name "
+ + "separator.",
+ entityType, ident.name());
+ }
+ }
+
/**
* Convert the given {@link NameIdentifier} and {@link Entity.EntityType} to
{@link
* MetadataObject}.
@@ -605,6 +624,7 @@ public class NameIdentifierUtil {
NameIdentifier ident, Entity.EntityType entityType) {
Preconditions.checkArgument(
ident != null && entityType != null, "The identifier and entity type
must not be null");
+ checkMetadataObjectName(ident, entityType);
Joiner dot = Joiner.on(".");
diff --git
a/core/src/test/java/org/apache/gravitino/utils/TestNameIdentifierUtil.java
b/core/src/test/java/org/apache/gravitino/utils/TestNameIdentifierUtil.java
index 99967d2ad3..8ff6f37e2b 100644
--- a/core/src/test/java/org/apache/gravitino/utils/TestNameIdentifierUtil.java
+++ b/core/src/test/java/org/apache/gravitino/utils/TestNameIdentifierUtil.java
@@ -163,6 +163,30 @@ public class TestNameIdentifierUtil {
assertTrue(e3.getMessage().contains("Entity type MODEL_VERSION is not
supported"));
}
+ @Test
+ public void testRejectDottedMetadataObjectName() {
+ NameIdentifier table = NameIdentifier.of("metalake1", "catalog1",
"schema1", "sales.2024");
+ IllegalNameIdentifierException tableException =
+ assertThrows(
+ IllegalNameIdentifierException.class,
+ () -> NameIdentifierUtil.toMetadataObject(table,
Entity.EntityType.TABLE));
+ assertEquals(
+ "The TABLE name 'sales.2024' is unsupported because '.' is reserved as
the "
+ + "qualified-name separator.",
+ tableException.getMessage());
+
+ NameIdentifier topic =
+ NameIdentifier.of("metalake1", "catalog1", "schema1",
"orders.created.v1");
+ IllegalNameIdentifierException topicException =
+ assertThrows(
+ IllegalNameIdentifierException.class,
+ () -> NameIdentifierUtil.toMetadataObject(topic,
Entity.EntityType.TOPIC));
+ assertEquals(
+ "The TOPIC name 'orders.created.v1' is unsupported because '.' is
reserved as the "
+ + "qualified-name separator.",
+ topicException.getMessage());
+ }
+
@Test
void testOfUser() {
String userName = "userA";
diff --git
a/server-common/src/main/java/org/apache/gravitino/server/authorization/MetadataAuthzHelper.java
b/server-common/src/main/java/org/apache/gravitino/server/authorization/MetadataAuthzHelper.java
index 744fdcb181..fcb4f2cf5e 100644
---
a/server-common/src/main/java/org/apache/gravitino/server/authorization/MetadataAuthzHelper.java
+++
b/server-common/src/main/java/org/apache/gravitino/server/authorization/MetadataAuthzHelper.java
@@ -31,6 +31,7 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.function.Function;
+import java.util.stream.Collectors;
import org.apache.gravitino.Config;
import org.apache.gravitino.Configs;
import org.apache.gravitino.Entity;
@@ -89,6 +90,11 @@ public class MetadataAuthzHelper {
private static final List<Entity.EntityType> REQUIRE_SCHEMA_EXISTS =
Arrays.asList(Entity.EntityType.TABLE, Entity.EntityType.TOPIC);
+ private static final Set<Entity.EntityType> METADATA_OBJECT_ENTITY_TYPES =
+ Arrays.stream(MetadataObject.Type.values())
+ .map(type -> Entity.EntityType.valueOf(type.name()))
+ .collect(Collectors.toUnmodifiableSet());
+
private static final String TABLE_PARENT_SCOPES = "METALAKE, CATALOG,
SCHEMA";
private static final String SCHEMA_PARENT_SCOPES = "METALAKE, CATALOG";
private static final String CATALOG_PARENT_SCOPES = "METALAKE";
@@ -326,13 +332,19 @@ public class MetadataAuthzHelper {
// per-object loop over every catalog in the metalake.
NameIdentifier[] nameIdentifiers =
Arrays.stream(entities).map(toNameIdentifier).toArray(NameIdentifier[]::new);
- if (enableAuthorization()
- && nameIdentifiers.length > 0
- && allVisibleViaParentScope(metalake, expression, entityType,
nameIdentifiers)) {
- // A privilege granted at a parent scope (metalake/catalog/schema) makes
every object in the
- // list visible, and no object-level deny exists, so the per-object
authorization loop is
- // skipped entirely. See
AuthorizationExpressionConstants.*_LIST_PARENT_SCOPE_*.
- return entities;
+ if (enableAuthorization() && nameIdentifiers.length > 0) {
+ if (METADATA_OBJECT_ENTITY_TYPES.contains(entityType)) {
+ Arrays.stream(nameIdentifiers)
+ .forEach(
+ identifier ->
NameIdentifierUtil.checkMetadataObjectName(identifier, entityType));
+ }
+
+ if (allVisibleViaParentScope(metalake, expression, entityType,
nameIdentifiers)) {
+ // A privilege granted at a parent scope (metalake/catalog/schema)
makes every object in
+ // the list visible, and no object-level deny exists, so the
per-object authorization loop
+ // is skipped entirely. See
AuthorizationExpressionConstants.*_LIST_PARENT_SCOPE_*.
+ return entities;
+ }
}
preloadToCache(entityType, nameIdentifiers);
preloadOwner(entityType, nameIdentifiers);
diff --git
a/server-common/src/test/java/org/apache/gravitino/server/authorization/TestMetadataAuthzHelper.java
b/server-common/src/test/java/org/apache/gravitino/server/authorization/TestMetadataAuthzHelper.java
index 83e575c28a..0b40a63c31 100644
---
a/server-common/src/test/java/org/apache/gravitino/server/authorization/TestMetadataAuthzHelper.java
+++
b/server-common/src/test/java/org/apache/gravitino/server/authorization/TestMetadataAuthzHelper.java
@@ -44,6 +44,7 @@ import org.apache.gravitino.authorization.GravitinoAuthorizer;
import org.apache.gravitino.authorization.Privilege;
import org.apache.gravitino.catalog.SchemaDispatcher;
import org.apache.gravitino.dto.tag.MetadataObjectDTO;
+import org.apache.gravitino.exceptions.IllegalNameIdentifierException;
import
org.apache.gravitino.server.authorization.expression.AuthorizationExpressionConstants;
import org.apache.gravitino.utils.NameIdentifierUtil;
import org.apache.gravitino.utils.PrincipalUtils;
@@ -115,6 +116,49 @@ public class TestMetadataAuthzHelper {
}
}
+ @ParameterizedTest
+ @EnumSource(
+ value = Entity.EntityType.class,
+ names = {"TABLE", "TOPIC"})
+ public void testFilterRejectsDottedExternalObjectName(Entity.EntityType
entityType) {
+ NameIdentifier[] identifiers = {
+ NameIdentifier.of("testMetalake", "testCatalog", "testSchema",
"object.with.dot")
+ };
+
+ IllegalNameIdentifierException exception =
+ Assertions.assertThrows(
+ IllegalNameIdentifierException.class,
+ () ->
+ MetadataAuthzHelper.filterByExpression(
+ "testMetalake", "", entityType, identifiers));
+
+ Assertions.assertEquals(
+ "The "
+ + entityType
+ + " name 'object.with.dot' is unsupported because '.' is reserved
as the "
+ + "qualified-name separator.",
+ exception.getMessage());
+ }
+
+ @Test
+ public void
testFilterPreservesDottedExternalObjectNameWithoutAuthorization() {
+ Config config = gravitinoEnv.config();
+ when(config.get(eq(Configs.ENABLE_AUTHORIZATION))).thenReturn(false);
+ NameIdentifier[] identifiers = {
+ NameIdentifier.of("testMetalake", "testCatalog", "testSchema",
"object.with.dot")
+ };
+
+ try {
+ NameIdentifier[] filtered =
+ MetadataAuthzHelper.filterByExpression(
+ "testMetalake", "", Entity.EntityType.TABLE, identifiers);
+
+ Assertions.assertSame(identifiers, filtered);
+ } finally {
+ when(config.get(eq(Configs.ENABLE_AUTHORIZATION))).thenReturn(true);
+ }
+ }
+
@Test
public void testPreloadUsesInternalDispatchers() throws Exception {
AccessControlDispatcher accessControlDispatcher =
mock(AccessControlDispatcher.class);
diff --git
a/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
b/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
index d515cec330..202e3f6d87 100644
---
a/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
+++
b/server/src/main/java/org/apache/gravitino/server/web/filter/GravitinoInterceptionService.java
@@ -45,6 +45,7 @@ import
org.apache.gravitino.authorization.AuthorizationRequestContext;
import org.apache.gravitino.authorization.AuthorizationUtils;
import org.apache.gravitino.exceptions.BadRequestException;
import org.apache.gravitino.exceptions.ForbiddenException;
+import org.apache.gravitino.exceptions.IllegalNameIdentifierException;
import org.apache.gravitino.exceptions.NoSuchMetalakeException;
import org.apache.gravitino.lineage.source.rest.LineageOperations;
import
org.apache.gravitino.listener.api.event.server.AuthorizationDenialFailureEvent;
@@ -255,6 +256,9 @@ public class GravitinoInterceptionService implements
InterceptionService {
}
}
return methodInvocation.proceed();
+ } catch (IllegalNameIdentifierException ex) {
+ LOG.warn("Invalid metadata object identifier during authorization",
ex);
+ return Utils.illegalArguments(ex.getMessage(), ex);
} catch (Exception ex) {
String currentUser = PrincipalUtils.getCurrentUserName();
String methodName = methodInvocation.getMethod().getName();
diff --git
a/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java
b/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java
index 93f3681170..6ff61d736b 100644
---
a/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java
+++
b/server/src/test/java/org/apache/gravitino/server/web/filter/TestGravitinoInterceptionService.java
@@ -59,6 +59,7 @@ import
org.apache.gravitino.server.authorization.annotations.AuthorizationExpres
import
org.apache.gravitino.server.authorization.annotations.AuthorizationMetadata;
import org.apache.gravitino.server.web.Utils;
import org.apache.gravitino.server.web.rest.SchemaOperations;
+import org.apache.gravitino.server.web.rest.TableOperations;
import org.apache.gravitino.server.web.rest.ViewOperations;
import org.apache.gravitino.utils.PrincipalUtils;
import org.apache.gravitino.utils.RequestContext;
@@ -297,6 +298,52 @@ public class TestGravitinoInterceptionService {
}
}
+ @Test
+ public void testDottedMetadataNameReturnsBadRequest() throws Throwable {
+ try (MockedStatic<PrincipalUtils> principalUtilsMocked =
mockStatic(PrincipalUtils.class);
+ MockedStatic<GravitinoAuthorizerProvider> authorizerMocked =
+ mockStatic(GravitinoAuthorizerProvider.class);
+ MockedStatic<AuthorizationUtils> authorizationUtilsMocked =
+ mockStatic(AuthorizationUtils.class)) {
+ principalUtilsMocked
+ .when(PrincipalUtils::getCurrentPrincipal)
+ .thenReturn(new UserPrincipal("tester"));
+
principalUtilsMocked.when(PrincipalUtils::getCurrentUserName).thenReturn("tester");
+ authorizationUtilsMocked
+ .when(
+ () ->
+ AuthorizationUtils.checkCurrentUser(
+ ArgumentMatchers.any(), ArgumentMatchers.any(),
ArgumentMatchers.any()))
+ .thenAnswer(invocation -> null);
+
+ GravitinoAuthorizerProvider provider =
mock(GravitinoAuthorizerProvider.class);
+ GravitinoAuthorizer authorizer = mock(GravitinoAuthorizer.class);
+ when(authorizer.authorize(any(), any(), any(), any(),
any())).thenReturn(true);
+
authorizerMocked.when(GravitinoAuthorizerProvider::getInstance).thenReturn(provider);
+ when(provider.getGravitinoAuthorizer()).thenReturn(authorizer);
+
+ Method method =
+ TableOperations.class.getMethod(
+ "loadTable", String.class, String.class, String.class,
String.class, String.class);
+ MethodInvocation invocation = mock(MethodInvocation.class);
+ when(invocation.getMethod()).thenReturn(method);
+ when(invocation.getArguments())
+ .thenReturn(
+ new Object[] {"testMetalake", "testCatalog", "testSchema",
"sales.2024", null});
+
+ MethodInterceptor interceptor =
+ new
GravitinoInterceptionService().getMethodInterceptors(method).get(0);
+ Response response = (Response) interceptor.invoke(invocation);
+
+ assertEquals(Response.Status.BAD_REQUEST.getStatusCode(),
response.getStatus());
+ assertEquals(
+ "The TABLE name 'sales.2024' is unsupported because '.' is reserved
as the "
+ + "qualified-name separator.",
+ ((ErrorResponse) response.getEntity()).getMessage());
+ verify(invocation, never()).proceed();
+ }
+ }
+
@Test
public void testMetalakeNotExist() throws Throwable {
try (MockedStatic<PrincipalUtils> principalUtilsMocked =
mockStatic(PrincipalUtils.class);