Skip to content

Commit 893259a

Browse files
authored
Merge pull request #181 from companieshouse/feature/configure-authentication-interceptor
Feature/configure authentication interceptor
2 parents a989046 + e9f1a46 commit 893259a

File tree

9 files changed

+561
-6
lines changed

9 files changed

+561
-6
lines changed

src/itest/java/uk/gov/companieshouse/pscdataapi/steps/PscDataSteps.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,10 @@ private HttpHeaders setupHeaders(final boolean includeEric, final String keyRole
766766
headers.set("ERIC-Authorised-Key-Roles", keyRoles);
767767
}
768768

769+
if (keyRoles.equals("*")) {
770+
headers.set("ERIC-Authorised-Key-Privileges", "sensitive-data");
771+
}
772+
769773
return headers;
770774
}
771775

src/main/java/uk/gov/companieshouse/pscdataapi/config/WebSecurityConfig.java

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,31 @@
1717
import uk.gov.companieshouse.api.filter.CustomCorsFilter;
1818
import uk.gov.companieshouse.api.interceptor.InternalUserInterceptor;
1919
import uk.gov.companieshouse.api.interceptor.UserAuthenticationInterceptor;
20+
import uk.gov.companieshouse.pscdataapi.interceptor.AuthenticationHelperImpl;
21+
import uk.gov.companieshouse.pscdataapi.interceptor.FullRecordAuthenticationInterceptor;
2022

