This is an automated email from the ASF dual-hosted git repository.
yuqi1129 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 d54358e1bc [#12622] improvement(server): apply the list authorization
short-circuit to every list shape (#12623)
d54358e1bc is described below
commit d54358e1bc637418b6b22c64ad5321745d81e50d
Author: Qi Yu <[email protected]>
AuthorDate: Wed Aug 26 20:52:35 2026 +0800
[#12622] improvement(server): apply the list authorization short-circuit to
every list shape (#12623)
### What changes were proposed in this pull request?
Move the parent-scope short-circuit and the `preloadToCache` /
`preloadOwner` calls from `MetadataAuthzHelper.filterByExpression(...,
NameIdentifier[])` into the generic `filterByExpression(..., E[],
Function)`, and let the identifier overload delegate to the generic one.
Every list endpoint now passes the same point regardless of the shape it
holds its results in, so a caller with a parent-scope grant is filtered
with one expression evaluation and one deny check instead of one
evaluation per object.
### Why are the changes needed?
`listCatalogs?details=true` carries `Catalog[]` and went through the
generic overload, which had neither the short-circuit nor the preloads,
so it ran the per-object loop over every catalog in the metalake.
Catalogs are the only entity type with a `details=true` variant, which
is why tables and schemas were unaffected and this went unnoticed.
The list endpoints for functions, groups, jobs, policies, roles, tags
and users also use the generic overload and gain the preloads.
Fix: #12622
### Does this PR introduce _any_ user-facing change?
No. Filtering results are unchanged; only the work done to reach them
is. No API or property is added.
### How was this patch tested?
- New `testListShortCircuitAppliesToNonIdentifierResults` filters a
non-identifier array through the generic overload. It fails without this
change and passes with it. It asserts `hasDenyPolicy` is consulted
exactly once, which only the short-circuit does; asserting on the
returned elements alone would not catch the regression, because the
metalake-scope grant satisfies the per-object expression as well.
- `./gradlew :server-common:test :server:test -PskipITs` —
`TestMetadataAuthzHelper` 10, `TestCatalogOperations` 9,
`TestCatalogAuthorizationExpression` 5, no failures.
- `./gradlew :server-common:spotlessCheck :server-common:javadoc
:server:compileJava :iceberg:iceberg-rest-server:compileJava -PskipITs`.
- `TestHttpsServerAuthentication` mutual-TLS cases fail intermittently
in a full `:server-common:test` run on `main` as well, with different
cases failing across runs; unrelated to this change.
---
.../server/authorization/MetadataAuthzHelper.java | 59 ++++++++++++----------
.../authorization/TestMetadataAuthzHelper.java | 55 ++++++++++++++++++++
2 files changed, 88 insertions(+), 26 deletions(-)
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 fc7db7b615..cd6178258f 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
@@ -206,32 +206,6 @@ public class MetadataAuthzHelper {
String expression,
Entity.EntityType entityType,
NameIdentifier[] nameIdentifiers) {
- if (enableAuthorization() && nameIdentifiers.length > 0) {
- String principalName = PrincipalUtils.getCurrentPrincipal().getName();
- 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_*.
- LOG.debug(
- "List authorization short-circuit HIT for principal {}, entity
type {} under metalake "
- + "{}: all {} listed object(s) are visible via a parent-scope
grant; skipping the "
- + "per-object authorization loop.",
- principalName,
- entityType,
- metalake,
- nameIdentifiers.length);
- return nameIdentifiers;
- }
- LOG.debug(
- "List authorization short-circuit MISS for principal {}, entity type
{} under metalake "
- + "{} ({} object(s)); falling back to the per-object
authorization loop.",
- principalName,
- entityType,
- metalake,
- nameIdentifiers.length);
- }
- preloadToCache(entityType, nameIdentifiers);
- preloadOwner(entityType, nameIdentifiers);
return filterByExpression(metalake, expression, entityType,
nameIdentifiers, e -> e);
}
@@ -356,6 +330,39 @@ public class MetadataAuthzHelper {
Entity.EntityType entityType,
E[] entities,
Function<E, NameIdentifier> toNameIdentifier) {
+ // Every list endpoint funnels through here, whichever shape it holds its
results in, so the
+ // short-circuit and the preloads live at this one point. Keeping them in
the NameIdentifier[]
+ // overload alone let the verbose catalog listing, which carries Catalog
objects, run the
+ // per-object loop over every catalog in the metalake.
+ NameIdentifier[] nameIdentifiers =
+
Arrays.stream(entities).map(toNameIdentifier).toArray(NameIdentifier[]::new);
+ if (enableAuthorization() && nameIdentifiers.length > 0) {
+ String principalName = PrincipalUtils.getCurrentPrincipal().getName();
+ 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_*.
+ LOG.debug(
+ "List authorization short-circuit HIT for principal {}, entity
type {} under metalake "
+ + "{}: all {} listed object(s) are visible via a parent-scope
grant; skipping the "
+ + "per-object authorization loop.",
+ principalName,
+ entityType,
+ metalake,
+ nameIdentifiers.length);
+ return entities;
+ }
+ LOG.debug(
+ "List authorization short-circuit MISS for principal {}, entity type
{} under metalake "
+ + "{} ({} object(s)); falling back to the per-object
authorization loop.",
+ principalName,
+ entityType,
+ metalake,
+ nameIdentifiers.length);
+ }
+ preloadToCache(entityType, nameIdentifiers);
+ preloadOwner(entityType, nameIdentifiers);
+
GravitinoAuthorizer authorizer =
GravitinoAuthorizerProvider.getInstance().getGravitinoAuthorizer();
AuthorizationRequestContext authorizationRequestContext = new
AuthorizationRequestContext();
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 a5019085cb..15783d818a 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
@@ -23,6 +23,8 @@ import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.lang.reflect.Field;
@@ -321,6 +323,59 @@ public class TestMetadataAuthzHelper {
}
}
+ /**
+ * The verbose catalog listing hands this helper {@code Catalog} objects
rather than identifiers,
+ * so it goes through the generic overload. That overload used to skip the
short-circuit, which
+ * left every catalog in the metalake on the per-object path.
+ *
+ * <p>Proving which path ran needs care. Comparing the returned elements
does not work, because
+ * the metalake-scope grant satisfies the per-object expression too and
every catalog comes back
+ * either way. Counting authorizer calls does not work either: the
per-request cache in {@link
+ * org.apache.gravitino.authorization.AuthorizationRequestContext} collapses
the repeated
+ * metalake-scope check, so the count is the same for three catalogs and for
thirty.
+ *
+ * <p>What does separate them is the array itself. The short-circuit hands
back the caller's own
+ * array untouched, while the per-object path collects survivors into a new
one, so identity says
+ * which branch produced the result.
+ */
+ @Test
+ public void testListShortCircuitAppliesToNonIdentifierResults() {
+ makeCompletableFutureUseCurrentThread();
+ try (MockedStatic<PrincipalUtils> principalUtilsMocked =
mockStatic(PrincipalUtils.class);
+ MockedStatic<GravitinoAuthorizerProvider> mockStatic =
+ mockStatic(GravitinoAuthorizerProvider.class)) {
+ principalUtilsMocked
+ .when(PrincipalUtils::getCurrentPrincipal)
+ .thenReturn(new UserPrincipal("tester"));
+ principalUtilsMocked.when(() -> PrincipalUtils.doAs(any(),
any())).thenCallRealMethod();
+ GravitinoAuthorizerProvider mockedProvider =
mock(GravitinoAuthorizerProvider.class);
+
mockStatic.when(GravitinoAuthorizerProvider::getInstance).thenReturn(mockedProvider);
+ GravitinoAuthorizer authorizer =
+ mockParentGrantAuthorizer(MetadataObject.Type.METALAKE,
Privilege.Name.USE_CATALOG);
+ when(mockedProvider.getGravitinoAuthorizer()).thenReturn(authorizer);
+
+ // Stands in for the Catalog objects the verbose listing carries:
anything that is not a
+ // NameIdentifier and needs a mapper to become one.
+ String[] catalogNames = new String[] {"c1", "c2", "c3"};
+
+ String[] filtered =
+ MetadataAuthzHelper.filterByExpression(
+ "testMetalake",
+
AuthorizationExpressionConstants.LOAD_CATALOG_AUTHORIZATION_EXPRESSION,
+ Entity.EntityType.CATALOG,
+ catalogNames,
+ name -> NameIdentifierUtil.ofCatalog("testMetalake", name));
+
+ Assertions.assertSame(
+ catalogNames,
+ filtered,
+ "The parent-scope short-circuit must return the caller's array as
is; a new array means "
+ + "the per-object authorization loop ran for every catalog");
+ // Only the short-circuit asks this, and it asks once, after the parent
grant is confirmed.
+ verify(authorizer, times(1)).hasDenyPolicy(any(), eq("testMetalake"),
anySet(), any());
+ }
+ }
+
/**
* Builds an authorizer that grants {@code SELECT_TABLE} at the schema
scope, but only for the
* schema whose simple name equals {@code grantedSchema}. Used to prove the
short-circuit never