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 0f2b1302 fix: harden authentication management boundaries (#947)
0f2b1302 is described below

commit 0f2b130241e60da73ff88d88b475ece7063050ab
Author: aias00 <[email protected]>
AuthorDate: Tue Aug 4 03:06:57 2026 -0700

    fix: harden authentication management boundaries (#947)
    
    * [ISSUE #840] Validate login request bodies
    
    * fix(server): validate acl user create requests
    
    * fix(server): normalize auth public paths
---
 .../rocketmq/studio/auth/AuthController.java       |  6 +++-
 .../rocketmq/studio/auth/AuthInterceptor.java      | 14 ++++++++
 .../apache/rocketmq/studio/auth/AuthService.java   |  4 +++
 .../studio/instance/acl/AclController.java         |  4 +--
 .../studio/instance/acl/CreateAclUserDTO.java      | 38 ++++++++++++++++++++++
 .../rocketmq/studio/auth/AuthControllerTest.java   | 12 +++++++
 .../rocketmq/studio/auth/AuthInterceptorTest.java  | 24 ++++++++++++++
 .../rocketmq/studio/auth/AuthServiceTest.java      |  8 +++++
 .../studio/instance/acl/AclControllerTest.java     | 32 +++++++++++++++++-
 9 files changed, 138 insertions(+), 4 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/auth/AuthController.java 
b/server/src/main/java/org/apache/rocketmq/studio/auth/AuthController.java
index d5398dbd..cda7e036 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/auth/AuthController.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/auth/AuthController.java
@@ -18,6 +18,7 @@
 package org.apache.rocketmq.studio.auth;
 
 import org.apache.rocketmq.studio.common.domain.Result;
+import org.apache.rocketmq.studio.common.exception.BusinessException;
 import lombok.RequiredArgsConstructor;
 import org.springframework.http.CacheControl;
 import org.springframework.http.HttpHeaders;
@@ -50,7 +51,10 @@ public class AuthController {
     }
 
     @PostMapping("/login")
-    public Result<LoginVO> login(@RequestBody LoginDTO request) {
+    public Result<LoginVO> login(@RequestBody(required = false) LoginDTO 
request) {
+        if (request == null) {
+            throw new BusinessException(400, "Login request is required");
+        }
         return Result.ok(authService.login(request));
     }
 
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 4d167137..1b1ae46d 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
@@ -50,6 +50,7 @@ public class AuthInterceptor implements HandlerInterceptor {
     }
 
     private boolean isPublicPath(String path) {
+        path = normalizePath(path);
         return path.equals("/api/auth/login")
                 || path.equals("/api/auth/status")
                 || path.startsWith("/api-docs")
@@ -57,6 +58,19 @@ public class AuthInterceptor implements HandlerInterceptor {
                 || path.startsWith("/actuator/health");
     }
 
+    private String normalizePath(String path) {
+        if (path == null || path.isBlank()) {
+            return "";
+        }
+        if (path.equals("/")) {
+            return path;
+        }
+        while (path.endsWith("/")) {
+            path = path.substring(0, path.length() - 1);
+        }
+        return path;
+    }
+
     private String requestPath(HttpServletRequest request) {
         String servletPath = request.getServletPath();
         if (servletPath != null && !servletPath.isBlank()) {
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/auth/AuthService.java 
b/server/src/main/java/org/apache/rocketmq/studio/auth/AuthService.java
index 66ec35dd..102e1cf1 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/auth/AuthService.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/auth/AuthService.java
@@ -50,6 +50,10 @@ public class AuthService {
     }
 
     public LoginVO login(LoginDTO request) {
+        if (request == null) {
+            throw new BusinessException(400, "Login request is required");
+        }
+
         log.info("Login attempt for user: {}", request.getUsername());
 
         if (request.getUsername() == null || request.getUsername().isBlank()) {
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclController.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclController.java
index 1acb7a3a..29671efe 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclController.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclController.java
@@ -70,8 +70,8 @@ public class AclController {
     }
 
     @PostMapping("/users/create")
-    public Result<AclUserVO> createUser(@RequestBody AclUserVO user) {
-        return Result.ok(aclService.createUser(user));
+    public Result<AclUserVO> createUser(@Valid @RequestBody CreateAclUserDTO 
user) {
+        return Result.ok(aclService.createUser(user.toAclUserVO()));
     }
 
     @PostMapping("/users/update")
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/CreateAclUserDTO.java
 
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/CreateAclUserDTO.java
new file mode 100644
index 00000000..6b2d674d
--- /dev/null
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/CreateAclUserDTO.java
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.rocketmq.studio.instance.acl;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class CreateAclUserDTO {
+    @NotBlank(message = "username is required")
+    private String username;
+    private boolean admin;
+    private List<String> clusters;
+
+    public AclUserVO toAclUserVO() {
+        AclUserVO user = new AclUserVO();
+        user.setUsername(username);
+        user.setAdmin(admin);
+        user.setClusters(clusters);
+        return user;
+    }
+}
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/auth/AuthControllerTest.java 
b/server/src/test/java/org/apache/rocketmq/studio/auth/AuthControllerTest.java
index 9d0a5f16..bed876d6 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/auth/AuthControllerTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/auth/AuthControllerTest.java
@@ -30,6 +30,7 @@ import org.springframework.test.web.servlet.MockMvc;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 import static 
org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@@ -144,6 +145,17 @@ class AuthControllerTest {
                 .andExpect(jsonPath("$.data.user.admin").value(true));
     }
 
+    @Test
+    void loginShouldRejectMissingRequestBody() throws Exception {
+        mockMvc.perform(post("/api/auth/login")
+                        .contentType(MediaType.APPLICATION_JSON))
+                .andExpect(status().isBadRequest())
+                .andExpect(jsonPath("$.code").value(400))
+                .andExpect(jsonPath("$.message").value("Login request is 
required"));
+
+        verify(authService, never()).login(any(LoginDTO.class));
+    }
+
     @Test
     void logoutShouldReturnSuccess() throws Exception {
         doNothing().when(authService).logout("Bearer token-1");
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 5aa924ba..afc91025 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
@@ -89,6 +89,18 @@ class AuthInterceptorTest {
         assertThat(allowed).isTrue();
     }
 
+    @Test
+    void shouldAllowLoginEndpointWithTrailingSlashWhenLoginIsEnabled() throws 
Exception {
+        AuthProperties properties = new AuthProperties();
+        properties.setLoginRequired(true);
+        AuthInterceptor interceptor = new AuthInterceptor(properties, new 
AuthService(properties));
+        MockHttpServletRequest request = new MockHttpServletRequest("POST", 
"/api/auth/login/");
+
+        boolean allowed = interceptor.preHandle(request, new 
MockHttpServletResponse(), new Object());
+
+        assertThat(allowed).isTrue();
+    }
+
     @Test
     void shouldAllowAuthStatusEndpointWhenLoginIsEnabled() throws Exception {
         AuthProperties properties = new AuthProperties();
@@ -100,4 +112,16 @@ class AuthInterceptorTest {
 
         assertThat(allowed).isTrue();
     }
+
+    @Test
+    void shouldAllowAuthStatusEndpointWithTrailingSlashWhenLoginIsEnabled() 
throws Exception {
+        AuthProperties properties = new AuthProperties();
+        properties.setLoginRequired(true);
+        AuthInterceptor interceptor = new AuthInterceptor(properties, new 
AuthService(properties));
+        MockHttpServletRequest request = new MockHttpServletRequest("GET", 
"/api/auth/status/");
+
+        boolean allowed = interceptor.preHandle(request, new 
MockHttpServletResponse(), new Object());
+
+        assertThat(allowed).isTrue();
+    }
 }
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/auth/AuthServiceTest.java 
b/server/src/test/java/org/apache/rocketmq/studio/auth/AuthServiceTest.java
index 31959b52..21a8d4ac 100644
--- a/server/src/test/java/org/apache/rocketmq/studio/auth/AuthServiceTest.java
+++ b/server/src/test/java/org/apache/rocketmq/studio/auth/AuthServiceTest.java
@@ -188,6 +188,14 @@ class AuthServiceTest {
         assertThat(activeTokens).containsOnlyKeys(activeSession.getToken());
     }
 
+    @Test
+    void loginShouldRejectNullRequest() {
+        assertThatThrownBy(() -> authService.login(null))
+                .isInstanceOf(BusinessException.class)
+                .hasMessage("Login request is required")
+                .satisfies(ex -> assertThat(((BusinessException) 
ex).getCode()).isEqualTo(400));
+    }
+
     @Test
     void loginShouldThrowWhenUsernameIsNull() {
         LoginDTO request = new LoginDTO();
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/instance/acl/AclControllerTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/instance/acl/AclControllerTest.java
index 9f780696..6b8baf1b 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/instance/acl/AclControllerTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/instance/acl/AclControllerTest.java
@@ -20,6 +20,7 @@ package org.apache.rocketmq.studio.instance.acl;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.rocketmq.studio.common.exception.BusinessException;
 import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
 import org.springframework.beans.factory.annotation.Autowired;
 import 
org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
 import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
@@ -31,6 +32,7 @@ import java.time.LocalDateTime;
 import java.util.List;
 import java.util.Map;
 
+import static org.assertj.core.api.Assertions.assertThat;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.ArgumentMatchers.isNull;
@@ -221,11 +223,39 @@ class AclControllerTest {
 
         mockMvc.perform(post("/api/acl/users/create")
                         .contentType(MediaType.APPLICATION_JSON)
-                        .content("{\"username\":\"new-user\"}"))
+                        .content("""
+                                {
+                                  "username": "new-user",
+                                  "admin": true,
+                                  "clusters": ["cluster-a"]
+                                }
+                                """))
                 .andExpect(status().isOk())
                 .andExpect(jsonPath("$.code").value(200))
                 
.andExpect(jsonPath("$.data.accessKey").value("access-key-123456"))
                 
.andExpect(jsonPath("$.data.secretKey").value("secret-key-987654"));
+
+        ArgumentCaptor<AclUserVO> captor = 
ArgumentCaptor.forClass(AclUserVO.class);
+        verify(aclService).createUser(captor.capture());
+        assertThat(captor.getValue().getUsername()).isEqualTo("new-user");
+        assertThat(captor.getValue().isAdmin()).isTrue();
+        
assertThat(captor.getValue().getClusters()).containsExactly("cluster-a");
+    }
+
+    @Test
+    void createUserShouldRejectMissingUsername() throws Exception {
+        mockMvc.perform(post("/api/acl/users/create")
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content("""
+                                {
+                                  "admin": false
+                                }
+                                """))
+                .andExpect(status().isBadRequest())
+                .andExpect(jsonPath("$.code").value(400))
+                .andExpect(jsonPath("$.message").value("username is 
required"));
+
+        verifyNoInteractions(aclService);
     }
 
     @Test

Reply via email to