This is an automated email from the ASF dual-hosted git repository.
jinsongzhou pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/amoro.git
The following commit(s) were added to refs/heads/master by this push:
new c23785920 [AMORO-3744] Add support for API Token service.
c23785920 is described below
commit c23785920787b1afadeed82d922a23df8637a592
Author: jzjsnow <[email protected]>
AuthorDate: Thu Aug 21 21:34:46 2025 +0800
[AMORO-3744] Add support for API Token service.
---
.../amoro/server/dashboard/APITokenManager.java | 10 ++
.../amoro/server/dashboard/DashboardServer.java | 74 +++------
.../dashboard/controller/ApiTokenController.java | 166 +++++++++++++++++++++
.../amoro/server/dashboard/model/ApiTokens.java | 7 +-
.../server/persistence/mapper/ApiTokensMapper.java | 25 +++-
5 files changed, 220 insertions(+), 62 deletions(-)
diff --git
a/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/APITokenManager.java
b/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/APITokenManager.java
index 316540148..ed17cef64 100644
---
a/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/APITokenManager.java
+++
b/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/APITokenManager.java
@@ -22,8 +22,14 @@ import org.apache.amoro.server.dashboard.model.ApiTokens;
import org.apache.amoro.server.persistence.PersistentBase;
import org.apache.amoro.server.persistence.mapper.ApiTokensMapper;
+import java.util.List;
+
public class APITokenManager extends PersistentBase {
+ public List<ApiTokens> getApiTokens() {
+ return getAs(ApiTokensMapper.class, ApiTokensMapper::getApiTokens);
+ }
+
public String getSecretByKey(String key) {
return getAs(ApiTokensMapper.class, mapper -> mapper.getSecretByKey(key));
}
@@ -35,4 +41,8 @@ public class APITokenManager extends PersistentBase {
public void deleteApiToken(Integer id) {
doAs(ApiTokensMapper.class, mapper -> mapper.delToken(id));
}
+
+ public void deleteApiTokenByKey(String apiKey) {
+ doAs(ApiTokensMapper.class, mapper -> mapper.delTokenByKey(apiKey));
+ }
}
diff --git
a/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/DashboardServer.java
b/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/DashboardServer.java
index 168743450..616b397a3 100644
---
a/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/DashboardServer.java
+++
b/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/DashboardServer.java
@@ -37,6 +37,7 @@ import org.apache.amoro.exception.SignatureCheckException;
import org.apache.amoro.server.AmoroManagementConf;
import org.apache.amoro.server.RestCatalogService;
import org.apache.amoro.server.catalog.CatalogManager;
+import org.apache.amoro.server.dashboard.controller.ApiTokenController;
import org.apache.amoro.server.dashboard.controller.CatalogController;
import org.apache.amoro.server.dashboard.controller.HealthCheckController;
import org.apache.amoro.server.dashboard.controller.LoginController;
@@ -49,12 +50,10 @@ import
org.apache.amoro.server.dashboard.controller.TableController;
import org.apache.amoro.server.dashboard.controller.TerminalController;
import org.apache.amoro.server.dashboard.controller.VersionController;
import org.apache.amoro.server.dashboard.response.ErrorResponse;
-import org.apache.amoro.server.dashboard.utils.ParamSignatureCalculator;
import org.apache.amoro.server.resource.OptimizerManager;
import org.apache.amoro.server.table.TableManager;
import org.apache.amoro.server.terminal.TerminalManager;
import org.apache.amoro.shade.guava32.com.google.common.base.Preconditions;
-import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -64,9 +63,6 @@ import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
-import java.util.Date;
-import java.util.List;
-import java.util.Map;
import java.util.Objects;
import java.util.function.Consumer;
@@ -88,6 +84,7 @@ public class DashboardServer {
private final TerminalController terminalController;
private final VersionController versionController;
private final OverviewController overviewController;
+ private final ApiTokenController apiTokenController;
private final String authType;
private final String basicAuthUser;
@@ -115,6 +112,8 @@ public class DashboardServer {
this.versionController = new VersionController();
OverviewManager manager = new OverviewManager(serviceConfig);
this.overviewController = new OverviewController(manager);
+ APITokenManager apiTokenManager = new APITokenManager();
+ this.apiTokenController = new ApiTokenController(apiTokenManager);
this.authType =
serviceConfig.get(AmoroManagementConf.HTTP_SERVER_REST_AUTH_TYPE);
this.basicAuthUser = serviceConfig.get(AmoroManagementConf.ADMIN_USERNAME);
@@ -367,6 +366,17 @@ public class DashboardServer {
get("/dataSize", overviewController::getDataSizeHistory);
get("/top", overviewController::getTopTables);
});
+
+ // api token apis
+ path(
+ "/api/token",
+ () -> {
+ post("/create", apiTokenController::createApiToken);
+ post("/delete", apiTokenController::deleteApiToken);
+ get("/info", apiTokenController::getApiTokens);
+ post("/calculate/signature",
apiTokenController::calculateSignature);
+ post("/calculate/encryptString",
apiTokenController::getEncryptStringFromQueryParam);
+ });
};
}
@@ -392,8 +402,7 @@ public class DashboardServer {
"Failed to authenticate via basic authentication for url:" +
uriPath);
}
} else {
- checkApiToken(
- ctx.url(), ctx.queryParam("apiKey"), ctx.queryParam("signature"),
ctx.queryParamMap());
+ apiTokenController.checkApiToken(ctx);
}
}
@@ -433,6 +442,8 @@ public class DashboardServer {
"/openapi-ui",
"/openapi-ui/*",
"/swagger-docs",
+ "/api/ams/v1/api/token/calculate/signature",
+ "/api/ams/v1/api/token/calculate/encryptString",
RestCatalogService.ICEBERG_REST_API_PREFIX + "/*"
};
@@ -450,53 +461,4 @@ public class DashboardServer {
}
return false;
}
-
- private void checkApiToken(
- String requestUrl, String apiKey, String signature, Map<String,
List<String>> params) {
- String plainText;
- String encryptString;
- String signCal;
-
- try {
- if (apiKey == null || signature == null) {
- throw new SignatureCheckException("API key or signature is missing");
- }
- APITokenManager apiTokenService = new APITokenManager();
- String secret = apiTokenService.getSecretByKey(apiKey);
-
- if (secret == null) {
- throw new SignatureCheckException("Invalid API key");
- }
-
- params.remove("apiKey");
- params.remove("signature");
-
- String paramString =
ParamSignatureCalculator.generateParamStringWithValueList(params);
-
- if (StringUtils.isBlank(paramString)) {
- encryptString = ParamSignatureCalculator.SIMPLE_DATE_FORMAT.format(new
Date());
- } else {
- encryptString = paramString;
- }
-
- plainText = String.format("%s%s%s", apiKey, encryptString, secret);
- signCal = ParamSignatureCalculator.getMD5(plainText);
- LOG.debug(
- "Calculated signature for url:{}, plain text:{}, calculated
signature:{}, signature in request: {}",
- requestUrl,
- plainText,
- signCal,
- signature);
-
- if (!signature.equals(signCal)) {
- throw new SignatureCheckException(
- String.format(
- "Check signature for url:%s failed,"
- + " calculated signature:%s, signature in request:%s",
- requestUrl, signCal, signature));
- }
- } catch (Exception e) {
- throw new SignatureCheckException("Check url signature failed", e);
- }
- }
}
diff --git
a/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/controller/ApiTokenController.java
b/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/controller/ApiTokenController.java
new file mode 100644
index 000000000..262ed547c
--- /dev/null
+++
b/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/controller/ApiTokenController.java
@@ -0,0 +1,166 @@
+/*
+ * 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.controller;
+
+import io.javalin.http.Context;
+import org.apache.amoro.exception.SignatureCheckException;
+import org.apache.amoro.server.dashboard.APITokenManager;
+import org.apache.amoro.server.dashboard.model.ApiTokens;
+import org.apache.amoro.server.dashboard.response.OkResponse;
+import org.apache.amoro.server.dashboard.utils.ParamSignatureCalculator;
+import org.apache.amoro.shade.guava32.com.google.common.base.Preconditions;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+/** The controller that handles api token requests. */
+public class ApiTokenController {
+ private static final Logger LOG =
LoggerFactory.getLogger(ApiTokenController.class);
+ private final APITokenManager apiTokenManager;
+ private static final String MASK_STRING = "******";
+
+ public ApiTokenController(APITokenManager apiTokenManager) {
+ this.apiTokenManager = apiTokenManager;
+ }
+
+ /** Get all api tokens. As secret shown once when created, the returned
secret is masked. */
+ public void getApiTokens(Context ctx) {
+ List<ApiTokens> apiTokens = apiTokenManager.getApiTokens();
+ // Mask the secret field for each ApiTokens object
+ apiTokens.forEach(apiToken -> apiToken.setSecret(MASK_STRING));
+ ctx.json(OkResponse.of(apiTokens));
+ }
+
+ /** Create an api token. */
+ public void createApiToken(Context ctx) {
+ ApiTokens apiTokens = createApiToken();
+ apiTokenManager.insertApiToken(apiTokens);
+ LOG.info(
+ "Create apiKey:{}, secret: {}, apply time: {}",
+ apiTokens.getApikey(),
+ MASK_STRING,
+ apiTokens.getApplyTime());
+ ctx.json(OkResponse.of(apiTokens));
+ }
+
+ /** Delete the api token for given api key. */
+ public void deleteApiToken(Context ctx) {
+ String apiKey = ctx.queryParam("apiKey");
+ apiTokenManager.deleteApiTokenByKey(apiKey);
+ LOG.info("Delete apiKey:{}", apiKey);
+ ctx.json(OkResponse.of("Success to delete apiKey"));
+ }
+
+ /** Check if the given apiKey exists. */
+ private boolean checkApiKeyExists(String apiKey) {
+ String secret = apiTokenManager.getSecretByKey(apiKey);
+ return apiKey != null && secret != null;
+ }
+
+ /**
+ * Calculate signature for request. Used only for clients to verify their
signature calculation.
+ */
+ public void calculateSignature(Context ctx) {
+ String apiKey = ctx.queryParam("apiKey");
+ String secret = ctx.queryParam("secret");
+ Map<String, List<String>> params = ctx.queryParamMap();
+ params.remove("secret");
+
+ Preconditions.checkArgument(checkApiKeyExists(apiKey), "API key does not
exist");
+ String signature = generateSignature(apiKey, params, secret);
+ ctx.json(OkResponse.of(signature));
+ }
+
+ /** Get encrypt string for request query params. Used for client to
calculate signature. */
+ public void getEncryptStringFromQueryParam(Context ctx) {
+ String apiKey = ctx.queryParam("apiKey");
+ Map<String, List<String>> params = ctx.queryParamMap();
+
+ Preconditions.checkArgument(checkApiKeyExists(apiKey), "API key does not
exist");
+ String encryptString = generateEncryptString(params);
+ ctx.json(OkResponse.of(encryptString));
+ }
+
+ /** check api token for requestUrl. */
+ public void checkApiToken(Context ctx) {
+ String requestUrl = ctx.url();
+ String apiKey = ctx.queryParam("apiKey");
+ String signature = ctx.queryParam("signature");
+ Map<String, List<String>> params = ctx.queryParamMap();
+
+ try {
+ if (apiKey == null || signature == null) {
+ throw new SignatureCheckException("API key or signature is missing");
+ }
+
+ String secret = apiTokenManager.getSecretByKey(apiKey);
+ String signCal = generateSignature(apiKey, params, secret);
+ LOG.debug(
+ "Calculated signature for url:{}, apiKey:{}, calculated
signature:{}, signature in request: {}",
+ requestUrl,
+ apiKey,
+ signCal,
+ signature);
+
+ if (!signature.equals(signCal)) {
+ throw new SignatureCheckException(
+ String.format(
+ "Check signature for url:%s failed,"
+ + " calculated signature:%s, signature in request:%s",
+ requestUrl, signCal, signature));
+ }
+ } catch (Exception e) {
+ throw new SignatureCheckException("Check url signature failed", e);
+ }
+ }
+
+ private ApiTokens createApiToken() {
+ String apiKey = UUID.randomUUID().toString().replace("-", "");
+ String secret = UUID.randomUUID().toString().replace("-", "");
+ return new ApiTokens(apiKey, secret);
+ }
+
+ private String generateSignature(String apiKey, Map<String, List<String>>
params, String secret) {
+ Preconditions.checkArgument(apiKey != null && secret != null, "Invalid API
key");
+
+ String encryptString = generateEncryptString(params);
+ String plainText = String.format("%s%s%s", apiKey, encryptString, secret);
+ String signCal = ParamSignatureCalculator.getMD5(plainText);
+ LOG.debug(
+ "Calculated signature for plain text:{}, calculated signature:{}",
plainText, signCal);
+ return signCal;
+ }
+
+ private String generateEncryptString(Map<String, List<String>> params) {
+ params.remove("apiKey");
+ params.remove("signature");
+ String paramString =
ParamSignatureCalculator.generateParamStringWithValueList(params);
+
+ if (StringUtils.isBlank(paramString)) {
+ return ParamSignatureCalculator.SIMPLE_DATE_FORMAT.format(new Date());
+ } else {
+ return paramString;
+ }
+ }
+}
diff --git
a/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/model/ApiTokens.java
b/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/model/ApiTokens.java
index 476d4a7b3..b7918035d 100644
---
a/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/model/ApiTokens.java
+++
b/amoro-ams/src/main/java/org/apache/amoro/server/dashboard/model/ApiTokens.java
@@ -22,13 +22,14 @@ public class ApiTokens {
Integer id;
String apikey;
String secret;
- String applyTime;
+ long applyTime;
public ApiTokens() {}
public ApiTokens(String apiKey, String secret) {
this.apikey = apiKey;
this.secret = secret;
+ this.applyTime = System.currentTimeMillis();
}
public Integer getId() {
@@ -55,11 +56,11 @@ public class ApiTokens {
this.secret = secret;
}
- public String getApplyTime() {
+ public long getApplyTime() {
return applyTime;
}
- public void setApplyTime(String applyTime) {
+ public void setApplyTime(long applyTime) {
this.applyTime = applyTime;
}
}
diff --git
a/amoro-ams/src/main/java/org/apache/amoro/server/persistence/mapper/ApiTokensMapper.java
b/amoro-ams/src/main/java/org/apache/amoro/server/persistence/mapper/ApiTokensMapper.java
index 2560fa855..e2f4b793a 100644
---
a/amoro-ams/src/main/java/org/apache/amoro/server/persistence/mapper/ApiTokensMapper.java
+++
b/amoro-ams/src/main/java/org/apache/amoro/server/persistence/mapper/ApiTokensMapper.java
@@ -19,23 +19,42 @@
package org.apache.amoro.server.persistence.mapper;
import org.apache.amoro.server.dashboard.model.ApiTokens;
+import org.apache.amoro.server.persistence.converter.Long2TsConverter;
+import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Result;
+import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
+import java.util.List;
+
public interface ApiTokensMapper {
String TABLE_NAME = "api_tokens";
+ @Select("SELECT id, apikey, apply_time FROM " + TABLE_NAME)
+ @Results({
+ @Result(property = "id", column = "id"),
+ @Result(property = "apikey", column = "apikey"),
+ @Result(property = "secret", column = "secret"),
+ @Result(property = "applyTime", column = "apply_time", typeHandler =
Long2TsConverter.class)
+ })
+ List<ApiTokens> getApiTokens();
+
@Select("SELECT secret FROM " + TABLE_NAME + " WHERE apikey = #{apikey}")
String getSecretByKey(String apikey);
@Insert(
"INSERT INTO "
+ TABLE_NAME
- + " (apikey, secret, apply_time) VALUES(#{apiTokens.apikey}, "
- + "#{apiTokens.secret}, #{apiTokens.applyTime})")
+ + " (apikey, secret, apply_time) VALUES(#{apiTokens.apikey},"
+ + " #{apiTokens.secret},"
+ + " #{apiTokens.applyTime,
typeHandler=org.apache.amoro.server.persistence.converter.Long2TsConverter})")
void insert(@Param("apiTokens") ApiTokens apiTokens);
- @Insert("DELETE FROM " + TABLE_NAME + " WHERE id = #{id}")
+ @Delete("DELETE FROM " + TABLE_NAME + " WHERE id = #{id}")
void delToken(@Param("id") Integer id);
+
+ @Delete("DELETE FROM " + TABLE_NAME + " WHERE apikey = #{apikey}")
+ void delTokenByKey(@Param("apikey") String apikey);
}