Skip to content

Commit 2470c4e

Browse files
authored
GEODE-10612: Encode region path in Pulse region-detail error messages (#8041)
When a region-detail request names a path that does not resolve, the region services return "Region [<path>] is not available" in the errorOnRegion field, which the Pulse UI displays. Paths containing characters such as '<' or '&' did not display correctly.
1 parent e58f40f commit 2470c4e

4 files changed

Lines changed: 295 additions & 2 deletions

File tree

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
3+
* agreements. See the NOTICE file distributed with this work for additional information regarding
4+
* copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
5+
* "License"); you may not use this file except in compliance with the License. You may obtain a
6+
* copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software distributed under the License
11+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12+
* or implied. See the License for the specific language governing permissions and limitations under
13+
* the License.
14+
*/
15+
package org.apache.geode.tools.pulse.controllers;
16+
17+
import static org.assertj.core.api.Assertions.assertThat;
18+
import static org.mockito.ArgumentMatchers.anyString;
19+
import static org.mockito.Mockito.when;
20+
import static org.mockito.quality.Strictness.LENIENT;
21+
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
22+
import static org.springframework.http.MediaType.parseMediaType;
23+
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
24+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
25+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
26+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
27+
28+
import java.security.Principal;
29+
30+
import com.fasterxml.jackson.databind.ObjectMapper;
31+
import com.fasterxml.jackson.databind.node.ObjectNode;
32+
import org.junit.Before;
33+
import org.junit.Rule;
34+
import org.junit.Test;
35+
import org.junit.experimental.categories.Category;
36+
import org.junit.runner.RunWith;
37+
import org.mockito.Mock;
38+
import org.mockito.junit.MockitoJUnit;
39+
import org.mockito.junit.MockitoRule;
40+
import org.springframework.beans.factory.annotation.Autowired;
41+
import org.springframework.http.MediaType;
42+
import org.springframework.test.context.ActiveProfiles;
43+
import org.springframework.test.context.ContextConfiguration;
44+
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
45+
import org.springframework.test.context.web.WebAppConfiguration;
46+
import org.springframework.test.web.servlet.MockMvc;
47+
import org.springframework.test.web.servlet.MvcResult;
48+
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
49+
import org.springframework.web.context.WebApplicationContext;
50+
51+
import org.apache.geode.test.junit.categories.PulseTest;
52+
import org.apache.geode.tools.pulse.internal.data.Cluster;
53+
import org.apache.geode.tools.pulse.internal.data.Repository;
54+
55+
/**
56+
* Covers the region-detail error message end to end, from the {@code /pulseUpdate} request the
57+
* Pulse UI posts through to the JSON it receives back.
58+
*/
59+
@Category({PulseTest.class})
60+
@RunWith(SpringJUnit4ClassRunner.class)
61+
@WebAppConfiguration
62+
@ContextConfiguration("classpath*:WEB-INF/pulse-servlet.xml")
63+
@ActiveProfiles({"pulse.controller.test"})
64+
public class RegionDetailErrorMessageIntegrationTest {
65+
66+
private static final String PATH_WITH_SPECIAL_CHARACTERS = "/orders<2026>&archive";
67+
private static final String ENCODED_MESSAGE =
68+
"Region [/orders&lt;2026&gt;&amp;archive] is not available";
69+
70+
private static final MediaType JSON_MEDIA_TYPE = parseMediaType(APPLICATION_JSON_VALUE);
71+
private static final Principal PRINCIPAL = () -> "test-user";
72+
private static final ObjectMapper MAPPER = new ObjectMapper();
73+
74+
@Rule
75+
public MockitoRule mockitoRule = MockitoJUnit.rule().strictness(LENIENT);
76+
77+
@Autowired
78+
private WebApplicationContext wac;
79+
80+
@Autowired
81+
private Repository repository;
82+
83+
@Mock
84+
Cluster cluster;
85+
86+
private MockMvc mockMvc;
87+
88+
@Before
89+
public void setup() {
90+
when(repository.getCluster()).thenReturn(cluster);
91+
when(cluster.getServerName()).thenReturn("mock-cluster");
92+
// The requested path resolves to no region, so the services take the error branch.
93+
when(cluster.getClusterRegion(anyString())).thenReturn(null);
94+
95+
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
96+
}
97+
98+
@Test
99+
public void pulseUpdateEncodesSpecialCharactersForClusterSelectedRegion() throws Exception {
100+
MvcResult result = mockMvc
101+
.perform(post("/pulseUpdate")
102+
.with(csrf())
103+
.param("pulseData", pulseData("ClusterSelectedRegion", PATH_WITH_SPECIAL_CHARACTERS))
104+
.principal(PRINCIPAL)
105+
.accept(JSON_MEDIA_TYPE))
106+
.andExpect(status().isOk())
107+
.andExpect(jsonPath("$.ClusterSelectedRegion.selectedRegion.errorOnRegion")
108+
.value(ENCODED_MESSAGE))
109+
.andReturn();
110+
111+
assertThat(result.getResponse().getContentAsString())
112+
.contains("/orders&lt;2026&gt;&amp;archive");
113+
}
114+
115+
@Test
116+
public void pulseUpdateEncodesSpecialCharactersForClusterSelectedRegionsMember()
117+
throws Exception {
118+
MvcResult result = mockMvc
119+
.perform(post("/pulseUpdate")
120+
.with(csrf())
121+
.param("pulseData",
122+
pulseData("ClusterSelectedRegionsMember", PATH_WITH_SPECIAL_CHARACTERS))
123+
.principal(PRINCIPAL)
124+
.accept(JSON_MEDIA_TYPE))
125+
.andExpect(status().isOk())
126+
.andExpect(jsonPath("$.ClusterSelectedRegionsMember.selectedRegionsMembers.errorOnRegion")
127+
.value(ENCODED_MESSAGE))
128+
.andReturn();
129+
130+
assertThat(result.getResponse().getContentAsString())
131+
.contains("/orders&lt;2026&gt;&amp;archive");
132+
}
133+
134+
@Test
135+
public void pulseUpdateLeavesOrdinaryRegionPathUnchanged() throws Exception {
136+
mockMvc
137+
.perform(post("/pulseUpdate")
138+
.with(csrf())
139+
.param("pulseData", pulseData("ClusterSelectedRegion", "/mock-region"))
140+
.principal(PRINCIPAL)
141+
.accept(JSON_MEDIA_TYPE))
142+
.andExpect(status().isOk())
143+
.andExpect(jsonPath("$.ClusterSelectedRegion.selectedRegion.errorOnRegion")
144+
.value("Region [/mock-region] is not available"));
145+
}
146+
147+
/** Builds the {@code pulseData} body the Pulse frontend posts for the region-detail page. */
148+
private static String pulseData(String service, String regionFullPath) {
149+
ObjectNode parameters = MAPPER.createObjectNode();
150+
parameters.put("regionFullPath", regionFullPath);
151+
ObjectNode root = MAPPER.createObjectNode();
152+
root.set(service, parameters);
153+
return root.toString();
154+
}
155+
}

