roryqi commented on code in PR #10868:
URL: https://github.com/apache/gravitino/pull/10868#discussion_r3186691302


##########
server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinAuthorizer.java:
##########
@@ -504,40 +516,106 @@ private void loadRolePrivilege(
                         Entity.EntityType.USER);
             List<CompletableFuture<Void>> loadRoleFutures = new ArrayList<>();
             for (RoleEntity role : entities) {
-              Long roleId = role.id();
-              allowEnforcer.addRoleForUser(String.valueOf(userId), 
String.valueOf(roleId));
-              denyEnforcer.addRoleForUser(String.valueOf(userId), 
String.valueOf(roleId));
-              if (loadedRoles.getIfPresent(roleId) != null) {
+              addRoleForUserAndLoadPolicies(
+                  userId, metalake, role.id(), role.name(), loadRoleFutures, 
entityStore);
+            }
+
+            // Load roles inherited from the user's groups.
+            for (GroupEntity groupEntity : resolveCurrentUserGroups(metalake, 
entityStore)) {
+              List<Long> roleIds = groupEntity.roleIds();
+              List<String> roleNames = groupEntity.roleNames();
+              if (roleIds == null || roleNames == null) {
+                continue;
+              }
+              if (roleIds.size() != roleNames.size()) {
+                LOG.warn(
+                    "Group {} has mismatched roleIds ({}) and roleNames ({}) 
-- skipping",
+                    groupEntity.name(),
+                    roleIds.size(),
+                    roleNames.size());
                 continue;
               }
-              CompletableFuture<Void> loadRoleFuture =
-                  CompletableFuture.supplyAsync(
-                          () -> {
-                            try {
-                              return entityStore.get(
-                                  NameIdentifierUtil.ofRole(metalake, 
role.name()),
-                                  Entity.EntityType.ROLE,
-                                  RoleEntity.class);
-                            } catch (Exception e) {
-                              throw new RuntimeException("Failed to load role: 
" + role.name(), e);
-                            }
-                          },
-                          executor)
-                      .thenAcceptAsync(
-                          roleEntity -> {
-                            loadPolicyByRoleEntity(roleEntity);
-                            loadedRoles.put(roleId, true);
-                          },
-                          executor);
-              loadRoleFutures.add(loadRoleFuture);
+              for (int i = 0; i < roleIds.size(); i++) {
+                addRoleForUserAndLoadPolicies(
+                    userId,
+                    metalake,
+                    roleIds.get(i),
+                    roleNames.get(i),
+                    loadRoleFutures,
+                    entityStore);
+              }
             }
+
             CompletableFuture.allOf(loadRoleFutures.toArray(new 
CompletableFuture[0])).join();
           } catch (IOException e) {
             throw new RuntimeException(e);
           }
         });
   }
 
+  /**
+   * Resolves GroupEntity objects for the current principal's groups, skipping 
any that are stale or
+   * not found in the store.
+   */
+  private List<GroupEntity> resolveCurrentUserGroups(String metalake, 
EntityStore entityStore) {
+    Principal principal = PrincipalUtils.getCurrentPrincipal();
+    List<GroupEntity> result = new ArrayList<>();
+    if (!(principal instanceof UserPrincipal)) {
+      return result;
+    }
+    for (UserGroup group : ((UserPrincipal) principal).getGroups()) {
+      try {
+        NameIdentifier groupIdent = NameIdentifierUtil.ofGroup(metalake, 
group.getGroupname());
+        result.add(entityStore.get(groupIdent, Entity.EntityType.GROUP, 
GroupEntity.class));
+      } catch (NoSuchEntityException e) {
+        LOG.debug("Group not found in store: {}", group.getGroupname());
+      } catch (Exception e) {
+        LOG.warn("Failed to resolve group: {}", group.getGroupname(), e);
+      }
+    }
+    return result;
+  }
+
+  /**
+   * Adds a role mapping for the given user in both enforcers and 
asynchronously loads the role's
+   * policies if they are not already cached. When a role needs loading, the 
resulting {@link
+   * CompletableFuture} is appended to {@code loadRoleFutures} so the caller 
can join all futures
+   * after processing both direct and group-inherited roles.
+   */
+  private void addRoleForUserAndLoadPolicies(
+      Long userId,
+      String metalake,
+      Long roleId,
+      String roleName,
+      List<CompletableFuture<Void>> loadRoleFutures,
+      EntityStore entityStore) {
+    allowEnforcer.addRoleForUser(String.valueOf(userId), 
String.valueOf(roleId));

Review Comment:
   > @roryqi In followup PR, if you agree with the approach, for the Q1 part, I 
can do:
   > 
   > At the start of 
[loadRolePrivilege](https://github.com/apache/gravitino/blob/3ca87d839eb5eeee6571b9ec95977e51ee3c78db/server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinAuthorizer.java#L490),
 call `allowEnforcer.deleteUser(userId)` and `denyEnforcer.deleteUser(userId)` 
before re-adding roles. This removes all existing g-rows (user→role mappings) 
for the user, then the lambda re-resolves current groups from the JWT and 
re-adds only the roles the user is actually entitled to.
   > 
   > Today, stale g-rows persist because `addRoleForUser` is idempotent - it 
skips if the mapping already exists but never removes old ones. With 
`deleteUser()` first, it becomes `clear → re-resolve → re-add`. Since this is 
an in-memory operation on the enforcer's policy list, I anticipate no 
performance degradation.
   
   If group membership changes, we should delete the user first. If not, we 
shouldn't delete the user.  If followup PR is merged in this version, I am OK.
   



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to