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

roryqi 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 ab2645a799 [#12112] fix(authz): Align group load authorization (#12113)
ab2645a799 is described below

commit ab2645a799eff40dbaeb36b1381695fc8fd7a802
Author: roryqi <[email protected]>
AuthorDate: Mon Jul 27 15:49:50 2026 +0800

    [#12112] fix(authz): Align group load authorization (#12113)
    
    ### What changes were proposed in this pull request?
    
    - Add an explicit authorization expression for loading group details.
    - Include metalake and group path parameters in the authorization
    metadata context.
    - Support GROUP::SELF in JCasbin authorization checks.
    - Add regression tests for group load authorization metadata and
    GROUP::SELF evaluation.
    
    ### Why are the changes needed?
    
    The group load API should follow the same authorization interception
    path as related user and group access-control APIs.
    
    Fix: #12112
    
    ### Does this PR introduce _any_ user-facing change?
    
    No public API change. Group detail loading now follows the expected
    authorization behavior.
    
    ### How was this patch tested?
    
    - `./gradlew :server:test --tests
    
org.apache.gravitino.server.web.rest.authorization.TestGroupAuthorizationExpression
    --tests org.apache.gravitino.server.web.rest.TestGroupOperations`
    - `./gradlew :server-common:test --tests
    org.apache.gravitino.server.authorization.jcasbin.TestJcasbinAuthorizer`
    - `git diff --check`
---
 .../authorization/jcasbin/JcasbinAuthorizer.java   | 19 ++++-
 .../jcasbin/TestJcasbinAuthorizer.java             | 40 +++++++++++
 .../gravitino/server/web/rest/GroupOperations.java | 33 +++++++--
 .../server/web/rest/TestGroupOperations.java       | 80 ++++++++++++++++++++++
 .../TestGroupAuthorizationExpression.java          | 66 ++++++++++++++++++
 5 files changed, 232 insertions(+), 6 deletions(-)

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 2535e8e336..66583526e1 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
@@ -425,10 +425,27 @@ public class JcasbinAuthorizer implements 
GravitinoAuthorizer {
       NameIdentifier nameIdentifier,
       AuthorizationRequestContext requestContext) {
     String metalake = nameIdentifier.namespace().level(0);
-    String currentUserName = PrincipalUtils.getCurrentUserName();
     if (Entity.EntityType.USER == type) {
+      String currentUserName = PrincipalUtils.getCurrentUserName();
       return Objects.equals(nameIdentifier.name(), currentUserName);
+    } else if (Entity.EntityType.GROUP == type) {
+      Principal currentPrincipal = PrincipalUtils.getCurrentPrincipal();
+      if (!(currentPrincipal instanceof UserPrincipal)) {
+        return false;
+      }
+
+      List<UserGroup> groups = ((UserPrincipal) currentPrincipal).getGroups();
+      if (groups.isEmpty()) {
+        return false;
+      }
+
+      boolean principalHasGroup =
+          groups.stream()
+              .map(UserGroup::getGroupName)
+              .anyMatch(groupName -> Objects.equals(groupName, 
nameIdentifier.name()));
+      return principalHasGroup;
     } else if (Entity.EntityType.ROLE == type) {
+      String currentUserName = PrincipalUtils.getCurrentUserName();
       try {
         Optional<Long> roleId =
             MetadataIdConverter.getID(
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 624c547908..177fda2d7b 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
@@ -888,6 +888,46 @@ public class TestJcasbinAuthorizer {
     restoreDefaultPrincipal();
   }
 
+  @Test
+  public void testIsSelfGroupViaPrincipalGroup() throws Exception {
+    NameIdentifier groupIdent = NameIdentifierUtil.ofGroup(METALAKE, 
GROUP_NAME);
+
+    setCurrentPrincipalWithGroup(GROUP_NAME);
+    Mockito.clearInvocations(groupMetaMapper);
+    assertTrue(
+        jcasbinAuthorizer.isSelf(
+            Entity.EntityType.GROUP, groupIdent, new 
AuthorizationRequestContext()));
+    Mockito.verify(groupMetaMapper, 
Mockito.never()).getGroupUpdatedAt(anyString(), anyString());
+
+    setCurrentPrincipalWithGroup("otherGroup");
+    assertFalse(
+        jcasbinAuthorizer.isSelf(
+            Entity.EntityType.GROUP, groupIdent, new 
AuthorizationRequestContext()));
+    Mockito.verify(groupMetaMapper, 
Mockito.never()).getGroupUpdatedAt(anyString(), anyString());
+
+    restoreDefaultPrincipal();
+  }
+
+  @Test
+  public void testIsSelfGroupDoesNotRequireCurrentUserName() throws Exception {
+    NameIdentifier groupIdent = NameIdentifierUtil.ofGroup(METALAKE, 
GROUP_NAME);
+
+    UserPrincipal groupPrincipal = mock(UserPrincipal.class);
+    when(groupPrincipal.getGroups())
+        .thenReturn(ImmutableList.of(new UserGroup(Optional.empty(), 
GROUP_NAME)));
+    when(groupPrincipal.getName())
+        .thenThrow(new AssertionError("GROUP self check should only use 
principal groups"));
+    
principalUtilsMockedStatic.when(PrincipalUtils::getCurrentPrincipal).thenReturn(groupPrincipal);
+
+    try {
+      assertTrue(
+          jcasbinAuthorizer.isSelf(
+              Entity.EntityType.GROUP, groupIdent, new 
AuthorizationRequestContext()));
+    } finally {
+      restoreDefaultPrincipal();
+    }
+  }
+
   @Test
   public void testIsSelfRoleReusesCacheAcrossCalls() throws Exception {
     // Acceptance criterion for #11088: repeated isSelf(ROLE) calls in the 
same logical request
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/GroupOperations.java
 
b/server/src/main/java/org/apache/gravitino/server/web/rest/GroupOperations.java
index 5cc083c7f7..ef44db299c 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/GroupOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/GroupOperations.java
@@ -49,10 +49,12 @@ import org.apache.gravitino.dto.responses.RemoveResponse;
 import org.apache.gravitino.dto.util.DTOConverters;
 import org.apache.gravitino.metalake.MetalakeManager;
 import org.apache.gravitino.metrics.MetricNames;
+import org.apache.gravitino.server.authorization.MetadataAuthzHelper;
 import org.apache.gravitino.server.authorization.NameBindings;
 import 
org.apache.gravitino.server.authorization.annotations.AuthorizationExpression;
 import 
org.apache.gravitino.server.authorization.annotations.AuthorizationMetadata;
 import org.apache.gravitino.server.web.Utils;
+import org.apache.gravitino.utils.NameIdentifierUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -62,6 +64,9 @@ public class GroupOperations {
 
   private static final Logger LOG = 
LoggerFactory.getLogger(GroupOperations.class);
 
+  private static final String LOAD_GROUP_PRIVILEGE =
+      "METALAKE::OWNER || METALAKE::MANAGE_GROUPS || GROUP::SELF";
+
   private final AccessControlDispatcher accessControlManager;
   private final OwnerDispatcher ownerDispatcher;
 
@@ -80,8 +85,11 @@ public class GroupOperations {
   @Produces("application/vnd.gravitino.v1+json")
   @Timed(name = "get-group." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
   @ResponseMetered(name = "get-group", absolute = true)
+  @AuthorizationExpression(expression = LOAD_GROUP_PRIVILEGE)
   public Response getGroup(
-      @PathParam("metalake") String metalake, @PathParam("group") String 
group) {
+      @PathParam("metalake") @AuthorizationMetadata(type = 
Entity.EntityType.METALAKE)
+          String metalake,
+      @PathParam("group") @AuthorizationMetadata(type = 
Entity.EntityType.GROUP) String group) {
     try {
       return Utils.doAs(
           httpRequest,
@@ -179,11 +187,26 @@ public class GroupOperations {
           () -> {
             MetalakeManager.checkMetalakeInUse(metalake);
             if (verbose) {
-              return Utils.ok(
-                  new GroupListResponse(
-                      
DTOConverters.toDTOs(accessControlManager.listGroups(metalake))));
+              Group[] groups = accessControlManager.listGroups(metalake);
+              groups =
+                  MetadataAuthzHelper.filterByExpression(
+                      metalake,
+                      LOAD_GROUP_PRIVILEGE,
+                      Entity.EntityType.GROUP,
+                      groups,
+                      groupEntity -> NameIdentifierUtil.ofGroup(metalake, 
groupEntity.name()));
+
+              return Utils.ok(new 
GroupListResponse(DTOConverters.toDTOs(groups)));
             } else {
-              return Utils.ok(new 
NameListResponse(accessControlManager.listGroupNames(metalake)));
+              String[] groups = accessControlManager.listGroupNames(metalake);
+              groups =
+                  MetadataAuthzHelper.filterByExpression(
+                      metalake,
+                      LOAD_GROUP_PRIVILEGE,
+                      Entity.EntityType.GROUP,
+                      groups,
+                      groupName -> NameIdentifierUtil.ofGroup(metalake, 
groupName));
+              return Utils.ok(new NameListResponse(groups));
             }
           });
 
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestGroupOperations.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestGroupOperations.java
index 5fd1084b6c..d35faee879 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestGroupOperations.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestGroupOperations.java
@@ -38,6 +38,7 @@ import javax.ws.rs.core.Response;
 import javax.ws.rs.core.Response.Status;
 import org.apache.commons.lang3.reflect.FieldUtils;
 import org.apache.gravitino.Config;
+import org.apache.gravitino.Entity.EntityType;
 import org.apache.gravitino.EntityStore;
 import org.apache.gravitino.GravitinoEnv;
 import org.apache.gravitino.authorization.AccessControlManager;
@@ -62,12 +63,14 @@ import org.apache.gravitino.meta.AuditInfo;
 import org.apache.gravitino.meta.BaseMetalake;
 import org.apache.gravitino.meta.GroupEntity;
 import org.apache.gravitino.rest.RESTUtils;
+import org.apache.gravitino.server.authorization.MetadataAuthzHelper;
 import org.glassfish.hk2.utilities.binding.AbstractBinder;
 import org.glassfish.jersey.server.ResourceConfig;
 import org.glassfish.jersey.test.TestProperties;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
 import org.mockito.Mockito;
 
 public class TestGroupOperations extends BaseOperationsTest {
@@ -380,6 +383,43 @@ public class TestGroupOperations extends 
BaseOperationsTest {
     Assertions.assertEquals(RuntimeException.class.getSimpleName(), 
errorResponse2.getType());
   }
 
+  @Test
+  public void testListGroupNamesFiltersByAuthorizationExpression() throws 
Exception {
+    Mockito.reset(manager, entityStore);
+    Mockito.doReturn(new String[] {"group", 
"filtered"}).when(manager).listGroupNames(any());
+
+    // Mock metalake with in-use property
+    BaseMetalake metalake = mock(BaseMetalake.class);
+    PropertiesMetadata propertiesMetadata = mock(PropertiesMetadata.class);
+    when(propertiesMetadata.getOrDefault(any(), any())).thenReturn(true);
+    when(metalake.propertiesMetadata()).thenReturn(propertiesMetadata);
+    when(entityStore.get(any(), any(), any())).thenReturn(metalake);
+
+    try (MockedStatic<MetadataAuthzHelper> metadataAuthzHelper =
+        Mockito.mockStatic(MetadataAuthzHelper.class)) {
+      metadataAuthzHelper
+          .when(
+              () ->
+                  MetadataAuthzHelper.filterByExpression(
+                      Mockito.eq("metalake1"),
+                      Mockito.eq("METALAKE::OWNER || METALAKE::MANAGE_GROUPS 
|| GROUP::SELF"),
+                      Mockito.eq(EntityType.GROUP),
+                      Mockito.any(String[].class),
+                      Mockito.any()))
+          .thenReturn(new String[] {"group"});
+
+      GroupOperations groupOperations = new GroupOperations();
+      HttpServletRequest request = mock(HttpServletRequest.class);
+      FieldUtils.writeField(groupOperations, "httpRequest", request, true);
+
+      Response resp = groupOperations.listGroups("metalake1", false);
+
+      Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp.getStatus());
+      NameListResponse listResponse = (NameListResponse) resp.getEntity();
+      Assertions.assertArrayEquals(new String[] {"group"}, 
listResponse.getNames());
+    }
+  }
+
   @Test
   public void testListGroups() throws IOException {
     Group group = buildGroup("group");
@@ -445,6 +485,46 @@ public class TestGroupOperations extends 
BaseOperationsTest {
     Assertions.assertEquals(RuntimeException.class.getSimpleName(), 
errorResponse2.getType());
   }
 
+  @Test
+  public void testListGroupsFiltersByAuthorizationExpression() throws 
Exception {
+    Mockito.reset(manager, entityStore);
+    Group group = buildGroup("group");
+    Group filteredGroup = buildGroup("filtered");
+    Mockito.doReturn(new Group[] {group, 
filteredGroup}).when(manager).listGroups(any());
+
+    // Mock metalake with in-use property
+    BaseMetalake metalake = mock(BaseMetalake.class);
+    PropertiesMetadata propertiesMetadata = mock(PropertiesMetadata.class);
+    when(propertiesMetadata.getOrDefault(any(), any())).thenReturn(true);
+    when(metalake.propertiesMetadata()).thenReturn(propertiesMetadata);
+    when(entityStore.get(any(), any(), any())).thenReturn(metalake);
+
+    try (MockedStatic<MetadataAuthzHelper> metadataAuthzHelper =
+        Mockito.mockStatic(MetadataAuthzHelper.class)) {
+      metadataAuthzHelper
+          .when(
+              () ->
+                  MetadataAuthzHelper.filterByExpression(
+                      Mockito.eq("metalake1"),
+                      Mockito.eq("METALAKE::OWNER || METALAKE::MANAGE_GROUPS 
|| GROUP::SELF"),
+                      Mockito.eq(EntityType.GROUP),
+                      Mockito.any(Group[].class),
+                      Mockito.any()))
+          .thenReturn(new Group[] {group});
+
+      GroupOperations groupOperations = new GroupOperations();
+      HttpServletRequest request = mock(HttpServletRequest.class);
+      FieldUtils.writeField(groupOperations, "httpRequest", request, true);
+
+      Response resp = groupOperations.listGroups("metalake1", true);
+
+      Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp.getStatus());
+      GroupListResponse listResponse = (GroupListResponse) resp.getEntity();
+      Assertions.assertEquals(1, listResponse.getGroups().length);
+      Assertions.assertEquals(group.name(), 
listResponse.getGroups()[0].name());
+    }
+  }
+
   @Test
   public void testRemoveGroup() throws IOException {
     // Mock metalake with in-use property
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/authorization/TestGroupAuthorizationExpression.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/authorization/TestGroupAuthorizationExpression.java
new file mode 100644
index 0000000000..7e0d39a08f
--- /dev/null
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/authorization/TestGroupAuthorizationExpression.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *  http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.gravitino.server.web.rest.authorization;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.google.common.collect.ImmutableSet;
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import ognl.OgnlException;
+import org.apache.gravitino.Entity;
+import 
org.apache.gravitino.server.authorization.annotations.AuthorizationExpression;
+import 
org.apache.gravitino.server.authorization.annotations.AuthorizationMetadata;
+import org.apache.gravitino.server.web.rest.GroupOperations;
+import org.junit.jupiter.api.Test;
+
+public class TestGroupAuthorizationExpression {
+
+  @Test
+  public void testGetGroupAuthorizationExpression() throws 
NoSuchMethodException, OgnlException {
+    Method method = GroupOperations.class.getMethod("getGroup", String.class, 
String.class);
+    AuthorizationExpression authorizationExpressionAnnotation =
+        method.getAnnotation(AuthorizationExpression.class);
+    assertNotNull(authorizationExpressionAnnotation);
+
+    MockAuthorizationExpressionEvaluator mockEvaluator =
+        new 
MockAuthorizationExpressionEvaluator(authorizationExpressionAnnotation.expression());
+    assertFalse(mockEvaluator.getResult(ImmutableSet.of()));
+    
assertFalse(mockEvaluator.getResult(ImmutableSet.of("METALAKE::MANAGE_USERS")));
+    assertTrue(mockEvaluator.getResult(ImmutableSet.of("METALAKE::OWNER")));
+    
assertTrue(mockEvaluator.getResult(ImmutableSet.of("METALAKE::MANAGE_GROUPS")));
+    assertTrue(mockEvaluator.getResult(ImmutableSet.of("GROUP::SELF")));
+  }
+
+  @Test
+  public void testGetGroupAuthorizationMetadata() throws NoSuchMethodException 
{
+    Method method = GroupOperations.class.getMethod("getGroup", String.class, 
String.class);
+    Parameter[] parameters = method.getParameters();
+
+    AuthorizationMetadata metalakeMetadata =
+        parameters[0].getAnnotation(AuthorizationMetadata.class);
+    assertNotNull(metalakeMetadata);
+    assertEquals(Entity.EntityType.METALAKE, metalakeMetadata.type());
+
+    AuthorizationMetadata groupMetadata = 
parameters[1].getAnnotation(AuthorizationMetadata.class);
+    assertNotNull(groupMetadata);
+    assertEquals(Entity.EntityType.GROUP, groupMetadata.type());
+  }
+}

Reply via email to