Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Apollo 3.0.0

------------------
* [Fix: include super admin in hasAnyPermission semantics](https://github.com/apolloconfig/apollo/pull/5568)

* [Feature: add intelligent configuration detection feature](https://github.com/apolloconfig/apollo/pull/5582)
*
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove the empty bullet item.

This leaves a dangling list marker in release notes and should be deleted.

Proposed fix
-* 
📝 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
*
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CHANGES.md` at line 10, Remove the dangling empty bullet marker ("*") in
CHANGES.md by deleting that lone asterisk line so the release notes no longer
contain an empty list item; ensure surrounding list formatting remains correct
after removal.

------------------
All issues and pull requests are [here](https://github.com/apolloconfig/apollo/milestone/18?closed=1)
17 changes: 17 additions & 0 deletions apollo-assembly/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,20 @@ server:
cookie:
# prevent csrf
same-site: Lax


# Intelligent Detection Configuration
apollo:
detection:
enabled: false
active-provider: qwen
timeout: 30000
providers:
openai:
api-key: ${OPENAI_API_KEY:}
base-url: https://api.openai.com/v1
model: gpt-4o
qwen:
api-key: ${QWEN_API_KEY:}
base-url: https://dashscope.aliyuncs.com/compatible-mode/v1
model: qwen-plus
9 changes: 9 additions & 0 deletions apollo-portal/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,15 @@
</dependency>
<!-- end of test -->

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
Comment on lines +140 to +143
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Check web stack dependencies in portal module =="
rg -n -C2 '<artifactId>spring-boot-starter-web</artifactId>|<artifactId>spring-boot-starter-webflux</artifactId>|<artifactId>spring-webflux</artifactId>' apollo-portal/pom.xml

echo
echo "== Verify blocking servlet SSE pattern in controller =="
rg -n -C3 'class ItemDetectionController|HttpServletResponse|blockLast\(' apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ItemDetectionController.java

echo
echo "== Verify reactive service contract =="
rg -n -C3 'Flux<.*>\s+detectStream|streamChat\(' apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/config/detection/ConfigDetectionService.java

Repository: apolloconfig/apollo

Length of output: 2115


WebFlux starter is mismatched with the current blocking servlet SSE flow.

At Line 142, adding spring-boot-starter-webflux does not resolve the current blocking pattern. The ItemDetectionController uses servlet-style HttpServletResponse and calls .blockLast() on the reactive Flux<String> from ConfigDetectionService. This pins a servlet thread for each SSE request, limiting concurrency.

Two options: (1) if only client-side reactive APIs are needed, replace with spring-webflux; (2) if you want end-to-end reactive SSE, migrate the controller to a reactive return type and remove blocking.

Suggested POM adjustment (if only client-side reactive APIs are needed)
-        <dependency>
-            <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-starter-webflux</artifactId>
-        </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-webflux</artifactId>
+        </dependency>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apollo-portal/pom.xml` around lines 140 - 143, The POM adds
spring-boot-starter-webflux while the code (ItemDetectionController) uses
servlet HttpServletResponse and calls .blockLast() on the Flux from
ConfigDetectionService, which blocks servlet threads; either remove the WebFlux
starter and depend only on spring-webflux if you only need reactive client APIs,
or migrate ItemDetectionController to a fully reactive controller that returns a
Flux/Server-Sent Events response (e.g., return Flux<ServerSentEvent<String>> or
Flux<String> with produces = TEXT_EVENT_STREAM) and remove .blockLast() calls so
the flow remains non-blocking end-to-end; update pom.xml accordingly and adjust
ConfigDetectionService and controller method signatures to match the chosen
option.

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>

</dependencies>
<build>
<!-- openapi-generator configuration -->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Copyright 2026 Apollo Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.ctrip.framework.apollo.portal.controller;


import com.ctrip.framework.apollo.portal.service.config.detection.ConfigDetectionService;
import com.ctrip.framework.apollo.portal.service.config.detection.model.DetectionRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.io.PrintWriter;

/**
* Item detection controller for intelligent configuration detection
*/
@RestController
@RequestMapping("/apps/{appId}/envs/{env}/clusters/{clusterName}/namespaces/{namespaceName}")
public class ItemDetectionController {

private final ConfigDetectionService configDetectionService;

public ItemDetectionController(ConfigDetectionService configDetectionService) {
this.configDetectionService = configDetectionService;
}

@GetMapping(value = "/items/intelligent-detection")
public void detectConfigStream(
@PathVariable String appId,
@PathVariable String env,
@PathVariable String clusterName,
@PathVariable String namespaceName,
@RequestParam String key,
@RequestParam String value,
@RequestParam(required = false) String comment,
HttpServletResponse response) throws IOException {
Comment on lines +41 to +50
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Move the detection payload out of the URL.

Lines 47-49 accept key, value, and comment as query params. For this feature those fields can be actual secrets, so they will leak into browser history, access logs, reverse-proxy logs, and tracing systems. Use a streaming POST body instead of a GET URL payload. As per coding guidelines, apollo-portal/src/main/java/**/*.java: For new/changed OpenAPI endpoints in apollo-portal, prefer implementing generated *ManagementApi interfaces and generated models instead of hand-written DTO/controller contracts that bypass the spec pipeline.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ItemDetectionController.java`
around lines 41 - 50, The endpoint detectConfigStream should not accept
sensitive fields via query params; change the handler from a GET to a streaming
POST, remove `@RequestParam` key/value/comment and instead accept the generated
management API request model (or an InputStream/RequestBody wrapper) so the
payload is sent in the request body; implement the generated *ManagementApi
interface for this controller and use the generated request model class (or
stream the request body) to read key, value, comment while keeping `@PathVariable`
appId/env/clusterName/namespaceName, update the mapping annotation to POST with
appropriate consumes (e.g. application/json or streaming) and adjust method
signature/exception handling accordingly so no secrets go into URLs or logs.

⚠️ Potential issue | 🔴 Critical

Protect this endpoint with namespace modify permission.

This route can send arbitrary config content to an external provider, but unlike the sibling item mutation endpoints it has no @PreAuthorize("@unifiedPermissionValidator.hasModifyNamespacePermission(...)"). Any authenticated user who can reach the path can currently trigger token spend and exfiltrate namespace-scoped data.

Suggested fix
+  `@PreAuthorize`(
+      value = "@unifiedPermissionValidator.hasModifyNamespacePermission(`#appId`, `#env`, `#clusterName`, `#namespaceName`)")
   `@GetMapping`(value = "/items/intelligent-detection")
   public void detectConfigStream(

Also add this import near the top of the file:

import org.springframework.security.access.prepost.PreAuthorize;
📝 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 = "/items/intelligent-detection")
public void detectConfigStream(
@PathVariable String appId,
@PathVariable String env,
@PathVariable String clusterName,
@PathVariable String namespaceName,
@RequestParam String key,
@RequestParam String value,
@RequestParam(required = false) String comment,
HttpServletResponse response) throws IOException {
`@PreAuthorize`(
value = "@unifiedPermissionValidator.hasModifyNamespacePermission(`#appId`, `#env`, `#clusterName`, `#namespaceName`)")
`@GetMapping`(value = "/items/intelligent-detection")
public void detectConfigStream(
`@PathVariable` String appId,
`@PathVariable` String env,
`@PathVariable` String clusterName,
`@PathVariable` String namespaceName,
`@RequestParam` String key,
`@RequestParam` String value,
`@RequestParam`(required = false) String comment,
HttpServletResponse response) throws IOException {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ItemDetectionController.java`
around lines 41 - 50, The detectConfigStream endpoint in ItemDetectionController
lacks namespace modify checks; add the Spring security annotation
`@PreAuthorize`("@unifiedPermissionValidator.hasModifyNamespacePermission(`#appId`,
`#env`, `#clusterName`, `#namespaceName`)") to the detectConfigStream method to
require modify permission, and add the import
org.springframework.security.access.prepost.PreAuthorize near the top of the
file; ensure the annotation references the method parameters appId, env,
clusterName, and namespaceName exactly as used in the method signature.


response.setContentType("text/event-stream;charset=UTF-8");
response.setCharacterEncoding("UTF-8");
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Connection", "keep-alive");

PrintWriter writer = response.getWriter();

DetectionRequest request = DetectionRequest.builder()
.appId(appId)
.env(env)
.clusterName(clusterName)
.namespaceName(namespaceName)
.key(key)
.value(value)
.comment(comment)
.build();

configDetectionService.detectStream(request)
.doOnNext(chunk -> {
try {
String[] lines = chunk.split("\n", -1);
writer.write("event: message\n");
for (String line : lines) {
writer.write("data: " + line + "\n");
}
writer.write("\n");
writer.flush();
} catch (Exception e) {
e.printStackTrace();
}
})
.doOnComplete(() -> {
try {
writer.write("event: done\n");
writer.write("data: [DONE]\n\n");
writer.flush();
writer.close();
} catch (Exception e) {
e.printStackTrace();
}
})
.doOnError(e -> {
try {
writer.write("event: error\n");
writer.write("data: check error:" + e.getMessage() + "\n\n");
writer.flush();
writer.close();
} catch (Exception ex) {
ex.printStackTrace();
}
})
.blockLast();
Comment on lines +69 to +103
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "ItemDetectionController.java" -type f

Repository: apolloconfig/apollo

Length of output: 166


🏁 Script executed:

cat -n apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ItemDetectionController.java | head -120

Repository: apolloconfig/apollo

Length of output: 4739


🏁 Script executed:

find apollo-portal -name "*ManagementApi*" -type f | head -20

Repository: apolloconfig/apollo

Length of output: 45


🏁 Script executed:

rg -A 50 "blockLast" apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/controller/ItemDetectionController.java

Repository: apolloconfig/apollo

Length of output: 100


🏁 Script executed:

find apollo-portal -name "*.openapi.*" -o -name "*openapi*" -type f | head -20

Repository: apolloconfig/apollo

Length of output: 45


🏁 Script executed:

find apollo-portal -path "*generated*" -name "*Api.java" -type f | head -20

Repository: apolloconfig/apollo

Length of output: 45


🏁 Script executed:

rg -l "text/event-stream" apollo-portal/src/main/java --type java

Repository: apolloconfig/apollo

Length of output: 164


🏁 Script executed:

rg -l "blockLast|subscribe|doOnError" apollo-portal/src/main/java --type java | head -10

Repository: apolloconfig/apollo

Length of output: 403


🏁 Script executed:

cat -n apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/config/detection/ConfigDetectionService.java

Repository: apolloconfig/apollo

Length of output: 4058


🏁 Script executed:

cat -n apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/config/detection/adapter/OpenAICompatibleAdapter.java | head -150

Repository: apolloconfig/apollo

Length of output: 7801


Replace blockLast() with proper subscription management and fix error handling in SSE stream.

Lines 69–103: Using blockLast() blocks the servlet thread for the entire duration of the LLM model call. The try/catch blocks that swallow e.printStackTrace() hide write failures instead of properly handling or terminating the subscription. When a client disconnects, the upstream stream has no cancellation signal and continues consuming resources and quota from the LLM provider.

Use subscribe() with a proper disposable/cancellation token and implement error handling that propagates or terminates the subscription when I/O fails or the client disconnects.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,20 @@

import com.ctrip.framework.apollo.portal.component.config.PortalConfig;
import com.ctrip.framework.apollo.portal.entity.vo.PageSetting;
import com.ctrip.framework.apollo.portal.service.config.detection.config.DetectionProperties;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class PageSettingController {

private final PortalConfig portalConfig;
private final DetectionProperties detectionProperties;

public PageSettingController(final PortalConfig portalConfig) {
public PageSettingController(final PortalConfig portalConfig,
final DetectionProperties detectionProperties) {
this.portalConfig = portalConfig;
this.detectionProperties = detectionProperties;
}

@GetMapping("/page-settings")
Expand All @@ -36,6 +40,7 @@ public PageSetting getPageSetting() {

setting.setWikiAddress(portalConfig.wikiAddress());
setting.setCanAppAdminCreatePrivateNamespace(portalConfig.canAppAdminCreatePrivateNamespace());
setting.setDetectionEnabled(detectionProperties.isEnabled());

return setting;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public class PageSetting {

private boolean canAppAdminCreatePrivateNamespace;

private boolean detectionEnabled;

public String getWikiAddress() {
return wikiAddress;
}
Expand All @@ -37,4 +39,12 @@ public boolean isCanAppAdminCreatePrivateNamespace() {
public void setCanAppAdminCreatePrivateNamespace(boolean canAppAdminCreatePrivateNamespace) {
this.canAppAdminCreatePrivateNamespace = canAppAdminCreatePrivateNamespace;
}

public boolean isDetectionEnabled() {
return detectionEnabled;
}

public void setDetectionEnabled(boolean detectionEnabled) {
this.detectionEnabled = detectionEnabled;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright 2026 Apollo Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package com.ctrip.framework.apollo.portal.service.config.detection;


import com.ctrip.framework.apollo.portal.service.config.detection.adapter.DetectionModelAdapter;
import com.ctrip.framework.apollo.portal.service.config.detection.adapter.DetectionModelAdapterFactory;
import com.ctrip.framework.apollo.portal.service.config.detection.config.DetectionProperties;
import com.ctrip.framework.apollo.portal.service.config.detection.model.ChatMessage;
import com.ctrip.framework.apollo.portal.service.config.detection.model.ChatRequest;
import com.ctrip.framework.apollo.portal.service.config.detection.model.DetectionRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;

import java.util.Arrays;
import java.util.Map;

/**
* Configuration detection service
*/
@Service
public class ConfigDetectionService {

private static final Logger logger = LoggerFactory.getLogger(ConfigDetectionService.class);

private final DetectionProperties properties;
private final DetectionModelAdapterFactory adapterFactory;
private final PromptBuilder promptBuilder;

public ConfigDetectionService(
DetectionProperties properties,
DetectionModelAdapterFactory adapterFactory,
PromptBuilder promptBuilder) {
this.properties = properties;
this.adapterFactory = adapterFactory;
this.promptBuilder = promptBuilder;
}

public Flux<String> detectStream(DetectionRequest request) {
if (!properties.isEnabled()) {
return Flux.error(new IllegalStateException("Detection is not enabled"));
}
try {
String provider = properties.getActiveProvider();
logger.info("Starting detection for key: {}, provider: {}", request.getKey(), provider);
DetectionModelAdapter adapter = adapterFactory.getAdapter(provider);
String systemPrompt = promptBuilder.getSystemPrompt();
String userPrompt = promptBuilder.buildDetectionPrompt(request);

ChatRequest chatRequest = ChatRequest.builder()
.messages(Arrays.asList(
new ChatMessage("system", systemPrompt),
new ChatMessage("user", userPrompt)
))
.stream(true)
.build();

return adapter.streamChat(chatRequest)
.doOnComplete(() -> logger.info("Detection completed for key: {}", request.getKey()))
.doOnError(error -> logger.error("Detection failed for key: {}", request.getKey(), error));
Comment on lines +74 to +76
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd 'ConfigDetectionService\.java$' apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/config/detection -x sed -n '55,120p' {}
fd 'DetectionModelAdapterFactory\.java$' apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/config/detection/adapter -x sed -n '48,90p' {}
fd 'OpenAICompatibleAdapter\.java$' apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/config/detection/adapter -x sed -n '45,170p' {}
rg -n '\.timeout\s*\(' apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/config/detection

Repository: apolloconfig/apollo

Length of output: 8009


Enforce the configured timeout on the returned stream.

Line 74 exposes the model Flux as-is. DetectionModelAdapterFactory computes properties.getTimeout() and passes it to the adapter, but the OpenAICompatibleAdapter constructor does not store or use this timeout parameter. The timeout is never applied, so a provider that keeps the connection half-open or responds slowly can hold this request indefinitely.

Suggested fix
             return adapter.streamChat(chatRequest)
+                    .timeout(Duration.ofMillis(properties.getTimeout()))
                     .doOnComplete(() -> logger.info("Detection completed for key: {}", request.getKey()))
                     .doOnError(error -> logger.error("Detection failed for key: {}", request.getKey(), error));

Also add this import near the top of the file:

import java.time.Duration;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apollo-portal/src/main/java/com/ctrip/framework/apollo/portal/service/config/detection/ConfigDetectionService.java`
around lines 74 - 76, The returned Flux from ConfigDetectionService exposes
adapter.streamChat(...) without applying the configured timeout, because
DetectionModelAdapterFactory passes properties.getTimeout() but
OpenAICompatibleAdapter does not store or use it; fix by updating
OpenAICompatibleAdapter to accept and store the timeout value provided by
DetectionModelAdapterFactory (use the same constructor parameter name), then
apply that timeout to the reactive stream returned by streamChat (e.g., use
Reactor's timeout with a Duration built from the stored timeout) so
ConfigDetectionService can safely return adapter.streamChat(...). Also add the
import java.time.Duration near the top of the file.


} catch (Exception e) {
logger.error("Failed to start detection", e);
return Flux.error(e);
}
}
}
Loading
Loading