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

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


The following commit(s) were added to refs/heads/main by this push:
     new a33fa0d1d7 [#12169] fix(auth): clear only role policies on cache 
eviction (#12172)
a33fa0d1d7 is described below

commit a33fa0d1d7fd5ee4e1010c14008662e901eef401
Author: jarred0214 <[email protected]>
AuthorDate: Fri Jul 24 16:44:50 2026 +0800

    [#12169] fix(auth): clear only role policies on cache eviction (#12172)
    
    ### What changes were proposed in this pull request?
    
    This PR updates `JcasbinLoadedRolesCache` to clear only the loaded role
    policies when a role cache entry is evicted.
    
    Instead of calling `deleteRole(roleId)`, the cache eviction now
    delegates to `JcasbinAuthorizer#clearRolePolicies`, which removes only
    `p(roleId, ...)` policies from the allow and deny enforcers.
    
    The test is also updated to verify that cache invalidation removes the
    role policies but preserves the user-role bindings.
    
    ### Why are the changes needed?
    
    `loadedRoles` tracks whether a role's permission policies have been
    loaded into jCasbin. It should only own role policy cleanup.
    
    Calling `deleteRole(roleId)` also removes `g(user/group, roleId)` role
    bindings. This can cause a transient authorization failure when a
    request has already bound user roles, then the loaded role cache entry
    expires and removes the binding before enforcement. In that case,
    jCasbin may return `Hit Policy: []`, while a later retry succeeds after
    the binding is added again.
    
    Using `clearRolePolicies` keeps role policy refresh behavior while
    avoiding accidental removal of user/group role bindings.
    
    Fix: #12169
    
    ### Does this PR introduce _any_ user-facing change?
    
    No.
    
    ### How was this patch tested?
    
    ```bash
    ./gradlew spotlessApply :server-common:test --tests 
org.apache.gravitino.server.authorization.jcasbin.TestJcasbinAuthorizer 
-PskipITs -PskipDockerTests=false
---
 .../server/authorization/jcasbin/JcasbinAuthorizer.java    |  2 +-
 .../authorization/jcasbin/JcasbinLoadedRolesCache.java     | 14 +++++++-------
 .../authorization/jcasbin/TestJcasbinAuthorizer.java       | 14 +++++++++++---
 3 files changed, 19 insertions(+), 11 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 ef355dea0c..2535e8e336 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
@@ -200,7 +200,7 @@ public class JcasbinAuthorizer implements 
GravitinoAuthorizer {
 
     // loadedRoles: roleId -> updated_at.
     // When evicted, we must clean up the corresponding JCasbin policies.
-    loadedRoles = new JcasbinLoadedRolesCache(ttlMs, roleCacheSize, 
allowEnforcer, denyEnforcer);
+    loadedRoles = new JcasbinLoadedRolesCache(ttlMs, roleCacheSize, 
this::clearRolePolicies);
 
     userRoleCache = new CaffeineGravitinoCache<>(ttlMs, roleCacheSize);
     groupRoleCache = new CaffeineGravitinoCache<>(ttlMs, roleCacheSize);
diff --git 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinLoadedRolesCache.java
 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinLoadedRolesCache.java
index 6799fe7dcc..75997b95a1 100644
--- 
a/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinLoadedRolesCache.java
+++ 
b/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinLoadedRolesCache.java
@@ -23,8 +23,8 @@ import com.github.benmanes.caffeine.cache.Caffeine;
 import com.github.benmanes.caffeine.cache.RemovalCause;
 import java.util.Optional;
 import java.util.concurrent.TimeUnit;
+import java.util.function.LongConsumer;
 import org.apache.gravitino.cache.GravitinoCache;
-import org.casbin.jcasbin.main.Enforcer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -33,9 +33,10 @@ import org.slf4j.LoggerFactory;
  * JCasbin policies from both enforcers when a key is evicted (by TTL, size, 
or explicit
  * invalidate).
  *
- * <p>Uses a raw Caffeine cache internally so it can attach a removal listener 
with {@code
- * executor(Runnable::run)} — eviction and policy cleanup must happen on the 
same thread, so the
- * {@link JcasbinAuthorizer} never sees a role bound in the enforcer without a 
backing policy.
+ * <p>This cache owns role permission policies only. Therefore, eviction must 
clear only {@code
+ * p(roleId, ...)} policies and must not delete the role itself, because 
JCasbin's {@code
+ * deleteRole(roleId)} also removes {@code g(user/group, roleId)} bindings 
that are managed
+ * separately by {@link JcasbinAuthorizer}.
  */
 class JcasbinLoadedRolesCache implements GravitinoCache<Long, Long> {
 
@@ -43,7 +44,7 @@ class JcasbinLoadedRolesCache implements GravitinoCache<Long, 
Long> {
 
   private final Cache<Long, Long> cache;
 
-  JcasbinLoadedRolesCache(long ttlMs, long maxSize, Enforcer allowEnforcer, 
Enforcer denyEnforcer) {
+  JcasbinLoadedRolesCache(long ttlMs, long maxSize, LongConsumer 
rolePolicyCleaner) {
     this.cache =
         Caffeine.newBuilder()
             .expireAfterAccess(ttlMs, TimeUnit.MILLISECONDS)
@@ -56,8 +57,7 @@ class JcasbinLoadedRolesCache implements GravitinoCache<Long, 
Long> {
                       roleId,
                       cause);
                   if (roleId != null && cause != RemovalCause.REPLACED) {
-                    allowEnforcer.deleteRole(String.valueOf(roleId));
-                    denyEnforcer.deleteRole(String.valueOf(roleId));
+                    rolePolicyCleaner.accept(roleId);
                   }
                 })
             .build();
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 7eb04c08cb..624c547908 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
@@ -1726,25 +1726,33 @@ public class TestJcasbinAuthorizer {
     // Add a role and its policy to the enforcer
     Long testRoleId = 300L;
     String roleIdStr = String.valueOf(testRoleId);
+    String userIdStr = String.valueOf(USER_ID);
 
-    // Add a policy for this role
+    // Add a policy and a user-role binding for this role.
+    allowEnforcer.addRoleForUser(userIdStr, roleIdStr);
+    denyEnforcer.addRoleForUser(userIdStr, roleIdStr);
     allowEnforcer.addPolicy(roleIdStr, "CATALOG", "999", "USE_CATALOG", 
"allow");
     denyEnforcer.addPolicy(roleIdStr, "CATALOG", "999", "USE_CATALOG", 
"allow");
 
     // Add role to cache
     loadedRoles.put(testRoleId, System.currentTimeMillis());
 
-    // Verify role exists in enforcer (has policy)
+    // Verify role exists in enforcer (has policy and grouping).
     assertTrue(allowEnforcer.hasPolicy(roleIdStr, "CATALOG", "999", 
"USE_CATALOG", "allow"));
     assertTrue(denyEnforcer.hasPolicy(roleIdStr, "CATALOG", "999", 
"USE_CATALOG", "allow"));
+    assertTrue(allowEnforcer.getRolesForUser(userIdStr).contains(roleIdStr));
+    assertTrue(denyEnforcer.getRolesForUser(userIdStr).contains(roleIdStr));
 
     // 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)
+    // Verify the role's policies have been deleted from enforcers 
(synchronous, no need to wait),
+    // but user-role bindings are preserved because loadedRoles owns role 
policies only.
     assertFalse(allowEnforcer.hasPolicy(roleIdStr, "CATALOG", "999", 
"USE_CATALOG", "allow"));
     assertFalse(denyEnforcer.hasPolicy(roleIdStr, "CATALOG", "999", 
"USE_CATALOG", "allow"));
+    assertTrue(allowEnforcer.getRolesForUser(userIdStr).contains(roleIdStr));
+    assertTrue(denyEnforcer.getRolesForUser(userIdStr).contains(roleIdStr));
   }
 
   @Test

Reply via email to