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