Conversation
Internal topics are not editable via the UI (the actions dropdown is disabled for them), but the corresponding REST endpoints happily accept edit/delete/recreate requests, so anything calling the API directly - or via MCP - can still modify them. Enforce the same rule server-side. A topic is treated as internal when Kafka marks it internal or its name starts with the configured `internalTopicPrefix` (default `_`); this predicate is extracted into `InternalTopic.isInternal()` so the check has a single source of truth. Rejected with `INTERNAL_TOPIC_MODIFICATION` (HTTP 400): - updateTopic - deleteTopic - recreateTopic - cloneTopic (when the source topic is internal) - increaseTopicPartitions - changeReplicationFactor - deleteTopicMessages (clear messages) Topic creation and message production are left untouched, matching what the UI still allows. As a side effect `deleteTopic` on a missing topic now fails with TopicNotFoundException, since the topic is described before deletion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Freshly created topics are not immediately visible via describeTopics(), so the assertions raced broker metadata propagation. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
AI Summary This GitHub issue addresses a security gap where internal Kafka topics (prefixed with |
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
Included review availability: Your plan provides up to 8 included reviews per hour; 7 remain after this review. 📝 WalkthroughWalkthroughThe API now detects internal topics by Kafka metadata or the configured prefix. Topic and message modifications fail with a dedicated ChangesInternal topic protection
Estimated code review effort: 3 (Moderate) | ~20 minutes Merge Risk: ⚪ Minimal · up to The PR adds server-side protection against modifying internal topics while preserving permitted operations; no actionable merge-blocking risk remains after normal checks and review. Sequence Diagram(s)sequenceDiagram
participant Client
participant TopicsService
participant InternalTopic
participant ReactiveAdminClient
Client->>TopicsService: Request topic modification
TopicsService->>InternalTopic: Check topic metadata and prefix
InternalTopic-->>TopicsService: Internal or non-internal
alt Internal topic
TopicsService-->>Client: InternalTopicModificationException, HTTP 400
else Non-internal topic
TopicsService->>ReactiveAdminClient: Execute modification
ReactiveAdminClient-->>Client: Return operation result
end
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
Full details: Linked Issues checkExplanation The changes satisfy issue
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
Fixes #1967
Breaking change? (if so, please describe the impact and migration path for existing application instances)
Behavioural change for API consumers, not for UI users. Requests that modify an internal topic —
and that succeed today — will start returning
400with error code4022(
INTERNAL_TOPIC_MODIFICATION). The UI is unaffected, since it already hides every one of theseactions for internal topics.
Anyone scripting against the API to, say, adjust
retention.mson a_-prefixed topic will needto do it with
kafka-configs.shor another admin tool instead. There's no migration path or opt-outin this PR by design — see the open question below if a toggle is preferred.
One smaller change:
DELETE /topics/{topic}on a non-existent topic now returnsTopicNotFoundException(404) rather than the admin client's error, because the topic is describedbefore deletion.
What changes did you make? (Give an overview)
Internal topics can't be edited, cleared, recreated or deleted from the UI — the actions dropdown is
disabled and the rows aren't selectable — but the backend never enforced that, so the same operations
went straight through when called against the REST API directly, or via the MCP tools that share the
controllers. #1967 has the details.
This adds the missing server-side check:
InternalTopic.isInternal(TopicDescription, prefix)— extracted fromInternalTopic.from(...),which already computed exactly this (
topicDescription.isInternal() || name.startsWith(prefix)) butonly used it for display. Now there's one definition both the display flag and the new guard read
from, so they can't drift apart.
TopicsService.validateTopicIsNotInternal(...)— applied toupdateTopic,deleteTopic,recreateTopic,cloneTopic,increaseTopicPartitionsandchangeReplicationFactor. It sits inthe service rather than the controller so the MCP tools are covered by the same code path.
MessagesService.deleteTopicMessages(...)— same rejection for "Clear messages", reusing theTopicDescriptionit already fetches, so it costs no extra call.InternalTopicModificationException→ newErrorCode.INTERNAL_TOPIC_MODIFICATION(4022)→HTTP 400.
Deliberately left alone, to match what the UI still permits:
a create.
In
deleteTopicthe internal check runs before theTOPIC_DELETIONcluster-feature check, so thereason for the rejection doesn't depend on how far cluster statistics have loaded.
Is there anything you'd like reviewers to focus on?
backend that quietly permits what the frontend forbids is how this bug arose. But it does remove a
capability that someone may have been relying on, so if you'd rather have a
kafka.allowInternalTopicModification-style property, I'm happy to add it.They match the UI exactly today, but "matches the UI" may not be the rule you want long-term.
ValidationException(400) is what the neighbouring "Topic deletionrestricted" check uses, so I followed it.
ReadOnlyModeExceptionuses 405, which is arguably thecloser analogue — this is a permanently unavailable operation, not a malformed request.
describeTopiccall onupdateTopicanddeleteTopic, which didn't load the topicbefore. The other four guarded methods already called
loadTopic, so they pay nothing extra. Theseare all rare, user-initiated operations, so I favoured one uniform guard over threading the flag
through each method — say the word if you'd rather avoid the round trip.
How Has This Been Tested? (put an "x" (case-sensitive!) next to an item)
InternalTopicsModificationTest— 10 tests, all passing against a real Kafka via testcontainers:InternalTopicModificationExceptionDELETEandPATCHon/topics/{topic}return400at the endpoint levelOne thing the first run caught, worth noting for anyone writing similar tests: a freshly created
topic isn't immediately visible through
describeTopics(), so asserting straight aftercreateTopic()races metadata propagation and surfacesTopicNotFoundExceptionrather than therejection. The test waits for the topic to become visible before acting on it.
I also ran
MessagesServiceTest,TopicsServicePaginationTest,ReadOnlyModeTestsandKafkaTopicCreateTests. Two failures show up there — `KafkaTopicCreateTests:api:compileJava,:api:compileTestJavaand:api:checkstyleMainpass.:api:checkstyleTestreports one import-order warning in
RegexBasedProviderAuthorityExtractorTest.java:47that ispresent on a clean
mainand untouched by this PR.Checklist (put an "x" (case-sensitive!) next to all the items, otherwise the build will fail)
changes needed: no new or changed properties, and this repo carries no prose docs.
pre-existing failures that also occur on a clean
main.Check out Contributing and Code of Conduct
Summary by CodeRabbit