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

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


The following commit(s) were added to refs/heads/master by this push:
     new cce52577af [core] Support data token in RESTCatalog (#4944)
cce52577af is described below

commit cce52577afcfc70d4b1294fe3d41316b1c7b3d9f
Author: jerry <[email protected]>
AuthorDate: Mon Jan 20 21:43:17 2025 +0800

    [core] Support data token in RESTCatalog (#4944)
---
 .../org/apache/paimon/catalog/AbstractCatalog.java |   3 +-
 .../org/apache/paimon/catalog/CatalogUtils.java    |   4 +-
 .../java/org/apache/paimon/rest/RESTCatalog.java   |  40 +++++-
 .../org/apache/paimon/rest/RESTCatalogOptions.java |   6 +
 .../paimon/rest/RefreshCredentialFileIO.java       | 148 +++++++++++++++++++++
 .../java/org/apache/paimon/rest/ResourcePaths.java |   4 +
 .../responses/GetTableCredentialsResponse.java     |  60 +++++++++
 .../org/apache/paimon/rest/MockRESTMessage.java    |   7 +
 .../org/apache/paimon/rest/RESTCatalogServer.java  |  16 +++
 .../org/apache/paimon/rest/RESTCatalogTest.java    |  23 +++-
 .../apache/paimon/rest/RESTObjectMapperTest.java   |  11 ++
 paimon-open-api/rest-catalog-open-api.yaml         |  47 +++++++
 .../paimon/open/api/RESTCatalogController.java     |  28 ++++
 13 files changed, 386 insertions(+), 11 deletions(-)

diff --git 
a/paimon-core/src/main/java/org/apache/paimon/catalog/AbstractCatalog.java 
b/paimon-core/src/main/java/org/apache/paimon/catalog/AbstractCatalog.java
index aa93b1ba32..72d09b785f 100644
--- a/paimon-core/src/main/java/org/apache/paimon/catalog/AbstractCatalog.java
+++ b/paimon-core/src/main/java/org/apache/paimon/catalog/AbstractCatalog.java
@@ -369,7 +369,8 @@ public abstract class AbstractCatalog implements Catalog {
         SnapshotCommit.Factory commitFactory =
                 new RenamingSnapshotCommit.Factory(
                         lockFactory().orElse(null), 
lockContext().orElse(null));
-        return CatalogUtils.loadTable(this, identifier, 
this::loadTableMetadata, commitFactory);
+        return CatalogUtils.loadTable(
+                this, identifier, fileIO(), this::loadTableMetadata, 
commitFactory);
     }
 
     /**
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/catalog/CatalogUtils.java 
b/paimon-core/src/main/java/org/apache/paimon/catalog/CatalogUtils.java
index fbd510692c..7282b308e7 100644
--- a/paimon-core/src/main/java/org/apache/paimon/catalog/CatalogUtils.java
+++ b/paimon-core/src/main/java/org/apache/paimon/catalog/CatalogUtils.java
@@ -171,6 +171,7 @@ public class CatalogUtils {
     public static Table loadTable(
             Catalog catalog,
             Identifier identifier,
+            FileIO fileIO,
             TableMetadata.Loader metadataLoader,
             SnapshotCommit.Factory commitFactory)
             throws Catalog.TableNotExistException {
@@ -189,8 +190,7 @@ public class CatalogUtils {
                 new CatalogEnvironment(
                         identifier, metadata.uuid(), catalog.catalogLoader(), 
commitFactory);
         Path path = new Path(schema.options().get(PATH.key()));
-        FileStoreTable table =
-                FileStoreTableFactory.create(catalog.fileIO(), path, schema, 
catalogEnv);
+        FileStoreTable table = FileStoreTableFactory.create(fileIO, path, 
schema, catalogEnv);
 
         if (options.type() == TableType.OBJECT_TABLE) {
             table = toObjectTable(catalog, table);
diff --git a/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java 
b/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java
index 87b8e8cb96..e06ef012b8 100644
--- a/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalog.java
@@ -109,6 +109,7 @@ public class RESTCatalog implements Catalog {
     private final ResourcePaths resourcePaths;
     private final AuthSession catalogAuth;
     private final Options options;
+    private final boolean fileIORefreshCredentialEnable;
     private final FileIO fileIO;
 
     private volatile ScheduledExecutorService refreshExecutor = null;
@@ -130,13 +131,19 @@ public class RESTCatalog implements Catalog {
                                 .merge(context.options().toMap()));
         this.resourcePaths = ResourcePaths.forCatalogProperties(options);
 
+        this.fileIORefreshCredentialEnable =
+                
options.get(RESTCatalogOptions.FILE_IO_REFRESH_CREDENTIAL_ENABLE);
         try {
-            String warehouseStr = options.get(CatalogOptions.WAREHOUSE);
-            this.fileIO =
-                    FileIO.get(
-                            new Path(warehouseStr),
-                            CatalogContext.create(
-                                    options, context.preferIO(), 
context.fallbackIO()));
+            if (fileIORefreshCredentialEnable) {
+                this.fileIO = null;
+            } else {
+                String warehouseStr = options.get(CatalogOptions.WAREHOUSE);
+                this.fileIO =
+                        FileIO.get(
+                                new Path(warehouseStr),
+                                CatalogContext.create(
+                                        options, context.preferIO(), 
context.fallbackIO()));
+            }
         } catch (IOException e) {
             LOG.warn("Can not get FileIO from options.");
             throw new RuntimeException(e);
@@ -149,6 +156,8 @@ public class RESTCatalog implements Catalog {
         this.options = options;
         this.resourcePaths = ResourcePaths.forCatalogProperties(options);
         this.fileIO = fileIO;
+        this.fileIORefreshCredentialEnable =
+                
options.get(RESTCatalogOptions.FILE_IO_REFRESH_CREDENTIAL_ENABLE);
     }
 
     @Override
@@ -168,12 +177,20 @@ public class RESTCatalog implements Catalog {
 
     @Override
     public FileIO fileIO() {
+        if (fileIORefreshCredentialEnable) {
+            throw new UnsupportedOperationException();
+        }
         return fileIO;
     }
 
     @Override
     public FileIO fileIO(Path path) {
-        return fileIO;
+        try {
+            return FileIO.get(path, CatalogContext.create(options));
+        } catch (IOException e) {
+            LOG.warn("Can not get FileIO from options.");
+            throw new RuntimeException(e);
+        }
     }
 
     @Override
@@ -289,6 +306,7 @@ public class RESTCatalog implements Catalog {
         return CatalogUtils.loadTable(
                 this,
                 identifier,
+                this.fileIO(identifier),
                 this::loadTableMetadata,
                 new RESTSnapshotCommitFactory(catalogLoader()));
     }
@@ -645,4 +663,12 @@ public class RESTCatalog implements Catalog {
 
         return refreshExecutor;
     }
+
+    private FileIO fileIO(Identifier identifier) {
+        if (fileIORefreshCredentialEnable) {
+            return new RefreshCredentialFileIO(
+                    resourcePaths, catalogAuth, options, client, identifier);
+        }
+        return fileIO;
+    }
 }
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalogOptions.java 
b/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalogOptions.java
index 843228fa07..61aed5f703 100644
--- a/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalogOptions.java
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/RESTCatalogOptions.java
@@ -77,4 +77,10 @@ public class RESTCatalogOptions {
                     .stringType()
                     .noDefaultValue()
                     .withDescription("REST Catalog auth token provider path.");
+
+    public static final ConfigOption<Boolean> 
FILE_IO_REFRESH_CREDENTIAL_ENABLE =
+            ConfigOptions.key("file-io-refresh-credential.enabled")
+                    .booleanType()
+                    .defaultValue(false)
+                    .withDescription("Whether to support file io refresh 
credential.");
 }
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/rest/RefreshCredentialFileIO.java 
b/paimon-core/src/main/java/org/apache/paimon/rest/RefreshCredentialFileIO.java
new file mode 100644
index 0000000000..b55486887d
--- /dev/null
+++ 
b/paimon-core/src/main/java/org/apache/paimon/rest/RefreshCredentialFileIO.java
@@ -0,0 +1,148 @@
+/*
+ * 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.paimon.rest;
+
+import org.apache.paimon.catalog.CatalogContext;
+import org.apache.paimon.catalog.Identifier;
+import org.apache.paimon.fs.FileIO;
+import org.apache.paimon.fs.FileStatus;
+import org.apache.paimon.fs.Path;
+import org.apache.paimon.fs.PositionOutputStream;
+import org.apache.paimon.fs.SeekableInputStream;
+import org.apache.paimon.options.CatalogOptions;
+import org.apache.paimon.options.Options;
+import org.apache.paimon.rest.auth.AuthSession;
+import org.apache.paimon.rest.responses.GetTableCredentialsResponse;
+
+import java.io.IOException;
+import java.util.Map;
+
+/** A {@link FileIO} to support refresh credential. */
+public class RefreshCredentialFileIO implements FileIO {
+
+    private static final long serialVersionUID = 1L;
+
+    private final ResourcePaths resourcePaths;
+    private final AuthSession catalogAuth;
+    protected Options options;
+    private final Identifier identifier;
+    private Long expireAtMillis;
+    private Map<String, String> credential;
+    private final transient RESTClient client;
+    private transient volatile FileIO lazyFileIO;
+
+    public RefreshCredentialFileIO(
+            ResourcePaths resourcePaths,
+            AuthSession catalogAuth,
+            Options options,
+            RESTClient client,
+            Identifier identifier) {
+        this.resourcePaths = resourcePaths;
+        this.catalogAuth = catalogAuth;
+        this.options = options;
+        this.identifier = identifier;
+        this.client = client;
+    }
+
+    @Override
+    public void configure(CatalogContext context) {
+        this.options = context.options();
+    }
+
+    @Override
+    public SeekableInputStream newInputStream(Path path) throws IOException {
+        return fileIO().newInputStream(path);
+    }
+
+    @Override
+    public PositionOutputStream newOutputStream(Path path, boolean overwrite) 
throws IOException {
+        return fileIO().newOutputStream(path, overwrite);
+    }
+
+    @Override
+    public FileStatus getFileStatus(Path path) throws IOException {
+        return fileIO().getFileStatus(path);
+    }
+
+    @Override
+    public FileStatus[] listStatus(Path path) throws IOException {
+        return fileIO().listStatus(path);
+    }
+
+    @Override
+    public boolean exists(Path path) throws IOException {
+        return fileIO().exists(path);
+    }
+
+    @Override
+    public boolean delete(Path path, boolean recursive) throws IOException {
+        return fileIO().delete(path, recursive);
+    }
+
+    @Override
+    public boolean mkdirs(Path path) throws IOException {
+        return fileIO().mkdirs(path);
+    }
+
+    @Override
+    public boolean rename(Path src, Path dst) throws IOException {
+        return fileIO().rename(src, dst);
+    }
+
+    @Override
+    public boolean isObjectStore() {
+        try {
+            return fileIO().isObjectStore();
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private FileIO fileIO() throws IOException {
+        if (lazyFileIO == null || shouldRefresh()) {
+            synchronized (this) {
+                if (lazyFileIO == null || shouldRefresh()) {
+                    GetTableCredentialsResponse response = getCredential();
+                    expireAtMillis = response.getExpiresAtMillis();
+                    credential = response.getCredential();
+                    Map<String, String> conf = RESTUtil.merge(options.toMap(), 
credential);
+                    Options updateCredentialOption = new Options(conf);
+                    lazyFileIO =
+                            FileIO.get(
+                                    new 
Path(updateCredentialOption.get(CatalogOptions.WAREHOUSE)),
+                                    
CatalogContext.create(updateCredentialOption));
+                }
+            }
+        }
+        return lazyFileIO;
+    }
+
+    // todo: handle exception
+    private GetTableCredentialsResponse getCredential() {
+        return client.get(
+                resourcePaths.tableCredentials(
+                        identifier.getDatabaseName(), 
identifier.getObjectName()),
+                GetTableCredentialsResponse.class,
+                catalogAuth.getHeaders());
+    }
+
+    private boolean shouldRefresh() {
+        return expireAtMillis != null && expireAtMillis > 
System.currentTimeMillis();
+    }
+}
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/rest/ResourcePaths.java 
b/paimon-core/src/main/java/org/apache/paimon/rest/ResourcePaths.java
index d77475fe40..1e843f99cb 100644
--- a/paimon-core/src/main/java/org/apache/paimon/rest/ResourcePaths.java
+++ b/paimon-core/src/main/java/org/apache/paimon/rest/ResourcePaths.java
@@ -70,6 +70,10 @@ public class ResourcePaths {
         return SLASH.join(V1, prefix, DATABASES, databaseName, TABLES, 
"commit");
     }
 
+    public String tableCredentials(String databaseName, String tableName) {
+        return SLASH.join(V1, prefix, DATABASES, databaseName, TABLES, 
tableName, "credentials");
+    }
+
     public String partitions(String databaseName, String tableName) {
         return SLASH.join(V1, prefix, DATABASES, databaseName, TABLES, 
tableName, "partitions");
     }
diff --git 
a/paimon-core/src/main/java/org/apache/paimon/rest/responses/GetTableCredentialsResponse.java
 
b/paimon-core/src/main/java/org/apache/paimon/rest/responses/GetTableCredentialsResponse.java
new file mode 100644
index 0000000000..2792940ff6
--- /dev/null
+++ 
b/paimon-core/src/main/java/org/apache/paimon/rest/responses/GetTableCredentialsResponse.java
@@ -0,0 +1,60 @@
+/*
+ * 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.paimon.rest.responses;
+
+import org.apache.paimon.rest.RESTResponse;
+
+import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonCreator;
+import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonGetter;
+import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import 
org.apache.paimon.shade.jackson2.com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.Map;
+
+/** Response for table credentials. */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class GetTableCredentialsResponse implements RESTResponse {
+
+    private static final String FIELD_CREDENTIAL = "credential";
+    private static final String FIELD_EXPIREAT_MILLIS = "expiresAtMillis";
+
+    @JsonProperty(FIELD_CREDENTIAL)
+    private final Map<String, String> credential;
+
+    @JsonProperty(FIELD_EXPIREAT_MILLIS)
+    private long expiresAtMillis;
+
+    @JsonCreator
+    public GetTableCredentialsResponse(
+            @JsonProperty(FIELD_EXPIREAT_MILLIS) long expiresAtMillis,
+            @JsonProperty(FIELD_CREDENTIAL) Map<String, String> credential) {
+        this.expiresAtMillis = expiresAtMillis;
+        this.credential = credential;
+    }
+
+    @JsonGetter(FIELD_CREDENTIAL)
+    public Map<String, String> getCredential() {
+        return credential;
+    }
+
+    @JsonGetter(FIELD_EXPIREAT_MILLIS)
+    public long getExpiresAtMillis() {
+        return expiresAtMillis;
+    }
+}
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTMessage.java 
b/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTMessage.java
index d64dd86da7..576f494b88 100644
--- a/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTMessage.java
+++ b/paimon-core/src/test/java/org/apache/paimon/rest/MockRESTMessage.java
@@ -32,6 +32,7 @@ import org.apache.paimon.rest.requests.RenameTableRequest;
 import org.apache.paimon.rest.responses.AlterDatabaseResponse;
 import org.apache.paimon.rest.responses.CreateDatabaseResponse;
 import org.apache.paimon.rest.responses.GetDatabaseResponse;
+import org.apache.paimon.rest.responses.GetTableCredentialsResponse;
 import org.apache.paimon.rest.responses.GetTableResponse;
 import org.apache.paimon.rest.responses.GetViewResponse;
 import org.apache.paimon.rest.responses.ListDatabasesResponse;
@@ -48,6 +49,7 @@ import org.apache.paimon.types.RowType;
 import org.apache.paimon.view.ViewSchema;
 
 import org.apache.paimon.shade.guava30.com.google.common.collect.ImmutableList;
+import org.apache.paimon.shade.guava30.com.google.common.collect.ImmutableMap;
 import org.apache.paimon.shade.guava30.com.google.common.collect.Lists;
 
 import java.util.ArrayList;
@@ -248,6 +250,11 @@ public class MockRESTMessage {
         return new ListViewsResponse(ImmutableList.of("view"));
     }
 
+    public static GetTableCredentialsResponse getTableCredentialsResponse() {
+        return new GetTableCredentialsResponse(
+                System.currentTimeMillis(), ImmutableMap.of("key", "value"));
+    }
+
     private static ViewSchema viewSchema() {
         List<DataField> fields =
                 Arrays.asList(
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java 
b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java
index 7faa853be9..22fde48e99 100644
--- a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java
+++ b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogServer.java
@@ -42,6 +42,7 @@ import 
org.apache.paimon.rest.responses.CreateDatabaseResponse;
 import org.apache.paimon.rest.responses.ErrorResponse;
 import org.apache.paimon.rest.responses.ErrorResponseResourceType;
 import org.apache.paimon.rest.responses.GetDatabaseResponse;
+import org.apache.paimon.rest.responses.GetTableCredentialsResponse;
 import org.apache.paimon.rest.responses.GetTableResponse;
 import org.apache.paimon.rest.responses.GetViewResponse;
 import org.apache.paimon.rest.responses.ListDatabasesResponse;
@@ -63,6 +64,7 @@ import okhttp3.mockwebserver.Dispatcher;
 import okhttp3.mockwebserver.MockResponse;
 import okhttp3.mockwebserver.MockWebServer;
 import okhttp3.mockwebserver.RecordedRequest;
+import org.testcontainers.shaded.com.google.common.collect.ImmutableMap;
 
 import java.io.IOException;
 import java.util.List;
@@ -149,6 +151,10 @@ public class RESTCatalogServer {
                                 resources.length == 3
                                         && "tables".equals(resources[1])
                                         && "commit".equals(resources[2]);
+                        boolean isTableCredentials =
+                                resources.length == 4
+                                        && "tables".equals(resources[1])
+                                        && "credentials".equals(resources[3]);
                         boolean isPartitions =
                                 resources.length == 4
                                         && "tables".equals(resources[1])
@@ -202,6 +208,16 @@ public class RESTCatalogServer {
                         } else if (isPartitions) {
                             String tableName = resources[2];
                             return partitionsApiHandler(catalog, request, 
databaseName, tableName);
+                        } else if (isTableCredentials) {
+                            GetTableCredentialsResponse 
getTableCredentialsResponse =
+                                    new GetTableCredentialsResponse(
+                                            System.currentTimeMillis(),
+                                            ImmutableMap.of("key", "value"));
+                            return new MockResponse()
+                                    .setResponseCode(200)
+                                    .setBody(
+                                            OBJECT_MAPPER.writeValueAsString(
+                                                    
getTableCredentialsResponse));
                         } else if (isTableRename) {
                             return renameTableApiHandler(catalog, request);
                         } else if (isTableCommit) {
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java 
b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java
index d1ce64b6c5..752f492078 100644
--- a/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/rest/RESTCatalogTest.java
@@ -26,6 +26,7 @@ import org.apache.paimon.options.Options;
 import org.apache.paimon.partition.Partition;
 import org.apache.paimon.rest.exceptions.NotAuthorizedException;
 import org.apache.paimon.schema.Schema;
+import org.apache.paimon.table.FileStoreTable;
 import org.apache.paimon.types.DataField;
 import org.apache.paimon.types.DataTypes;
 
@@ -49,12 +50,12 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
 class RESTCatalogTest extends CatalogTestBase {
 
     private RESTCatalogServer restCatalogServer;
+    private String initToken = "init_token";
 
     @BeforeEach
     @Override
     public void setUp() throws Exception {
         super.setUp();
-        String initToken = "init_token";
         restCatalogServer = new RESTCatalogServer(warehouse, initToken);
         restCatalogServer.start();
         Options options = new Options();
@@ -107,6 +108,26 @@ class RESTCatalogTest extends CatalogTestBase {
         assertEquals(0, result.size());
     }
 
+    @Test
+    void testRefreshFileIO() throws Exception {
+        Options options = new Options();
+        options.set(RESTCatalogOptions.URI, restCatalogServer.getUrl());
+        options.set(RESTCatalogOptions.TOKEN, initToken);
+        options.set(RESTCatalogOptions.THREAD_POOL_SIZE, 1);
+        options.set(RESTCatalogOptions.FILE_IO_REFRESH_CREDENTIAL_ENABLE, 
true);
+        this.catalog = new RESTCatalog(CatalogContext.create(options));
+        List<Identifier> identifiers =
+                Lists.newArrayList(
+                        Identifier.create("test_db_a", "test_table_a"),
+                        Identifier.create("test_db_b", "test_table_b"),
+                        Identifier.create("test_db_c", "test_table_c"));
+        for (Identifier identifier : identifiers) {
+            createTable(identifier, Maps.newHashMap(), 
Lists.newArrayList("col1"));
+            FileStoreTable fileStoreTable = (FileStoreTable) 
catalog.getTable(identifier);
+            assertEquals(true, 
fileStoreTable.fileIO().exists(fileStoreTable.location()));
+        }
+    }
+
     @Override
     protected boolean supportsFormatTable() {
         return true;
diff --git 
a/paimon-core/src/test/java/org/apache/paimon/rest/RESTObjectMapperTest.java 
b/paimon-core/src/test/java/org/apache/paimon/rest/RESTObjectMapperTest.java
index fa56f81118..4c3b622a8c 100644
--- a/paimon-core/src/test/java/org/apache/paimon/rest/RESTObjectMapperTest.java
+++ b/paimon-core/src/test/java/org/apache/paimon/rest/RESTObjectMapperTest.java
@@ -32,6 +32,7 @@ import org.apache.paimon.rest.responses.ConfigResponse;
 import org.apache.paimon.rest.responses.CreateDatabaseResponse;
 import org.apache.paimon.rest.responses.ErrorResponse;
 import org.apache.paimon.rest.responses.GetDatabaseResponse;
+import org.apache.paimon.rest.responses.GetTableCredentialsResponse;
 import org.apache.paimon.rest.responses.GetTableResponse;
 import org.apache.paimon.rest.responses.GetViewResponse;
 import org.apache.paimon.rest.responses.ListDatabasesResponse;
@@ -273,4 +274,14 @@ public class RESTObjectMapperTest {
         ListViewsResponse parseData = OBJECT_MAPPER.readValue(responseStr, 
ListViewsResponse.class);
         assertEquals(response.getViews(), parseData.getViews());
     }
+
+    @Test
+    public void getTableCredentialsResponseParseTest() throws Exception {
+        GetTableCredentialsResponse response = 
MockRESTMessage.getTableCredentialsResponse();
+        String responseStr = OBJECT_MAPPER.writeValueAsString(response);
+        GetTableCredentialsResponse parseData =
+                OBJECT_MAPPER.readValue(responseStr, 
GetTableCredentialsResponse.class);
+        assertEquals(response.getCredential(), parseData.getCredential());
+        assertEquals(response.getExpiresAtMillis(), 
parseData.getExpiresAtMillis());
+    }
 }
diff --git a/paimon-open-api/rest-catalog-open-api.yaml 
b/paimon-open-api/rest-catalog-open-api.yaml
index 02ea7de8d0..128514d7a5 100644
--- a/paimon-open-api/rest-catalog-open-api.yaml
+++ b/paimon-open-api/rest-catalog-open-api.yaml
@@ -432,6 +432,43 @@ paths:
                 $ref: '#/components/schemas/ErrorResponse'
         "500":
           description: Internal Server Error
+  /v1/{prefix}/databases/{database}/tables/{table}/credentials:
+    get:
+      tags:
+        - table
+      summary: List credentials
+      operationId: listCredentials
+      parameters:
+        - name: prefix
+          in: path
+          required: true
+          schema:
+            type: string
+        - name: database
+          in: path
+          required: true
+          schema:
+            type: string
+        - name: table
+          in: path
+          required: true
+          schema:
+            type: string
+      responses:
+        "200":
+          description: OK
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/GetTableCredentialsResponse'
+        "404":
+          description: Resource not found
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/ErrorResponse'
+        "500":
+          description: Internal Server Error
   /v1/{prefix}/databases/{database}/tables/{table}/partitions:
     get:
       tags:
@@ -1166,6 +1203,16 @@ components:
       properties:
         success:
           type: boolean
+    GetTableCredentialsResponse:
+      type: object
+      properties:
+        expiresAt:
+          type: integer
+          format: int64
+        credentials:
+          type: object
+          additionalProperties:
+            type: string
     AlterDatabaseRequest:
       type: object
       properties:
diff --git 
a/paimon-open-api/src/main/java/org/apache/paimon/open/api/RESTCatalogController.java
 
b/paimon-open-api/src/main/java/org/apache/paimon/open/api/RESTCatalogController.java
index e657407a47..c8eae97dec 100644
--- 
a/paimon-open-api/src/main/java/org/apache/paimon/open/api/RESTCatalogController.java
+++ 
b/paimon-open-api/src/main/java/org/apache/paimon/open/api/RESTCatalogController.java
@@ -37,6 +37,7 @@ import org.apache.paimon.rest.responses.ConfigResponse;
 import org.apache.paimon.rest.responses.CreateDatabaseResponse;
 import org.apache.paimon.rest.responses.ErrorResponse;
 import org.apache.paimon.rest.responses.GetDatabaseResponse;
+import org.apache.paimon.rest.responses.GetTableCredentialsResponse;
 import org.apache.paimon.rest.responses.GetTableResponse;
 import org.apache.paimon.rest.responses.GetViewResponse;
 import org.apache.paimon.rest.responses.ListDatabasesResponse;
@@ -49,6 +50,7 @@ import org.apache.paimon.types.RowType;
 import org.apache.paimon.view.ViewSchema;
 
 import org.apache.paimon.shade.guava30.com.google.common.collect.ImmutableList;
+import org.apache.paimon.shade.guava30.com.google.common.collect.ImmutableMap;
 import org.apache.paimon.shade.guava30.com.google.common.collect.Lists;
 
 import io.swagger.v3.oas.annotations.Operation;
@@ -355,6 +357,32 @@ public class RESTCatalogController {
         return new CommitTableResponse(true);
     }
 
+    @Operation(
+            summary = "List credentials",
+            tags = {"table"})
+    @ApiResponses({
+        @ApiResponse(
+                responseCode = "200",
+                content = {
+                    @Content(schema = @Schema(implementation = 
GetTableCredentialsResponse.class))
+                }),
+        @ApiResponse(
+                responseCode = "404",
+                description = "Resource not found",
+                content = {@Content(schema = @Schema(implementation = 
ErrorResponse.class))}),
+        @ApiResponse(
+                responseCode = "500",
+                content = {@Content(schema = @Schema())})
+    })
+    @GetMapping("/v1/{prefix}/databases/{database}/tables/{table}/credentials")
+    public GetTableCredentialsResponse listCredentials(
+            @PathVariable String prefix,
+            @PathVariable String database,
+            @PathVariable String table) {
+        return new GetTableCredentialsResponse(
+                System.currentTimeMillis(), ImmutableMap.of("key", "value"));
+    }
+
     @Operation(
             summary = "List partitions",
             tags = {"partition"})

Reply via email to