Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release/11.0 #141

Merged
merged 11 commits into from
May 1, 2024
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package gov.cabinetoffice.gap.applybackend.annotations;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LambdasHeaderValidator {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package gov.cabinetoffice.gap.applybackend.config;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Configuration("lambdaProperties")
@ConfigurationProperties(prefix = "lambda")
public class LambdaSecretConfigProperties {

private String secret;

private String privateKey;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package gov.cabinetoffice.gap.applybackend.config;

import gov.cabinetoffice.gap.applybackend.security.interceptors.AuthorizationHeaderInterceptor;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@RequiredArgsConstructor
@Configuration
public class LambdasInterceptor implements WebMvcConfigurer {

private static final String UUID_REGEX_STRING = "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}";
private final LambdaSecretConfigProperties lambdaSecretConfigProperties;

@Bean(name="lambdas_applicant_interceptor")
AuthorizationHeaderInterceptor lambdasInterceptor() {
return new AuthorizationHeaderInterceptor(lambdaSecretConfigProperties.getSecret(),
lambdaSecretConfigProperties.getPrivateKey());
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(lambdasInterceptor())
.addPathPatterns( "/submissions/{submissionId:" + UUID_REGEX_STRING
+ "}/question/{questionId:" + UUID_REGEX_STRING + "}/attachment/scanresult")
.order(Ordered.HIGHEST_PRECEDENCE);
}

}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public class CreateGrantBeneficiaryDto {
private Boolean locationSco;
private Boolean locationWal;
private Boolean locationNir;
private Boolean locationLon;
private Boolean locationOutUk;

@NotNull(message = "Select 'Yes, answer the equality questions' or 'No, skip the equality questions'")
private Boolean hasProvidedAdditionalAnswers;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ public class GetGrantBeneficiaryDto {
private Boolean locationSco;
private Boolean locationWal;
private Boolean locationNir;
private Boolean locationLon;
private Boolean locationOutUk;
private Boolean hasProvidedAdditionalAnswers;
private Boolean ageGroup1;
private Boolean ageGroup2;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package gov.cabinetoffice.gap.applybackend.dto.api;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SchemeMandatoryQuestionApplicationFormInfosDto {
private boolean hasAdvertPublished;
private boolean hasInternalApplication;
private boolean hasPublishedInternalApplication;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package gov.cabinetoffice.gap.applybackend.exception;

public class AdvertNotPublishedException extends RuntimeException {
public AdvertNotPublishedException(String message) {
super(message);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ public class GrantBeneficiary {
@Column
private Boolean locationNir;

@Column
private Boolean locationLon;

@Column
private Boolean locationOutUk;

@Column
private Boolean hasProvidedAdditionalAnswers;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package gov.cabinetoffice.gap.applybackend.security.filters;

import org.springframework.stereotype.Component;
import org.springframework.web.filter.AbstractRequestLoggingFilter;

import javax.servlet.http.HttpServletRequest;

@Component
public class CustomRequestLoggingFilter extends AbstractRequestLoggingFilter {

public CustomRequestLoggingFilter() {
this.setIncludeHeaders(true);
this.setIncludeQueryString(true);
this.setMaxPayloadLength(10000);
this.setIncludePayload(true);
}

@Override
protected boolean shouldLog(HttpServletRequest request) {
if (request.getRequestURI().endsWith("/health")) {
return false;
}

return logger.isDebugEnabled();
}

@Override
protected void beforeRequest(HttpServletRequest request, String message) {
logger.debug(message);
}

@Override
protected void afterRequest(HttpServletRequest request, String message) {
logger.debug(message);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package gov.cabinetoffice.gap.applybackend.security.interceptors;


import gov.cabinetoffice.gap.applybackend.annotations.LambdasHeaderValidator;
import lombok.extern.log4j.Log4j2;
import org.springframework.http.HttpHeaders;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;

//this is needed to "authenticate" all the call from the Lambdas
@Log4j2
public class AuthorizationHeaderInterceptor implements HandlerInterceptor {

private final String expectedAuthorizationValue;

private final String privateKey;

public AuthorizationHeaderInterceptor(String expectedAuthorizationValue, String privateKey) {
this.expectedAuthorizationValue = expectedAuthorizationValue;
this.privateKey = privateKey;
}

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
log.debug("Intercepting request: " + request.getRequestURI());
if (handler instanceof HandlerMethod handlerMethod) {
final Method method = handlerMethod.getMethod();

final LambdasHeaderValidator annotation = method.getAnnotation(LambdasHeaderValidator.class);

if (annotation != null) {
log.debug("Request is coming from lambda, validating authorization header");

final String authorizationHeader = request.getHeader(HttpHeaders.AUTHORIZATION);
final boolean isAuthorizationHeaderCorrect = compareAuthorizationSecret(authorizationHeader,
expectedAuthorizationValue, privateKey);
if (authorizationHeader == null || !isAuthorizationHeaderCorrect) {

log.debug("Authorization Header Value: " + authorizationHeader
+ " does not match the expected value");

response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return false;
}

log.debug("Authorization Header Value matches the expected value");
}
}

return true;
}

private boolean compareAuthorizationSecret(String authorizationHeader, String expectedAuthorizationValue,
String privateKey) {

if (authorizationHeader == null || privateKey == null) {
return false;
}

return decrypt(authorizationHeader, privateKey).equals(expectedAuthorizationValue);

}

private String decrypt(String encryptedText, String privateKeyString) {

try {

final byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyString);
final byte[] encryptedMessageBytes = Base64.getDecoder().decode(encryptedText);

final PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
final KeyFactory keyFactory = KeyFactory.getInstance("RSA");
final PrivateKey rsaPrivateKey = keyFactory.generatePrivate(keySpec);

final Cipher decryptCipher = Cipher.getInstance("RSA");
decryptCipher.init(Cipher.DECRYPT_MODE, rsaPrivateKey);

final byte[] decryptedMessageBytes = decryptCipher.doFinal(encryptedMessageBytes);

return new String(decryptedMessageBytes, StandardCharsets.UTF_8);
}
catch (IllegalArgumentException | InvalidKeyException | NoSuchAlgorithmException | InvalidKeySpecException
| BadPaddingException | IllegalBlockSizeException | NoSuchPaddingException e) {
log.error("Error decrypting authorization header from lambdas: " + e.getMessage());
return "";
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public class GrantAdvertService {
private final GrantMandatoryQuestionService grantMandatoryQuestionService;
private final GrantApplicantService grantApplicantService;

protected static String getExternalSubmissionUrl(GrantAdvert advert) {
public String getApplyToUrl(GrantAdvert advert) {
return advert.getResponse().getSections().stream()
.filter(section -> section.getId().equals("howToApply"))
.flatMap(section -> section.getPages().stream())
Expand All @@ -60,7 +60,7 @@ public GetGrantAdvertDto generateGetGrantAdvertDto(GrantAdvert advert, GetGrantM
return GetGrantAdvertDto.builder()
.id(advert.getId())
.version(advert.getVersion())
.externalSubmissionUrl(getExternalSubmissionUrl(advert))
.externalSubmissionUrl(getApplyToUrl(advert))
.isInternal(isInternal)
.grantApplicationId(grantApplicationId)
.grantSchemeId(advert.getScheme().getId())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ public boolean doesSchemeHaveAPublishedApplication(final GrantScheme grantScheme
return grantApplicationRepository.findByGrantSchemeAndApplicationStatus(grantScheme, GrantApplicationStatus.PUBLISHED).isPresent();
}

public boolean doesSchemeHaveAnApplication(final GrantScheme grantScheme) {
return grantApplicationRepository.findByGrantScheme(grantScheme).isPresent();
}

public Integer getGrantApplicationId(final GrantScheme grantScheme) {
final Optional<GrantApplication> grantApplication = grantApplicationRepository.findByGrantScheme(grantScheme);
return grantApplication.map(GrantApplication::getId).orElse(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,14 @@ public GrantMandatoryQuestions getMandatoryQuestionBySchemeId(Integer schemeId,
return grantMandatoryQuestion.get();
}

public GrantMandatoryQuestions createMandatoryQuestion(GrantScheme scheme, GrantApplicant applicant) {
public GrantMandatoryQuestions createMandatoryQuestion(GrantScheme scheme, GrantApplicant applicant, boolean isForInternalApplication) {
if (mandatoryQuestionExistsBySchemeIdAndApplicantId(scheme.getId(), applicant.getId())) {
log.debug("Mandatory question for scheme {}, and applicant {} already exist", scheme.getId(), applicant.getId());
return grantMandatoryQuestionRepository.findByGrantSchemeAndCreatedBy(scheme, applicant).get(0);
}

if (scheme.getGrantApplication() != null && scheme.getGrantApplication().getApplicationStatus() == GrantApplicationStatus.REMOVED) {
if (isForInternalApplication && scheme.getGrantApplication() != null &&
scheme.getGrantApplication().getApplicationStatus() == GrantApplicationStatus.REMOVED) {
throw new GrantApplicationNotPublishedException(String.format("Mandatory question for scheme %d could not be created as the application is not published", scheme.getId()));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,8 @@ private void createGrantBeneficiary(final Submission submission) {
.locationSco(containsLocation(locations, "Scotland"))
.locationWal(containsLocation(locations, "Wales"))
.locationNir(containsLocation(locations, "Northern Ireland"))
.locationLon(containsLocation(locations, "London"))
.locationOutUk(containsLocation(locations, "Outside of the UK"))
.gapId(submission.getGapId())
.build());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ private int getNumberOfWords(String response) {
}

private boolean containsSpecialCharacters(String response) {
return !response.matches("^(?![\\s\\S])|^[a-zA-Z0-9à-üÀ-Ü\\s',!@£$%^&*()_+=\\[\\];./?><:\"{}|`~߀•–¥¢…µèéêëěẽýŷÿùúûüǔũūűìíîïǐĩiòóôöǒàáâäśğźžżćçčċñńņň-]+$");
return !response.matches("^(?![\\s\\S])|^[a-zA-Z0-9à-üÀ-Ü\\s',!@£$%^&*()_+=\\[\\];./?><:\"{}|`~߀•–¥¢…µèéêëěẽýŷÿùúûüǔũūűìíîïǐĩiòóôöǒàáâäśğźžżćçčċñńņň-“”‘’]+$");
}

private ValidationResult validateDate(final String[] dateComponents, final boolean isMandatory) {
Expand Down Expand Up @@ -297,7 +297,7 @@ private ValidationResult validateDate(final String[] dateComponents, final boole
return dateValidationResult;
}

if (month > 12) {
if (month < 1 || month > 12 ) {
dateValidationResult.addError(ValidationConstants.MULTI_RESPONSE_FIELD + "[1]", "Date must include a real month");
return dateValidationResult;
}
Expand All @@ -307,7 +307,7 @@ private ValidationResult validateDate(final String[] dateComponents, final boole
return dateValidationResult;
}

if (!dayIsValidForMonth(day, month, year)) {
if (day < 1 || !dayIsValidForMonth(day, month, year) ) {
dateValidationResult.addError(ValidationConstants.MULTI_RESPONSE_FIELD + "[0]", "Date must include a real day");
return dateValidationResult;
}
Expand Down
Loading
Loading