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

CalvinKirs pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/master by this push:
     new 94a791e6644 [fix](auth) add auth check for manager node and query 
qerror REST APIs (#65042)
94a791e6644 is described below

commit 94a791e664469a9b1261b1779759380163c3547e
Author: Calvin Kirs <[email protected]>
AuthorDate: Wed Jul 1 14:33:00 2026 +0800

    [fix](auth) add auth check for manager node and query qerror REST APIs 
(#65042)
    
    ## Proposed changes
    
    Several manager REST APIs under `/rest/v2/manager` were missing
    authentication and/or authorization. This PR closes those gaps.
    
    ### 1. Node management endpoints — missing auth + authz
    
    `POST /rest/v2/manager/node/{action}/fe`, `/{action}/be`,
    `/{action}/broker` (`operateFrontends` / `operateBackend` /
    `operateBroker`) could add or drop FE / BE / Broker nodes **without any
    authentication or authorization**. Any caller able to reach the FE HTTP
    port could change cluster topology.
    
    Added, consistent with the sibling `set_config/fe` and `set_config/be`
    endpoints:
    
    ```java
    ActionAuthorizationInfo authInfo = executeCheckPassword(request, response);
    checkAdminAuth(authInfo.userIdentity);
    ```
    
    ### 2. `GET /rest/v2/manager/query/qerror/{id}` (`getStats`) — fully
    unauthenticated
    
    This endpoint had **neither authentication nor authorization**: its
    method signature didn't even take
    `HttpServletRequest`/`HttpServletResponse`, so it could not call
    `executeCheckPassword`, and the global `AuthInterceptor` only covers
    `/rest/v1/**`. As a result it was reachable anonymously **even with
    `enable_all_http_auth=true`**, leaking per-query stats-error
    information.
    
    Aligned it with the `/profile` and `/trace_id` endpoints — authenticate,
    then restrict non-admin users to their own queries:
    
    ```java
    executeCheckPassword(request, response);
    try {
        checkAuthByUserAndQueryId(id);
    } catch (AuthenticationException e) {
        return ResponseEntityBuilder.badRequest(e.getMessage());
    }
    ```
    
    ## Test
    
    Added `regression-test/suites/auth_p0/test_http_node_action_auth.groovy`
    (`p0,auth,nonConcurrent`):
    - a non-admin user calling `ADD /fe` and `ADD /be` is rejected;
    - after `grant 'admin'`, the request passes the auth check;
    - an unauthenticated call to `/qerror/{id}` is rejected.
    
    FE compiles cleanly (`build.sh --fe`).
---
 .../doris/httpv2/rest/manager/NodeAction.java      |   6 ++
 .../httpv2/rest/manager/QueryProfileAction.java    |  10 +-
 .../auth_p0/test_http_node_action_auth.groovy      | 113 +++++++++++++++++++++
 3 files changed, 128 insertions(+), 1 deletion(-)

diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/manager/NodeAction.java 
b/fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/manager/NodeAction.java
index 04a3eb2a1c2..1ad24aaf3d6 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/manager/NodeAction.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/manager/NodeAction.java
@@ -614,6 +614,8 @@ public class NodeAction extends RestBaseController {
     @PostMapping("/{action}/be")
     public Object operateBackend(HttpServletRequest request, 
HttpServletResponse response,
             @PathVariable("action") String action, @RequestBody BackendReqInfo 
reqInfo) {
+        ActionAuthorizationInfo authInfo = executeCheckPassword(request, 
response);
+        checkAdminAuth(authInfo.userIdentity);
         try {
             if (needRedirect(request.getScheme())) {
                 return redirectToHttps(request);
@@ -661,6 +663,8 @@ public class NodeAction extends RestBaseController {
     @PostMapping("/{action}/fe")
     public Object operateFrontends(HttpServletRequest request, 
HttpServletResponse response,
             @PathVariable("action") String action, @RequestBody 
FrontendReqInfo reqInfo) {
+        ActionAuthorizationInfo authInfo = executeCheckPassword(request, 
response);
+        checkAdminAuth(authInfo.userIdentity);
         try {
             if (needRedirect(request.getScheme())) {
                 return redirectToHttps(request);
@@ -693,6 +697,8 @@ public class NodeAction extends RestBaseController {
     @PostMapping("/{action}/broker")
     public Object operateBroker(HttpServletRequest request, 
HttpServletResponse response,
                                 @PathVariable("action") String action, 
@RequestBody BrokerReqInfo reqInfo) {
+        ActionAuthorizationInfo authInfo = executeCheckPassword(request, 
response);
+        checkAdminAuth(authInfo.userIdentity);
         try {
             if (!Env.getCurrentEnv().isMaster()) {
                 return redirectToMasterOrException(request, response);
diff --git 
a/fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/manager/QueryProfileAction.java
 
b/fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/manager/QueryProfileAction.java
index e619a81326e..27b955522ae 100644
--- 
a/fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/manager/QueryProfileAction.java
+++ 
b/fe/fe-core/src/main/java/org/apache/doris/httpv2/rest/manager/QueryProfileAction.java
@@ -373,7 +373,15 @@ public class QueryProfileAction extends RestBaseController 
{
      *  Query qError.
      */
     @RequestMapping(path = "/qerror/{id}", method = RequestMethod.GET)
-    public ResponseEntity<String> getStats(@PathVariable(value = "id") String 
id) {
+    public Object getStats(HttpServletRequest request, HttpServletResponse 
response,
+            @PathVariable(value = "id") String id) {
+        executeCheckPassword(request, response);
+        try {
+            checkAuthByUserAndQueryId(id);
+        } catch (AuthenticationException e) {
+            return ResponseEntityBuilder.badRequest(e.getMessage());
+        }
+
         ProfileElement profile = 
ProfileManager.getInstance().findProfileElementObject(id);
         if (profile == null) {
             return ResponseEntityBuilder.notFound(null);
diff --git a/regression-test/suites/auth_p0/test_http_node_action_auth.groovy 
b/regression-test/suites/auth_p0/test_http_node_action_auth.groovy
new file mode 100644
index 00000000000..5b1774a44d0
--- /dev/null
+++ b/regression-test/suites/auth_p0/test_http_node_action_auth.groovy
@@ -0,0 +1,113 @@
+// 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.
+
+import org.junit.Assert;
+
+// Verify the node management endpoints (add/drop fe/be/broker) require
+// authentication and ADMIN privilege. Without the check, any caller could
+// add or drop cluster nodes via these REST APIs.
+//
+// NOTE on cluster safety: the bogus node addresses below use the RFC 5737
+// TEST-NET-1 range (192.0.2.0/24), which can never match a real FE/BE in any
+// cluster. The negative (non-admin) cases use ADD, but the ADMIN check runs
+// before the node operation, so the add is never executed. The positive
+// (admin) cases use DROP, which on a non-existent node returns a harmless
+// "does not exist" error -- it never mutates real cluster state.
+suite("test_http_node_action_auth", "p0,auth,nonConcurrent") {
+    String suiteName = "test_http_node_action_auth"
+    String user = "${suiteName}_user"
+    String pwd = 'C123_567p'
+    String bogusFe = "192.0.2.111:12345"
+    String bogusBe = "192.0.2.112:12345"
+    try_sql("DROP USER ${user}")
+    sql """CREATE USER '${user}' IDENTIFIED BY '${pwd}'"""
+
+    try {
+        sql """ ADMIN SET ALL FRONTENDS CONFIG ("enable_all_http_auth" = 
"true"); """
+
+        def operateFe = { user_name, password, action, check_func ->
+            httpTest {
+                basicAuthorization "${user_name}", "${password}"
+                endpoint "${context.config.feHttpAddress}"
+                uri "/rest/v2/manager/node/${action}/fe"
+                op "post"
+                body """{"role": "OBSERVER", "hostPort": "${bogusFe}"}"""
+                check check_func
+            }
+        }
+
+        def operateBe = { user_name, password, action, check_func ->
+            httpTest {
+                basicAuthorization "${user_name}", "${password}"
+                endpoint "${context.config.feHttpAddress}"
+                uri "/rest/v2/manager/node/${action}/be"
+                op "post"
+                body """{"hostPorts": ["${bogusBe}"]}"""
+                check check_func
+            }
+        }
+
+        // A non-admin user must be rejected by the ADMIN privilege check.
+        // The node operation is never reached, so nothing is mutated.
+        operateFe.call(user, pwd, "ADD") {
+            respCode, body ->
+                log.info("add fe (non-admin) body:${body}")
+                assertTrue("${body}".contains("Unauthorized"))
+        }
+
+        operateBe.call(user, pwd, "ADD") {
+            respCode, body ->
+                log.info("add be (non-admin) body:${body}")
+                assertTrue("${body}".contains("Unauthorized"))
+        }
+
+        sql """grant 'admin' to ${user}"""
+
+        // After granting ADMIN, the request passes the auth check. We use DROP
+        // on a bogus (TEST-NET) node so the call reaches the operation but 
only
+        // gets a "does not exist" error -- it must no longer be rejected with 
an
+        // authorization error, and must not touch any real node.
+        operateFe.call(user, pwd, "DROP") {
+            respCode, body ->
+                log.info("drop fe (admin) body:${body}")
+                assertFalse("${body}".contains("Unauthorized"))
+        }
+
+        operateBe.call(user, pwd, "DROP") {
+            respCode, body ->
+                log.info("drop be (admin) body:${body}")
+                assertFalse("${body}".contains("Unauthorized"))
+        }
+
+        // The query qerror endpoint must require authentication. Without
+        // credentials it must not return the stats payload.
+        httpTest {
+            endpoint "${context.config.feHttpAddress}"
+            uri "/rest/v2/manager/query/qerror/no_such_query_id"
+            op "get"
+            check {
+                respCode, body ->
+                    log.info("qerror (no auth) respCode:${respCode} 
body:${body}")
+                    assertTrue(respCode == 401 || 
"${body}".contains("Unauthorized")
+                            || "${body}".contains("Authentication"))
+            }
+        }
+    } finally {
+        sql """ ADMIN SET ALL FRONTENDS CONFIG ("enable_all_http_auth" = 
"false"); """
+        try_sql("DROP USER ${user}")
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to