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 354e5fd4 fix(auth): restrict privileged reads and harden ACL updates 
(#2046)
354e5fd4 is described below

commit 354e5fd4584e1a2f52b1ff1dc58070635e2257d6
Author: youngkermit8-coder <[email protected]>
AuthorDate: Thu Aug 13 19:50:25 2026 +0800

    fix(auth): restrict privileged reads and harden ACL updates (#2046)
    
    Consolidates #1716, #1753, #2046: limit cloud catalog GET endpoints to
    administrators, reject blank ACL usernames on update, and restrict remote
    ACL rule discovery to administrators.
---
 .../rocketmq/studio/auth/AuthInterceptor.java      |  7 ++
 .../rocketmq/studio/instance/acl/AclService.java   |  3 +
 .../rocketmq/studio/auth/AuthInterceptorTest.java  | 74 ++++++++++++++++++++++
 .../studio/instance/acl/AclServiceTest.java        | 15 +++++
 4 files changed, 99 insertions(+)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/auth/AuthInterceptor.java 
b/server/src/main/java/org/apache/rocketmq/studio/auth/AuthInterceptor.java
index ee87fc81..9669a9b9 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/auth/AuthInterceptor.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/auth/AuthInterceptor.java
@@ -102,10 +102,17 @@ public class AuthInterceptor implements 
HandlerInterceptor {
     private boolean isAdminOnlyGetPath(String path) {
         String normalizedPath = normalizePath(stripPathParameters(path));
         return "/api/llm/models".equals(normalizedPath)
+                || isCloudCatalogPath(normalizedPath)
+                || "/api/acl/remote/rules".equals(normalizedPath)
                 || isCredentialRevealPath(normalizedPath, "/api/acl/users/")
                 || isCredentialRevealPath(normalizedPath, 
"/api/cloud-credentials/");
     }
 
+    private boolean isCloudCatalogPath(String path) {
+        return path.startsWith("/api/cloud/aliyun/")
+                || path.startsWith("/api/cloud/tencent/");
+    }
+
     private boolean isCredentialRevealPath(String path, String prefix) {
         return path != null && path.startsWith(prefix) && 
path.endsWith("/credentials");
     }
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 d6ea5435..f9b98804 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
@@ -125,6 +125,9 @@ public class AclService {
         log.info("Updating ACL user id={}, username={}", user.getId(), 
user.getUsername());
         AclUserVO existing = aclRepository.findUserById(user.getId())
                 .orElseThrow(() -> new BusinessException(404, "ACL user not 
found: " + user.getId()));
+        if (user.getUsername() != null && 
!StringUtils.hasText(user.getUsername())) {
+            throw new BusinessException(400, "ACL username is required");
+        }
         AclUserVO merged = AclUserVO.builder()
                 .id(existing.getId())
                 .username(user.getUsername() == null ? existing.getUsername() 
: user.getUsername())
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/auth/AuthInterceptorTest.java 
b/server/src/test/java/org/apache/rocketmq/studio/auth/AuthInterceptorTest.java
index 64727985..936de2e3 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/auth/AuthInterceptorTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/auth/AuthInterceptorTest.java
@@ -21,6 +21,8 @@ import org.junit.jupiter.api.AfterEach;
 import org.apache.rocketmq.studio.settings.GeneralSettingsVO;
 import org.apache.rocketmq.studio.settings.SettingsRepository;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
 import org.springframework.http.HttpHeaders;
 import org.springframework.mock.web.MockHttpServletRequest;
 import org.springframework.mock.web.MockHttpServletResponse;
@@ -217,6 +219,36 @@ class AuthInterceptorTest {
         assertThat(allowed).isTrue();
     }
 
+    @ParameterizedTest
+    @ValueSource(strings = {
+        "/api/cloud/aliyun/regions",
+        "/api/cloud/aliyun/instances",
+        "/api/cloud/tencent/regions",
+        "/api/cloud/tencent/instances"
+    })
+    void shouldRejectCloudCatalogReadsForNonAdminUser(String path) throws 
Exception {
+        TestSession session = login(false);
+        MockHttpServletRequest request = authenticatedRequest("GET", path, 
session.token());
+        MockHttpServletResponse response = new MockHttpServletResponse();
+
+        boolean allowed = session.interceptor().preHandle(request, response, 
new Object());
+
+        assertThat(allowed).isFalse();
+        assertThat(response.getStatus()).isEqualTo(403);
+    }
+
+    @Test
+    void shouldAllowCloudCatalogReadsForAdminUser() throws Exception {
+        TestSession session = login(true);
+        MockHttpServletRequest request = authenticatedRequest(
+                "GET", "/api/cloud/aliyun/instances", session.token());
+
+        boolean allowed = session.interceptor().preHandle(
+                request, new MockHttpServletResponse(), new Object());
+
+        assertThat(allowed).isTrue();
+    }
+
     @Test
     void shouldRejectLlmModelDiscoveryForNonAdminUser() throws Exception {
         TestSession session = login(false);
@@ -382,6 +414,48 @@ class AuthInterceptorTest {
         assertThat(allowed).isTrue();
     }
 
+    @ParameterizedTest
+    @ValueSource(strings = {
+        "/api/acl/remote/rules",
+        "/api/acl/remote/rules/",
+        "/api/acl;source=remote/remote;view=all/rules;format=json"
+    })
+    void shouldRejectRemoteAclPolicyDiscoveryForNonAdminUser(String path) 
throws Exception {
+        TestSession session = login(false);
+        MockHttpServletRequest request = authenticatedRequest("GET", path, 
session.token());
+        MockHttpServletResponse response = new MockHttpServletResponse();
+
+        boolean allowed = session.interceptor().preHandle(request, response, 
new Object());
+
+        assertThat(allowed).isFalse();
+        assertThat(response.getStatus()).isEqualTo(403);
+        assertThat(response.getContentAsString()).contains("Admin permission 
required");
+    }
+
+    @Test
+    void shouldAllowRemoteAclPolicyDiscoveryForAdminUser() throws Exception {
+        TestSession session = login(true);
+        MockHttpServletRequest request = authenticatedRequest(
+                "GET", "/api/acl/remote/rules", session.token());
+
+        boolean allowed = session.interceptor().preHandle(
+                request, new MockHttpServletResponse(), new Object());
+
+        assertThat(allowed).isTrue();
+    }
+
+    @Test
+    void shouldKeepLocalAclRulesReadableForNonAdminUser() throws Exception {
+        TestSession session = login(false);
+        MockHttpServletRequest request = authenticatedRequest(
+                "GET", "/api/acl/rules", session.token());
+
+        boolean allowed = session.interceptor().preHandle(
+                request, new MockHttpServletResponse(), new Object());
+
+        assertThat(allowed).isTrue();
+    }
+
     private TestSession login(boolean admin) {
         AuthProperties properties = new AuthProperties();
         properties.setLoginRequired(true);
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 4408bcca..1acbb2be 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
@@ -454,6 +454,21 @@ class AclServiceTest {
         assertThat(captor.getValue().isAdmin()).isTrue();
     }
 
+    @Test
+    void updateUserShouldRejectBlankUsernameWithoutSaving() {
+        UpdateAclUserDTO input = new UpdateAclUserDTO();
+        input.setId("user-1");
+        input.setUsername("   ");
+
+        
when(aclRepository.findUserById("user-1")).thenReturn(Optional.of(existingUser));
+
+        assertThatThrownBy(() -> aclService.updateUser(input))
+                .isInstanceOf(BusinessException.class)
+                .hasMessage("ACL username is required")
+                .satisfies(ex -> assertThat(((BusinessException) 
ex).getCode()).isEqualTo(400));
+        verify(aclRepository, never()).saveUser(any(AclUserVO.class));
+    }
+
     @Test
     void updateUserShouldThrowWhenUserDoesNotExist() {
         UpdateAclUserDTO input = new UpdateAclUserDTO();

Reply via email to