Skip to content

feat(openapi): cluster domain service impl, controller migration and tests#5472

Merged
hezhangjian merged 1 commit into
apolloconfig:masterfrom
tacklequestions:pr/a05-cluster
Oct 19, 2025
Merged

feat(openapi): cluster domain service impl, controller migration and tests#5472
hezhangjian merged 1 commit into
apolloconfig:masterfrom
tacklequestions:pr/a05-cluster

Conversation

@tacklequestions
Copy link
Copy Markdown
Member

@tacklequestions tacklequestions commented Sep 26, 2025

What's the purpose of this PR

Refactor the Cluster domain end-to-end on top of generated OpenAPI models: add a server-side OpenAPI service interface and implementation for Cluster,
migrate the OpenAPI ClusterController to use it, and add/update tests. Keep the legacy service bean (named) to avoid Java 8 bean conflicts.

Which issue(s) this PR fixes:

Fixes #5462

Brief changelog

  • Add server-side interface: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ClusterOpenApiService.java

  • Add implementation: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/impl/ServerClusterOpenApiService.java

    • Use OpenApiModelConverters for DTO conversions
  • Migrate controller: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterController.java

  • Add tests: apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterControllerTest.java

  • Retain legacy service: apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerClusterOpenApiService.java with
    @service("ServerClusterOpenApiServiceOld") to ensure Java 8 build compatibility

  • No changes to OpenApiBeanUtils; scope limited to Cluster domain; base on PR03 (converters)

  • openapi: GET /openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName} → portal: GET /apps/{appId}/envs/{env}/clusters/{clusterName} — 查询指定集群详情(original openapi) (apollo-portal/target/generated-sources/openapi/
    src/main/java/com/ctrip/framework/apollo/openapi/api/ClusterManagementApi.java:154, apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ClusterController.java:68)

  • openapi: POST /openapi/v1/envs/{env}/apps/{appId}/clusters → portal: POST /apps/{appId}/envs/{env}/clusters — 创建集群(original openapi) (apollo-portal/target/generated-sources/openapi/src/main/java/com/ctrip/framework/
    apollo/openapi/api/ClusterManagementApi.java:45, apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ClusterController.java:47)

  • openapi: DELETE /openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName} → portal: DELETE /apps/{appId}/envs/{env}/clusters/{clusterName} — 删除集群 (apollo-portal/target/generated-sources/openapi/src/main/java/com/ctrip/framework/apollo/openapi/api/ClusterManagementApi.java:112, apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/
    ClusterController.java:59)

Summary by CodeRabbit

  • New Features

    • Added DELETE cluster endpoint requiring an operator, returning 200 OK with audit logging and authorization.
  • Refactor

    • Standardized HTTP responses for get/create cluster APIs (consistent response wrappers and status codes).
    • API surface updated to use unified OpenAPI models and interface-based routing; strengthened validation for appId, cluster name, and operator with tighter audit tracing.
  • Tests

    • Added unit and parameter-binding tests for get, create, and delete cluster endpoints covering permissions, validation, and response payloads.

@dosubot dosubot Bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label Sep 26, 2025
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Sep 26, 2025

Walkthrough

Controller endpoints were refactored to implement a generated API interface and return ResponseEntity; DTO conversions switched from OpenApiBeanUtils to OpenApiModelConverters; a deleteCluster operation was added to controller, server service interface and implementation; unit and parameter-binding tests for GET/POST/DELETE were added.

Changes

Cohort / File(s) Summary
Controller API refactor
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterController.java
Implements ClusterManagementApi; methods now return ResponseEntity<OpenClusterDTO>; parameter ordering/validation adjusted; integrates audit logging and pre-authorization; adds deleteCluster endpoint with operator validation.
Server OpenAPI service impl
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerClusterOpenApiService.java
Replaces OpenApiBeanUtils with OpenApiModelConverters for DTO conversions in getCluster/createCluster; adds deleteCluster(String env, String appId, String clusterName) delegating to core clusterService.
New service interface
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ClusterOpenApiService.java
Adds ClusterOpenApiService interface declaring getCluster, createCluster, and deleteCluster using OpenClusterDTO.
Controller tests (unit & binding)
apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterControllerTest.java, apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterControllerParamBindLowLevelTest.java
Adds tests exercising GET/POST/DELETE flows, authentication/authorization stubs, appId mismatch validation, and ArgumentCaptor verification of path/body/query binding to service layer.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Client
  participant Controller as ClusterController
  participant Service as ClusterOpenApiService
  participant Impl as ServerClusterOpenApiService
  participant Core as ClusterService

  rect rgb(235,245,255)
    Note over Controller: getCluster / createCluster (DTO conversion)
    Client->>Controller: HTTP GET/POST (env, appId, clusterName / OpenClusterDTO)
    Controller->>Service: getCluster/createCluster(...)
    Service->>Impl: delegate
    Impl->>Core: read/create ClusterDTO
    Core-->>Impl: ClusterDTO
    Impl-->>Service: OpenClusterDTO (via OpenApiModelConverters)
    Service-->>Controller: OpenClusterDTO
    Controller-->>Client: 200 OK (ResponseEntity)
  end
Loading
sequenceDiagram
  autonumber
  actor Client
  participant Controller as ClusterController
  participant Auth as PermissionValidator/UserService
  participant Service as ClusterOpenApiService
  participant Impl as ServerClusterOpenApiService
  participant Core as ClusterService

  rect rgb(255,245,235)
    Note over Controller: deleteCluster (audit + auth)
    Client->>Controller: HTTP DELETE (env, appId, clusterName, operator)
    Controller->>Auth: validate operator/authz
    alt authorized
      Controller->>Service: deleteCluster(env, appId, clusterName)
      Service->>Impl: delegate
      Impl->>Core: deleteCluster(...)
      Core-->>Impl: void
      Impl-->>Service: void
      Service-->>Controller: void
      Controller-->>Client: 200 OK
    else unauthorized
      Controller-->>Client: 403/4xx
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

size:XL, lgtm

Suggested reviewers

  • arrow1991
  • hezhangjian

Poem

I hop through code with eager paws,
New routes align and follow laws.
Converters leap, old beans retire,
Delete now lands when callers require.
Tests munch paths — the warren's wired. 🐇

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.55% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (4 passed)
Check name Status Explanation
Title Check ✅ Passed The pull request title "feat(openapi): cluster domain service impl, controller migration and tests" directly and clearly summarizes the main changes in the changeset. It specifies that the work involves implementing a cluster domain service, migrating the controller, and adding tests—all of which are accurately reflected in the file changes. The title is concise, uses descriptive terms rather than vague language, and provides a teammate scanning the history with meaningful information about what was refactored.
Linked Issues Check ✅ Passed The pull request addresses the core objectives from linked issue #5462, which calls for standardizing Portal API design to follow resource-oriented RESTful principles. The changeset implements key requirements including: explicit HTTP methods (GET, POST, DELETE) on resourceful paths; consistent use of DTOs (OpenClusterDTO) for parameters and responses; unified response formats using ResponseEntity instead of raw return types; and addition of auditing and operator validation. The new ClusterManagementApi interface and controller implementation reflect the standardization goals. While the issue also includes requirements around pagination, validation standardization, and unified authorization error handling that extend beyond the cluster scope, the PR successfully delivers the refactoring objectives specific to the Cluster domain that were outlined in the linked issue.
Out of Scope Changes Check ✅ Passed All changes in the pull request remain within the stated scope of refactoring the Cluster domain. The modifications are focused on: adding the server-side ClusterOpenApiService interface and ServerClusterOpenApiService implementation; migrating ClusterController to use the new service with ResponseEntity-wrapped responses; and adding comprehensive tests (ClusterControllerTest and ClusterControllerParamBindLowLevelTest). No changes are introduced to other domains, OpenApiBeanUtils, or unrelated functionality, and the use of OpenApiModelConverters aligns with the stated objective of leveraging converters from a prior PR. The addition of audit logging and parameter validation in deleteCluster are consistent with the standardization goals.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/springsecurity/SpringSecurityUserInfoHolder.java (1)

