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

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 0c54d985f fix(acl): read rules by exact principal and keep the 
whitelist on update (#4113)
0c54d985f is described below

commit 0c54d985f5d3f3cab068183226210ecb4a6ee5c0
Author: Zhao Jianing <[email protected]>
AuthorDate: Wed Sep 9 20:39:40 2026 +0800

    fix(acl): read rules by exact principal and keep the whitelist on update 
(#4113)
    
    Two ACL account defects, folded into one change because they touch the same 
account read/write path.
    
    `toPlainAccessConfig` read an account's rules through `ruleQuery()`, whose 
principal filter is a substring `LIKE`. Accounts whose access keys contain one 
another were merged: reading `svc-a` also returned the rules of `svc-a-v2`, and 
because `upsertPlainAccessRules` deletes by exact principal before writing, the 
absorbed rules were written back verbatim onto the wrong account. The read now 
uses an exact `eq("principal", ...)` wrapper with the same ordering, mirroring 
the delete.
    
    `AclService.updateUser` merged the incoming account onto the stored one but 
never carried `whiteRemoteAddress` across, so the update response reported a 
null whitelist even though `replaceUser` does not write that column at all. The 
web ACL page replaces its local row with that response without refetching, so 
the whitelist visibly disappeared until the next reload. The merged builder now 
keeps the existing value.
    
    Folded in from #4122, which made the same one-line whitelist fix 
independently; that PR is closed as superseded by this one.
---
 .../rocketmq/studio/instance/acl/AclService.java   |  1 +
 .../instance/acl/MybatisPlusAclRepository.java     |  8 ++++-
 .../studio/instance/acl/AclServiceTest.java        | 15 +++++++++
 .../instance/acl/MybatisPlusAclRepositoryTest.java | 37 ++++++++++++++++++++++
 4 files changed, 60 insertions(+), 1 deletion(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclService.java 
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclService.java
index 31bf49c19..97b4b8dbe 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclService.java
@@ -208,6 +208,7 @@ public class AclService {
                 .secretKey(existing.getSecretKey())
                 .admin(user.getAdmin() == null ? existing.isAdmin() : 
user.getAdmin())
                 .clusters(user.getClusters() == null ? existing.getClusters() 
: user.getClusters())
+                .whiteRemoteAddress(existing.getWhiteRemoteAddress())
                 .gmtCreate(existing.getGmtCreate())
                 .build();
         AclUserVO saved = aclRepository.replaceUser(merged)
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepository.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepository.java
index 681fe84a4..0b569c922 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepository.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepository.java
@@ -342,7 +342,13 @@ public class MybatisPlusAclRepository implements 
AclRepository {
     }
 
     private PlainAccessConfigVO toPlainAccessConfig(AclUserVO user) {
-        List<AclRuleVO> userRules = 
ruleMapper.selectList(ruleQuery(user.getAccessKey(), null, null, null, null))
+        // Exact principal match, mirroring the delete in 
upsertPlainAccessRules: the
+        // substring LIKE in ruleQuery would also absorb rules of other 
accounts whose
+        // accessKey contains this one (e.g. "svc-a" also matching "svc-a-v2").
+        List<AclRuleVO> userRules = ruleMapper.selectList(new 
QueryWrapper<RmqAclRule>()
+                        .eq("principal", user.getAccessKey())
+                        .orderByDesc("gmt_create")
+                        .orderByDesc("id"))
                 .stream()
                 .map(MybatisPlusAclRepository::toRuleVO)
                 .collect(Collectors.toList());
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/instance/acl/AclServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/instance/acl/AclServiceTest.java
index 472d60ee3..b82582214 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/instance/acl/AclServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/instance/acl/AclServiceTest.java
@@ -542,6 +542,21 @@ class AclServiceTest {
                 eq("username=newuser, admin=true"), eq("SUCCESS"), eq(null));
     }
 
+    @Test
+    void updateUserShouldKeepTheExistingWhiteRemoteAddress() {
+        existingUser.setWhiteRemoteAddress("10.0.1.0/24");
+        UpdateAclUserDTO input = new UpdateAclUserDTO();
+        input.setId("1");
+        input.setUsername("renamed");
+
+        
when(aclRepository.findUserById(1L)).thenReturn(Optional.of(existingUser));
+        when(aclRepository.replaceUser(any(AclUserVO.class))).thenAnswer(inv 
-> Optional.of(inv.getArgument(0)));
+
+        AclUserVO result = aclService.updateUser(input, null);
+
+        assertThat(result.getWhiteRemoteAddress()).isEqualTo("10.0.1.0/24");
+    }
+
     @Test
     void updateUserShouldPreserveAdminWhenNotProvided() {
         AclUserVO adminUser = AclUserVO.builder()
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepositoryTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepositoryTest.java
index 569d592ad..0d8948afb 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepositoryTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepositoryTest.java
@@ -551,6 +551,28 @@ class MybatisPlusAclRepositoryTest {
         
assertThat(captor.getValue().getClusters()).isEqualTo("cluster-a,cluster-b");
     }
 
+    @Test
+    void examineShouldNotAbsorbRulesOfAccountsWhoseAccessKeysOverlap() {
+        RmqAclUser user = userEntity(1L, "svc-a", 
CredentialUtils.encodeBase64("secret-a-value"));
+        
when(userMapper.selectList(any(QueryWrapper.class))).thenReturn(List.of(user));
+        RmqAclRule ownRule = plainRuleEntity("svc-a", "orders", "Topic", 
"PUB");
+        RmqAclRule otherAccountRule = plainRuleEntity("svc-a-v2", "payments", 
"Topic", "SUB");
+        // Simulate SQL semantics: a substring LIKE on the principal matches 
both
+        // accounts, an exact equality only the requested one.
+        
when(ruleMapper.selectList(any(QueryWrapper.class))).thenAnswer(invocation -> {
+            QueryWrapper<RmqAclRule> query = invocation.getArgument(0);
+            return query.getSqlSegment().contains("LIKE")
+                    ? List.of(ownRule, otherAccountRule)
+                    : List.of(ownRule);
+        });
+
+        AclClusterConfigVO config = 
repository.examineBrokerClusterAclConfig("cluster-a");
+
+        assertThat(config.getAccounts()).hasSize(1);
+        PlainAccessConfigVO account = config.getAccounts().get(0);
+        assertThat(account.getTopicPerms()).containsExactly("orders=PUB");
+    }
+
     private static RmqAclUser userEntity(Long id, String accessKey, String 
encodedSecret) {
         RmqAclUser entity = new RmqAclUser();
         entity.setId(id);
@@ -561,4 +583,19 @@ class MybatisPlusAclRepositoryTest {
         entity.setGmtCreate(LocalDateTime.of(2026, 1, 1, 0, 0));
         return entity;
     }
+
+    private static RmqAclRule plainRuleEntity(String principal, String 
resource,
+            String resourceType, String actions) {
+        RmqAclRule rule = new RmqAclRule();
+        rule.setPrincipal(principal);
+        rule.setResource(resource);
+        rule.setResourceType(resourceType);
+        rule.setResourcePattern("LITERAL");
+        rule.setActions(actions);
+        rule.setDecision("ALLOW");
+        rule.setScope("*");
+        rule.setAclVersion("2.0");
+        rule.setGmtCreate(LocalDateTime.of(2026, 1, 1, 0, 0));
+        return rule;
+    }
 }

Reply via email to