This is an automated email from the ASF dual-hosted git repository.

yuqi1129 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 3a0bace80e [Cherry-pick to branch-1.3] [#11403] improvement(authz): 
use per-request cache for group owner check in JcasbinAuthorizer (#11404) 
(#11435)
3a0bace80e is described below

commit 3a0bace80e514c4ff3430ccda912a597a4b23da7
Author: github-actions[bot] 
<41898282+github-actions[bot]@users.noreply.github.com>
AuthorDate: Fri Jun 5 09:14:31 2026 +0800

    [Cherry-pick to branch-1.3] [#11403] improvement(authz): use per-request 
cache for group owner check in JcasbinAuthorizer (#11404) (#11435)
    
    **Cherry-pick Information:**
    - Original commit: 651ceea8fcf9c31f122d6010de06b667883616ef
    - Target branch: `branch-1.3`
    - Status: ✅ Clean cherry-pick (no conflicts)
    
    Co-authored-by: Qi Yu <[email protected]>
---
 .../main/java/org/apache/gravitino/UserGroup.java  | 22 +++---
 .../gravitino/auth/TestGroupMapperFactory.java     |  8 +--
 .../gravitino/idp/auth/TestBasicAuthenticator.java |  4 +-
 .../authorization/jcasbin/JcasbinAuthorizer.java   | 33 +++------
 .../authentication/TestJwksTokenValidator.java     |  4 +-
 .../authentication/TestStaticSignKeyValidator.java |  8 +--
 .../jcasbin/TestJcasbinAuthorizer.java             | 84 ++++++++++++++++------
 7 files changed, 94 insertions(+), 69 deletions(-)

diff --git a/core/src/main/java/org/apache/gravitino/UserGroup.java 
b/core/src/main/java/org/apache/gravitino/UserGroup.java
index 30e6109cd8..1bdccd5b73 100644
--- a/core/src/main/java/org/apache/gravitino/UserGroup.java
+++ b/core/src/main/java/org/apache/gravitino/UserGroup.java
@@ -27,18 +27,18 @@ import java.util.Optional;
 public class UserGroup {
 
   private final Optional<String> groupExternalUID;
-  private final String groupname;
+  private final String groupName;
 
   /**
    * Constructs a UserGroup instance.
    *
    * @param groupExternalUID The external UID of the group.
-   * @param groupname The name of the group.
+   * @param groupName The name of the group.
    */
-  public UserGroup(Optional<String> groupExternalUID, String groupname) {
-    Preconditions.checkArgument(groupname != null, "groupname cannot be null");
+  public UserGroup(Optional<String> groupExternalUID, String groupName) {
+    Preconditions.checkArgument(groupName != null, "groupName cannot be null");
     this.groupExternalUID = groupExternalUID;
-    this.groupname = groupname;
+    this.groupName = groupName;
   }
 
   /**
@@ -55,8 +55,8 @@ public class UserGroup {
    *
    * @return The group name.
    */
-  public String getGroupname() {
-    return groupname;
+  public String getGroupName() {
+    return groupName;
   }
 
   @Override
@@ -69,12 +69,12 @@ public class UserGroup {
     }
     UserGroup userGroup = (UserGroup) o;
     return Objects.equals(groupExternalUID, userGroup.groupExternalUID)
-        && Objects.equals(groupname, userGroup.groupname);
+        && Objects.equals(groupName, userGroup.groupName);
   }
 
   @Override
   public int hashCode() {
-    return Objects.hash(groupExternalUID, groupname);
+    return Objects.hash(groupExternalUID, groupName);
   }
 
   @Override
@@ -82,8 +82,8 @@ public class UserGroup {
     return "UserGroup{"
         + "groupExternalUID="
         + groupExternalUID
-        + ", groupname='"
-        + groupname
+        + ", groupName='"
+        + groupName
         + '\''
         + '}';
   }
diff --git 
a/core/src/test/java/org/apache/gravitino/auth/TestGroupMapperFactory.java 
b/core/src/test/java/org/apache/gravitino/auth/TestGroupMapperFactory.java
index 6b7dbc6b29..e6f1847308 100644
--- a/core/src/test/java/org/apache/gravitino/auth/TestGroupMapperFactory.java
+++ b/core/src/test/java/org/apache/gravitino/auth/TestGroupMapperFactory.java
@@ -49,7 +49,7 @@ public class TestGroupMapperFactory {
 
     assertEquals(2, mappedGroups.size());
     List<String> groupNames =
-        
mappedGroups.stream().map(UserGroup::getGroupname).collect(Collectors.toList());
+        
mappedGroups.stream().map(UserGroup::getGroupName).collect(Collectors.toList());
     assertTrue(groupNames.contains("admin"));
     assertTrue(groupNames.contains("user"));
   }
@@ -66,7 +66,7 @@ public class TestGroupMapperFactory {
 
     assertEquals(2, mappedGroups.size());
     List<String> groupNames =
-        
mappedGroups.stream().map(UserGroup::getGroupname).collect(Collectors.toList());
+        
mappedGroups.stream().map(UserGroup::getGroupName).collect(Collectors.toList());
     assertTrue(groupNames.contains("admin"));
     assertTrue(groupNames.contains("user"));
   }
@@ -83,7 +83,7 @@ public class TestGroupMapperFactory {
 
     assertEquals(2, mappedGroups.size());
     List<String> groupNames =
-        
mappedGroups.stream().map(UserGroup::getGroupname).collect(Collectors.toList());
+        
mappedGroups.stream().map(UserGroup::getGroupName).collect(Collectors.toList());
     assertTrue(groupNames.contains("admin"));
     assertTrue(groupNames.contains("user"));
   }
@@ -136,7 +136,7 @@ public class TestGroupMapperFactory {
     List<UserGroup> mappedGroups = mapper.map(groups);
 
     assertEquals(1, mappedGroups.size());
-    assertEquals("custom:foo", mappedGroups.get(0).getGroupname());
+    assertEquals("custom:foo", mappedGroups.get(0).getGroupName());
   }
 
   @Test
diff --git 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/auth/TestBasicAuthenticator.java
 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/auth/TestBasicAuthenticator.java
index 661ea88bda..e31c9c8bb7 100644
--- 
a/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/auth/TestBasicAuthenticator.java
+++ 
b/plugins/idp-basic/src/test/java/org/apache/gravitino/idp/auth/TestBasicAuthenticator.java
@@ -96,8 +96,8 @@ class TestBasicAuthenticator {
     assertEquals("alice", principal.getName());
     assertEquals(authHeader, principal.getAccessToken().orElse(null));
     assertEquals(2, principal.getGroups().size());
-    assertEquals("group-a", principal.getGroups().get(0).getGroupname());
-    assertEquals("group-b", principal.getGroups().get(1).getGroupname());
+    assertEquals("group-a", principal.getGroups().get(0).getGroupName());
+    assertEquals("group-b", principal.getGroups().get(1).getGroupName());
   }
 
   @Test
diff --git 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinAuthorizer.java
 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinAuthorizer.java
index f22e6a39f6..347900e606 100644
--- 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinAuthorizer.java
+++ 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinAuthorizer.java
@@ -55,7 +55,6 @@ import org.apache.gravitino.authorization.Privilege;
 import org.apache.gravitino.authorization.SecurableObject;
 import org.apache.gravitino.cache.CaffeineGravitinoCache;
 import org.apache.gravitino.cache.GravitinoCache;
-import org.apache.gravitino.meta.GroupEntity;
 import org.apache.gravitino.meta.RoleEntity;
 import org.apache.gravitino.server.authorization.MetadataIdConverter;
 import org.apache.gravitino.storage.relational.SupportsEntityChangeLog;
@@ -871,6 +870,8 @@ public class JcasbinAuthorizer implements 
GravitinoAuthorizer {
    * Returns true when the cached owner type and ID match the given principal 
or one of the
    * principal's groups. The user id is resolved via the version-validated 
{@link #loadUserInfo}
    * cache so back-to-back ownership checks in the same request do not 
re-query {@code user_meta}.
+   * Group ids are resolved via {@link #loadGroupInfo}, which deduplicates 
within the request via
+   * {@code requestContext.groupInfoCache} and avoids loading full group 
entity objects.
    */
   private boolean ownerMatchesUserOrGroups(
       Optional<OwnerInfo> owner,
@@ -889,9 +890,9 @@ public class JcasbinAuthorizer implements 
GravitinoAuthorizer {
     if 
(!Entity.EntityType.GROUP.name().equalsIgnoreCase(ownerInfo.getOwnerType())) {
       return false;
     }
-    EntityStore entityStore = GravitinoEnv.getInstance().entityStore();
-    for (GroupEntity groupEntity : resolveCurrentUserGroups(metalake, 
entityStore)) {
-      if (Objects.equals(groupEntity.id(), ownerInfo.getOwnerId())) {
+    for (String groupName : principalGroupNames(principal)) {
+      Optional<GroupUpdatedAt> groupInfo = loadGroupInfo(metalake, groupName, 
requestContext);
+      if (groupInfo.isPresent() && groupInfo.get().getGroupId() == 
ownerInfo.getOwnerId()) {
         return true;
       }
     }
@@ -1033,24 +1034,10 @@ public class JcasbinAuthorizer implements 
GravitinoAuthorizer {
    * or has no groups.
    */
   private List<String> currentPrincipalGroupNames() {
-    Principal principal = PrincipalUtils.getCurrentPrincipal();
-    if (!(principal instanceof UserPrincipal)) {
-      return new ArrayList<>();
-    }
-    List<UserGroup> groups = ((UserPrincipal) principal).getGroups();
-    if (groups.isEmpty()) {
-      return new ArrayList<>();
-    }
-    return 
groups.stream().map(UserGroup::getGroupname).collect(Collectors.toList());
+    return principalGroupNames(PrincipalUtils.getCurrentPrincipal());
   }
 
-  /**
-   * Resolves GroupEntity objects for the current principal's groups, skipping 
any that are stale or
-   * not found in the store. Used by owner checks that need full group 
entities instead of only
-   * group names.
-   */
-  private List<GroupEntity> resolveCurrentUserGroups(String metalake, 
EntityStore entityStore) {
-    Principal principal = PrincipalUtils.getCurrentPrincipal();
+  private List<String> principalGroupNames(Principal principal) {
     if (!(principal instanceof UserPrincipal)) {
       return new ArrayList<>();
     }
@@ -1058,11 +1045,7 @@ public class JcasbinAuthorizer implements 
GravitinoAuthorizer {
     if (groups.isEmpty()) {
       return new ArrayList<>();
     }
-    List<NameIdentifier> groupIdents =
-        groups.stream()
-            .map(g -> NameIdentifierUtil.ofGroup(metalake, g.getGroupname()))
-            .collect(Collectors.toList());
-    return entityStore.batchGet(groupIdents, Entity.EntityType.GROUP, 
GroupEntity.class);
+    return 
groups.stream().map(UserGroup::getGroupName).collect(Collectors.toList());
   }
 
   private void versionCheckAndLoadRoles(
diff --git 
a/server-common/src/test/java/org/apache/gravitino/server/authentication/TestJwksTokenValidator.java
 
b/server-common/src/test/java/org/apache/gravitino/server/authentication/TestJwksTokenValidator.java
index 3acbac918e..2fb1c1f2f7 100644
--- 
a/server-common/src/test/java/org/apache/gravitino/server/authentication/TestJwksTokenValidator.java
+++ 
b/server-common/src/test/java/org/apache/gravitino/server/authentication/TestJwksTokenValidator.java
@@ -606,9 +606,9 @@ public class TestJwksTokenValidator {
       assertEquals("test-subject", userPrincipal.getName());
       assertEquals(2, userPrincipal.getGroups().size());
       assertTrue(
-          userPrincipal.getGroups().stream().anyMatch(g -> 
g.getGroupname().equals("group1")));
+          userPrincipal.getGroups().stream().anyMatch(g -> 
g.getGroupName().equals("group1")));
       assertTrue(
-          userPrincipal.getGroups().stream().anyMatch(g -> 
g.getGroupname().equals("group2")));
+          userPrincipal.getGroups().stream().anyMatch(g -> 
g.getGroupName().equals("group2")));
     }
   }
 
diff --git 
a/server-common/src/test/java/org/apache/gravitino/server/authentication/TestStaticSignKeyValidator.java
 
b/server-common/src/test/java/org/apache/gravitino/server/authentication/TestStaticSignKeyValidator.java
index 6cf6836169..c6f603ff47 100644
--- 
a/server-common/src/test/java/org/apache/gravitino/server/authentication/TestStaticSignKeyValidator.java
+++ 
b/server-common/src/test/java/org/apache/gravitino/server/authentication/TestStaticSignKeyValidator.java
@@ -514,8 +514,8 @@ public class TestStaticSignKeyValidator {
     // Check if principal is UserPrincipal and has groups
     UserPrincipal userPrincipal = assertInstanceOf(UserPrincipal.class, 
principal);
     assertEquals(2, userPrincipal.getGroups().size());
-    assertTrue(userPrincipal.getGroups().stream().anyMatch(g -> 
g.getGroupname().equals("group1")));
-    assertTrue(userPrincipal.getGroups().stream().anyMatch(g -> 
g.getGroupname().equals("group2")));
+    assertTrue(userPrincipal.getGroups().stream().anyMatch(g -> 
g.getGroupName().equals("group1")));
+    assertTrue(userPrincipal.getGroups().stream().anyMatch(g -> 
g.getGroupName().equals("group2")));
   }
 
   @Test
@@ -584,7 +584,7 @@ public class TestStaticSignKeyValidator {
     // Check if principal is UserPrincipal and has groups
     UserPrincipal userPrincipal = assertInstanceOf(UserPrincipal.class, 
principal);
     assertEquals(2, userPrincipal.getGroups().size());
-    assertTrue(userPrincipal.getGroups().stream().anyMatch(g -> 
g.getGroupname().equals("groupa")));
-    assertTrue(userPrincipal.getGroups().stream().anyMatch(g -> 
g.getGroupname().equals("groupb")));
+    assertTrue(userPrincipal.getGroups().stream().anyMatch(g -> 
g.getGroupName().equals("groupa")));
+    assertTrue(userPrincipal.getGroups().stream().anyMatch(g -> 
g.getGroupName().equals("groupb")));
   }
 }
diff --git 
a/server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinAuthorizer.java
 
b/server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinAuthorizer.java
index 4b3292027f..67f84a6f99 100644
--- 
a/server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinAuthorizer.java
+++ 
b/server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinAuthorizer.java
@@ -618,27 +618,12 @@ public class TestJcasbinAuthorizer {
 
     NameIdentifier catalogIdent = NameIdentifierUtil.ofCatalog(METALAKE, 
"testCatalog");
 
-    // Mock entityStore.batchGet for group entity lookup (needed for ID-based 
ownership
-    // verification)
-    when(entityStore.batchGet(
-            eq(ImmutableList.of(NameIdentifierUtil.ofGroup(METALAKE, 
GROUP_NAME))),
-            eq(Entity.EntityType.GROUP),
-            eq(GroupEntity.class)))
-        .thenReturn(ImmutableList.of(getGroupEntity()));
-
-    // For non-member principal, mock batchGet for "otherGroup"
-    when(entityStore.batchGet(
-            eq(ImmutableList.of(NameIdentifierUtil.ofGroup(METALAKE, 
"otherGroup"))),
-            eq(Entity.EntityType.GROUP),
-            eq(GroupEntity.class)))
-        .thenReturn(
-            ImmutableList.of(
-                GroupEntity.builder()
-                    .withId(99L)
-                    .withName("otherGroup")
-                    .withNamespace(Namespace.of(METALAKE, "group"))
-                    .withAuditInfo(AuditInfo.EMPTY)
-                    .build()));
+    // Group identity is now resolved via groupMetaMapper.getGroupUpdatedAt 
(per-request cache path)
+    // instead of entityStore.batchGet(GROUP); verify the new path is wired 
correctly.
+    when(groupMetaMapper.getGroupUpdatedAt(eq(METALAKE), eq(GROUP_NAME)))
+        .thenReturn(new GroupUpdatedAt(GROUP_ID, 
groupVersionCounter.incrementAndGet()));
+    when(groupMetaMapper.getGroupUpdatedAt(eq(METALAKE), eq("otherGroup")))
+        .thenReturn(new GroupUpdatedAt(99L, 
groupVersionCounter.incrementAndGet()));
 
     // Mock owner_meta lookup returning a GROUP-typed OwnerInfo (the owner is 
GROUP_ID).
     OwnerInfo groupOwnerInfo = new OwnerInfo(GROUP_ID, "GROUP");
@@ -649,6 +634,10 @@ public class TestJcasbinAuthorizer {
     // The principal belongs to the owning group, so isOwner should return true
     assertTrue(doAuthorizeOwner(groupPrincipal));
 
+    // entityStore.batchGet must NOT be called for GROUP entity lookups in the 
owner-check path
+    Mockito.verify(entityStore, Mockito.never())
+        .batchGet(anyList(), eq(Entity.EntityType.GROUP), 
eq(GroupEntity.class));
+
     // Clear owner and verify it returns false
     when(ownerMetaMapper.selectOwnerByMetadataObjectIdAndType(eq(CATALOG_ID), 
eq("CATALOG")))
         .thenReturn(null);
@@ -675,6 +664,59 @@ public class TestJcasbinAuthorizer {
         .thenReturn(new UserPrincipal(USERNAME));
   }
 
+  @Test
+  public void testGroupOwnerCheckDeduplicatesGroupInfoWithinRequest() throws 
Exception {
+    // Verify that repeated isOwner calls within the same 
AuthorizationRequestContext
+    // do not re-query group_meta; groupMetaMapper.getGroupUpdatedAt should be 
called at most once
+    // per (metalake, groupName) per request thanks to 
requestContext.groupInfoCache.
+    Mockito.clearInvocations(groupMetaMapper);
+
+    UserPrincipal groupPrincipal =
+        new UserPrincipal(USERNAME, ImmutableList.of(new 
UserGroup(Optional.empty(), GROUP_NAME)));
+    
principalUtilsMockedStatic.when(PrincipalUtils::getCurrentPrincipal).thenReturn(groupPrincipal);
+
+    when(groupMetaMapper.getGroupUpdatedAt(eq(METALAKE), eq(GROUP_NAME)))
+        .thenReturn(new GroupUpdatedAt(GROUP_ID, 
groupVersionCounter.incrementAndGet()));
+
+    OwnerInfo groupOwnerInfo = new OwnerInfo(GROUP_ID, "GROUP");
+    when(ownerMetaMapper.selectOwnerByMetadataObjectIdAndType(eq(CATALOG_ID), 
eq("CATALOG")))
+        .thenReturn(groupOwnerInfo);
+    getOwnerRelCache(jcasbinAuthorizer).invalidateAll();
+
+    MetadataObject catalog = MetadataObjects.of(null, "testCatalog", 
MetadataObject.Type.CATALOG);
+
+    // Call isOwner twice within the same request context
+    AuthorizationRequestContext sharedContext = new 
AuthorizationRequestContext();
+    assertTrue(jcasbinAuthorizer.isOwner(groupPrincipal, METALAKE, catalog, 
sharedContext));
+    assertTrue(jcasbinAuthorizer.isOwner(groupPrincipal, METALAKE, catalog, 
sharedContext));
+
+    // group_meta should have been queried exactly once despite two isOwner 
calls
+    Mockito.verify(groupMetaMapper, Mockito.times(1))
+        .getGroupUpdatedAt(eq(METALAKE), eq(GROUP_NAME));
+  }
+
+  @Test
+  public void testGroupOwnerCheckUsesProvidedPrincipalGroups() throws 
Exception {
+    UserPrincipal ownerGroupPrincipal =
+        new UserPrincipal(USERNAME, ImmutableList.of(new 
UserGroup(Optional.empty(), GROUP_NAME)));
+    UserPrincipal currentPrincipalWithoutOwnerGroup =
+        new UserPrincipal(
+            USERNAME, ImmutableList.of(new UserGroup(Optional.empty(), 
"otherGroup")));
+    principalUtilsMockedStatic
+        .when(PrincipalUtils::getCurrentPrincipal)
+        .thenReturn(currentPrincipalWithoutOwnerGroup);
+
+    when(groupMetaMapper.getGroupUpdatedAt(eq(METALAKE), eq(GROUP_NAME)))
+        .thenReturn(new GroupUpdatedAt(GROUP_ID, 
groupVersionCounter.incrementAndGet()));
+
+    OwnerInfo groupOwnerInfo = new OwnerInfo(GROUP_ID, "GROUP");
+    when(ownerMetaMapper.selectOwnerByMetadataObjectIdAndType(eq(CATALOG_ID), 
eq("CATALOG")))
+        .thenReturn(groupOwnerInfo);
+    getOwnerRelCache(jcasbinAuthorizer).invalidateAll();
+
+    assertTrue(doAuthorizeOwner(ownerGroupPrincipal));
+  }
+
   @Test
   public void testAuthorizeByGroupRole() throws Exception {
     makeCompletableFutureUseCurrentThread(jcasbinAuthorizer);

Reply via email to