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

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 945f09ae4 fix(catalog): reject a cloud catalog call whose credential 
is unknown or mismatched (#6509)
945f09ae4 is described below

commit 945f09ae4cfad818d94e67ff3b79cf831c7412e4
Author: Apulupie <[email protected]>
AuthorDate: Thu Oct 15 18:13:14 2026 +0800

    fix(catalog): reject a cloud catalog call whose credential is unknown or 
mismatched (#6509)
    
    Both cloud catalog services accepted a credential id verbatim without 
resolving it:
    `TencentCatalogService.listRegions` answered with the full static region 
list for an id
    that does not exist, and every catalog entry point of both vendors answered 
for a
    credential belonging to the other vendor. The mismatch only surfaced on the 
next call as
    the cloud SDK's "Cloud credential is invalid", and a deleted credential 
still produced a
    region list, so the create-instance wizard rendered a region picker the 
server could not
    use.
    
    `InstanceService.importCloudInstances` already rejects both cases (404 for 
an unknown id,
    400 for a vendor mismatch); the catalog entry points now perform the same 
checks against
    `CloudCredentialRepository` before any cloud call.
---
 .../provider/alibaba/AliyunCatalogService.java     | 29 ++++++++---
 .../provider/tencent/TencentCatalogService.java    | 28 ++++++++---
 .../provider/alibaba/AliyunCatalogServiceTest.java | 56 +++++++++++++++++++++-
 .../tencent/TencentCatalogServiceTest.java         | 55 ++++++++++++++++++++-
 4 files changed, 154 insertions(+), 14 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunCatalogService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunCatalogService.java
index 561a85b28..0d7bbcb0c 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunCatalogService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/alibaba/AliyunCatalogService.java
@@ -32,6 +32,8 @@ import 
org.apache.rocketmq.studio.provider.CloudCatalogProvider;
 import org.apache.rocketmq.studio.provider.CloudInstanceDetailVO;
 import org.apache.rocketmq.studio.provider.CloudInstanceOptionVO;
 import org.apache.rocketmq.studio.provider.CloudRegionVO;
+import 
org.apache.rocketmq.studio.provider.credential.CloudCredentialRepository;
+import org.apache.rocketmq.studio.provider.credential.CloudCredentialVO;
 import org.springframework.stereotype.Component;
 import lombok.RequiredArgsConstructor;
 
@@ -53,6 +55,7 @@ public class AliyunCatalogService implements 
CloudCatalogProvider {
     static final String DEFAULT_REGION = "cn-hangzhou";
 
     private final AliyunClientFactory clientFactory;
+    private final CloudCredentialRepository credentialRepository;
 
     @Override
     public InstanceVendor vendor() {
@@ -61,7 +64,7 @@ public class AliyunCatalogService implements 
CloudCatalogProvider {
 
     @Override
     public List<CloudRegionVO> listRegions(Long credentialId) {
-        requireId(credentialId, "credentialId");
+        requireCredential(credentialId);
         ListRegionsResponse response = clientFactory.call(credentialId, 
DEFAULT_REGION,
                 client -> 
client.listRegions(ListRegionsRequest.builder().build()));
         ListRegionsResponseBody body = 
AliyunResponseValidator.requireReadableBody(
@@ -85,7 +88,7 @@ public class AliyunCatalogService implements 
CloudCatalogProvider {
 
     @Override
     public List<CloudInstanceOptionVO> listCloudInstances(Long credentialId, 
String regionId, String search) {
-        requireId(credentialId, "credentialId");
+        requireCredential(credentialId);
         requireNonBlank(regionId, "regionId");
         String normalizedRegionId = regionId.strip();
         String normalizedSearch = search == null ? null : search.strip();
@@ -105,7 +108,7 @@ public class AliyunCatalogService implements 
CloudCatalogProvider {
 
     @Override
     public CloudInstanceDetailVO getCloudInstance(Long credentialId, String 
regionId, String cloudInstanceId) {
-        requireId(credentialId, "credentialId");
+        requireCredential(credentialId);
         requireNonBlank(regionId, "regionId");
         requireNonBlank(cloudInstanceId, "cloudInstanceId");
         String normalizedRegionId = regionId.strip();
@@ -174,9 +177,23 @@ public class AliyunCatalogService implements 
CloudCatalogProvider {
         }
     }
 
-    private static void requireId(Long value, String name) {
-        if (value == null) {
-            throw new BusinessException(400, name + " is required");
+    /**
+     * Resolves the credential the catalog request is scoped to before any 
cloud call. The
+     * endpoints take a credential id verbatim, so without this an id that no 
longer exists (a
+     * deleted credential, a stale wizard) relied on the SDK client factory's 
own 404, and a
+     * credential of the other vendor only surfaced as the cloud SDK's "Cloud 
credential is
+     * invalid" on the next call. Matches the checks {@code 
InstanceService.importCloudInstances}
+     * performs.
+     */
+    private void requireCredential(Long credentialId) {
+        if (credentialId == null) {
+            throw new BusinessException(400, "credentialId is required");
+        }
+        CloudCredentialVO credential = 
credentialRepository.findById(credentialId)
+                .orElseThrow(() -> new BusinessException(404, "Cloud 
credential not found: " + credentialId));
+        if (credential.getVendor() != InstanceVendor.ALIYUN) {
+            throw new BusinessException(400,
+                    "Cloud credential vendor does not match " + 
InstanceVendor.ALIYUN);
         }
     }
 }
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentCatalogService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentCatalogService.java
index f06be61e2..29b28c12b 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentCatalogService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/tencent/TencentCatalogService.java
@@ -28,6 +28,8 @@ import 
org.apache.rocketmq.studio.provider.CloudCatalogProvider;
 import org.apache.rocketmq.studio.provider.CloudInstanceDetailVO;
 import org.apache.rocketmq.studio.provider.CloudInstanceOptionVO;
 import org.apache.rocketmq.studio.provider.CloudRegionVO;
+import 
org.apache.rocketmq.studio.provider.credential.CloudCredentialRepository;
+import org.apache.rocketmq.studio.provider.credential.CloudCredentialVO;
 import org.springframework.stereotype.Component;
 import lombok.RequiredArgsConstructor;
 
@@ -65,6 +67,7 @@ public class TencentCatalogService implements 
CloudCatalogProvider {
             region("sa-saopaulo", "Sao Paulo"));
 
     private final TencentClientFactory clientFactory;
+    private final CloudCredentialRepository credentialRepository;
 
     @Override
     public InstanceVendor vendor() {
@@ -73,13 +76,13 @@ public class TencentCatalogService implements 
CloudCatalogProvider {
 
     @Override
     public List<CloudRegionVO> listRegions(Long credentialId) {
-        requireId(credentialId, "credentialId");
+        requireCredential(credentialId);
         return SUPPORTED_REGIONS;
     }
 
     @Override
     public List<CloudInstanceOptionVO> listCloudInstances(Long credentialId, 
String regionId, String search) {
-        requireId(credentialId, "credentialId");
+        requireCredential(credentialId);
         requireNonBlank(regionId, "regionId");
         List<CloudInstanceOptionVO> instances = new ArrayList<>();
         for (long offset = 0L; ; offset += PAGE_SIZE) {
@@ -129,7 +132,7 @@ public class TencentCatalogService implements 
CloudCatalogProvider {
 
     @Override
     public CloudInstanceDetailVO getCloudInstance(Long credentialId, String 
regionId, String cloudInstanceId) {
-        requireId(credentialId, "credentialId");
+        requireCredential(credentialId);
         requireNonBlank(regionId, "regionId");
         requireNonBlank(cloudInstanceId, "cloudInstanceId");
         DescribeInstanceRequest request = new DescribeInstanceRequest();
@@ -219,9 +222,22 @@ public class TencentCatalogService implements 
CloudCatalogProvider {
         return region;
     }
 
-    private static void requireId(Long value, String name) {
-        if (value == null) {
-            throw new BusinessException(400, name + " is required");
+    /**
+     * Resolves the credential the catalog request is scoped to before any 
cloud call. The
+     * endpoints take a credential id verbatim, so without this an id that no 
longer exists (a
+     * deleted credential, a stale wizard) still produced a region list, and a 
credential of the
+     * other vendor only surfaced as the cloud SDK's "Cloud credential is 
invalid" on the next
+     * call. Matches the checks {@code InstanceService.importCloudInstances} 
performs.
+     */
+    private void requireCredential(Long credentialId) {
+        if (credentialId == null) {
+            throw new BusinessException(400, "credentialId is required");
+        }
+        CloudCredentialVO credential = 
credentialRepository.findById(credentialId)
+                .orElseThrow(() -> new BusinessException(404, "Cloud 
credential not found: " + credentialId));
+        if (credential.getVendor() != InstanceVendor.TENCENT) {
+            throw new BusinessException(400,
+                    "Cloud credential vendor does not match " + 
InstanceVendor.TENCENT);
         }
     }
 
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/alibaba/AliyunCatalogServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/alibaba/AliyunCatalogServiceTest.java
index ab82d0355..4e5fbd57b 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/alibaba/AliyunCatalogServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/alibaba/AliyunCatalogServiceTest.java
@@ -22,10 +22,13 @@ import 
com.aliyun.sdk.service.rocketmq20220801.models.ListInstancesResponse;
 import 
com.aliyun.sdk.service.rocketmq20220801.models.ListInstancesResponseBody;
 import com.aliyun.sdk.service.rocketmq20220801.models.ListRegionsResponse;
 import com.aliyun.sdk.service.rocketmq20220801.models.ListRegionsResponseBody;
+import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
 import org.apache.rocketmq.studio.common.exception.BusinessException;
 import org.apache.rocketmq.studio.provider.CloudInstanceDetailVO;
 import org.apache.rocketmq.studio.provider.CloudInstanceOptionVO;
 import org.apache.rocketmq.studio.provider.CloudRegionVO;
+import 
org.apache.rocketmq.studio.provider.credential.CloudCredentialRepository;
+import org.apache.rocketmq.studio.provider.credential.CloudCredentialVO;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -35,13 +38,16 @@ import org.mockito.junit.jupiter.MockitoExtension;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.Optional;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.lenient;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 
 @ExtendWith(MockitoExtension.class)
@@ -53,11 +59,59 @@ class AliyunCatalogServiceTest {
     @Mock
     private AliyunClientFactory clientFactory;
 
+    @Mock
+    private CloudCredentialRepository credentialRepository;
+
     private AliyunCatalogService service;
 
     @BeforeEach
     void setUp() {
-        service = new AliyunCatalogService(clientFactory);
+        service = new AliyunCatalogService(clientFactory, 
credentialRepository);
+        lenient().when(credentialRepository.findById(CREDENTIAL_ID))
+                .thenReturn(Optional.of(credential(CREDENTIAL_ID, 
InstanceVendor.ALIYUN)));
+    }
+
+    private static CloudCredentialVO credential(Long id, InstanceVendor 
vendor) {
+        CloudCredentialVO credential = new CloudCredentialVO();
+        credential.setId(id);
+        credential.setVendor(vendor);
+        return credential;
+    }
+
+    @Test
+    void listRegionsShouldRejectAnUnknownCredentialTest() {
+        when(credentialRepository.findById(99L)).thenReturn(Optional.empty());
+
+        assertThatThrownBy(() -> service.listRegions(99L))
+                .isInstanceOfSatisfying(BusinessException.class, exception -> {
+                    assertThat(exception.getCode()).isEqualTo(404);
+                    assertThat(exception.getMessage()).contains("Cloud 
credential not found", "99");
+                });
+        verifyNoInteractions(clientFactory);
+    }
+
+    @Test
+    void listRegionsShouldRejectACredentialOfTheOtherVendorTest() {
+        when(credentialRepository.findById(CREDENTIAL_ID))
+                .thenReturn(Optional.of(credential(CREDENTIAL_ID, 
InstanceVendor.TENCENT)));
+
+        assertThatThrownBy(() -> service.listRegions(CREDENTIAL_ID))
+                .isInstanceOfSatisfying(BusinessException.class, exception -> {
+                    assertThat(exception.getCode()).isEqualTo(400);
+                    assertThat(exception.getMessage()).contains("does not 
match", "ALIYUN");
+                });
+        verifyNoInteractions(clientFactory);
+    }
+
+    @Test
+    void listCloudInstancesShouldRejectACredentialOfTheOtherVendorTest() {
+        when(credentialRepository.findById(CREDENTIAL_ID))
+                .thenReturn(Optional.of(credential(CREDENTIAL_ID, 
InstanceVendor.TENCENT)));
+
+        assertThatThrownBy(() -> service.listCloudInstances(CREDENTIAL_ID, 
REGION, null))
+                .isInstanceOfSatisfying(BusinessException.class,
+                        exception -> 
assertThat(exception.getCode()).isEqualTo(400));
+        verifyNoInteractions(clientFactory);
     }
 
     @Test
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentCatalogServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentCatalogServiceTest.java
index 3ed018119..7e4d71da3 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentCatalogServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/tencent/TencentCatalogServiceTest.java
@@ -22,10 +22,13 @@ import 
com.tencentcloudapi.trocket.v20230308.models.DescribeInstanceResponse;
 import com.tencentcloudapi.trocket.v20230308.models.Endpoint;
 import com.tencentcloudapi.trocket.v20230308.models.InstanceItem;
 import com.tencentcloudapi.trocket.v20230308.TrocketClient;
+import org.apache.rocketmq.studio.common.domain.enums.InstanceVendor;
 import org.apache.rocketmq.studio.common.exception.BusinessException;
 import org.apache.rocketmq.studio.provider.CloudInstanceDetailVO;
 import org.apache.rocketmq.studio.provider.CloudInstanceOptionVO;
 import org.apache.rocketmq.studio.provider.CloudRegionVO;
+import 
org.apache.rocketmq.studio.provider.credential.CloudCredentialRepository;
+import org.apache.rocketmq.studio.provider.credential.CloudCredentialVO;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.ExtendWith;
@@ -33,6 +36,7 @@ import org.mockito.Mock;
 import org.mockito.junit.jupiter.MockitoExtension;
 
 import java.util.List;
+import java.util.Optional;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -41,6 +45,7 @@ import static org.mockito.ArgumentMatchers.eq;
 import static org.mockito.Mockito.lenient;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 import static org.mockito.Mockito.when;
 
 @ExtendWith(MockitoExtension.class)
@@ -52,6 +57,9 @@ class TencentCatalogServiceTest {
     @Mock
     private TencentClientFactory clientFactory;
 
+    @Mock
+    private CloudCredentialRepository credentialRepository;
+
     @Mock
     private TrocketClient client;
 
@@ -59,13 +67,58 @@ class TencentCatalogServiceTest {
 
     @BeforeEach
     void setUp() {
-        service = new TencentCatalogService(clientFactory);
+        service = new TencentCatalogService(clientFactory, 
credentialRepository);
+        lenient().when(credentialRepository.findById(CREDENTIAL_ID))
+                .thenReturn(Optional.of(credential(CREDENTIAL_ID, 
InstanceVendor.TENCENT)));
         lenient().when(clientFactory.call(eq(CREDENTIAL_ID), eq(REGION), 
any())).thenAnswer(invocation -> {
             TencentClientFactory.TencentCall<Object> action = 
invocation.getArgument(2);
             return action.execute(client);
         });
     }
 
+    private static CloudCredentialVO credential(Long id, InstanceVendor 
vendor) {
+        CloudCredentialVO credential = new CloudCredentialVO();
+        credential.setId(id);
+        credential.setVendor(vendor);
+        return credential;
+    }
+
+    @Test
+    void listRegionsShouldRejectAnUnknownCredentialTest() {
+        when(credentialRepository.findById(99L)).thenReturn(Optional.empty());
+
+        assertThatThrownBy(() -> service.listRegions(99L))
+                .isInstanceOfSatisfying(BusinessException.class, exception -> {
+                    assertThat(exception.getCode()).isEqualTo(404);
+                    assertThat(exception.getMessage()).contains("Cloud 
credential not found", "99");
+                });
+        verifyNoInteractions(clientFactory);
+    }
+
+    @Test
+    void listRegionsShouldRejectACredentialOfTheOtherVendorTest() {
+        when(credentialRepository.findById(CREDENTIAL_ID))
+                .thenReturn(Optional.of(credential(CREDENTIAL_ID, 
InstanceVendor.ALIYUN)));
+
+        assertThatThrownBy(() -> service.listRegions(CREDENTIAL_ID))
+                .isInstanceOfSatisfying(BusinessException.class, exception -> {
+                    assertThat(exception.getCode()).isEqualTo(400);
+                    assertThat(exception.getMessage()).contains("does not 
match", "TENCENT");
+                });
+        verifyNoInteractions(clientFactory);
+    }
+
+    @Test
+    void listCloudInstancesShouldRejectACredentialOfTheOtherVendorTest() {
+        when(credentialRepository.findById(CREDENTIAL_ID))
+                .thenReturn(Optional.of(credential(CREDENTIAL_ID, 
InstanceVendor.ALIYUN)));
+
+        assertThatThrownBy(() -> service.listCloudInstances(CREDENTIAL_ID, 
REGION, null))
+                .isInstanceOfSatisfying(BusinessException.class,
+                        exception -> 
assertThat(exception.getCode()).isEqualTo(400));
+        verifyNoInteractions(clientFactory);
+    }
+
     @Test
     void listRegionsShouldContainAllSupportedRocketmq5RegionsTest() {
         List<CloudRegionVO> regions = service.listRegions(CREDENTIAL_ID);

Reply via email to