2123
@Configuration
2224
public class WebSecurityConfig implements WebMvcConfigurer {
2325
// feature flag marked for future removal
2426
@Value("${feature.identity_verification:false}")
2527
private Boolean identityVerificationEnabled;
2628

29+
public static final String PATTERN_FULL_RECORD =
30+
"/company/{company_number}/persons-with-significant-control/individual/{notification_id}/full_record";
31+
public static final String PATTERN_VERIFICATION_STATE =
32+
"/company/{company_number}/persons-with-significant-control/individual/{notification_id}/verification-state";
33+
2734
List<String> otherAllowedAuthMethods = Arrays.asList("oauth2");
2835

2936
@Override
3037
public void addInterceptors(final InterceptorRegistry registry) {
3138
registry.addInterceptor(userAuthenticationInterceptor());
32-
if (identityVerificationEnabled)
39+
if (Boolean.TRUE.equals(identityVerificationEnabled))
3340
{
3441
registry.addInterceptor(internalUserInterceptor())
35-
.addPathPatterns("/company/{company_number}/persons-with-significant-control/individual/{notification_id}/full_record")
36-
.addPathPatterns("/company/{company_number}/persons-with-significant-control/individual/{notification_id}/verification-state");
42+
.addPathPatterns(PATTERN_VERIFICATION_STATE);
43+
registry.addInterceptor(fullRecordAuthenticationInterceptor())
44+
.addPathPatterns(PATTERN_FULL_RECORD);
3745
}
3846
}
3947

@@ -47,6 +55,15 @@ public InternalUserInterceptor internalUserInterceptor() {
4755
return new InternalUserInterceptor();
4856
}
4957

58+
@Bean
59+
public FullRecordAuthenticationInterceptor fullRecordAuthenticationInterceptor() {
60+
return new FullRecordAuthenticationInterceptor(authenticationHelper());
61+
}
62+
63+
public AuthenticationHelperImpl authenticationHelper() {
64+
return new AuthenticationHelperImpl();
65+
}
66+
5067
/**
5168
* Create UserAuthenticationInterceptor.
5269
*/
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package uk.gov.companieshouse.pscdataapi.interceptor;
2+
3+
import java.util.List;
4+
import java.util.Map;
5+
import jakarta.servlet.http.HttpServletRequest;
6+
7+
/**
8+
* Helper class for authenticating users
9+
*/
10+
public interface AuthenticationHelper {
11+
12+
/**
13+
* Returns the authorised identity type
14+
*
15+
* @param request the {@link HttpServletRequest}
16+
* @return the identity type
17+
*/
18+
String getAuthorisedIdentityType(HttpServletRequest request);
19+
20+
/**
21+
* Verifies that the identity type is key
22+
*
23+
* @param identityType the identify type to be checked
24+
* @return true if the identity type is the key
25+
*/
26+
boolean isApiKeyIdentityType(final String identityType);
27+
28+
/**
29+
* Verifies that the identity type is Oauth2
30+
*
31+
* @param identityType the identity type to be checked
32+
* @return true if the identity type is Oauth2
33+
*/
34+
boolean isOauth2IdentityType(final String identityType);
35+
36+
/**
37+
* Returns the authorised user information
38+
*
39+
* @param request the {@link HttpServletRequest}
40+
* @return the authorised user
41+
*/
42+
String getAuthorisedUser(HttpServletRequest request);
43+
44+
/**
45+
* Returns the privileges granted to the API key
46+
*
47+
* @param request the {@link HttpServletRequest}
48+
* @return the privileges of the API key
49+
*/
50+
String[] getApiKeyPrivileges(HttpServletRequest request);
51+
52+
/**
53+
* Checks whether the key has elevated privileges
54+
*
55+
* @param request the {@link HttpServletRequest}
56+
* @return true if the key has elevated privileges
57+
*/
58+
boolean isKeyElevatedPrivilegesAuthorised(HttpServletRequest request);
59+
60+
/**
61+
* Returns the permissions granted to the OAuth2 Token
62+
*
63+
* @param request the {@link HttpServletRequest}
64+
* @return the privileges of the OAuth2 Token
65+
*/
66+
Map<String, List<String>> getTokenPermissions(HttpServletRequest request);
67+
68+
/**
69+
* Checks whether the token has required permissions
70+
*
71+
* @param request the {@link HttpServletRequest}
72+
* @return true if the token has required permissions
73+
*/
74+
boolean isTokenProtected(HttpServletRequest request);
75+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
package uk.gov.companieshouse.pscdataapi.interceptor;
2+
3+
import java.util.Arrays;
4+
import java.util.HashMap;
5+
import java.util.List;
6+
import java.util.Map;
7+
import java.util.Optional;
8+
import jakarta.servlet.http.HttpServletRequest;
9+
import org.apache.commons.lang.ArrayUtils;
10+
import org.springframework.stereotype.Component;
11+
12+
/**
13+
* Helper class for user authentication
14+
*/
15+
@Component
16+
public class AuthenticationHelperImpl implements AuthenticationHelper {
17+
public static final String OAUTH2_IDENTITY_TYPE = "oauth2";
18+
public static final String API_KEY_IDENTITY_TYPE = "key";
19+
20+
public static final String INTERNAL_APP_PRIVILEGE = "internal-app";
21+
private static final String SENSITIVE_DATA_PRIVILEGE = "sensitive-data";
22+
public static final String ERIC_AUTHORISED_KEY_PRIVILEGES_HEADER
23+
= "ERIC-Authorised-Key-Privileges";
24+
private static final String ERIC_AUTHORISED_TOKEN_PERMISSIONS_HEADER
25+
= "ERIC-Authorised-Token-Permissions";
26+
private static final String ERIC_IDENTITY_TYPE = "ERIC-Identity-Type";
27+
private static final String ERIC_AUTHORISED_USER = "ERIC-Authorised-User";
28+
private static final String COMPANY_PSCS_PERMISSION = "company_pscs";
29+
private static final String READ_PROTECTED = "readprotected";
30+
31+
private static final String GET_METHOD = "GET";
32+
33+
@Override
34+
public String getAuthorisedIdentityType(HttpServletRequest request) {
35+
return getRequestHeader(request, ERIC_IDENTITY_TYPE);
36+
}
37+
38+
@Override
39+
public boolean isApiKeyIdentityType(final String identityType) {
40+
return API_KEY_IDENTITY_TYPE.equals(identityType);
41+
}
42+
43+
@Override
44+
public boolean isOauth2IdentityType(final String identityType) {
45+
return OAUTH2_IDENTITY_TYPE.equals(identityType);
46+
}
47+
48+
@Override
49+
public String getAuthorisedUser(HttpServletRequest request) {
50+
return getRequestHeader(request, ERIC_AUTHORISED_USER);
51+
}
52+
53+
@Override
54+
public String[] getApiKeyPrivileges(HttpServletRequest request) {
55+
// Could be null if header is not present
56+
final String commaSeparatedPrivilegeString = request
57+
.getHeader(ERIC_AUTHORISED_KEY_PRIVILEGES_HEADER);
58+
59+
return Optional.ofNullable(commaSeparatedPrivilegeString)
60+
.map(v -> v.split(","))
61+
.orElse(new String[]{});
62+
}
63+
64+
@Override
65+
public boolean isKeyElevatedPrivilegesAuthorised(HttpServletRequest request) {
66+
String[] privileges = getApiKeyPrivileges(request);
67+
return request.getMethod().equals(GET_METHOD) ? ArrayUtils.contains(privileges, SENSITIVE_DATA_PRIVILEGE) :
68+
ArrayUtils.contains(privileges, INTERNAL_APP_PRIVILEGE);
69+
}
70+
71+
@Override
72+
public Map<String, List<String>> getTokenPermissions(HttpServletRequest request) {
73+
String tokenPermissionsHeader = request.getHeader(ERIC_AUTHORISED_TOKEN_PERMISSIONS_HEADER);
74+
75+
Map<String, List<String>> permissions = new HashMap<>();
76+
77+
if (tokenPermissionsHeader != null) {
78+
for (String pair : tokenPermissionsHeader.split(" ")) {
79+
String[] parts = pair.split("=");
80+
permissions.put(parts[0], Arrays.asList(parts[1].split(",")));
81+
}
82+
}
83+
84+
return permissions;
85+
}
86+
87+
@Override
88+
public boolean isTokenProtected(HttpServletRequest request) {
89+
Map<String, List<String>> privileges = getTokenPermissions(request);
90+
91+
return privileges.containsKey(COMPANY_PSCS_PERMISSION) &&
92+
privileges.get(COMPANY_PSCS_PERMISSION).contains(READ_PROTECTED);
93+
}
94+
95+
private String getRequestHeader(HttpServletRequest request, String header) {
96+
return request == null ? null : request.getHeader(header);
97+
}
98+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package uk.gov.companieshouse.pscdataapi.interceptor;
2+
3+
import static uk.gov.companieshouse.pscdataapi.PscDataApiApplication.APPLICATION_NAME_SPACE;
4+
5+
import jakarta.servlet.http.HttpServletRequest;
6+
import jakarta.servlet.http.HttpServletResponse;
7+
import java.util.Arrays;
8+
import java.util.Map;
9+
import org.apache.http.HttpStatus;
10+
import org.springframework.lang.NonNull;
11+
import org.springframework.stereotype.Component;
12+
import org.springframework.web.servlet.HandlerInterceptor;
13+
import uk.gov.companieshouse.logging.Logger;
14+
import uk.gov.companieshouse.logging.LoggerFactory;
15+
import uk.gov.companieshouse.pscdataapi.logging.DataMapHolder;
16+
17+
@Component
18+
public class FullRecordAuthenticationInterceptor implements HandlerInterceptor {
19+
20+
private static final Logger LOGGER = LoggerFactory.getLogger(APPLICATION_NAME_SPACE);
21+
22+
private final AuthenticationHelper authHelper;
23+
24+
public FullRecordAuthenticationInterceptor(AuthenticationHelper authHelper) {
25+
this.authHelper = authHelper;
26+
}
27+
28+
@Override
29+
public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
30+
@NonNull Object handler) {
31+
final String identityType = authHelper.getAuthorisedIdentityType(request);
32+
Map<String, Object> logMap = DataMapHolder.getLogMap();
33+
34+
if (authHelper.isOauth2IdentityType(identityType)) {
35+
36+
if (authHelper.isTokenProtected(request)) {
37+
return true;
38+
} else {
39+
LOGGER.errorRequest(request, "User not authorised. Token has insufficient permissions.", logMap);
40+
response.setStatus(HttpStatus.SC_UNAUTHORIZED);
41+
return false;
42+
}
43+
}
44+
45+
if (!authHelper.isApiKeyIdentityType(identityType)) {
46+
logMap.put("identityType", identityType);
47+
LOGGER.errorRequest(request, "User not authorised. Identity type not correct", logMap);
48+
49+
response.setStatus(HttpStatus.SC_UNAUTHORIZED);
50+
return false;
51+
}
52+
53+
if (!authHelper.isKeyElevatedPrivilegesAuthorised(request)) {
54+
logMap.put("privileges", Arrays.asList(authHelper.getApiKeyPrivileges(request)));
55+
LOGGER.errorRequest(request,
56+
"User not authorised. API key does not have sufficient privileges.",
57+
logMap);
58+
59+
response.setStatus(HttpStatus.SC_FORBIDDEN);
60+
return false;
61+
}
62+
63+
return true;
64+
}
65+
}

src/test/java/uk/gov/companieshouse/pscdataapi/config/WebSecurityConfigTest.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import static org.springframework.http.MediaType.APPLICATION_JSON;
44
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
55
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
6+
import static uk.gov.companieshouse.pscdataapi.interceptor.AuthenticationHelperImpl.ERIC_AUTHORISED_KEY_PRIVILEGES_HEADER;
67

78
import org.junit.jupiter.api.DisplayName;
89
import org.junit.jupiter.api.Test;
@@ -83,6 +84,8 @@ private HttpHeaders createHttpHeaders(final boolean hasInternalPrivilege) {
8384
headers.add(EricConstants.ERIC_IDENTITY_TYPE, SecurityConstants.API_KEY_IDENTITY_TYPE);
8485
headers.add(EricConstants.ERIC_AUTHORISED_KEY_ROLES,
8586
hasInternalPrivilege ? SecurityConstants.INTERNAL_USER_ROLE : "any_other_role");
87+
headers.add(ERIC_AUTHORISED_KEY_PRIVILEGES_HEADER,
88+
hasInternalPrivilege ? "sensitive-data" : "internal-app");
8689

8790
return headers;
8891
}

src/test/java/uk/gov/companieshouse/pscdataapi/controller/CompanyPscFullRecordGetControllerTest.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class CompanyPscFullRecordGetControllerTest {
3939
private static final String ERIC_IDENTITY = "Test-Identity";
4040
private static final String ERIC_IDENTITY_TYPE = "key";
4141
private static final String ERIC_PRIVILEGES = "*";
42-
private static final String ERIC_AUTH_INTERNAL = "internal-app";
42+
private static final String ERIC_AUTH_SENSITIVE = "sensitive-data";
4343

4444
private static final String GET_INDIVIDUAL_FULL_RECORD_URL = String.format(
4545
"/company/%s/persons-with-significant-control/individual/%s/full_record", MOCK_COMPANY_NUMBER,
@@ -109,7 +109,7 @@ void getIndividualPSC() throws Exception {
109109
.contentType(APPLICATION_JSON)
110110
.header("x-request-id", X_REQUEST_ID)
111111
.header("ERIC-Authorised-Key-Roles", ERIC_PRIVILEGES)
112-
.header("ERIC-Authorised-Key-Privileges", ERIC_AUTH_INTERNAL)).andExpect(status().isOk())
112+
.header("ERIC-Authorised-Key-Privileges", ERIC_AUTH_SENSITIVE)).andExpect(status().isOk())
113113
.andDo(print())
114114
.andExpect(content().json(expectedData, true));
115115
}
@@ -126,7 +126,7 @@ void shouldReturn404WhenIndividualPscNotFound() throws Exception {
126126
.contentType(APPLICATION_JSON)
127127
.header("x-request-id", X_REQUEST_ID)
128128
.header("ERIC-Authorised-Key-Roles", ERIC_PRIVILEGES)
129-
.header("ERIC-Authorised-Key-Privileges", ERIC_AUTH_INTERNAL))
129+
.header("ERIC-Authorised-Key-Privileges", ERIC_AUTH_SENSITIVE))
130130
.andDo(print())
131131
.andExpect(status().isNotFound());
132132
}

0 commit comments

Comments
 (0)