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);
 }

Reply via email to