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 f4482cd27e [#9770] improvement(authz): Convert Jcasbin internal map to 
the cache (#9812)
f4482cd27e is described below

commit f4482cd27eefa043f1bb00216def952cbe0ae79a
Author: roryqi <[email protected]>
AuthorDate: Fri Jan 30 14:34:07 2026 +0800

    [#9770] improvement(authz): Convert Jcasbin internal map to the cache 
(#9812)
    
    ### What changes were proposed in this pull request?
    
    Convert Jcasbin internal map to the cache
    
    ### Why are the changes needed?
    
    Fix: #9770
    
    ### Does this PR introduce _any_ user-facing change?
    
    Add documents.
    
    ### How was this patch tested?
    
    Add UTs.
---
 .../main/java/org/apache/gravitino/Configs.java    |  27 ++++
 docs/security/access-control.md                    |  30 ++++-
 server-common/build.gradle.kts                     |   1 +
 .../authorization/jcasbin/JcasbinAuthorizer.java   |  79 ++++++++----
 .../jcasbin/TestJcasbinAuthorizer.java             | 142 +++++++++++++++++++--
 5 files changed, 244 insertions(+), 35 deletions(-)

diff --git a/core/src/main/java/org/apache/gravitino/Configs.java 
b/core/src/main/java/org/apache/gravitino/Configs.java
index 9f04aa9318..bdf604770b 100644
--- a/core/src/main/java/org/apache/gravitino/Configs.java
+++ b/core/src/main/java/org/apache/gravitino/Configs.java
@@ -302,6 +302,33 @@ public class Configs {
           .intConf()
           .createWithDefault(DEFAULT_GRAVITINO_AUTHORIZATION_THREAD_POOL_SIZE);
 
+  public static final long 
DEFAULT_GRAVITINO_AUTHORIZATION_CACHE_EXPIRATION_SECS = 3600L;
+
+  public static final ConfigEntry<Long> 
GRAVITINO_AUTHORIZATION_CACHE_EXPIRATION_SECS =
+      new ConfigBuilder("gravitino.authorization.jcasbin.cacheExpirationSecs")
+          .doc("The expiration time in seconds for authorization cache 
entries")
+          .version(ConfigConstants.VERSION_1_2_0)
+          .longConf()
+          
.createWithDefault(DEFAULT_GRAVITINO_AUTHORIZATION_CACHE_EXPIRATION_SECS);
+
+  public static final long DEFAULT_GRAVITINO_AUTHORIZATION_ROLE_CACHE_SIZE = 
10000L;
+
+  public static final ConfigEntry<Long> 
GRAVITINO_AUTHORIZATION_ROLE_CACHE_SIZE =
+      new ConfigBuilder("gravitino.authorization.jcasbin.roleCacheSize")
+          .doc("The maximum size of the role cache for authorization")
+          .version(ConfigConstants.VERSION_1_2_0)
+          .longConf()
+          .createWithDefault(DEFAULT_GRAVITINO_AUTHORIZATION_ROLE_CACHE_SIZE);
+
+  public static final long DEFAULT_GRAVITINO_AUTHORIZATION_OWNER_CACHE_SIZE = 
100000L;
+
+  public static final ConfigEntry<Long> 
GRAVITINO_AUTHORIZATION_OWNER_CACHE_SIZE =
+      new ConfigBuilder("gravitino.authorization.jcasbin.ownerCacheSize")
+          .doc("The maximum size of the owner cache for authorization")
+          .version(ConfigConstants.VERSION_1_2_0)
+          .longConf()
+          .createWithDefault(DEFAULT_GRAVITINO_AUTHORIZATION_OWNER_CACHE_SIZE);
+
   public static final ConfigEntry<List<String>> SERVICE_ADMINS =
       new ConfigBuilder("gravitino.authorization.serviceAdmins")
           .doc("The admins of Gravitino service")
diff --git a/docs/security/access-control.md b/docs/security/access-control.md
index af5d26e6ce..55d81177ba 100644
--- a/docs/security/access-control.md
+++ b/docs/security/access-control.md
@@ -440,10 +440,27 @@ This model ensures that denials cannot be circumvented by 
grants at lower levels
 
 To enable access control in Gravitino, configure the following settings in 
your server configuration file:
 
-| Configuration Item                      | Description                        
                                       | Default Value | Required               
                     | Since Version |
-|-----------------------------------------|---------------------------------------------------------------------------|---------------|---------------------------------------------|---------------|
-| `gravitino.authorization.enable`        | Enable or disable authorization in 
Gravitino                              | `false`       | No                     
                     | 0.5.0         |
-| `gravitino.authorization.serviceAdmins` | Comma-separated list of service 
administrator usernames                   | (none)        | Yes (when 
authorization is enabled)         | 0.5.0         |
+| Configuration Item                                      | Description        
                                                       | Default Value | 
Required                                    | Since Version |
+|---------------------------------------------------------|---------------------------------------------------------------------------|---------------|---------------------------------------------|---------------|
+| `gravitino.authorization.enable`                        | Enable or disable 
authorization in Gravitino                              | `false`       | No    
                                      | 0.5.0         |
+| `gravitino.authorization.serviceAdmins`                 | Comma-separated 
list of service administrator usernames                   | (none)        | Yes 
(when authorization is enabled)         | 0.5.0         |
+| `gravitino.authorization.jcasbin.cacheExpirationSecs`   | The expiration 
time in seconds for authorization cache entries            | `3600`        | No 
                                         | 1.2.0         |
+| `gravitino.authorization.jcasbin.roleCacheSize`         | The maximum size 
of the role cache for authorization                      | `10000`       | No   
                                       | 1.2.0         |
+| `gravitino.authorization.jcasbin.ownerCacheSize`        | The maximum size 
of the owner cache for authorization                     | `100000`      | No   
                                       | 1.2.0         |
+
+### Authorization Cache
+
+Gravitino uses Caffeine caches to improve authorization performance by caching 
role and owner information. The cache configuration options allow you to tune 
the cache behavior:
+
+- **`cacheExpirationSecs`**: Controls how long cache entries remain valid. 
After this time, entries are automatically evicted and reloaded from the 
backend on the next access. Lower values provide more up-to-date authorization 
decisions but may increase load on the backend.
+
+- **`roleCacheSize`**: Controls the maximum number of role entries that can be 
cached. When the cache reaches this size, the least recently used entries are 
evicted.
+
+- **`ownerCacheSize`**: Controls the maximum number of owner relationship 
entries that can be cached. This cache maps metadata object IDs to their owner 
IDs.
+
+:::info
+When role privileges or ownership are changed through the Gravitino API, the 
corresponding cache entries are automatically invalidated to ensure 
authorization decisions reflect the latest state.
+:::
 
 ### Important Notes
 
@@ -462,6 +479,11 @@ gravitino.authorization.enable = true
 
 # Define service administrators
 gravitino.authorization.serviceAdmins = admin1,admin2
+
+# Optional: Configure authorization cache (default values shown)
+gravitino.authorization.jcasbin.cacheExpirationSecs = 3600
+gravitino.authorization.jcasbin.roleCacheSize = 10000
+gravitino.authorization.jcasbin.ownerCacheSize = 100000
 ```
 
 ## Migration Guide
diff --git a/server-common/build.gradle.kts b/server-common/build.gradle.kts
index 6bd6a31f45..fdcdf2f931 100644
--- a/server-common/build.gradle.kts
+++ b/server-common/build.gradle.kts
@@ -40,6 +40,7 @@ dependencies {
   implementation(libs.bundles.kerby)
   implementation(libs.bundles.log4j)
   implementation(libs.bundles.metrics)
+  implementation(libs.caffeine)
   implementation(libs.commons.lang3)
   implementation(libs.guava)
   implementation(libs.jackson.datatype.jdk8)
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 2b569933da..f957bd8b96 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
@@ -17,6 +17,8 @@
 
 package org.apache.gravitino.server.authorization.jcasbin;
 
+import com.github.benmanes.caffeine.cache.Cache;
+import com.github.benmanes.caffeine.cache.Caffeine;
 import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
@@ -26,14 +28,13 @@ import java.security.Principal;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
-import java.util.Set;
+import java.util.Optional;
 import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.Executor;
 import java.util.concurrent.Executors;
 import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
 import org.apache.commons.io.IOUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.Configs;
@@ -84,14 +85,47 @@ public class JcasbinAuthorizer implements 
GravitinoAuthorizer {
    * 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();
+  private Cache<Long, Boolean> loadedRoles;
 
-  private Map<Long, Long> ownerRel = new ConcurrentHashMap<>();
+  private Cache<Long, Optional<Long>> ownerRel;
 
   private Executor executor = null;
 
   @Override
   public void initialize() {
+    long cacheExpirationSecs =
+        GravitinoEnv.getInstance()
+            .config()
+            .get(Configs.GRAVITINO_AUTHORIZATION_CACHE_EXPIRATION_SECS);
+    long roleCacheSize =
+        
GravitinoEnv.getInstance().config().get(Configs.GRAVITINO_AUTHORIZATION_ROLE_CACHE_SIZE);
+    long ownerCacheSize =
+        
GravitinoEnv.getInstance().config().get(Configs.GRAVITINO_AUTHORIZATION_OWNER_CACHE_SIZE);
+
+    // Initialize enforcers before the caches that reference them in removal 
listeners
+    allowEnforcer = new SyncedEnforcer(getModel("/jcasbin_model.conf"), new 
GravitinoAdapter());
+    allowInternalAuthorizer = new InternalAuthorizer(allowEnforcer);
+    denyEnforcer = new SyncedEnforcer(getModel("/jcasbin_model.conf"), new 
GravitinoAdapter());
+    denyInternalAuthorizer = new InternalAuthorizer(denyEnforcer);
+
+    loadedRoles =
+        Caffeine.newBuilder()
+            .expireAfterAccess(cacheExpirationSecs, TimeUnit.SECONDS)
+            .maximumSize(roleCacheSize)
+            .executor(Runnable::run)
+            .removalListener(
+                (roleId, value, cause) -> {
+                  if (roleId != null) {
+                    allowEnforcer.deleteRole(String.valueOf(roleId));
+                    denyEnforcer.deleteRole(String.valueOf(roleId));
+                  }
+                })
+            .build();
+    ownerRel =
+        Caffeine.newBuilder()
+            .expireAfterAccess(cacheExpirationSecs, TimeUnit.SECONDS)
+            .maximumSize(ownerCacheSize)
+            .build();
     executor =
         Executors.newFixedThreadPool(
             GravitinoEnv.getInstance()
@@ -102,10 +136,6 @@ public class JcasbinAuthorizer implements 
GravitinoAuthorizer {
               thread.setName("GravitinoAuthorizer-ThreadPool-" + 
thread.getId());
               return thread;
             });
-    allowEnforcer = new SyncedEnforcer(getModel("/jcasbin_model.conf"), new 
GravitinoAdapter());
-    allowInternalAuthorizer = new InternalAuthorizer(allowEnforcer);
-    denyEnforcer = new SyncedEnforcer(getModel("/jcasbin_model.conf"), new 
GravitinoAdapter());
-    denyInternalAuthorizer = new InternalAuthorizer(denyEnforcer);
   }
 
   private Model getModel(String modelFilePath) {
@@ -196,7 +226,7 @@ public class JcasbinAuthorizer implements 
GravitinoAuthorizer {
       UserEntity userEntity = getUserEntity(principal.getName(), metalake);
       userId = userEntity.id();
       metadataId = MetadataIdConverter.getID(metadataObject, metalake);
-      result = Objects.equals(userId, ownerRel.get(metadataId));
+      result = Objects.equals(Optional.of(userId), 
ownerRel.getIfPresent(metadataId));
     } catch (Exception e) {
       LOG.debug("Can not get entity id", e);
       result = false;
@@ -353,9 +383,7 @@ public class JcasbinAuthorizer implements 
GravitinoAuthorizer {
 
   @Override
   public void handleRolePrivilegeChange(Long roleId) {
-    loadedRoles.remove(roleId);
-    allowEnforcer.deleteRole(String.valueOf(roleId));
-    denyEnforcer.deleteRole(String.valueOf(roleId));
+    loadedRoles.invalidate(roleId);
   }
 
   @Override
@@ -363,7 +391,7 @@ public class JcasbinAuthorizer implements 
GravitinoAuthorizer {
       String metalake, Long oldOwnerId, NameIdentifier nameIdentifier, 
Entity.EntityType type) {
     MetadataObject metadataObject = 
NameIdentifierUtil.toMetadataObject(nameIdentifier, type);
     Long metadataId = MetadataIdConverter.getID(metadataObject, metalake);
-    ownerRel.remove(metadataId);
+    ownerRel.invalidate(metadataId);
   }
 
   @Override
@@ -417,7 +445,8 @@ public class JcasbinAuthorizer implements 
GravitinoAuthorizer {
     private boolean authorizeByJcasbin(
         Long userId, MetadataObject metadataObject, Long metadataId, String 
privilege) {
       if (AuthConstants.OWNER.equals(privilege)) {
-        return Objects.equals(userId, ownerRel.get(metadataId));
+        Optional<Long> owner = ownerRel.getIfPresent(metadataId);
+        return Objects.equals(Optional.of(userId), owner);
       }
       return enforcer.enforce(
           String.valueOf(userId),
@@ -457,7 +486,7 @@ public class JcasbinAuthorizer implements 
GravitinoAuthorizer {
               Long roleId = role.id();
               allowEnforcer.addRoleForUser(String.valueOf(userId), 
String.valueOf(roleId));
               denyEnforcer.addRoleForUser(String.valueOf(userId), 
String.valueOf(roleId));
-              if (loadedRoles.contains(roleId)) {
+              if (loadedRoles.getIfPresent(roleId) != null) {
                 continue;
               }
               CompletableFuture<Void> loadRoleFuture =
@@ -476,7 +505,7 @@ public class JcasbinAuthorizer implements 
GravitinoAuthorizer {
                       .thenAcceptAsync(
                           roleEntity -> {
                             loadPolicyByRoleEntity(roleEntity);
-                            loadedRoles.add(roleId);
+                            loadedRoles.put(roleId, true);
                           },
                           executor);
               loadRoleFutures.add(loadRoleFuture);
@@ -489,8 +518,8 @@ public class JcasbinAuthorizer implements 
GravitinoAuthorizer {
   }
 
   private void loadOwnerPolicy(String metalake, MetadataObject metadataObject, 
Long metadataId) {
-    if (ownerRel.containsKey(metadataId)) {
-      LOG.debug("Metadata {} OWNER has bean loaded.", metadataId);
+    if (ownerRel.getIfPresent(metadataId) != null) {
+      LOG.debug("Metadata {} OWNER has been loaded.", metadataId);
       return;
     }
     try {
@@ -503,10 +532,14 @@ public class JcasbinAuthorizer implements 
GravitinoAuthorizer {
                   SupportsRelationOperations.Type.OWNER_REL,
                   entityIdent,
                   Entity.EntityType.valueOf(metadataObject.type().name()));
-      for (Entity ownerEntity : owners) {
-        if (ownerEntity instanceof UserEntity) {
-          UserEntity user = (UserEntity) ownerEntity;
-          ownerRel.put(metadataId, user.id());
+      if (owners.isEmpty()) {
+        ownerRel.put(metadataId, Optional.empty());
+      } else {
+        for (Entity ownerEntity : owners) {
+          if (ownerEntity instanceof UserEntity) {
+            UserEntity user = (UserEntity) ownerEntity;
+            ownerRel.put(metadataId, Optional.of(user.id()));
+          }
         }
       }
     } catch (IOException e) {
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 3533dfab53..4388ef0952 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
@@ -19,21 +19,26 @@ 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.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
 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.doReturn;
 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.github.benmanes.caffeine.cache.Cache;
 import com.google.common.collect.ImmutableList;
 import java.io.IOException;
 import java.lang.reflect.Field;
 import java.security.Principal;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 import java.util.concurrent.Executor;
 import java.util.stream.Collectors;
 import org.apache.commons.lang3.reflect.FieldUtils;
@@ -62,6 +67,7 @@ import 
org.apache.gravitino.storage.relational.utils.POConverters;
 import org.apache.gravitino.utils.NameIdentifierUtil;
 import org.apache.gravitino.utils.NamespaceUtil;
 import org.apache.gravitino.utils.PrincipalUtils;
+import org.casbin.jcasbin.main.Enforcer;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
@@ -160,7 +166,7 @@ public class TestJcasbinAuthorizer {
   }
 
   @Test
-  public void testAuthorize() throws IOException {
+  public void testAuthorize() throws Exception {
     makeCompletableFutureUseCurrentThread(jcasbinAuthorizer);
     Principal currentPrincipal = PrincipalUtils.getCurrentPrincipal();
     assertFalse(doAuthorize(currentPrincipal));
@@ -228,21 +234,25 @@ public class TestJcasbinAuthorizer {
   }
 
   @Test
-  public void testAuthorizeByOwner() throws IOException {
+  public void testAuthorizeByOwner() throws Exception {
     Principal currentPrincipal = PrincipalUtils.getCurrentPrincipal();
     assertFalse(doAuthorizeOwner(currentPrincipal));
     NameIdentifier catalogIdent = NameIdentifierUtil.ofCatalog(METALAKE, 
"testCatalog");
-    when(supportsRelationOperations.listEntitiesByRelation(
+    List<UserEntity> owners = ImmutableList.of(getUserEntity());
+    doReturn(owners)
+        .when(supportsRelationOperations)
+        .listEntitiesByRelation(
             eq(SupportsRelationOperations.Type.OWNER_REL),
             eq(catalogIdent),
-            eq(Entity.EntityType.CATALOG)))
-        .thenReturn(ImmutableList.of(getUserEntity()));
+            eq(Entity.EntityType.CATALOG));
+    getOwnerRelCache(jcasbinAuthorizer).invalidateAll();
     assertTrue(doAuthorizeOwner(currentPrincipal));
-    when(supportsRelationOperations.listEntitiesByRelation(
+    doReturn(new ArrayList<>())
+        .when(supportsRelationOperations)
+        .listEntitiesByRelation(
             eq(SupportsRelationOperations.Type.OWNER_REL),
             eq(catalogIdent),
-            eq(Entity.EntityType.CATALOG)))
-        .thenReturn(new ArrayList<>());
+            eq(Entity.EntityType.CATALOG));
     jcasbinAuthorizer.handleMetadataOwnerChange(
         METALAKE, USER_ID, catalogIdent, Entity.EntityType.CATALOG);
     assertFalse(doAuthorizeOwner(currentPrincipal));
@@ -347,4 +357,120 @@ public class TestJcasbinAuthorizer {
       throw new RuntimeException(e);
     }
   }
+
+  @Test
+  public void testRoleCacheInvalidation() throws Exception {
+    makeCompletableFutureUseCurrentThread(jcasbinAuthorizer);
+
+    // Get the loadedRoles cache via reflection
+    Cache<Long, Boolean> loadedRoles = getLoadedRolesCache(jcasbinAuthorizer);
+
+    // Manually add a role to the cache
+    Long testRoleId = 100L;
+    loadedRoles.put(testRoleId, true);
+
+    // Verify it's in the cache
+    assertNotNull(loadedRoles.getIfPresent(testRoleId));
+
+    // Call handleRolePrivilegeChange which should invalidate the cache entry
+    jcasbinAuthorizer.handleRolePrivilegeChange(testRoleId);
+
+    // Verify it's removed from the cache
+    assertNull(loadedRoles.getIfPresent(testRoleId));
+  }
+
+  @Test
+  public void testOwnerCacheInvalidation() throws Exception {
+    // Get the ownerRel cache via reflection
+    Cache<Long, Optional<Long>> ownerRel = getOwnerRelCache(jcasbinAuthorizer);
+
+    // Manually add an owner relation to the cache
+    ownerRel.put(CATALOG_ID, Optional.of(USER_ID));
+
+    // Verify it's in the cache
+    assertNotNull(ownerRel.getIfPresent(CATALOG_ID));
+
+    // Create a mock NameIdentifier for the metadata object
+    NameIdentifier catalogIdent = NameIdentifierUtil.ofCatalog(METALAKE, 
"testCatalog");
+
+    // Call handleMetadataOwnerChange which should invalidate the cache entry
+    jcasbinAuthorizer.handleMetadataOwnerChange(
+        METALAKE, USER_ID, catalogIdent, Entity.EntityType.CATALOG);
+
+    // Verify it's removed from the cache
+    assertNull(ownerRel.getIfPresent(CATALOG_ID));
+  }
+
+  @Test
+  public void testRoleCacheSynchronousRemovalListenerDeletesPolicy() throws 
Exception {
+    makeCompletableFutureUseCurrentThread(jcasbinAuthorizer);
+
+    // Get the enforcers via reflection
+    Enforcer allowEnforcer = getAllowEnforcer(jcasbinAuthorizer);
+    Enforcer denyEnforcer = getDenyEnforcer(jcasbinAuthorizer);
+
+    // Get the loadedRoles cache
+    Cache<Long, Boolean> loadedRoles = getLoadedRolesCache(jcasbinAuthorizer);
+
+    // Add a role and its policy to the enforcer
+    Long testRoleId = 300L;
+    String roleIdStr = String.valueOf(testRoleId);
+
+    // Add a policy for this role
+    allowEnforcer.addPolicy(roleIdStr, "CATALOG", "999", "USE_CATALOG", 
"allow");
+    denyEnforcer.addPolicy(roleIdStr, "CATALOG", "999", "USE_CATALOG", 
"allow");
+
+    // Add role to cache
+    loadedRoles.put(testRoleId, true);
+
+    // Verify role exists in enforcer (has policy)
+    assertTrue(allowEnforcer.hasPolicy(roleIdStr, "CATALOG", "999", 
"USE_CATALOG", "allow"));
+    assertTrue(denyEnforcer.hasPolicy(roleIdStr, "CATALOG", "999", 
"USE_CATALOG", "allow"));
+
+    // Invalidate the cache entry - this triggers the synchronous removal 
listener
+    // (using executor(Runnable::run) to ensure synchronous execution)
+    loadedRoles.invalidate(testRoleId);
+
+    // Verify the role's policies have been deleted from enforcers 
(synchronous, no need to wait)
+    assertFalse(allowEnforcer.hasPolicy(roleIdStr, "CATALOG", "999", 
"USE_CATALOG", "allow"));
+    assertFalse(denyEnforcer.hasPolicy(roleIdStr, "CATALOG", "999", 
"USE_CATALOG", "allow"));
+  }
+
+  @Test
+  public void testCacheInitialization() throws Exception {
+    // Verify that caches are initialized
+    Cache<Long, Boolean> loadedRoles = getLoadedRolesCache(jcasbinAuthorizer);
+    Cache<Long, Optional<Long>> ownerRel = getOwnerRelCache(jcasbinAuthorizer);
+
+    assertNotNull(loadedRoles, "loadedRoles cache should be initialized");
+    assertNotNull(ownerRel, "ownerRel cache should be initialized");
+  }
+
+  @SuppressWarnings("unchecked")
+  private static Cache<Long, Boolean> getLoadedRolesCache(JcasbinAuthorizer 
authorizer)
+      throws Exception {
+    Field field = JcasbinAuthorizer.class.getDeclaredField("loadedRoles");
+    field.setAccessible(true);
+    return (Cache<Long, Boolean>) field.get(authorizer);
+  }
+
+  @SuppressWarnings("unchecked")
+  private static Cache<Long, Optional<Long>> 
getOwnerRelCache(JcasbinAuthorizer authorizer)
+      throws Exception {
+    Field field = JcasbinAuthorizer.class.getDeclaredField("ownerRel");
+    field.setAccessible(true);
+    return (Cache<Long, Optional<Long>>) field.get(authorizer);
+  }
+
+  private static Enforcer getAllowEnforcer(JcasbinAuthorizer authorizer) 
throws Exception {
+    Field field = JcasbinAuthorizer.class.getDeclaredField("allowEnforcer");
+    field.setAccessible(true);
+    return (Enforcer) field.get(authorizer);
+  }
+
+  private static Enforcer getDenyEnforcer(JcasbinAuthorizer authorizer) throws 
Exception {
+    Field field = JcasbinAuthorizer.class.getDeclaredField("denyEnforcer");
+    field.setAccessible(true);
+    return (Enforcer) field.get(authorizer);
+  }
 }

Reply via email to