This is an automated email from the ASF dual-hosted git repository. xxubai pushed a commit to branch 0.9.x in repository https://gitbox.apache.org/repos/asf/amoro.git
commit 09d8364ca3a68d95d86d3d59a8c6e85bf04a83b1 Author: Xu Bai <[email protected]> AuthorDate: Wed Aug 12 14:53:21 2026 +0800 [Hotfix][AMS] Unify authentication handling for dashboard requests (#4312) Remove single page token validation and related utility methods (cherry picked from commit 1b094b4105ee84f77587bbc0f2e602d0471f2b1e) --- .../apache/amoro/server/AmoroServiceContainer.java | 11 +-- .../dashboard/controller/TableController.java | 15 --- .../amoro/server/dashboard/utils/CommonUtil.java | 72 -------------- .../dashboard/TestDashboardAuthentication.java | 108 +++++++++++++++++++++ 4 files changed, 109 insertions(+), 97 deletions(-) diff --git a/amoro-ams/src/main/java/org/apache/amoro/server/AmoroServiceContainer.java b/amoro-ams/src/main/java/org/apache/amoro/server/AmoroServiceContainer.java index b9eb95f2d..e48961d9e 100644 --- a/amoro-ams/src/main/java/org/apache/amoro/server/AmoroServiceContainer.java +++ b/amoro-ams/src/main/java/org/apache/amoro/server/AmoroServiceContainer.java @@ -42,7 +42,6 @@ import org.apache.amoro.server.dashboard.DashboardServer; import org.apache.amoro.server.dashboard.JavalinJsonMapper; import org.apache.amoro.server.dashboard.response.ErrorResponse; import org.apache.amoro.server.dashboard.utils.AmsUtil; -import org.apache.amoro.server.dashboard.utils.CommonUtil; import org.apache.amoro.server.ha.HighAvailabilityContainer; import org.apache.amoro.server.ha.HighAvailabilityContainerFactory; import org.apache.amoro.server.manager.EventsManager; @@ -419,15 +418,7 @@ public class AmoroServiceContainer { } }); - httpServer.before( - ctx -> { - String token = ctx.queryParam("token"); - if (StringUtils.isNotEmpty(token)) { - CommonUtil.checkSinglePageToken(ctx); - } else { - dashboardServer.preHandleRequest(ctx); - } - }); + httpServer.before(dashboardServer::preHandleRequest); httpServer.exception( Exception.class, (e, ctx) -> { diff --git a/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/controller/TableController.java b/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/controller/TableController.java index 81d29a425..772e8d0dd 100644 --- a/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/controller/TableController.java +++ b/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/controller/TableController.java @@ -50,7 +50,6 @@ import org.apache.amoro.server.dashboard.model.UpgradeStatus; import org.apache.amoro.server.dashboard.response.OkResponse; import org.apache.amoro.server.dashboard.response.PageResult; import org.apache.amoro.server.dashboard.utils.AmsUtil; -import org.apache.amoro.server.dashboard.utils.CommonUtil; import org.apache.amoro.server.optimizing.OptimizingStatus; import org.apache.amoro.server.persistence.TableRuntimeMeta; import org.apache.amoro.server.process.TableProcessMeta; @@ -630,20 +629,6 @@ public class TableController { ctx.json(OkResponse.of(catalogs)); } - /** - * get single page query token. - * - * @param ctx - context for handling the request and response - */ - public void getTableDetailTabToken(Context ctx) { - String catalog = ctx.pathParam("catalog"); - String db = ctx.pathParam("db"); - String table = ctx.pathParam("table"); - - String signCal = CommonUtil.generateTablePageToken(catalog, db, table); - ctx.json(OkResponse.of(signCal)); - } - public void getTableTags(Context ctx) { String catalog = ctx.pathParam("catalog"); String database = ctx.pathParam("db"); diff --git a/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/utils/CommonUtil.java b/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/utils/CommonUtil.java index 378f60087..5538e4421 100644 --- a/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/utils/CommonUtil.java +++ b/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/utils/CommonUtil.java @@ -18,22 +18,15 @@ package org.apache.amoro.server.dashboard.utils; -import io.javalin.http.Context; -import org.apache.amoro.exception.SignatureCheckException; -import org.apache.commons.lang3.StringUtils; import org.apache.commons.net.telnet.TelnetClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.net.InetAddress; -import java.util.HashMap; -import java.util.Map; public class CommonUtil { private static final Logger LOG = LoggerFactory.getLogger(CommonUtil.class); - private static final String[] TOKEN_WHITE_LIST = {"/login/current", "/versionInfo"}; - /** * @param addresses support type 127.0.0.1:2181/ddd,host2:2181,host3:2181/service or * music-hbase64.jd.163.org,music-hbase65.jd.163.org, @@ -92,69 +85,4 @@ public class CommonUtil { return false; } } - - /** - * Check single page access token. - * - * @param ctx The context object containing the request information. - * @throws SignatureCheckException If the token is invalid or missing. - */ - public static void checkSinglePageToken(Context ctx) { - // check if query parameters contain token key - String token = ctx.queryParam("token"); - - if (StringUtils.isNotEmpty(token)) { - // regex extract catalog, db, table - String url = ctx.req.getRequestURI(); - for (String whiteListUrl : TOKEN_WHITE_LIST) { - if (url.contains(whiteListUrl)) { - return; - } - } - String catalog = ctx.queryParam("catalog"); - String db = ctx.queryParam("db"); - String table = ctx.queryParam("table"); - if (StringUtils.isEmpty(catalog) && StringUtils.isEmpty(db) && StringUtils.isEmpty(table)) { - String[] splitResult = url.split("/"); - for (int i = 0; i < splitResult.length; i++) { - switch (splitResult[i]) { - case "catalogs": - catalog = splitResult[i + 1]; - break; - case "dbs": - db = splitResult[i + 1]; - break; - case "tables": - table = splitResult[i + 1]; - break; - } - } - } - if (StringUtils.isEmpty(catalog) - || StringUtils.isEmpty(db) - || StringUtils.isEmpty(table) - || !token.equals(generateTablePageToken(catalog, db, table))) { - throw new SignatureCheckException(); - } - } - } - - /** - * Generate the token for single table page access. - * - * @param catalog The catalog name. - * @param db The database name. - * @param table The table name. - * @return The generated token. - */ - public static String generateTablePageToken(String catalog, String db, String table) { - Map<String, String> params = new HashMap<>(); - params.put("catalog", catalog); - params.put("db", db); - params.put("table", table); - - String paramString = ParamSignatureCalculator.generateParamStringWithValue(params); - String plainText = String.format("%s%s%s", paramString, paramString, paramString); - return ParamSignatureCalculator.getMD5(plainText); - } } diff --git a/amoro-ams/src/test/java/org/apache/amoro/server/dashboard/TestDashboardAuthentication.java b/amoro-ams/src/test/java/org/apache/amoro/server/dashboard/TestDashboardAuthentication.java new file mode 100644 index 000000000..cecd5225e --- /dev/null +++ b/amoro-ams/src/test/java/org/apache/amoro/server/dashboard/TestDashboardAuthentication.java @@ -0,0 +1,108 @@ +/* + * 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.amoro.server.dashboard; + +import org.apache.amoro.server.AmsEnvironment; +import org.apache.amoro.shade.jackson2.com.fasterxml.jackson.databind.JsonNode; +import org.apache.amoro.shade.jackson2.com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; + +public class TestDashboardAuthentication { + + private static final AmsEnvironment AMS = AmsEnvironment.getIntegrationInstances(); + private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient(); + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + // MD5 of "catalogxdbxtablex" + private static final String FORGED_SINGLE_PAGE_TOKEN = "b383ed05fbc9d4b7e3888a70ed19398c"; + private static final String FORGED_TOKEN_QUERY = + "token=" + FORGED_SINGLE_PAGE_TOKEN + "&catalog=x&db=x&table=x"; + + @BeforeAll + public static void beforeAll() throws Exception { + AMS.start(); + } + + @AfterAll + public static void afterAll() throws IOException { + AMS.stop(); + } + + @Test + public void testForgedSinglePageTokenCannotAccessCatalogApi() + throws IOException, InterruptedException { + HttpRequest request = + HttpRequest.newBuilder( + URI.create(AMS.getHttpUrl() + "/api/ams/v1/catalogs?" + FORGED_TOKEN_QUERY)) + .GET() + .build(); + + assertForbidden( + HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString()), "Signature check failed"); + } + + @Test + public void testForgedSinglePageTokenCannotAccessTerminalApi() + throws IOException, InterruptedException { + HttpRequest request = + HttpRequest.newBuilder( + URI.create( + AMS.getHttpUrl() + + "/api/ams/v1/terminal/catalogs/" + + AmsEnvironment.INTERNAL_ICEBERG_CATALOG + + "/execute?" + + FORGED_TOKEN_QUERY)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString("{\"sql\":\"select 1\"}")) + .build(); + + assertForbidden( + HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString()), "Signature check failed"); + } + + @Test + public void testForgedSinglePageTokenCannotBypassWebSessionAuthentication() + throws IOException, InterruptedException { + HttpRequest request = + HttpRequest.newBuilder( + URI.create(AMS.getHttpUrl() + "/api/ams/v1/catalogs?" + FORGED_TOKEN_QUERY)) + .header("X-Request-Source", "Web") + .GET() + .build(); + + assertForbidden( + HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString()), "Please login first"); + } + + private static void assertForbidden(HttpResponse<String> response, String expectedMessage) + throws IOException { + JsonNode responseBody = OBJECT_MAPPER.readTree(response.body()); + Assertions.assertEquals(403, responseBody.get("code").asInt()); + Assertions.assertEquals(expectedMessage, responseBody.get("message").asText()); + } +}
