This is an automated email from the ASF dual-hosted git repository.
lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git
The following commit(s) were added to refs/heads/rocketmq-studio by this push:
new 09f0adcad fix(message-history): scope result snapshots to operator
(#3213)
09f0adcad is described below
commit 09f0adcadc95cbc98f52df9f29c258c4aec2d967
Author: btlqql <[email protected]>
AuthorDate: Tue Sep 15 19:26:48 2026 +0800
fix(message-history): scope result snapshots to operator (#3213)
* fix(message-history): scope result snapshots to operator
`getMessageQueryResults` loaded the stored snapshot with
`messageQueryMapper.selectById(id)`, with no ownership predicate, while
every
other read path in the same service scopes by `eq("queried_by", queriedBy)`.
It was the only by-id read over query history that ignored the operator.
`id` is an auto-increment key, so it is enumerable, and `result_snapshot`
carries the message rows including `bornHost`, `storeHost` and `brokerName`
alongside the topic names. Any authenticated operator could therefore walk
ids
and read another operator's results together with the cluster topology
embedded
in them.
The lookup is now scoped to the current operator. A record owned by another
operator is reported as not found rather than forbidden: a 403 would confirm
that the record exists and turn enumeration into a directory of valid ids.
`QueryHistoryServiceTest` covers the owner path, the other-operator path
and the
bound parameters.
Fixes #4007
* fix(message-history): address review feedback
Assert the 404 code, not only the exception type and message. The code is
the
substance of `new BusinessException(404, ...)`; without pinning it, a later
change of 404 to 500 — or a split of "not found" from "not yours" into
404/403
that reintroduces the existence leak — would leave both tests green.
Use the static `assertThatThrownBy` import the file already uses for
`assertThat`, state the operator scoping in the `getMessageQueryResults`
Javadoc so the deliberate 404 is not mistaken for an oversight, fix the
import
ordering, and drop the fully-qualified `BusinessException` at the production
throw site now that the type is imported.
---
.../instance/message/QueryHistoryService.java | 13 ++++--
.../instance/message/QueryHistoryServiceTest.java | 47 +++++++++++++++++++++-
2 files changed, 56 insertions(+), 4 deletions(-)
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/message/QueryHistoryService.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/message/QueryHistoryService.java
index 759888de4..98055144c 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/message/QueryHistoryService.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/message/QueryHistoryService.java
@@ -29,6 +29,7 @@ import
org.apache.rocketmq.studio.persistence.entity.RmqTraceQuery;
import org.apache.rocketmq.studio.persistence.mapper.RmqMessageQueryMapper;
import org.apache.rocketmq.studio.persistence.mapper.RmqTraceQueryMapper;
import org.apache.rocketmq.studio.common.domain.PageResult;
+import org.apache.rocketmq.studio.common.exception.BusinessException;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
@@ -129,12 +130,18 @@ public class QueryHistoryService {
}
/**
- * Retrieves the stored result snapshot for a given history record.
+ * Retrieves the stored result snapshot for a history record owned by the
authenticated operator.
+ *
+ * <p>The lookup is scoped to the current authenticated operator. A record
owned by another
+ * operator is deliberately reported as not found (404) to prevent history
id enumeration.
*/
public List<MessageRecordVO> getMessageQueryResults(long id) {
- RmqMessageQuery query = messageQueryMapper.selectById(id);
+ String queriedBy = AuthenticatedUserContext.currentUsernameOrSystem();
+ RmqMessageQuery query = messageQueryMapper.selectOne(new
QueryWrapper<RmqMessageQuery>()
+ .eq("id", id)
+ .eq("queried_by", queriedBy));
if (query == null) {
- throw new
org.apache.rocketmq.studio.common.exception.BusinessException(404, "Query
history record not found");
+ throw new BusinessException(404, "Query history record not found");
}
String snapshot = query.getResultSnapshot();
if (!StringUtils.hasText(snapshot)) {
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/instance/message/QueryHistoryServiceTest.java
b/server/src/test/java/org/apache/rocketmq/studio/instance/message/QueryHistoryServiceTest.java
index b29359f82..3d52d789d 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/instance/message/QueryHistoryServiceTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/instance/message/QueryHistoryServiceTest.java
@@ -17,10 +17,12 @@
package org.apache.rocketmq.studio.instance.message;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.databind.ObjectMapper;
-import org.apache.rocketmq.studio.common.domain.PageResult;
import org.apache.rocketmq.studio.auth.AuthenticatedUserContext;
+import org.apache.rocketmq.studio.common.domain.PageResult;
+import org.apache.rocketmq.studio.common.exception.BusinessException;
import org.apache.rocketmq.studio.persistence.entity.RmqMessageQuery;
import org.apache.rocketmq.studio.persistence.entity.RmqTraceQuery;
import org.apache.rocketmq.studio.persistence.mapper.RmqMessageQueryMapper;
@@ -36,6 +38,7 @@ import java.time.ZoneOffset;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
@@ -241,6 +244,48 @@ class QueryHistoryServiceTest {
assertThat(queryCaptor.getValue().getCustomSqlSegment()).contains("queried_by");
}
+ @Test
+ void loadsResultSnapshotOnlyForTheAuthenticatedOperatorTest() {
+ AuthenticatedUserContext.setUsername("alice");
+ RmqMessageQuery entity = new RmqMessageQuery();
+ entity.setId(9L);
+ entity.setQueriedBy("alice");
+
entity.setResultSnapshot("[{\"msgId\":\"msg-9\",\"topic\":\"orders\"}]");
+ when(messageQueryMapper.selectOne(any())).thenReturn(entity);
+
+ List<MessageRecordVO> results = service.getMessageQueryResults(9L);
+
+ assertThat(results).singleElement().satisfies(result -> {
+ assertThat(result.getMsgId()).isEqualTo("msg-9");
+ assertThat(result.getTopic()).isEqualTo("orders");
+ });
+ ArgumentCaptor<QueryWrapper<RmqMessageQuery>> queryCaptor =
ArgumentCaptor.forClass(QueryWrapper.class);
+ verify(messageQueryMapper).selectOne(queryCaptor.capture());
+ assertThat(queryCaptor.getValue().getCustomSqlSegment())
+ .contains("id", "queried_by");
+ assertThat(queryCaptor.getValue().getParamNameValuePairs().values())
+ .contains(9L, "alice");
+ }
+
+ @Test
+ void hidesResultSnapshotOwnedByAnotherOperatorTest() {
+ AuthenticatedUserContext.setUsername("bob");
+ when(messageQueryMapper.selectOne(any())).thenReturn(null);
+
+ assertThatThrownBy(() -> service.getMessageQueryResults(9L))
+ .isInstanceOf(BusinessException.class)
+ .hasMessage("Query history record not found")
+ .extracting(e -> ((BusinessException) e).getCode())
+ .isEqualTo(404);
+
+ ArgumentCaptor<QueryWrapper<RmqMessageQuery>> queryCaptor =
ArgumentCaptor.forClass(QueryWrapper.class);
+ verify(messageQueryMapper).selectOne(queryCaptor.capture());
+ assertThat(queryCaptor.getValue().getCustomSqlSegment())
+ .contains("id", "queried_by");
+ assertThat(queryCaptor.getValue().getParamNameValuePairs().values())
+ .contains(9L, "bob");
+ }
+
@Test
void summarizesBothHistoryStreams() {
AuthenticatedUserContext.setUsername("alice");