112-120: Guard against null Authentication to avoid NPE

SecurityContext or Authentication can be null; calling getPrincipal unguarded risks NPE on unauthenticated requests.

Apply:

-  private String getCurrentUsername() {
-    Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
+  private String getCurrentUsername() {
+    var context = SecurityContextHolder.getContext();
+    if (context == null || context.getAuthentication() == null) {
+      return "anonymousUser";
+    }
+    Object principal = context.getAuthentication().getPrincipal();
     if (principal instanceof UserDetails) {
       return ((UserDetails) principal).getUsername();
     }
     if (principal instanceof Principal) {
       return ((Principal) principal).getName();
     }
     return String.valueOf(principal);
   }
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterController.java (1)

39-47: Ambiguous ClusterOpenApiService injection in ClusterController: two beans implement ClusterOpenApiService (legacy ServerClusterOpenApiServiceOld and new ServerClusterOpenApiService), causing NoUniqueBeanDefinitionException at runtime; annotate the intended implementation with @primary or inject with @qualifier.

🧹 Nitpick comments (21)
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/springsecurity/SpringSecurityUserInfoHolder.java (2)

102-105: Don’t swallow exceptions silently; log at debug

Silent fallback makes diagnosing auth propagation issues hard. Log at debug/trace.

Apply:

-    } catch (Exception e) {
-      // 如果获取Consumer信息失败,返回null,让系统回退到默认方式
-      return null;
-    }
+    } catch (Exception e) {
+      // 如果获取Consumer信息失败,返回null,让系统回退到默认方式
+      if (logger.isDebugEnabled()) {
+        logger.debug("Failed to resolve OpenAPI Consumer from request, falling back to Spring Security principal.", e);
+      }
+      return null;
+    }

Add logger (outside this range):

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SpringSecurityUserInfoHolder implements UserInfoHolder {
  private static final Logger logger = LoggerFactory.getLogger(SpringSecurityUserInfoHolder.class);
  // ...
}

65-68: Comment language consistency

Project code/comments are predominantly in English. Consider switching these method comments to English for consistency.

apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ClusterOpenApiService.java (1)

25-29: Consider adding Javadoc for interface methods