geode-pulse/src/main/java/org/apache/geode/tools/pulse/internal/service/ClusterSelectedRegionService.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import com.fasterxml.jackson.databind.node.ObjectNode;
3131
import jakarta.servlet.http.HttpServletRequest;
3232
import org.apache.commons.lang3.StringUtils;
33+
import org.apache.commons.text.StringEscapeUtils;
3334
import org.apache.logging.log4j.LogManager;
3435
import org.apache.logging.log4j.Logger;
3536
import org.springframework.beans.factory.annotation.Autowired;
@@ -222,7 +223,9 @@ private ObjectNode getSelectedRegionJson(Cluster cluster, String selectedRegionF
222223
return regionJSON;
223224
} else {
224225
ObjectNode responseJSON = mapper.createObjectNode();
225-
responseJSON.put("errorOnRegion", "Region [" + selectedRegionFullPath + "] is not available");
226+
responseJSON.put("errorOnRegion",
227+
"Region [" + StringEscapeUtils.escapeHtml4(selectedRegionFullPath)
228+
+ "] is not available");
226229
return responseJSON;
227230
}
228231
}

geode-pulse/src/main/java/org/apache/geode/tools/pulse/internal/service/ClusterSelectedRegionsMemberService.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import com.fasterxml.jackson.databind.ObjectMapper;
2626
import com.fasterxml.jackson.databind.node.ObjectNode;
2727
import jakarta.servlet.http.HttpServletRequest;
28+
import org.apache.commons.text.StringEscapeUtils;
2829
import org.apache.logging.log4j.LogManager;
2930
import org.apache.logging.log4j.Logger;
3031
import org.springframework.beans.factory.annotation.Autowired;
@@ -146,7 +147,9 @@ private ObjectNode getSelectedRegionsMembersJson(Cluster cluster, String selecte
146147
return regionMemberJSON;
147148
} else {
148149
ObjectNode responseJSON = mapper.createObjectNode();
149-
responseJSON.put("errorOnRegion", "Region [" + selectedRegionFullPath + "] is not available");
150+
responseJSON.put("errorOnRegion",
151+
"Region [" + StringEscapeUtils.escapeHtml4(selectedRegionFullPath)
152+
+ "] is not available");
150153
return responseJSON;
151154
}
152155
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license
3+
* agreements. See the NOTICE file distributed with this work for additional information regarding
4+
* copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
5+
* "License"); you may not use this file except in compliance with the License. You may obtain a
6+
* copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software distributed under the License
11+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12+
* or implied. See the License for the specific language governing permissions and limitations under
13+
* the License.
14+
*/
15+
package org.apache.geode.tools.pulse.internal.service;
16+
17+
import static org.assertj.core.api.Assertions.assertThat;
18+
import static org.mockito.ArgumentMatchers.anyString;
19+
import static org.mockito.Mockito.mock;
20+
import static org.mockito.Mockito.verify;
21+
import static org.mockito.Mockito.when;
22+
23+
import java.security.Principal;
24+
25+
import com.fasterxml.jackson.databind.ObjectMapper;
26+
import com.fasterxml.jackson.databind.node.ObjectNode;
27+
import jakarta.servlet.http.HttpServletRequest;
28+
import org.junit.Before;
29+
import org.junit.Test;
30+
31+
import org.apache.geode.tools.pulse.internal.data.Cluster;
32+
import org.apache.geode.tools.pulse.internal.data.Repository;
33+
34+
/**
35+
* Tests the {@code errorOnRegion} message the region-detail services produce when the requested
36+
* region path does not resolve.
37+
*
38+
* <p>
39+
* Paths containing characters such as {@code <} or {@code &} are encoded so the message displays
40+
* as written. Lookup is unaffected and uses the path as supplied.
41+
*/
42+
public class RegionErrorMessageEncodingTest {
43+
44+
private static final String PATH_WITH_SPECIAL_CHARACTERS = "/orders<2026>&archive";
45+
private static final String ENCODED_MESSAGE =
46+
"Region [/orders&lt;2026&gt;&amp;archive] is not available";
47+
private static final String ORDINARY_PATH = "/mock-region";
48+
49+
private static final ObjectMapper MAPPER = new ObjectMapper();
50+
51+
private Repository repository;
52+
private Cluster cluster;
53+
private HttpServletRequest request;
54+
55+
@Before
56+
public void setUp() {
57+
repository = mock(Repository.class);
58+
cluster = mock(Cluster.class);
59+
request = mock(HttpServletRequest.class);
60+
Principal principal = mock(Principal.class);
61+
when(principal.getName()).thenReturn("admin");
62+
when(request.getUserPrincipal()).thenReturn(principal);
63+
when(repository.getCluster()).thenReturn(cluster);
64+
when(cluster.getServerName()).thenReturn("mock-cluster");
65+
// No region resolves, so every call below takes the error branch.
66+
when(cluster.getClusterRegion(anyString())).thenReturn(null);
67+
}
68+
69+
@Test
70+
public void clusterSelectedRegionEncodesSpecialCharactersInErrorMessage() throws Exception {
71+
assertThat(selectedRegionError(PATH_WITH_SPECIAL_CHARACTERS)).isEqualTo(ENCODED_MESSAGE);
72+
}
73+
74+
@Test
75+
public void clusterSelectedRegionsMemberEncodesSpecialCharactersInErrorMessage()
76+
throws Exception {
77+
assertThat(selectedRegionsMemberError(PATH_WITH_SPECIAL_CHARACTERS)).isEqualTo(ENCODED_MESSAGE);
78+
}
79+
80+
@Test
81+
public void clusterSelectedRegionLeavesOrdinaryPathUnchanged() throws Exception {
82+
assertThat(selectedRegionError(ORDINARY_PATH))
83+
.isEqualTo("Region [" + ORDINARY_PATH + "] is not available");
84+
}
85+
86+
@Test
87+
public void clusterSelectedRegionsMemberLeavesOrdinaryPathUnchanged() throws Exception {
88+
assertThat(selectedRegionsMemberError(ORDINARY_PATH))
89+
.isEqualTo("Region [" + ORDINARY_PATH + "] is not available");
90+
}
91+
92+
@Test
93+
public void clusterSelectedRegionLooksTheRegionUpByTheSuppliedPath() throws Exception {
94+
selectedRegionError(PATH_WITH_SPECIAL_CHARACTERS);
95+
96+
verify(cluster).getClusterRegion(PATH_WITH_SPECIAL_CHARACTERS);
97+
}
98+
99+
@Test
100+
public void clusterSelectedRegionsMemberLooksTheRegionUpByTheSuppliedPath() throws Exception {
101+
selectedRegionsMemberError(PATH_WITH_SPECIAL_CHARACTERS);
102+
103+
verify(cluster).getClusterRegion(PATH_WITH_SPECIAL_CHARACTERS);
104+
}
105+
106+
private String selectedRegionError(String regionFullPath) throws Exception {
107+
when(request.getParameter("pulseData"))
108+
.thenReturn(pulseData("ClusterSelectedRegion", regionFullPath));
109+
110+
ObjectNode json = new ClusterSelectedRegionService(repository).execute(request);
111+
112+
return json.get("selectedRegion").get("errorOnRegion").asText();
113+
}
114+
115+
private String selectedRegionsMemberError(String regionFullPath) throws Exception {
116+
when(request.getParameter("pulseData"))
117+
.thenReturn(pulseData("ClusterSelectedRegionsMember", regionFullPath));
118+
119+
ObjectNode json = new ClusterSelectedRegionsMemberService(repository).execute(request);
120+
121+
return json.get("selectedRegionsMembers").get("errorOnRegion").asText();
122+
}
123+
124+
/** Builds the {@code pulseData} body the Pulse frontend posts for the region-detail page. */
125+
private static String pulseData(String service, String regionFullPath) {
126+
ObjectNode parameters = MAPPER.createObjectNode();
127+
parameters.put("regionFullPath", regionFullPath);
128+
ObjectNode root = MAPPER.createObjectNode();
129+
root.set(service, parameters);
130+
return root.toString();
131+
}
132+
}

0 commit comments

Comments
 (0)