Skip to content

Commit bcd4a51

Browse files
Daniel-Lamhemantt
authored andcommitted
RDM-4369: Support reserved characters in userId email address (#420)
* RDM-4369: Support reserved characters in userId email address Ensure `GET` request to `/user-profile/users` endpoint uses a fully encoded URL, to allow reserved characters in the `uid` query parameter. * Change method of encoding request URL Switch to using `DefaultUriBuilderFactory` in Spring Boot 2 to encode the request URL, to fix a problem with "double encoding" of query parameters (see spring-projects/spring-framework#20750 (comment)). * Add missing tests to increase coverage to required level * Switch to use ApplicationParams.encode() to encode the userId query string parameter Avoid setting the encoding mode on `restTemplate` because this is an autowired singleton bean, shared across the application. A future ticket will address encoding query string parameters for *all* requests, using `restTemplate`. * Fix "double encoding" problem introduced by previous commit * Fix missing import statement * Remove unused import
1 parent 0eb92fe commit bcd4a51

File tree

4 files changed

+101
-17
lines changed

4 files changed

+101
-17
lines changed

src/main/java/uk/gov/hmcts/ccd/ApplicationParams.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ public class ApplicationParams {
103103
@Value("${search.elastic.nodes.discovery.filter}")
104104
private String elasticsearchNodeDiscoveryFilter;
105105

106-
private static String encode(final String stringToEncode) {
106+
public static String encode(final String stringToEncode) {
107107
try {
108108
return URLEncoder.encode(stringToEncode, "UTF-8");
109109
} catch (UnsupportedEncodingException e) {

src/main/java/uk/gov/hmcts/ccd/data/user/DefaultUserRepository.java

+15-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
package uk.gov.hmcts.ccd.data.user;
22

3+
import java.net.URI;
4+
import java.net.URISyntaxException;
35
import java.util.HashMap;
46
import java.util.List;
57
import java.util.Map;
68
import java.util.Objects;
9+
import java.util.Optional;
710
import java.util.Set;
811
import java.util.stream.Collectors;
912

@@ -21,9 +24,10 @@
2124
import org.springframework.security.core.GrantedAuthority;
2225
import org.springframework.security.core.context.SecurityContextHolder;
2326
import org.springframework.stereotype.Repository;
24-
import org.springframework.web.client.HttpStatusCodeException;
2527
import org.springframework.web.client.RestClientException;
28+
import org.springframework.web.client.RestClientResponseException;
2629
import org.springframework.web.client.RestTemplate;
30+
import org.springframework.web.util.UriComponentsBuilder;
2731
import uk.gov.hmcts.ccd.ApplicationParams;
2832
import uk.gov.hmcts.ccd.AuthCheckerConfiguration;
2933
import uk.gov.hmcts.ccd.data.SecurityUtils;
@@ -111,17 +115,22 @@ public UserDefault getUserDefaultSettings(final String userId) {
111115
LOG.debug("retrieving default user settings for user {}", userId);
112116
final HttpEntity requestEntity = new HttpEntity(securityUtils.authorizationHeaders());
113117
final Map<String, String> queryParams = new HashMap<>();
114-
queryParams.put("uid", userId);
115-
return restTemplate.exchange(applicationParams.userDefaultSettingsURL(),
116-
HttpMethod.GET, requestEntity, UserDefault.class, queryParams).getBody();
117-
} catch (HttpStatusCodeException e) {
118+
queryParams.put("uid", ApplicationParams.encode(userId));
119+
final String encodedUrl = UriComponentsBuilder.fromHttpUrl(applicationParams.userDefaultSettingsURL())
120+
.buildAndExpand(queryParams).toUriString();
121+
return restTemplate.exchange(new URI(encodedUrl), HttpMethod.GET, requestEntity, UserDefault.class)
122+
.getBody();
123+
} catch (RestClientResponseException e) {
118124
LOG.error("Failed to retrieve user profile", e);
119-
final List<String> headerMessages = e.getResponseHeaders().get("Message");
125+
final List<String> headerMessages = Optional.ofNullable(e.getResponseHeaders())
126+
.map(headers -> headers.get("Message")).orElse(null);
120127
final String message = headerMessages != null ? headerMessages.get(0) : e.getMessage();
121128
if (message != null) {
122129
throw new BadRequestException(message);
123130
}
124131
throw new ServiceException("Problem getting user default settings for " + userId);
132+
} catch (URISyntaxException e) {
133+
throw new BadRequestException(e.getMessage());
125134
}
126135
}
127136

src/test/java/uk/gov/hmcts/ccd/data/user/DefaultUserRepositoryTest.java

+84-9
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
package uk.gov.hmcts.ccd.data.user;
22

3+
import java.net.URI;
34
import java.util.Arrays;
45
import java.util.Collection;
56
import java.util.Set;
67
import java.util.stream.Collectors;
78

89
import static java.util.Arrays.asList;
910
import static java.util.Collections.emptyList;
11+
import static java.util.Collections.singletonList;
1012
import static org.hamcrest.CoreMatchers.is;
1113
import static org.hamcrest.MatcherAssert.assertThat;
1214
import static org.hamcrest.Matchers.hasItems;
1315
import static org.hamcrest.Matchers.hasSize;
1416
import static org.junit.jupiter.api.Assertions.assertAll;
1517
import static org.junit.jupiter.api.Assertions.assertThrows;
16-
import static org.mockito.Matchers.any;
17-
import static org.mockito.Matchers.anyListOf;
18-
import static org.mockito.Matchers.anyString;
19-
import static org.mockito.Matchers.eq;
18+
import static org.mockito.ArgumentMatchers.any;
19+
import static org.mockito.ArgumentMatchers.anyList;
20+
import static org.mockito.ArgumentMatchers.anyString;
21+
import static org.mockito.ArgumentMatchers.eq;
22+
import static org.mockito.ArgumentMatchers.same;
2023
import static org.mockito.Mockito.doReturn;
2124
import static org.mockito.Mockito.doThrow;
2225
import static org.mockito.Mockito.verify;
@@ -31,20 +34,26 @@
3134
import org.mockito.Mock;
3235
import org.mockito.MockitoAnnotations;
3336
import org.springframework.http.HttpEntity;
37+
import org.springframework.http.HttpHeaders;
3438
import org.springframework.http.HttpMethod;
39+
import org.springframework.http.HttpStatus;
3540
import org.springframework.http.ResponseEntity;
3641
import org.springframework.security.core.Authentication;
3742
import org.springframework.security.core.GrantedAuthority;
3843
import org.springframework.security.core.context.SecurityContext;
3944
import org.springframework.security.core.context.SecurityContextHolder;
4045
import org.springframework.web.client.RestClientException;
46+
import org.springframework.web.client.RestClientResponseException;
4147
import org.springframework.web.client.RestTemplate;
4248
import uk.gov.hmcts.ccd.ApplicationParams;
4349
import uk.gov.hmcts.ccd.data.SecurityUtils;
4450
import uk.gov.hmcts.ccd.data.casedetails.SecurityClassification;
4551
import uk.gov.hmcts.ccd.data.definition.CaseDefinitionRepository;
4652
import uk.gov.hmcts.ccd.domain.model.aggregated.IdamUser;
53+
import uk.gov.hmcts.ccd.domain.model.aggregated.UserDefault;
54+
import uk.gov.hmcts.ccd.domain.model.definition.Jurisdiction;
4755
import uk.gov.hmcts.ccd.domain.model.definition.UserRole;
56+
import uk.gov.hmcts.ccd.endpoint.exceptions.BadRequestException;
4857
import uk.gov.hmcts.ccd.endpoint.exceptions.ServiceException;
4958
import uk.gov.hmcts.reform.auth.checker.spring.serviceanduser.ServiceAndUserDetails;
5059

@@ -123,7 +132,8 @@ void shouldOnlyConsiderRolesForJurisdiction() {
123132
userRepository.getUserClassifications(JURISDICTION_ID);
124133

125134
assertAll(
126-
() -> verify(caseDefinitionRepository).getClassificationsForUserRoleList(asList(ROLE_CASEWORKER_CMC)),
135+
() -> verify(caseDefinitionRepository).getClassificationsForUserRoleList(
136+
singletonList(ROLE_CASEWORKER_CMC)),
127137
() -> verifyNoMoreInteractions(caseDefinitionRepository)
128138
);
129139
}
@@ -141,7 +151,7 @@ void shouldConsiderCitizen() {
141151
userRepository.getUserClassifications(JURISDICTION_ID);
142152

143153
assertAll(
144-
() -> verify(caseDefinitionRepository).getClassificationsForUserRoleList(asList(CITIZEN)),
154+
() -> verify(caseDefinitionRepository).getClassificationsForUserRoleList(singletonList(CITIZEN)),
145155
() -> verifyNoMoreInteractions(caseDefinitionRepository)
146156
);
147157
}
@@ -154,7 +164,8 @@ void shouldConsiderLetterHolder() {
154164
userRepository.getUserClassifications(JURISDICTION_ID);
155165

156166
assertAll(
157-
() -> verify(caseDefinitionRepository).getClassificationsForUserRoleList(asList(LETTER_HOLDER)),
167+
() -> verify(caseDefinitionRepository).getClassificationsForUserRoleList(
168+
singletonList(LETTER_HOLDER)),
158169
() -> verifyNoMoreInteractions(caseDefinitionRepository)
159170
);
160171
}
@@ -169,6 +180,69 @@ void shouldExcludeOtherRoles() {
169180
}
170181
}
171182

183+
@Nested
184+
@DisplayName("getUserDefaultSettings()")
185+
class GetUserDefaultSettings {
186+
187+
@Test
188+
@DisplayName("should return the User Profile defaults for the user")
189+
void shouldReturnUserProfileDefaultsForUser() {
190+
when(applicationParams.userDefaultSettingsURL()).thenReturn("http://test.hmcts.net/users?uid={uid}");
191+
final Jurisdiction jurisdiction = new Jurisdiction();
192+
jurisdiction.setId("TEST");
193+
jurisdiction.setName("Test");
194+
jurisdiction.setDescription("Test Jurisdiction");
195+
final UserDefault userDefault = new UserDefault();
196+
userDefault.setJurisdictions(singletonList(jurisdiction));
197+
final ResponseEntity<UserDefault> responseEntity = new ResponseEntity<>(userDefault, HttpStatus.OK);
198+
when(restTemplate
199+
.exchange(any(URI.class), eq(HttpMethod.GET), any(HttpEntity.class), eq(UserDefault.class)))
200+
.thenReturn(responseEntity);
201+
202+
final UserDefault result = userRepository.getUserDefaultSettings("[email protected]");
203+
assertThat(result, is(userDefault));
204+
verify(restTemplate).exchange(
205+
any(URI.class), same(HttpMethod.GET), any(HttpEntity.class), (Class<?>)any(Class.class));
206+
}
207+
208+
@Test
209+
@DisplayName("should throw a BadRequestException if the User Profile defaults cannot be retrieved")
210+
void shouldThrowExceptionIfUserProfileCannotBeRetrieved() {
211+
when(applicationParams.userDefaultSettingsURL()).thenReturn("http://test.hmcts.net/users?uid={uid}");
212+
final HttpHeaders headers = new HttpHeaders();
213+
headers.add("Message", "User Profile data could not be retrieved");
214+
final RestClientResponseException exception =
215+
new RestClientResponseException("Error on GET", 400, "Bad Request", headers, null, null);
216+
when(restTemplate
217+
.exchange(any(URI.class), eq(HttpMethod.GET), any(HttpEntity.class), eq(UserDefault.class)))
218+
.thenThrow(exception);
219+
220+
final BadRequestException badRequestException =
221+
assertThrows(BadRequestException.class,
222+
() -> userRepository.getUserDefaultSettings("[email protected]"),
223+
"Expected getUserDefaultSettings() to throw, but it didn't");
224+
assertThat(badRequestException.getMessage(), is(headers.getFirst("Message")));
225+
}
226+
227+
@Test
228+
@DisplayName("should throw a ServiceException if an error occurs retrieving the User Profile defaults")
229+
void shouldThrowExceptionIfErrorOnRetrievingUserProfile() {
230+
when(applicationParams.userDefaultSettingsURL()).thenReturn("http://test.hmcts.net/users?uid={uid}");
231+
final RestClientResponseException exception =
232+
new RestClientResponseException(null, 500, "Internal Server Error", null, null, null);
233+
when(restTemplate
234+
.exchange(any(URI.class), eq(HttpMethod.GET), any(HttpEntity.class), eq(UserDefault.class)))
235+
.thenThrow(exception);
236+
237+
final String userId = "[email protected]";
238+
final ServiceException serviceException =
239+
assertThrows(ServiceException.class,
240+
() -> userRepository.getUserDefaultSettings(userId),
241+
"Expected getUserDefaultSettings() to throw, but it didn't");
242+
assertThat(serviceException.getMessage(), is("Problem getting user default settings for " + userId));
243+
}
244+
}
245+
172246
@Nested
173247
@DisplayName("getHighestUserClassification()")
174248
class GetHighestUserClassification {
@@ -184,7 +258,8 @@ void shouldReturnHighestClassification() {
184258
userRole2.setSecurityClassification(SecurityClassification.PUBLIC.name());
185259
UserRole userRole3 = new UserRole();
186260
userRole3.setSecurityClassification(SecurityClassification.RESTRICTED.name());
187-
when(caseDefinitionRepository.getClassificationsForUserRoleList(anyListOf(String.class))).thenReturn(asList(userRole1, userRole2, userRole3));
261+
when(caseDefinitionRepository.getClassificationsForUserRoleList(anyList()))
262+
.thenReturn(asList(userRole1, userRole2, userRole3));
188263

189264
SecurityClassification result = userRepository.getHighestUserClassification(JURISDICTION_ID);
190265

@@ -195,7 +270,7 @@ void shouldReturnHighestClassification() {
195270
@DisplayName("should throw exception when no user roles returned")
196271
void shouldThrowExceptionWhenNoUserRolesReturned() {
197272
asCaseworker();
198-
when(caseDefinitionRepository.getClassificationsForUserRoleList(anyListOf(String.class))).thenReturn(emptyList());
273+
when(caseDefinitionRepository.getClassificationsForUserRoleList(anyList())).thenReturn(emptyList());
199274

200275
assertThrows(ServiceException.class, () -> userRepository.getHighestUserClassification(JURISDICTION_ID));
201276
}

src/test/resources/mappings/user1-profile.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"request": {
33
"method": "GET",
4-
"url": "/user-profile/users?uid=Cloud.Strife@test.com"
4+
"url": "/user-profile/users?uid=Cloud.Strife%40test.com"
55
},
66
"response": {
77
"status": 200,

0 commit comments

Comments
 (0)