While the interface is well-structured, adding Javadoc comments for each method would improve API documentation and clarify expected behavior, especially regarding:

  • Parameter validation requirements
  • Return value semantics (e.g., when getCluster returns null)
  • Exception handling expectations
 public interface ClusterOpenApiService {
 
+  /**
+   * Retrieves cluster information.
+   * @param appId application ID
+   * @param env environment name
+   * @param clusterName cluster name
+   * @return cluster DTO or null if not found
+   */
   OpenClusterDTO getCluster(String appId, String env, String clusterName);
 
+  /**
+   * Creates a new cluster.
+   * @param env environment name
+   * @param openClusterDTO cluster data to create
+   * @return created cluster DTO
+   * @throws IllegalArgumentException if validation fails
+   */
   OpenClusterDTO createCluster(String env, OpenClusterDTO openClusterDTO);
 
+  /**
+   * Deletes a cluster.
+   * @param env environment name
+   * @param appId application ID
+   * @param clusterName cluster name
+   * @throws NotFoundException if cluster doesn't exist
+   */
   void deleteCluster(String env, String appId, String clusterName);
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/AuthConfiguration.java (2)

22-22: Remove unnecessary empty line

There's an extra empty line after imports that can be removed for consistency.

 import java.util.Collections;
-
 import javax.persistence.EntityManagerFactory;

112-114: Document the use of @lazy for ConsumerService

The @Lazy annotation is used for ConsumerService injection in both auth and ldap profiles. Consider adding a comment explaining why lazy initialization is needed here (likely to avoid circular dependencies).

   @Bean
   @ConditionalOnMissingBean(UserInfoHolder.class)
+  // @Lazy is used to avoid circular dependency with ConsumerService initialization
   public UserInfoHolder springSecurityUserInfoHolder(UserService userService, @Lazy ConsumerService consumerService) {
     return new SpringSecurityUserInfoHolder(userService, consumerService);
   }

Also applies to: 216-218

apollo-portal/pom.xml (2)

140-172: Consider adding skip configuration for OpenAPI generation

Add a property to skip OpenAPI generation during development builds to improve build performance.

 <properties>
   <apollo.openapi.spec.url>https://raw.githubusercontent.com/tacklequestions/apollo-openapi/main/apollo-openapi.yaml</apollo.openapi.spec.url>
+  <openapi.skip>false</openapi.skip>
   <github.path>${project.artifactId}</github.path>

And in the plugin configuration:

 <configuration>
   <inputSpec>${apollo.openapi.spec.url}</inputSpec>
+  <skip>${openapi.skip}</skip>
   <generatorName>spring</generatorName>

271-273: Redundant source path in comment

The comment mentions the path that's already clear from the configuration below.

-<!-- 注意这层 src/main/java -->
+<!-- Add generated OpenAPI sources to compilation -->
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/util/OpenApiModelConverters.java (4)

42-45: Improve class-level documentation

The current Javadoc could be more descriptive about the purpose and usage patterns of this converter class.

 /**
- * Non-invasive converters for OpenAPI generated model classes.
- * This class mirrors/OpenApiBeanUtils functions but targets com.ctrip.framework.apollo.openapi.model.* types.
+ * Utility class for converting between internal Apollo domain objects and OpenAPI generated model classes.
+ * 
+ * <p>This class provides bidirectional conversion methods for all OpenAPI model types used in the
+ * Apollo Portal OpenAPI implementation. It handles null checks, collection transformations, and
+ * maintains data integrity during conversions.</p>
+ * 
+ * <p>Note: This class parallels OpenApiBeanUtils functionality but specifically targets 
+ * com.ctrip.framework.apollo.openapi.model.* types generated from the OpenAPI specification.</p>
  */

115-124: Inefficient collection conversion in fromReleaseBO

The conversion from Set to List for items could be simplified using streams.

-    Set<com.ctrip.framework.apollo.portal.entity.bo.KVEntity> items = releaseBO.getItems();
-    List<KVEntity> itemsList = new ArrayList<>();
-    if (!CollectionUtils.isEmpty(items)) {
-      for (com.ctrip.framework.apollo.portal.entity.bo.KVEntity item : items) {
-        KVEntity kvEntity = new KVEntity();
-        kvEntity.setKey(item.getKey());
-        kvEntity.setValue(item.getValue());
-        itemsList.add(kvEntity);
-      }
-    }
-    openReleaseBO.setItems(itemsList);
+    Set<com.ctrip.framework.apollo.portal.entity.bo.KVEntity> items = releaseBO.getItems();
+    if (!CollectionUtils.isEmpty(items)) {
+      List<KVEntity> itemsList = items.stream()
+          .map(item -> {
+            KVEntity kvEntity = new KVEntity();
+            kvEntity.setKey(item.getKey());
+            kvEntity.setValue(item.getValue());
+            return kvEntity;
+          })
+          .collect(Collectors.toList());
+      openReleaseBO.setItems(itemsList);
+    } else {
+      openReleaseBO.setItems(Collections.emptyList());
+    }

248-257: Inefficient Set operations in toGrayReleaseRuleDTO

Creating a new HashSet from the list just to iterate over it is unnecessary. Also, using forEach with side effects inside could be replaced with a more functional approach.

-    Set<OpenGrayReleaseRuleItemDTO> openGrayReleaseRuleItemDTOSet = new HashSet<>(openGrayReleaseRuleDTO.getRuleItems());
-    openGrayReleaseRuleItemDTOSet.forEach(openGrayReleaseRuleItemDTO -> {
-      String clientAppId = openGrayReleaseRuleItemDTO.getClientAppId();
-      Set<String> clientIpList = new HashSet<>(openGrayReleaseRuleItemDTO.getClientIpList());
-      Set<String> clientLabelList = new HashSet<>(openGrayReleaseRuleItemDTO.getClientLabelList());
-      GrayReleaseRuleItemDTO ruleItem = new GrayReleaseRuleItemDTO(clientAppId, clientIpList, clientLabelList);
-      grayReleaseRuleDTO.addRuleItem(ruleItem);
-    });
+    if (openGrayReleaseRuleDTO.getRuleItems() != null) {
+      openGrayReleaseRuleDTO.getRuleItems().forEach(openGrayReleaseRuleItemDTO -> {
+        String clientAppId = openGrayReleaseRuleItemDTO.getClientAppId();
+        Set<String> clientIpList = openGrayReleaseRuleItemDTO.getClientIpList() != null 
+            ? new HashSet<>(openGrayReleaseRuleItemDTO.getClientIpList()) 
+            : new HashSet<>();
+        Set<String> clientLabelList = openGrayReleaseRuleItemDTO.getClientLabelList() != null
+            ? new HashSet<>(openGrayReleaseRuleItemDTO.getClientLabelList())
+            : new HashSet<>();
+        GrayReleaseRuleItemDTO ruleItem = new GrayReleaseRuleItemDTO(clientAppId, clientIpList, clientLabelList);
+        grayReleaseRuleDTO.addRuleItem(ruleItem);
+      });
+    }

47-48: Consider making GSON thread-safe with GsonBuilder configuration

While Gson instances are thread-safe, consider using GsonBuilder to configure specific behaviors like null handling and date formatting for consistency.

-  private static final Gson GSON = new Gson();
+  private static final Gson GSON = new GsonBuilder()
+      .serializeNulls()
+      .setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ")
+      .create();
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerClusterOpenApiService.java (1)

40-43: Ensure consistent error handling in OpenApiService#get methods
All getXXX methods (e.g. getItem, getCluster, getLatestActiveRelease, getNamespace…) currently return null when the underlying DTO is missing. Either:

  • Retain this null‐return pattern for all endpoints (controllers should map null→404), or
  • Uniformly refactor every getXXX to throw a 404 exception and update controllers accordingly.
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterController.java (3)

66-93: Use 201 Created for successful creation (optionally set Location header)

Creation should respond with 201; 200 is suboptimal. Optionally set Location to the new resource URL.

Apply:

-    OpenClusterDTO createdCluster = this.clusterOpenApiService.createCluster(env, cluster);
-    return ResponseEntity.ok(createdCluster);
+    OpenClusterDTO createdCluster = this.clusterOpenApiService.createCluster(env, cluster);
+    return ResponseEntity.status(201).body(createdCluster);

If you want a Location header, also import ServletUriComponentsBuilder and build the URI; I can provide the snippet. Based on objectives.


99-107: Return 204 No Content on delete

DELETE should respond with 204 No Content when successful.

Apply:

-    clusterOpenApiService.deleteCluster(env, appId, clusterName);
-    return ResponseEntity.ok().build();
+    clusterOpenApiService.deleteCluster(env, appId, clusterName);
+    return ResponseEntity.noContent().build();

66-93: Consider unified response wrapper

To align with linked objectives (issue #5462), consider wrapping responses as ResponseEntity<ApiResponse> instead of raw DTOs, with consistent error payloads.

Based on objectives.

apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/impl/ServerClusterOpenApiService.java (2)

40-44: Map invalid env to a 400-style error instead of IllegalArgumentException

Env.valueOf(env) throws IllegalArgumentException for unknown env, which may surface as 500 if not mapped. Prefer validating and throwing a BadRequestException.invalidEnvFormat to produce a clear 400.

Apply:

+import com.ctrip.framework.apollo.common.exception.BadRequestException;
@@
   @Override
   public OpenClusterDTO getCluster(String appId, String env, String clusterName) {
-    ClusterDTO clusterDTO = clusterService.loadCluster(appId, Env.valueOf(env), clusterName);
+    if (!Env.exists(env)) {
+      throw BadRequestException.invalidEnvFormat(env);
+    }
+    ClusterDTO clusterDTO = clusterService.loadCluster(appId, Env.valueOf(env), clusterName);
     return clusterDTO == null ? null : OpenApiModelConverters.fromClusterDTO(clusterDTO);
   }

Consider the same guard in createCluster and deleteCluster for consistency.


59-63: Delete delegation LGTM

Delegation to ClusterService with Env.valueOf is straightforward. Add the env validation as suggested above for consistency.

apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterControllerTest.java (4)

63-79: Add a 404 test for non-existent cluster

Currently only the happy-path GET is tested. Add a test expecting 404 when service returns null.

Example method to add:

@Test
public void testGetCluster_NotFound() throws Exception {
  String appId = "test-app";
  String env = "DEV";
  String clusterName = "missing";
  when(clusterOpenApiService.getCluster(appId, env, clusterName)).thenReturn(null);

  this.mockMvc.perform(
      MockMvcRequestBuilders.get("/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}", env, appId, clusterName))
      .andDo(MockMvcResultHandlers.print())
      .andExpect(status().isNotFound());
}

100-109: Adjust expectation to 201 if create uses Created

If you adopt 201 for creation, update the assertion accordingly.

Apply:

-                .andExpect(status().isOk())
+                .andExpect(status().isCreated())
                 .andExpect(jsonPath("$.appId", is(appId)))
                 .andExpect(jsonPath("$.name", is(clusterName)));

141-149: Adjust expectation to 204 if delete uses No Content

If DELETE returns 204, update the assertion accordingly.

Apply:

-                .andExpect(status().isOk());
+                .andExpect(status().isNoContent());

45-47: Ensure method security is active in slice tests

@PreAuthorize is used in the controller. In @WebMvcTest slices, method security may be disabled unless imported. Consider importing your security config or enabling method security for the test slice to exercise the permission checks.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4fa881a and 049dc71.

📒 Files selected for processing (9)
  • apollo-portal/pom.xml (4 hunks)
  • apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ClusterOpenApiService.java (1 hunks)
  • apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerClusterOpenApiService.java (2 hunks)
  • apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/impl/ServerClusterOpenApiService.java (1 hunks)
  • apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/util/OpenApiModelConverters.java (1 hunks)
  • apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterController.java (3 hunks)
  • apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/AuthConfiguration.java (5 hunks)
  • apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/springsecurity/SpringSecurityUserInfoHolder.java (1 hunks)
  • apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterControllerTest.java (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/impl/ServerClusterOpenApiService.java (3)
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerClusterOpenApiService.java (1)
  • Service (30-51)
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/util/OpenApiModelConverters.java (1)
  • OpenApiModelConverters (45-351)
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/environment/Env.java (1)
  • Env (34-237)
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterController.java (3)
apollo-common/src/main/java/com/ctrip/framework/apollo/common/exception/BadRequestException.java (1)
  • BadRequestException (22-138)
apollo-common/src/main/java/com/ctrip/framework/apollo/common/utils/InputValidator.java (1)
  • InputValidator (26-47)
apollo-common/src/main/java/com/ctrip/framework/apollo/common/utils/RequestPrecondition.java (1)
  • RequestPrecondition (24-43)
apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterControllerTest.java (1)
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/bo/UserInfo.java (1)
  • UserInfo (19-79)
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/springsecurity/SpringSecurityUserInfoHolder.java (1)
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/bo/UserInfo.java (1)
  • UserInfo (19-79)
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerClusterOpenApiService.java (1)
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/impl/ServerClusterOpenApiService.java (1)
  • Service (31-63)
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/AuthConfiguration.java (1)
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/springsecurity/SpringSecurityUserInfoHolder.java (1)
  • SpringSecurityUserInfoHolder (33-121)
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/util/OpenApiModelConverters.java (8)
apollo-common/src/main/java/com/ctrip/framework/apollo/common/utils/BeanUtils.java (1)
  • BeanUtils (37-255)
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/bo/ItemBO.java (1)
  • ItemBO (21-76)
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/bo/NamespaceBO.java (1)
  • NamespaceBO (23-103)
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/bo/ReleaseBO.java (1)
  • ReleaseBO (24-46)
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/model/NamespaceSyncModel.java (1)
  • NamespaceSyncModel (26-70)
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/model/NamespaceTextModel.java (1)
  • NamespaceTextModel (24-104)
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/EnvClusterInfo.java (1)
  • EnvClusterInfo (24-48)
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/vo/NamespaceIdentifier.java (1)
  • NamespaceIdentifier (23-76)
🔇 Additional comments (7)
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/springsecurity/SpringSecurityUserInfoHolder.java (2)

39-43: Constructor injection with @lazy is fine

Using @lazy for ConsumerService helps avoid early init/cycles. LGTM.

Please confirm no circular dependency remains at runtime with the new OpenAPI service wiring.


47-56: Approve code changes; add missing tests for SpringSecurityUserInfoHolder. Tests should cover: valid OpenAPI consumer (consumerUserInfo path), invalid/missing OpenAPI consumer (fallback to Spring Security), and non-OpenAPI requests (Spring Security path).

apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/util/OpenApiModelConverters.java (1)

210-210: Drop null-check on getEnv()
getEnv() invokes Env.valueOf(env) and never returns null, so guarding the call before toString() isn’t needed.

Likely an incorrect or invalid review comment.

apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ClusterOpenApiService.java (1)

18-19: Ignore import path warning. The import com.ctrip.framework.apollo.openapi.model.OpenClusterDTO matches the <modelPackage> in apollo-portal/pom.xml; no change required.

Likely an incorrect or invalid review comment.

apollo-portal/pom.xml (1)

48-62: Ensure Jakarta EE 9+ runtime and Jackson ≥2.12 compatibility

File: apollo-portal/pom.xml Lines: 48–62

  • Confirm your runtime/JEE libs target Jakarta EE 9+ APIs
  • Upgrade Jackson to ≥ 2.12 with the jakarta classifier
  • Exclude any legacy javax-namespace swagger artifacts to prevent class-path conflicts
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/configuration/AuthConfiguration.java (1)

58-82: Imports follow project conventions: grouped as JDK → javax → third-party (org.hibernate, org.springframework) → Apollo modules (com.ctrip.framework.apollo.*); no reordering needed.

apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/impl/ServerClusterOpenApiService.java (1)

46-51: Converters usage LGTM

Conversion between OpenClusterDTO and ClusterDTO is clean and consistent.

Comment thread apollo-portal/pom.xml Outdated
<artifactId>apollo-portal</artifactId>
<name>Apollo Portal</name>
<properties>
<apollo.openapi.spec.url>https://raw.githubusercontent.com/tacklequestions/apollo-openapi/main/apollo-openapi.yaml</apollo.openapi.spec.url>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

External OpenAPI spec URL may cause build reliability issues

Using an external URL for the OpenAPI spec can cause build failures if the URL is unavailable. Consider:

  1. Storing the spec file locally in the repository
  2. Using a fallback mechanism
  3. Adding build-time validation

Consider storing the OpenAPI spec locally:

-<apollo.openapi.spec.url>https://raw.githubusercontent.com/tacklequestions/apollo-openapi/main/apollo-openapi.yaml</apollo.openapi.spec.url>
+<apollo.openapi.spec.file>${project.basedir}/src/main/resources/openapi/apollo-openapi.yaml</apollo.openapi.spec.file>

And update the plugin configuration:

-<inputSpec>${apollo.openapi.spec.url}</inputSpec>
+<inputSpec>${apollo.openapi.spec.file}</inputSpec>

🏁 Script executed:

#!/bin/bash
# Check if the OpenAPI spec URL is accessible
curl -s -o /dev/null -w "%{http_code}" https://raw.githubusercontent.com/tacklequestions/apollo-openapi/main/apollo-openapi.yaml

Length of output: 3


Store OpenAPI spec locally to avoid external dependency
External URLs may become unavailable and break builds. Add the spec under src/main/resources/openapi, update the POM to reference the local file:

- <apollo.openapi.spec.url>https://raw.githubusercontent.com/tacklequestions/apollo-openapi/main/apollo-openapi.yaml</apollo.openapi.spec.url>
+ <apollo.openapi.spec.file>${project.basedir}/src/main/resources/openapi/apollo-openapi.yaml</apollo.openapi.spec.file>

And in your plugin config:

- <inputSpec>${apollo.openapi.spec.url}</inputSpec>
+ <inputSpec>${apollo.openapi.spec.file}</inputSpec>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<apollo.openapi.spec.url>https://raw.githubusercontent.com/tacklequestions/apollo-openapi/main/apollo-openapi.yaml</apollo.openapi.spec.url>
<!-- in the <properties> section -->
<properties>
<apollo.openapi.spec.file>${project.basedir}/src/main/resources/openapi/apollo-openapi.yaml</apollo.openapi.spec.file>
</properties>
<!-- in the OpenAPI Generator plugin configuration -->
<build>
<plugins>
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>${openapi.generator.version}</version>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${apollo.openapi.spec.file}</inputSpec>
<!-- other configuration -->
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
🤖 Prompt for AI Agents
In apollo-portal/pom.xml around line 30, the project references the OpenAPI spec
via an external URL; add a local copy of the spec under
src/main/resources/openapi (e.g., apollo-openapi.yaml) and update the POM
property apollo.openapi.spec.url to point to the classpath/resource path (or
relative file path) instead of the external URL; also update any plugin/config
sections that consume that property to use the local resource (or include
src/main/resources in the plugin’s config), and ensure the resource is packaged
by verifying <resources> includes src/main/resources so builds no longer depend
on the external URL.

Comment on lines +31 to +38
@Service
public class ServerClusterOpenApiService implements ClusterOpenApiService {

private final ClusterService clusterService;

public ServerClusterOpenApiService(ClusterService clusterService) {
this.clusterService = clusterService;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Make this the primary bean to resolve injection ambiguity

Mark this implementation as @primary to avoid NoUniqueBeanDefinitionException alongside the legacy @service("ServerClusterOpenApiServiceOld") bean.

Apply:

-import org.springframework.stereotype.Service;
+import org.springframework.stereotype.Service;
+import org.springframework.context.annotation.Primary;
@@
-@Service
+@Service
+@Primary
 public class ServerClusterOpenApiService implements ClusterOpenApiService {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Service
public class ServerClusterOpenApiService implements ClusterOpenApiService {
private final ClusterService clusterService;
public ServerClusterOpenApiService(ClusterService clusterService) {
this.clusterService = clusterService;
}
++ b/apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/impl/ServerClusterOpenApiService.java
@@
import org.springframework.stereotype.Service;
import org.springframework.context.annotation.Primary;
@@
@Service
@Primary
public class ServerClusterOpenApiService implements ClusterOpenApiService {
private final ClusterService clusterService;
public ServerClusterOpenApiService(ClusterService clusterService) {
this.clusterService = clusterService;
}
🤖 Prompt for AI Agents
In
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/impl/ServerClusterOpenApiService.java
around lines 31 to 38, the class should be marked as the primary bean to resolve
injection ambiguity with the legacy Service("ServerClusterOpenApiServiceOld")
bean; add the @Primary annotation to the class declaration (and import
org.springframework.context.annotation.Primary) so Spring will prefer this
implementation when autowiring ClusterOpenApiService.

Comment on lines +106 to +108
Map<String, String> configs = GSON.fromJson(release.getConfigurations(), TYPE);
openReleaseDTO.setConfigurations(configs);
return openReleaseDTO;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add null check for configurations field

The fromJson call could return null if the configurations field is null or invalid JSON.

     OpenReleaseDTO openReleaseDTO = BeanUtils.transform(OpenReleaseDTO.class, release);
-    Map<String, String> configs = GSON.fromJson(release.getConfigurations(), TYPE);
-    openReleaseDTO.setConfigurations(configs);
+    if (release.getConfigurations() != null) {
+      Map<String, String> configs = GSON.fromJson(release.getConfigurations(), TYPE);
+      openReleaseDTO.setConfigurations(configs != null ? configs : new HashMap<>());
+    } else {
+      openReleaseDTO.setConfigurations(new HashMap<>());
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Map<String, String> configs = GSON.fromJson(release.getConfigurations(), TYPE);
openReleaseDTO.setConfigurations(configs);
return openReleaseDTO;
OpenReleaseDTO openReleaseDTO = BeanUtils.transform(OpenReleaseDTO.class, release);
if (release.getConfigurations() != null) {
Map<String, String> configs = GSON.fromJson(release.getConfigurations(), TYPE);
openReleaseDTO.setConfigurations(configs != null ? configs : new HashMap<>());
} else {
openReleaseDTO.setConfigurations(new HashMap<>());
}
return openReleaseDTO;
🤖 Prompt for AI Agents
In
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/util/OpenApiModelConverters.java
around lines 106 to 108, the result of
GSON.fromJson(release.getConfigurations(), TYPE) can be null when configurations
is null or invalid JSON; after the fromJson call check if configs is null and if
so assign an empty map (e.g., Collections.emptyMap() or new HashMap<>()) before
calling openReleaseDTO.setConfigurations(configs); optionally consider catching
JsonSyntaxException around the parse to handle invalid JSON and set the empty
map in that case as well.

Comment on lines 53 to 59
@GetMapping(value = "/apps/{appId}/clusters/{clusterName:.+}")
public ResponseEntity<OpenClusterDTO> getCluster(@PathVariable String env,
@PathVariable String appId,
@PathVariable String clusterName) {
OpenClusterDTO cluster = this.clusterOpenApiService.getCluster(appId, env, clusterName);
return ResponseEntity.ok(cluster);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Return 404 when cluster not found

Current code returns 200 OK with a null body. REST should return 404 Not Found if the cluster doesn’t exist.

Apply:

-  public ResponseEntity<OpenClusterDTO> getCluster(@PathVariable String env,
-                                                   @PathVariable String appId, 
-                                                   @PathVariable String clusterName) {
-    OpenClusterDTO cluster = this.clusterOpenApiService.getCluster(appId, env, clusterName);
-    return ResponseEntity.ok(cluster);
-  }
+  public ResponseEntity<OpenClusterDTO> getCluster(@PathVariable String env,
+                                                   @PathVariable String appId,
+                                                   @PathVariable String clusterName) {
+    OpenClusterDTO cluster = this.clusterOpenApiService.getCluster(appId, env, clusterName);
+    if (cluster == null) {
+      return ResponseEntity.notFound().build();
+    }
+    return ResponseEntity.ok(cluster);
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@GetMapping(value = "/apps/{appId}/clusters/{clusterName:.+}")
public ResponseEntity<OpenClusterDTO> getCluster(@PathVariable String env,
@PathVariable String appId,
@PathVariable String clusterName) {
OpenClusterDTO cluster = this.clusterOpenApiService.getCluster(appId, env, clusterName);
return ResponseEntity.ok(cluster);
}
@GetMapping(value = "/apps/{appId}/clusters/{clusterName:.+}")
public ResponseEntity<OpenClusterDTO> getCluster(@PathVariable String env,
@PathVariable String appId,
@PathVariable String clusterName) {
OpenClusterDTO cluster = this.clusterOpenApiService.getCluster(appId, env, clusterName);
if (cluster == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(cluster);
}
🤖 Prompt for AI Agents
In
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterController.java
around lines 53-59, the controller currently returns 200 OK with a null body
when the cluster isn't found; change the method to detect a null OpenClusterDTO
and return a 404 Not Found response (e.g., return
ResponseEntity.notFound().build()) instead of ResponseEntity.ok(null) so callers
receive the correct HTTP status when the cluster does not exist.

Comment on lines +75 to +81
HttpServletRequest request = attributes.getRequest();
String requestURI = request.getRequestURI();

// 只对OpenAPI请求处理Consumer用户信息
if (!requestURI.startsWith("/openapi/")) {
return null;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

OpenAPI path check should account for contextPath

getRequestURI() includes the context path. startsWith("/openapi/") will fail when the app is deployed under a non-root context (e.g., "/portal"). Compare against contextPath + "/openapi/".

Apply:

-      // 只对OpenAPI请求处理Consumer用户信息
-      if (!requestURI.startsWith("/openapi/")) {
+      // 只对OpenAPI请求处理Consumer用户信息(考虑 contextPath)
+      String contextPath = request.getContextPath();
+      String openApiPrefix = (contextPath == null || contextPath.isEmpty() ? "" : contextPath) + "/openapi/";
+      if (!requestURI.startsWith(openApiPrefix)) {
         return null;
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
HttpServletRequest request = attributes.getRequest();
String requestURI = request.getRequestURI();
// 只对OpenAPI请求处理Consumer用户信息
if (!requestURI.startsWith("/openapi/")) {
return null;
}
HttpServletRequest request = attributes.getRequest();
String requestURI = request.getRequestURI();
// 只对OpenAPI请求处理Consumer用户信息(考虑 contextPath)
String contextPath = request.getContextPath();
String openApiPrefix = (contextPath == null || contextPath.isEmpty() ? "" : contextPath) + "/openapi/";
if (!requestURI.startsWith(openApiPrefix)) {
return null;
}
🤖 Prompt for AI Agents
In
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/springsecurity/SpringSecurityUserInfoHolder.java
around lines 75 to 81, the code uses
request.getRequestURI().startsWith("/openapi/") which fails when the application
runs under a non-root context; replace the raw "/openapi/" check with one that
prefixes the servlet context path: obtain String contextPath =
request.getContextPath() (may be empty for root) and check
request.getRequestURI().startsWith(contextPath + "/openapi/") (or
normalize/concatenate safely) so OpenAPI routes are correctly detected
regardless of deployment context.

Comment on lines +83 to +91
// 获取Consumer ID
Object consumerIdObj = request.getAttribute("Authorization");
if (consumerIdObj == null) {
return null;
}

long consumerId = Long.parseLong(consumerIdObj.toString());
Consumer consumer = consumerService.getConsumerByConsumerId(consumerId);
if (consumer == null) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Authorization attribute misuse; resolve Consumer from validated token or canonical attribute

Reading a numeric Consumer ID from request attribute "Authorization" is incorrect. "Authorization" is an HTTP header (typically a Bearer token), not a request attribute. Parsing it as long will often throw and silently fall back due to the catch-all, producing unexpected identity behavior for OpenAPI calls.

Prefer one of:

  • Option A (attribute set by auth filter): read a canonical attribute key like "consumerId" (or the constant used by your OpenAPI auth filter), keep getConsumerByConsumerId.
  • Option B (token-based): read the Authorization header, extract the token, and resolve the Consumer by token via ConsumerService.

Proposed minimal fix (Option A) if your filter sets a consumerId attribute:

-      // 获取Consumer ID
-      Object consumerIdObj = request.getAttribute("Authorization");
+      // 获取Consumer ID(由OpenAPI认证过滤器注入的 request attribute)
+      Object consumerIdObj = request.getAttribute("consumerId");
       if (consumerIdObj == null) {
         return null;
       }

       long consumerId = Long.parseLong(consumerIdObj.toString());
       Consumer consumer = consumerService.getConsumerByConsumerId(consumerId);

If you already resolve Consumers by token, apply Option B:

-      Object consumerIdObj = request.getAttribute("Authorization");
-      if (consumerIdObj == null) {
-        return null;
-      }
-
-      long consumerId = Long.parseLong(consumerIdObj.toString());
-      Consumer consumer = consumerService.getConsumerByConsumerId(consumerId);
+      String authHeader = request.getHeader("Authorization");
+      if (authHeader == null || authHeader.isEmpty()) {
+        return null;
+      }
+      String token = authHeader.startsWith("Bearer ") ? authHeader.substring(7) : authHeader;
+      // 根据实际可用的 service API 选择方法名
+      Consumer consumer = consumerService.getConsumerByToken(token); // e.g. getConsumerByToken(...)

Run this script to confirm which attribute/header your auth filter actually sets and which ConsumerService API exists:

Also applies to: 95-101


🏁 Script executed:

#!/bin/bash
# Inspect how OpenAPI auth propagates consumer identity
rg -n -C3 -g '!**/target/**' -P 'setAttribute\(\s*"(?i:authorization|consumerId)"'
rg -n -C3 -P 'RequestContextHolder|getHeader\(\s*"Authorization"'
rg -n -C3 -P 'interface\s+ConsumerService|class\s+\w*ConsumerService'
rg -n -C3 -P '\bConsumerService\b.*\('

Length of output: 36413


🏁 Script executed:

#!/bin/bash
# Display ConsumerAuthUtil.java to see how consumer ID is retrieved
sed -n '1,200p' apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/util/ConsumerAuthUtil.java

echo "-----"

# Display ConsumerService.java to inspect available methods
sed -n '1,200p' apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/service/ConsumerService.java

echo "-----"

# Display SpringSecurityUserInfoHolder.java to view getConsumerUserInfo implementation
sed -n '1,200p' apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/springsecurity/SpringSecurityUserInfoHolder.java

Length of output: 14693


Use the correct consumer ID attribute (ApolloConsumerId) or header token

  • Replace
    Object consumerIdObj = request.getAttribute("Authorization");
    with
    Object consumerIdObj = request.getAttribute(ConsumerAuthUtil.CONSUMER_ID);
    (import com.ctrip.framework.apollo.openapi.util.ConsumerAuthUtil)
  • Or parse the header instead:
    String authHeader = request.getHeader("Authorization");
    if (!StringUtils.hasText(authHeader)) return null;
    String token = authHeader.startsWith("Bearer ") ? authHeader.substring(7) : authHeader;
    Long consumerId = consumerService.getConsumerIdByToken(token);
    if (consumerId == null) return null;
    Consumer consumer = consumerService.getConsumerByConsumerId(consumerId);

Apply to both /openapi/ blocks in getConsumerUserInfo().

🤖 Prompt for AI Agents
In
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/spi/springsecurity/SpringSecurityUserInfoHolder.java
around lines 83 to 91, the code reads the wrong request attribute
("Authorization") to get the consumer ID; replace it to use the correct
attribute ConsumerAuthUtil.CONSUMER_ID (import
com.ctrip.framework.apollo.openapi.util.ConsumerAuthUtil) or alternatively read
the Authorization header, extract the Bearer token, call
consumerService.getConsumerIdByToken(token) to resolve the Long consumerId, then
call consumerService.getConsumerByConsumerId(consumerId); apply the same fix to
both /openapi/ blocks inside getConsumerUserInfo().

@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines, ignoring generated files. and removed size:XL This PR changes 500-999 lines, ignoring generated files. labels Oct 14, 2025
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:XXL This PR changes 1000+ lines, ignoring generated files. labels Oct 14, 2025
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/impl/ServerClusterOpenApiService.java (1)

31-32: Add @primary annotation to resolve potential bean conflicts.

As noted in a previous review, this implementation should be marked as @Primary to ensure it's the preferred bean when autowiring ClusterOpenApiService by type, avoiding ambiguity with the legacy bean ServerClusterOpenApiServiceOld.

Apply this diff:

 import org.springframework.stereotype.Service;
+import org.springframework.context.annotation.Primary;
 
-@Service
+@Service
+@Primary
 public class ServerClusterOpenApiService implements ClusterOpenApiService {
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterController.java (1)

53-59: Return 404 when cluster not found.

The controller returns 200 OK with a null body when the cluster is not found. Per REST conventions, it should return 404 Not Found to indicate the resource doesn't exist.

Apply this diff:

   public ResponseEntity<OpenClusterDTO> getCluster(@PathVariable String env,
                                                    @PathVariable String appId, 
                                                    @PathVariable String clusterName) {
     OpenClusterDTO cluster = this.clusterOpenApiService.getCluster(appId, env, clusterName);
+    if (cluster == null) {
+      return ResponseEntity.notFound().build();
+    }
     return ResponseEntity.ok(cluster);
   }
🧹 Nitpick comments (5)
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerClusterOpenApiService.java (1)

31-31: Consider renaming to avoid class name collision.

Having two classes named ServerClusterOpenApiService in different packages (this one in .server.service and the new one in .server.service.impl) creates confusion and makes code navigation harder. Consider renaming this legacy class to LegacyServerClusterOpenApiService or ServerClusterOpenApiServiceV1 to make the distinction clearer.

Apply this diff to rename the class:

-public class ServerClusterOpenApiService implements ClusterOpenApiService {
+public class LegacyServerClusterOpenApiService implements ClusterOpenApiService {

Note: You'll also need to update the constructor name on line 35.

apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/impl/ServerClusterOpenApiService.java (1)

53-58: Consider using English for Javadoc comments.

The Javadoc is written in Chinese, which is inconsistent with the rest of the codebase (e.g., English comments in other files). For international collaboration and maintainability, consider translating to English:

   /**
-   * 删除集群
-   * @param env 环境
-   * @param appId 应用ID
-   * @param clusterName 集群名称
+   * Delete a cluster.
+   * @param env environment
+   * @param appId application ID
+   * @param clusterName cluster name
    */
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterController.java (2)

95-107: Consider using 204 No Content for DELETE operations.

The method returns 200 OK with an empty body. REST best practices suggest 204 No Content is more appropriate for successful DELETE operations that don't return a response body.

Apply this diff:

   public ResponseEntity<Void> deleteCluster(@PathVariable String env,
                                             @PathVariable String appId, 
                                             @PathVariable String clusterName) {
     clusterOpenApiService.deleteCluster(env, appId, clusterName);
-    return ResponseEntity.ok().build();
+    return ResponseEntity.noContent().build();
   }

Otherwise, the authorization check and audit logging are correctly implemented.


49-52: Consider using English for Javadoc comments.

The Javadoc comments are in Chinese (lines 50-51, 63-64, 96-97), which is inconsistent with the rest of the codebase. For better international collaboration and maintainability, consider translating to English:

Example for getCluster:

   /**
-   * 获取指定集群信息
-   * GET /openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}
+   * Get cluster information
+   * @param env environment
+   * @param appId application ID
+   * @param clusterName cluster name
+   * @return cluster DTO
    */

Also applies to: 62-65, 95-98

apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterControllerTest.java (1)

81-149: LGTM: Well-structured tests cover key scenarios.

The existing tests properly validate:

  • testCreateCluster: Happy path with authorization and user validation (lines 82-109)
  • testCreateClusterWithAppIdMismatch: Input validation for appId consistency (lines 112-133)
  • testDeleteCluster: Successful deletion with admin authorization (lines 136-149)

Mocking strategy and assertions are appropriate.

Consider adding tests for additional edge cases to improve coverage:

  • Invalid cluster name format (should return 400)
  • Non-existent operator user (should return 400)
  • Unauthorized cluster creation attempt (permission denied)
  • Unauthorized cluster deletion attempt (non-admin)

Example for invalid cluster name:

@Test
public void testCreateClusterWithInvalidName() throws Exception {
    String appId = "test-app";
    String env = "DEV";
    String invalidClusterName = "invalid@name";
    String operator = "apollo";

    OpenClusterDTO clusterDTO = new OpenClusterDTO();
    clusterDTO.setAppId(appId);
    clusterDTO.setName(invalidClusterName);
    clusterDTO.setDataChangeCreatedBy(operator);

    when(consumerPermissionValidator.hasCreateClusterPermission(appId)).thenReturn(true);

    this.mockMvc.perform(
            MockMvcRequestBuilders.post("/openapi/v1/envs/{env}/apps/{appId}/clusters", env, appId)
                    .contentType(MediaType.APPLICATION_JSON)
                    .content(objectMapper.writeValueAsString(clusterDTO))
    )
            .andDo(MockMvcResultHandlers.print())
            .andExpect(status().isBadRequest());
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 049dc71 and e166e53.

📒 Files selected for processing (5)
  • apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ClusterOpenApiService.java (1 hunks)
  • apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerClusterOpenApiService.java (2 hunks)
  • apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/impl/ServerClusterOpenApiService.java (1 hunks)
  • apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterController.java (3 hunks)
  • apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterControllerTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ClusterOpenApiService.java
🧰 Additional context used
🧬 Code graph analysis (4)
apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterControllerTest.java (1)
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/entity/bo/UserInfo.java (1)
  • UserInfo (19-79)
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerClusterOpenApiService.java (1)
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/impl/ServerClusterOpenApiService.java (1)
  • Service (31-63)
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterController.java (3)
apollo-common/src/main/java/com/ctrip/framework/apollo/common/exception/BadRequestException.java (1)
  • BadRequestException (22-138)
apollo-common/src/main/java/com/ctrip/framework/apollo/common/utils/InputValidator.java (1)
  • InputValidator (26-47)
apollo-common/src/main/java/com/ctrip/framework/apollo/common/utils/RequestPrecondition.java (1)
  • RequestPrecondition (24-43)
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/impl/ServerClusterOpenApiService.java (2)
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerClusterOpenApiService.java (1)
  • Service (30-51)
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/util/OpenApiModelConverters.java (1)
  • OpenApiModelConverters (77-426)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (8)
🔇 Additional comments (2)
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/impl/ServerClusterOpenApiService.java (1)

40-44: LGTM: Null return delegates 404 handling to controller.

The method correctly returns null when the cluster is not found, allowing the controller layer to map this to an HTTP 404 response. Ensure the controller (ClusterController) handles this case appropriately.

apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterController.java (1)

66-93: LGTM: Comprehensive validation and authorization.

The createCluster endpoint properly validates:

  • Path vs payload appId consistency (line 72-75)
  • Required fields are non-empty (line 80-81)
  • Cluster name format (line 83-85)
  • Operator user existence (line 87-89)

Authorization is correctly enforced via @PreAuthorize checking cluster creation permissions.

Comment on lines +63 to +79
@Test
public void testGetCluster() throws Exception {
String appId = "test-app";
String env = "DEV";
String clusterName = "default";
OpenClusterDTO clusterDTO = new OpenClusterDTO();
clusterDTO.setAppId(appId);
clusterDTO.setName(clusterName);

when(clusterOpenApiService.getCluster(appId, env, clusterName)).thenReturn(clusterDTO);

this.mockMvc.perform(MockMvcRequestBuilders.get("/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}", env, appId, clusterName))
.andDo(MockMvcResultHandlers.print())
.andExpect(status().isOk())
.andExpect(jsonPath("$.appId", is(appId)))
.andExpect(jsonPath("$.name", is(clusterName)));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Add test case for cluster not found (404).

The test only covers the happy path where the cluster exists. Add a test case to verify the controller returns 404 Not Found when the cluster doesn't exist:

@Test
public void testGetClusterNotFound() throws Exception {
    String appId = "test-app";
    String env = "DEV";
    String clusterName = "nonexistent";

    when(clusterOpenApiService.getCluster(appId, env, clusterName)).thenReturn(null);

    this.mockMvc.perform(MockMvcRequestBuilders.get("/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}", 
                         env, appId, clusterName))
            .andDo(MockMvcResultHandlers.print())
            .andExpect(status().isNotFound());
}

This test is essential to verify the controller correctly handles the not-found case once the controller is fixed to return 404.

🤖 Prompt for AI Agents
In
apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterControllerTest.java
around lines 63 to 79, add a new test that verifies the controller returns 404
when a cluster is not found: create a test method (e.g., testGetClusterNotFound)
that sets appId/env/clusterName (use a nonexistent name), mock
clusterOpenApiService.getCluster(appId, env, clusterName) to return null,
perform the same MockMvc GET to
/openapi/v1/envs/{env}/apps/{appId}/clusters/{clusterName}, and assert the
response status isNotFound(); place this test next to the existing
testGetCluster method.

@tacklequestions tacklequestions force-pushed the pr/a05-cluster branch 2 times, most recently from 9094846 to 7f3b123 Compare October 14, 2025 14:05
@dosubot dosubot Bot added size:XL This PR changes 500-999 lines, ignoring generated files. and removed size:L This PR changes 100-499 lines, ignoring generated files. labels Oct 14, 2025
@tacklequestions tacklequestions force-pushed the pr/a05-cluster branch 3 times, most recently from b057923 to b7a6c49 Compare October 17, 2025 02:55
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterController.java (1)

47-50: Return 404 when cluster not found.

The method returns 200 OK with a null body when the cluster doesn't exist. RESTful convention dictates returning 404 Not Found for missing resources.

Apply this diff:

   @Override
   public ResponseEntity<OpenClusterDTO> getCluster(String appId, String clusterName, String env) {
-    return ResponseEntity.ok(this.clusterOpenApiService.getCluster(appId, env, clusterName));
+    OpenClusterDTO cluster = this.clusterOpenApiService.getCluster(appId, env, clusterName);
+    if (cluster == null) {
+      return ResponseEntity.notFound().build();
+    }
+    return ResponseEntity.ok(cluster);
   }
🧹 Nitpick comments (1)
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ClusterOpenApiService.java (1)

20-27: Add interface-level Javadoc and consider parameter order consistency.

The interface would benefit from:

  1. Interface-level Javadoc describing the service's purpose
  2. Method-level Javadoc for each operation

Additionally, the parameter order varies across methods:

  • getCluster: appId, env, clusterName
  • createCluster: env, openClusterDTO (appId is in DTO)
  • deleteCluster: env, appId, clusterName

While this may be intentional based on usage patterns, consider standardizing the order (e.g., consistently placing env first or appId first) to improve API consistency and reduce confusion.

Example Javadoc:

+/**
+ * Server-side OpenAPI service interface for cluster operations.
+ * Provides methods to retrieve, create, and delete clusters across environments.
+ */
 public interface ClusterOpenApiService {
 
+  /**
+   * Retrieves a cluster by application ID, environment, and cluster name.
+   *
+   * @param appId the application ID
+   * @param env the environment name
+   * @param clusterName the cluster name
+   * @return the cluster DTO, or null if not found
+   */
   OpenClusterDTO getCluster(String appId, String env, String clusterName);
 
+  /**
+   * Creates a new cluster in the specified environment.
+   *
+   * @param env the environment name
+   * @param openClusterDTO the cluster data
+   * @return the created cluster DTO
+   */
   OpenClusterDTO createCluster(String env, OpenClusterDTO openClusterDTO);
 
+  /**
+   * Deletes a cluster from the specified environment.
+   *
+   * @param env the environment name
+   * @param appId the application ID
+   * @param clusterName the cluster name
+   */
   void deleteCluster(String env, String appId, String clusterName);
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cb967d0 and b7a6c49.

📒 Files selected for processing (5)
  • apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ClusterOpenApiService.java (1 hunks)
  • apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerClusterOpenApiService.java (2 hunks)
  • apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterController.java (3 hunks)
  • apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterControllerParamBindLowLevelTest.java (1 hunks)
  • apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterControllerTest.java (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterControllerParamBindLowLevelTest.java
  • apollo-portal/src/test/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterControllerTest.java
🧰 Additional context used
🧬 Code graph analysis (2)
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerClusterOpenApiService.java (2)
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/util/OpenApiModelConverters.java (1)
  • OpenApiModelConverters (77-426)
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/environment/Env.java (1)
  • Env (34-237)
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterController.java (3)
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/AppController.java (1)
  • RestController (45-199)
apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ClusterController.java (1)
  • RestController (36-74)
apollo-common/src/main/java/com/ctrip/framework/apollo/common/exception/BadRequestException.java (1)
  • BadRequestException (22-138)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: build (8)
🔇 Additional comments (6)
apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/server/service/ServerClusterOpenApiService.java (4)

20-21: LGTM!

The import change from OpenApiBeanUtils to OpenApiModelConverters aligns with the PR's objective to use the new converter utility for DTO transformations.


41-41: LGTM!

The conversion method call is updated correctly. The null check before conversion ensures that null is returned when the cluster doesn't exist, which is then handled by the controller layer.


46-48: LGTM!

The bidirectional conversion using OpenApiModelConverters is correct. The converter methods include null checks via Preconditions.checkArgument, ensuring robustness.


51-54: LGTM!

The deleteCluster method correctly delegates to the underlying clusterService and follows the same pattern as other methods in this service (e.g., using Env.valueOf(env)).

apollo-portal/src/main/java/com/ctrip/framework/apollo/openapi/v1/controller/ClusterController.java (2)

52-76: LGTM!

The createCluster method properly:

  • Validates that path variable appId matches the payload appId
  • Checks for non-empty cluster name and operator
  • Validates cluster name format
  • Verifies operator exists in the user service
  • Returns the created cluster wrapped in ResponseEntity

The validation logic is thorough and follows the pattern established in other controllers (e.g., AppController).


78-94: LGTM!

The deleteCluster method correctly:

  • Applies authorization check via @PreAuthorize
  • Adds audit logging via @ApolloAuditLog
  • Validates operator is not empty
  • Verifies the operator exists
  • Delegates deletion to the service
  • Returns appropriate response

The implementation follows the established pattern seen in AppController.deleteApp.

@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:XL This PR changes 500-999 lines, ignoring generated files. labels Oct 17, 2025
@dosubot dosubot Bot added the lgtm This PR has been approved by a maintainer label Oct 19, 2025
@hezhangjian hezhangjian merged commit aba9a28 into apolloconfig:master Oct 19, 2025
8 checks passed
@github-actions github-actions Bot locked and limited conversation to collaborators Oct 19, 2025
@nobodyiam nobodyiam added this to the 2.5.0 milestone Feb 19, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

lgtm This PR has been approved by a maintainer size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Refactor] 统一 Apollo-portal API 设计规范

4 participants