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

jshao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git

commit 40798a58603296955b7066583e224790da11c980
Author: yangyang zhong <[email protected]>
AuthorDate: Mon May 26 14:21:39 2025 +0800

    [#6786] feat(authz): Introduce JcasbinAuthorizer & GravitinoAdapter (#7197)
    
    ### What changes were proposed in this pull request?
    
    Introduce JcasbinAuthorizer and GravitinoAdapter for metadata
    authorization.
    
    ### Why are the changes needed?
    
    Fix: #6786
    
    ### Does this PR introduce _any_ user-facing change?
    
    None
    
    ### How was this patch tested?
    
    org.apache.gravitino.server.authorization.jcasbin.TestJcasbinAuthorizer
---
 .../relational/service/RoleMetaService.java        |   2 +-
 .../server/authorization/GravitinoAuthorizer.java  |   8 +
 .../authorization/GravitinoAuthorizerProvider.java |   7 +-
 .../authorization/PassThroughAuthorizer.java       |   3 +
 .../authorization/jcasbin/GravitinoAdapter.java    |  57 +++++
 .../authorization/jcasbin/JcasbinAuthorizer.java   | 189 +++++++++++++++
 .../TestAuthorizationExpressionEvaluator.java      |   3 +
 .../jcasbin/TestJcasbinAuthorizer.java             | 257 +++++++++++++++++++++
 8 files changed, 521 insertions(+), 5 deletions(-)

diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/service/RoleMetaService.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/service/RoleMetaService.java
index 58e4a6e7fc..0522a3cb95 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/service/RoleMetaService.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/service/RoleMetaService.java
@@ -293,7 +293,7 @@ public class RoleMetaService {
     return true;
   }
 
-  private static List<SecurableObjectPO> listSecurableObjectsByRoleId(Long 
roleId) {
+  public static List<SecurableObjectPO> listSecurableObjectsByRoleId(Long 
roleId) {
     return SessionUtils.getWithoutCommit(
         SecurableObjectMapper.class, mapper -> 
mapper.listSecurableObjectsByRoleId(roleId));
   }
diff --git 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/GravitinoAuthorizer.java
 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/GravitinoAuthorizer.java
index 710fe93601..bf53ca7b5a 100644
--- 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/GravitinoAuthorizer.java
+++ 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/GravitinoAuthorizer.java
@@ -55,4 +55,12 @@ public interface GravitinoAuthorizer extends Closeable {
    * @return authorization result.
    */
   boolean isOwner(Principal principal, String metalake, MetadataObject 
metadataObject);
+
+  /**
+   * When the permissions of a role change, it is necessary to notify the 
GravitinoAuthorizer in
+   * order to clear the cache.
+   *
+   * @param roleId The role id;
+   */
+  void handleRolePrivilegeChange(Long roleId);
 }
diff --git 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/GravitinoAuthorizerProvider.java
 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/GravitinoAuthorizerProvider.java
index 49390cf27f..3d837ba955 100644
--- 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/GravitinoAuthorizerProvider.java
+++ 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/GravitinoAuthorizerProvider.java
@@ -19,6 +19,7 @@ package org.apache.gravitino.server.authorization;
 
 import org.apache.gravitino.Configs;
 import org.apache.gravitino.server.ServerConfig;
+import org.apache.gravitino.server.authorization.jcasbin.JcasbinAuthorizer;
 
 /**
  * Used to initialize and store {@link GravitinoAuthorizer}. When Gravitino 
Server starts up, it
@@ -46,13 +47,11 @@ public class GravitinoAuthorizerProvider {
         if (gravitinoAuthorizer == null) {
           boolean enableAuthorization = 
serverConfig.get(Configs.ENABLE_AUTHORIZATION);
           if (enableAuthorization) {
-            // TODO
+            gravitinoAuthorizer = new JcasbinAuthorizer();
           } else {
             gravitinoAuthorizer = new PassThroughAuthorizer();
           }
-          if (gravitinoAuthorizer != null) {
-            gravitinoAuthorizer.initialize();
-          }
+          gravitinoAuthorizer.initialize();
         }
       }
     }
diff --git 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/PassThroughAuthorizer.java
 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/PassThroughAuthorizer.java
index fa655617dc..72e3b810a3 100644
--- 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/PassThroughAuthorizer.java
+++ 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/PassThroughAuthorizer.java
@@ -45,6 +45,9 @@ public class PassThroughAuthorizer implements 
GravitinoAuthorizer {
     return true;
   }
 
+  @Override
+  public void handleRolePrivilegeChange(Long roleId) {}
+
   @Override
   public void close() throws IOException {}
 }
diff --git 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/GravitinoAdapter.java
 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/GravitinoAdapter.java
new file mode 100644
index 0000000000..e354ece115
--- /dev/null
+++ 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/GravitinoAdapter.java
@@ -0,0 +1,57 @@
+/*
+ * 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.authorization.jcasbin;
+
+import java.util.List;
+import org.casbin.jcasbin.model.Model;
+import org.casbin.jcasbin.persist.Adapter;
+
+/**
+ * The {@link Adapter} in Jcasbin is used to load the policy from the Adpater 
when initializing the
+ * {@link org.casbin.jcasbin.main.Enforcer} , and to persist the policy when 
the method of executing
+ * the Enforcer changes the policy
+ *
+ * <p>GravitinoAdapter will not perform any actions because there is no need 
to persist Privilege.
+ * All Privileges will be temporarily loaded into the Jcasbin cache when a 
user requests them.
+ */
+public class GravitinoAdapter implements Adapter {
+
+  /** Gravitino does not require an initialization strategy when an Enforcer 
is instantiated */
+  @Override
+  public void loadPolicy(Model model) {}
+
+  /** Gravitino does not need persistent Policy when modifying the permission 
policy */
+  @Override
+  public void savePolicy(Model model) {}
+
+  /** Gravitino does not need persistent Policy when modifying the permission 
policy */
+  @Override
+  public void addPolicy(String jcasbinModelSection, String policyType, 
List<String> rule) {}
+
+  /** Gravitino does not need persistent Policy when modifying the permission 
policy */
+  @Override
+  public void removePolicy(String jcasbinModelSection, String policyType, 
List<String> rule) {}
+
+  /** Gravitino does not need persistent Policy when modifying the permission 
policy */
+  @Override
+  public void removeFilteredPolicy(
+      String jcasbinModelSection,
+      String policyType,
+      int policyFieldIndex,
+      String... policyFieldValues) {}
+}
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
new file mode 100644
index 0000000000..0534412be3
--- /dev/null
+++ 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinAuthorizer.java
@@ -0,0 +1,189 @@
+/*
+ * 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.authorization.jcasbin;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.base.Preconditions;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.Principal;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.commons.io.IOUtils;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.EntityStore;
+import org.apache.gravitino.GravitinoEnv;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.SupportsRelationOperations;
+import org.apache.gravitino.auth.AuthConstants;
+import org.apache.gravitino.authorization.Privilege;
+import org.apache.gravitino.meta.BaseMetalake;
+import org.apache.gravitino.meta.RoleEntity;
+import org.apache.gravitino.server.authorization.GravitinoAuthorizer;
+import org.apache.gravitino.server.authorization.MetadataIdConverter;
+import org.apache.gravitino.server.web.ObjectMapperProvider;
+import org.apache.gravitino.storage.relational.po.SecurableObjectPO;
+import org.apache.gravitino.storage.relational.service.RoleMetaService;
+import org.apache.gravitino.storage.relational.service.UserMetaService;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+import org.casbin.jcasbin.main.Enforcer;
+import org.casbin.jcasbin.model.Model;
+
+/** The Jcasbin implementation of GravitinoAuthorizer. */
+public class JcasbinAuthorizer implements GravitinoAuthorizer {
+
+  /** Jcasbin enforcer is used for metadata authorization. */
+  private Enforcer enforcer;
+
+  /**
+   * loadedRoles is used to cache roles that have loaded permissions. When the 
permissions of a role
+   * are updated, they should be removed from it.
+   */
+  private Set<Long> loadedRoles = ConcurrentHashMap.newKeySet();
+
+  @Override
+  public void initialize() {
+    try (InputStream modelStream =
+        JcasbinAuthorizer.class.getResourceAsStream("/jcasbin_model.conf")) {
+      Preconditions.checkNotNull(modelStream, "Jcasbin model file can not 
found.");
+      String modelData = IOUtils.toString(modelStream, StandardCharsets.UTF_8);
+      Model model = new Model();
+      model.loadModelFromText(modelData);
+      enforcer = new Enforcer(model, new GravitinoAdapter());
+    } catch (IOException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  @Override
+  public boolean authorize(
+      Principal principal,
+      String metalake,
+      MetadataObject metadataObject,
+      Privilege.Name privilege) {
+    return authorizeInternal(principal, metalake, metadataObject, 
privilege.name());
+  }
+
+  @Override
+  public boolean isOwner(Principal principal, String metalake, MetadataObject 
metadataObject) {
+    return authorizeInternal(principal, metalake, metadataObject, 
AuthConstants.OWNER);
+  }
+
+  @Override
+  public void handleRolePrivilegeChange(Long roleId) {
+    loadedRoles.remove(roleId);
+    enforcer.deleteRole(String.valueOf(roleId));
+  }
+
+  @Override
+  public void close() throws IOException {}
+
+  private boolean authorizeInternal(
+      Principal principal, String metalake, MetadataObject metadataObject, 
String privilege) {
+    String username = principal.getName();
+    return loadPrivilegeAndAuthorize(username, metalake, metadataObject, 
privilege);
+  }
+
+  private static Long getMetalakeId(String metalake) {
+    try {
+      EntityStore entityStore = GravitinoEnv.getInstance().entityStore();
+      BaseMetalake metalakeEntity =
+          entityStore.get(
+              NameIdentifierUtil.ofMetalake(metalake),
+              Entity.EntityType.METALAKE,
+              BaseMetalake.class);
+      Long metalakeId = metalakeEntity.id();
+      return metalakeId;
+    } catch (Exception e) {
+      throw new RuntimeException("Can not get metalake id by entity store", e);
+    }
+  }
+
+  private boolean authorizeByJcasbin(Long userId, MetadataObject 
metadataObject, String privilege) {
+    Long metadataId = MetadataIdConverter.doConvert(metadataObject);
+    return enforcer.enforce(
+        String.valueOf(userId),
+        String.valueOf(metadataObject.type()),
+        String.valueOf(metadataId),
+        privilege);
+  }
+
+  private boolean loadPrivilegeAndAuthorize(
+      String username, String metalake, MetadataObject metadataObject, String 
privilege) {
+    Long metalakeId = getMetalakeId(metalake);
+    Long userId = 
UserMetaService.getInstance().getUserIdByMetalakeIdAndName(metalakeId, 
username);
+    loadPrivilege(metalake, username, userId);
+    return authorizeByJcasbin(userId, metadataObject, privilege);
+  }
+
+  private void loadPrivilege(String metalake, String username, Long userId) {
+    try {
+      EntityStore entityStore = GravitinoEnv.getInstance().entityStore();
+      List<RoleEntity> entities =
+          entityStore
+              .relationOperations()
+              .listEntitiesByRelation(
+                  SupportsRelationOperations.Type.ROLE_USER_REL,
+                  NameIdentifierUtil.ofUser(metalake, username),
+                  Entity.EntityType.ROLE);
+
+      for (RoleEntity role : entities) {
+        Long roleId = role.id();
+        if (loadedRoles.contains(roleId)) {
+          continue;
+        }
+        enforcer.addRoleForUser(String.valueOf(userId), 
String.valueOf(roleId));
+        loadPolicyByRoleId(roleId);
+        loadedRoles.add(roleId);
+      }
+      // TODO load owner relationship
+    } catch (Exception e) {
+      throw new RuntimeException("Can not load privilege", e);
+    }
+  }
+
+  private void loadPolicyByRoleId(Long roleId) {
+    try {
+      List<SecurableObjectPO> securableObjects =
+          RoleMetaService.listSecurableObjectsByRoleId(roleId);
+      for (SecurableObjectPO securableObject : securableObjects) {
+        String privilegeNamesString = securableObject.getPrivilegeNames();
+        String privilegeConditionsString = 
securableObject.getPrivilegeConditions();
+        ObjectMapper objectMapper = ObjectMapperProvider.objectMapper();
+        List<String> privileges =
+            objectMapper.readValue(privilegeNamesString, new 
TypeReference<List<String>>() {});
+        List<String> privilegeConditions =
+            objectMapper.readValue(privilegeConditionsString, new 
TypeReference<List<String>>() {});
+        for (int i = 0; i < privileges.size(); i++) {
+          enforcer.addPolicy(
+              String.valueOf(securableObject.getRoleId()),
+              securableObject.getType(),
+              String.valueOf(securableObject.getMetadataObjectId()),
+              privileges.get(i).toUpperCase(),
+              privilegeConditions.get(i).toLowerCase());
+        }
+      }
+    } catch (JsonProcessingException e) {
+      throw new RuntimeException("Can not parse privilege names", e);
+    }
+  }
+}
diff --git 
a/server-common/src/test/java/org/apache/gravitino/server/authorization/expression/TestAuthorizationExpressionEvaluator.java
 
b/server-common/src/test/java/org/apache/gravitino/server/authorization/expression/TestAuthorizationExpressionEvaluator.java
index 2d2386ac76..4b25da563d 100644
--- 
a/server-common/src/test/java/org/apache/gravitino/server/authorization/expression/TestAuthorizationExpressionEvaluator.java
+++ 
b/server-common/src/test/java/org/apache/gravitino/server/authorization/expression/TestAuthorizationExpressionEvaluator.java
@@ -143,6 +143,9 @@ public class TestAuthorizationExpressionEvaluator {
           && Objects.equals("metalakeWithOwner", metadataObject.name());
     }
 
+    @Override
+    public void handleRolePrivilegeChange(Long roleId) {}
+
     @Override
     public void close() throws IOException {}
   }
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
new file mode 100644
index 0000000000..d1a44cedc3
--- /dev/null
+++ 
b/server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinAuthorizer.java
@@ -0,0 +1,257 @@
+/*
+ * 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.authorization.jcasbin;
+
+import static org.apache.gravitino.authorization.Privilege.Name.USE_CATALOG;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.google.common.collect.ImmutableList;
+import java.io.IOException;
+import java.security.Principal;
+import org.apache.gravitino.Entity;
+import org.apache.gravitino.EntityStore;
+import org.apache.gravitino.GravitinoEnv;
+import org.apache.gravitino.MetadataObject;
+import org.apache.gravitino.MetadataObjects;
+import org.apache.gravitino.NameIdentifier;
+import org.apache.gravitino.SupportsRelationOperations;
+import org.apache.gravitino.UserPrincipal;
+import org.apache.gravitino.authorization.Privilege;
+import org.apache.gravitino.meta.AuditInfo;
+import org.apache.gravitino.meta.BaseMetalake;
+import org.apache.gravitino.meta.RoleEntity;
+import org.apache.gravitino.meta.SchemaVersion;
+import org.apache.gravitino.server.authorization.MetadataIdConverter;
+import org.apache.gravitino.server.web.ObjectMapperProvider;
+import org.apache.gravitino.storage.relational.po.SecurableObjectPO;
+import org.apache.gravitino.storage.relational.service.MetalakeMetaService;
+import org.apache.gravitino.storage.relational.service.RoleMetaService;
+import org.apache.gravitino.storage.relational.service.UserMetaService;
+import org.apache.gravitino.utils.NameIdentifierUtil;
+import org.apache.gravitino.utils.PrincipalUtils;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+
+/** Test of {@link JcasbinAuthorizer} */
+public class TestJcasbinAuthorizer {
+
+  private static final Long USER_METALAKE_ID = 1L;
+
+  private static final Long USER_ID = 2L;
+
+  private static final Long ALLOW_ROLE_ID = 3L;
+
+  private static final Long DENY_ROLE_ID = 5L;
+
+  private static final Long CATALOG_ID = 4L;
+
+  private static final String USERNAME = "tester";
+
+  private static final String METALAKE = "testMetalake";
+
+  private static UserMetaService mockUserMetaService = 
mock(UserMetaService.class);
+
+  private static RoleMetaService roleMetaService = mock(RoleMetaService.class);
+
+  private static EntityStore entityStore = mock(EntityStore.class);
+
+  private static GravitinoEnv gravitinoEnv = mock(GravitinoEnv.class);
+
+  private static SupportsRelationOperations supportsRelationOperations =
+      mock(SupportsRelationOperations.class);
+
+  private static MockedStatic<PrincipalUtils> principalUtilsMockedStatic;
+
+  private static MockedStatic<UserMetaService> userMetaServiceMockedStatic;
+
+  private static MockedStatic<GravitinoEnv> gravitinoEnvMockedStatic;
+
+  private static MockedStatic<MetalakeMetaService> 
metalakeMetaServiceMockedStatic;
+
+  private static MockedStatic<MetadataIdConverter> 
metadataIdConverterMockedStatic;
+
+  private static MockedStatic<RoleMetaService> roleMetaServiceMockedStatic;
+
+  private static JcasbinAuthorizer jcasbinAuthorizer;
+
+  @BeforeAll
+  public static void setup() throws IOException {
+    jcasbinAuthorizer = new JcasbinAuthorizer();
+    jcasbinAuthorizer.initialize();
+    when(mockUserMetaService.getUserIdByMetalakeIdAndName(USER_METALAKE_ID, 
USERNAME))
+        .thenReturn(USER_ID);
+    principalUtilsMockedStatic = mockStatic(PrincipalUtils.class);
+    userMetaServiceMockedStatic = mockStatic(UserMetaService.class);
+    metalakeMetaServiceMockedStatic = mockStatic(MetalakeMetaService.class);
+    roleMetaServiceMockedStatic = mockStatic(RoleMetaService.class);
+    metadataIdConverterMockedStatic = mockStatic(MetadataIdConverter.class);
+    gravitinoEnvMockedStatic = mockStatic(GravitinoEnv.class);
+    
gravitinoEnvMockedStatic.when(GravitinoEnv::getInstance).thenReturn(gravitinoEnv);
+    
roleMetaServiceMockedStatic.when(RoleMetaService::getInstance).thenReturn(roleMetaService);
+    
userMetaServiceMockedStatic.when(UserMetaService::getInstance).thenReturn(mockUserMetaService);
+    principalUtilsMockedStatic
+        .when(PrincipalUtils::getCurrentPrincipal)
+        .thenReturn(new UserPrincipal(USERNAME));
+    metadataIdConverterMockedStatic
+        .when(() -> MetadataIdConverter.doConvert(any()))
+        .thenReturn(CATALOG_ID);
+    roleMetaServiceMockedStatic
+        .when(() -> 
RoleMetaService.listSecurableObjectsByRoleId(eq(ALLOW_ROLE_ID)))
+        .thenReturn(ImmutableList.of(getAllowSecurableObjectPO()));
+    roleMetaServiceMockedStatic
+        .when(() -> 
RoleMetaService.listSecurableObjectsByRoleId(eq(DENY_ROLE_ID)))
+        .thenReturn(ImmutableList.of(getDenySecurableObjectPO()));
+    when(gravitinoEnv.entityStore()).thenReturn(entityStore);
+    
when(entityStore.relationOperations()).thenReturn(supportsRelationOperations);
+    BaseMetalake baseMetalake =
+        BaseMetalake.builder()
+            .withId(USER_METALAKE_ID)
+            .withVersion(SchemaVersion.V_0_1)
+            .withAuditInfo(AuditInfo.EMPTY)
+            .withName(METALAKE)
+            .build();
+    when(entityStore.get(
+            eq(NameIdentifierUtil.ofMetalake(METALAKE)),
+            eq(Entity.EntityType.METALAKE),
+            eq(BaseMetalake.class)))
+        .thenReturn(baseMetalake);
+  }
+
+  @AfterAll
+  public static void stop() {
+    if (principalUtilsMockedStatic != null) {
+      principalUtilsMockedStatic.close();
+    }
+    if (userMetaServiceMockedStatic != null) {
+      userMetaServiceMockedStatic.close();
+    }
+    if (metalakeMetaServiceMockedStatic != null) {
+      metalakeMetaServiceMockedStatic.close();
+    }
+  }
+
+  @Test
+  public void testAuthorize() throws IOException {
+    Principal currentPrincipal = PrincipalUtils.getCurrentPrincipal();
+    assertFalse(doAuthorize(currentPrincipal));
+    RoleEntity allowRole = getRoleEntity(ALLOW_ROLE_ID);
+    NameIdentifier userNameIdentifier = NameIdentifierUtil.ofUser(METALAKE, 
USERNAME);
+    // Mock adds roles to users.
+    when(supportsRelationOperations.listEntitiesByRelation(
+            eq(SupportsRelationOperations.Type.ROLE_USER_REL),
+            eq(userNameIdentifier),
+            eq(Entity.EntityType.ROLE)))
+        .thenReturn(ImmutableList.of(allowRole));
+    assertTrue(doAuthorize(currentPrincipal));
+    // Test role cache.
+    // When permissions are changed but handleRolePrivilegeChange is not 
executed, the system will
+    // use the cached permissions in JCasbin, so authorize can succeed.
+    Long newRoleId = -1L;
+    RoleEntity tempNewRole = getRoleEntity(newRoleId);
+    when(supportsRelationOperations.listEntitiesByRelation(
+            eq(SupportsRelationOperations.Type.ROLE_USER_REL),
+            eq(userNameIdentifier),
+            eq(Entity.EntityType.ROLE)))
+        .thenReturn(ImmutableList.of(tempNewRole));
+    assertTrue(doAuthorize(currentPrincipal));
+    // After clearing the cache, authorize will fail
+    jcasbinAuthorizer.handleRolePrivilegeChange(ALLOW_ROLE_ID);
+    assertFalse(doAuthorize(currentPrincipal));
+    // When the user is re-assigned the correct role, the authorization will 
succeed.
+    when(supportsRelationOperations.listEntitiesByRelation(
+            eq(SupportsRelationOperations.Type.ROLE_USER_REL),
+            eq(userNameIdentifier),
+            eq(Entity.EntityType.ROLE)))
+        .thenReturn(ImmutableList.of(allowRole));
+    assertTrue(doAuthorize(currentPrincipal));
+    // Test deny
+    RoleEntity denyRole = getRoleEntity(DENY_ROLE_ID);
+    when(supportsRelationOperations.listEntitiesByRelation(
+            eq(SupportsRelationOperations.Type.ROLE_USER_REL),
+            eq(userNameIdentifier),
+            eq(Entity.EntityType.ROLE)))
+        .thenReturn(ImmutableList.of(allowRole, denyRole));
+    assertFalse(doAuthorize(currentPrincipal));
+  }
+
+  private boolean doAuthorize(Principal currentPrincipal) {
+    return jcasbinAuthorizer.authorize(
+        currentPrincipal,
+        "testMetalake",
+        MetadataObjects.of(null, "testCatalog", MetadataObject.Type.CATALOG),
+        USE_CATALOG);
+  }
+
+  private static RoleEntity getRoleEntity(Long roleId) {
+    return RoleEntity.builder()
+        .withId(roleId)
+        .withName("roleName")
+        .withAuditInfo(AuditInfo.EMPTY)
+        .build();
+  }
+
+  private static SecurableObjectPO getAllowSecurableObjectPO() {
+    ImmutableList<Privilege.Name> privileges = ImmutableList.of(USE_CATALOG);
+    ImmutableList<String> conditions = ImmutableList.of("ALLOW");
+    ObjectMapper objectMapper = ObjectMapperProvider.objectMapper();
+    try {
+      return SecurableObjectPO.builder()
+          .withType(String.valueOf(MetadataObject.Type.CATALOG))
+          .withMetadataObjectId(CATALOG_ID)
+          .withRoleId(ALLOW_ROLE_ID)
+          .withPrivilegeNames(objectMapper.writeValueAsString(privileges))
+          .withPrivilegeConditions(objectMapper.writeValueAsString(conditions))
+          .withDeletedAt(0L)
+          .withCurrentVersion(1L)
+          .withLastVersion(1L)
+          .build();
+    } catch (JsonProcessingException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  private static SecurableObjectPO getDenySecurableObjectPO() {
+    ImmutableList<Privilege.Name> privileges = ImmutableList.of(USE_CATALOG);
+    ImmutableList<String> conditions = ImmutableList.of("DENY");
+    ObjectMapper objectMapper = ObjectMapperProvider.objectMapper();
+    try {
+      return SecurableObjectPO.builder()
+          .withType(String.valueOf(MetadataObject.Type.CATALOG))
+          .withMetadataObjectId(CATALOG_ID)
+          .withRoleId(DENY_ROLE_ID)
+          .withPrivilegeNames(objectMapper.writeValueAsString(privileges))
+          .withPrivilegeConditions(objectMapper.writeValueAsString(conditions))
+          .withDeletedAt(0L)
+          .withCurrentVersion(1L)
+          .withLastVersion(1L)
+          .build();
+    } catch (JsonProcessingException e) {
+      throw new RuntimeException(e);
+    }
+  }
+}

Reply via email to