Commit 76031011 authored by Deniro StopCovid's avatar Deniro StopCovid

Merge branch 'release/v1.7.0' into 'develop'

Merge Back Release/v1.7.0

See merge request !107
parents e9eb6a4d 9c45d333
......@@ -19,7 +19,7 @@
<groupId>fr.gouv.stopc</groupId>
<artifactId>robert-server</artifactId>
<version>1.7.0-SNAPSHOT</version>
<version>1.8.0-SNAPSHOT</version>
<name>robert-server</name>
<packaging>pom</packaging>
<description>Projet principal</description>
......
......@@ -13,7 +13,7 @@
<parent>
<groupId>fr.gouv.stopc</groupId>
<artifactId>robert-server</artifactId>
<version>1.7.0-SNAPSHOT</version>
<version>1.8.0-SNAPSHOT</version>
</parent>
<artifactId>robert-crypto-grpc-server-messaging</artifactId>
......
......@@ -13,7 +13,7 @@
<parent>
<groupId>fr.gouv.stopc</groupId>
<artifactId>robert-server</artifactId>
<version>1.7.0-SNAPSHOT</version>
<version>1.8.0-SNAPSHOT</version>
</parent>
......
......@@ -13,7 +13,7 @@
<parent>
<groupId>fr.gouv.stopc</groupId>
<artifactId>robert-server</artifactId>
<version>1.7.0-SNAPSHOT</version>
<version>1.8.0-SNAPSHOT</version>
</parent>
<artifactId>robert-crypto-grpc-server</artifactId>
......
......@@ -13,7 +13,7 @@
<parent>
<groupId>fr.gouv.stopc</groupId>
<artifactId>robert-server</artifactId>
<version>1.7.0-SNAPSHOT</version>
<version>1.8.0-SNAPSHOT</version>
</parent>
<artifactId>robert-server-batch</artifactId>
......
......@@ -13,7 +13,7 @@
<parent>
<groupId>fr.gouv.stopc</groupId>
<artifactId>robert-server</artifactId>
<version>1.7.0-SNAPSHOT</version>
<version>1.8.0-SNAPSHOT</version>
</parent>
<artifactId>robert-server-common</artifactId>
......
......@@ -13,7 +13,7 @@
<parent>
<groupId>fr.gouv.stopc</groupId>
<artifactId>robert-server</artifactId>
<version>1.7.0-SNAPSHOT</version>
<version>1.8.0-SNAPSHOT</version>
</parent>
<artifactId>robert-server-crypto</artifactId>
......
......@@ -13,7 +13,7 @@
<parent>
<groupId>fr.gouv.stopc</groupId>
<artifactId>robert-server</artifactId>
<version>1.7.0-SNAPSHOT</version>
<version>1.8.0-SNAPSHOT</version>
</parent>
......
......@@ -13,7 +13,7 @@
<parent>
<groupId>fr.gouv.stopc</groupId>
<artifactId>robert-server</artifactId>
<version>1.7.0-SNAPSHOT</version>
<version>1.8.0-SNAPSHOT</version>
</parent>
......
......@@ -13,7 +13,7 @@
<parent>
<groupId>fr.gouv.stopc</groupId>
<artifactId>robert-server</artifactId>
<version>1.7.0-SNAPSHOT</version>
<version>1.8.0-SNAPSHOT</version>
</parent>
<artifactId>robert-server-ws-rest</artifactId>
......@@ -134,6 +134,24 @@
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.2</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
<version>0.11.2</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
......
......@@ -16,10 +16,18 @@ import lombok.ToString;
@RefreshScope
public class WsServerConfiguration {
@Value("${robert.epoch-bundle-duration-in-days}")
private Integer epochBundleDurationInDays;
@Value("${robert.epoch-bundle-duration-in-days}")
private Integer epochBundleDurationInDays;
@Value("${robert.server.status-request-minimum-epoch-gap}")
private Integer statusRequestMinimumEpochGap;
@Value("${robert.server.status-request-minimum-epoch-gap}")
private Integer statusRequestMinimumEpochGap;
@Value("${robert.jwt.privatekey}")
private String jwtPrivateKey;
@Value("${robert.jwt.lifetime}")
private int jwtLifeTime;
@Value("${robert.jwt.use-transient-key}")
private Boolean jwtUseTransientKey;
}
......@@ -17,8 +17,8 @@ import fr.gouv.stopc.robertserver.ws.utils.UriConstants;
import fr.gouv.stopc.robertserver.ws.vo.DeleteHistoryRequestVo;
@RestController
@RequestMapping(value = {"${controller.path.prefix}" + UriConstants.API_V2,
"${controller.path.prefix}" + UriConstants.API_V3 })
@RequestMapping(value = {"${controller.path.prefix}" + UriConstants.API_V1, "${controller.path.prefix}" + UriConstants.API_V2,
"${controller.path.prefix}" + UriConstants.API_V3, "${controller.path.prefix}" + UriConstants.API_V4})
@Consumes(MediaType.APPLICATION_JSON_VALUE)
@Produces(MediaType.APPLICATION_JSON_VALUE)
public interface IDeleteHistoryController {
......
......@@ -17,7 +17,8 @@ import fr.gouv.stopc.robertserver.ws.exception.RobertServerException;
import fr.gouv.stopc.robertserver.ws.utils.UriConstants;
@RestController
@RequestMapping(value = {"${controller.path.prefix}" + UriConstants.API_V2, "${controller.path.prefix}" + UriConstants.API_V3})
@RequestMapping(value = {"${controller.path.prefix}" + UriConstants.API_V2, "${controller.path.prefix}" + UriConstants.API_V3,
"${controller.path.prefix}" + UriConstants.API_V4})
@Consumes(MediaType.APPLICATION_JSON_VALUE)
@Produces(MediaType.APPLICATION_JSON_VALUE)
public interface IRegisterController {
......
......@@ -24,7 +24,8 @@ import fr.gouv.stopc.robertserver.ws.vo.ReportBatchRequestVo;
public interface IReportController {
@PostMapping(value = UriConstants.REPORT)
ResponseEntity<ReportBatchResponseDto> reportContactHistory(@Valid @RequestBody(required = true) ReportBatchRequestVo reportBatchRequestVo)
ResponseEntity<ReportBatchResponseDto> reportContactHistory(
@Valid @RequestBody(required = true) ReportBatchRequestVo reportBatchRequestVo)
throws RobertServerException;
}
package fr.gouv.stopc.robertserver.ws.controller;
import javax.validation.Valid;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import fr.gouv.stopc.robertserver.ws.dto.ReportBatchResponseV4Dto;
import fr.gouv.stopc.robertserver.ws.exception.RobertServerException;
import fr.gouv.stopc.robertserver.ws.utils.UriConstants;
import fr.gouv.stopc.robertserver.ws.vo.ReportBatchRequestVo;
@RestController
@RequestMapping(value = "${controller.path.prefix}" + UriConstants.API_V4,
consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
public interface IReportControllerV4 {
@PostMapping(value = UriConstants.REPORT)
ResponseEntity<ReportBatchResponseV4Dto> reportContactHistory(@Valid @RequestBody(required = true) ReportBatchRequestVo reportBatchRequestVo)
throws RobertServerException;
}
......@@ -18,7 +18,7 @@ import fr.gouv.stopc.robertserver.ws.vo.StatusVo;
@RestController
@RequestMapping(value = {"${controller.path.prefix}" + UriConstants.API_V1, "${controller.path.prefix}" + UriConstants.API_V2,
"${controller.path.prefix}" + UriConstants.API_V3})
"${controller.path.prefix}" + UriConstants.API_V3, "${controller.path.prefix}" + UriConstants.API_V4})
@Consumes(MediaType.APPLICATION_JSON_VALUE)
@Produces(MediaType.APPLICATION_JSON_VALUE)
public interface IStatusController {
......
......@@ -16,8 +16,8 @@ import fr.gouv.stopc.robertserver.ws.utils.UriConstants;
import fr.gouv.stopc.robertserver.ws.vo.UnregisterRequestVo;
@RestController
@RequestMapping(value = {"${controller.path.prefix}" + UriConstants.API_V2,
"${controller.path.prefix}" + UriConstants.API_V3})
@RequestMapping(value = {"${controller.path.prefix}" + UriConstants.API_V1, "${controller.path.prefix}" + UriConstants.API_V2,
"${controller.path.prefix}" + UriConstants.API_V3, "${controller.path.prefix}" + UriConstants.API_V4})
@Consumes(MediaType.APPLICATION_JSON_VALUE)
@Produces(MediaType.APPLICATION_JSON_VALUE)
public interface IUnregisterController {
......
package fr.gouv.stopc.robertserver.ws.controller.impl;
import java.util.Objects;
import java.util.Optional;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import fr.gouv.stopc.robertserver.ws.dto.VerifyResponseDto;
import fr.gouv.stopc.robertserver.ws.exception.RobertServerBadRequestException;
import fr.gouv.stopc.robertserver.ws.exception.RobertServerException;
import fr.gouv.stopc.robertserver.ws.exception.RobertServerUnauthorizedException;
import fr.gouv.stopc.robertserver.ws.service.IRestApiService;
import fr.gouv.stopc.robertserver.ws.utils.MessageConstants;
import fr.gouv.stopc.robertserver.ws.vo.ReportBatchRequestVo;
import io.micrometer.core.instrument.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Service
public class ReportControllerDelegate {
private IRestApiService restApiService;
public ReportControllerDelegate(final IRestApiService restApiService) {
this.restApiService = restApiService;
}
public boolean isReportRequestValid(ReportBatchRequestVo reportBatchRequestVo) throws RobertServerException {
if (this.areBothFieldsPresent(reportBatchRequestVo)) {
log.warn("Contacts and ContactsAsBinary are both present");
return false;
} else if (Objects.isNull(reportBatchRequestVo.getContacts())) {
log.warn("Contacts are null. They could be empty([]) but not null");
return false;
} else if (this.areBothFieldsAbsent(reportBatchRequestVo)) {
log.warn("Contacts and ContactsAsBinary are both absent");
return false;
}
this.checkValidityToken(reportBatchRequestVo.getToken());
return true;
}
private boolean areBothFieldsPresent(ReportBatchRequestVo reportBatchRequestVo) {
return !CollectionUtils.isEmpty(reportBatchRequestVo.getContacts())
&& StringUtils.isNotEmpty(reportBatchRequestVo.getContactsAsBinary());
}
private boolean areBothFieldsAbsent(ReportBatchRequestVo reportBatchRequestVo) {
return Objects.isNull(reportBatchRequestVo.getContacts())
&& StringUtils.isEmpty(reportBatchRequestVo.getContactsAsBinary());
}
private void checkValidityToken(String token) throws RobertServerException {
if (StringUtils.isEmpty(token)) {
log.warn("No token provided");
throw new RobertServerBadRequestException(MessageConstants.INVALID_DATA.getValue());
}
if (token.length() != 6 && token.length() != 36) {
log.warn("Token size is incorrect");
throw new RobertServerBadRequestException(MessageConstants.INVALID_DATA.getValue());
}
Optional<VerifyResponseDto> response = this.restApiService.verifyReportToken(token, getCodeType(token));
if (!response.isPresent() || !response.get().isValid()) {
log.warn("Verifying the token failed");
throw new RobertServerUnauthorizedException(MessageConstants.INVALID_AUTHENTICATION.getValue());
}
log.info("Verifying the token succeeded");
}
private String getCodeType(String token) {
// TODO: create enum for long and short codes
return token.length() == 6 ? "2" : "1";
}
}
package fr.gouv.stopc.robertserver.ws.controller.impl;
import java.util.Optional;
import javax.inject.Inject;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import fr.gouv.stopc.robertserver.ws.controller.IReportController;
import fr.gouv.stopc.robertserver.ws.dto.ReportBatchResponseDto;
import fr.gouv.stopc.robertserver.ws.dto.VerifyResponseDto;
import fr.gouv.stopc.robertserver.ws.exception.RobertServerBadRequestException;
import fr.gouv.stopc.robertserver.ws.exception.RobertServerException;
import fr.gouv.stopc.robertserver.ws.exception.RobertServerUnauthorizedException;
import fr.gouv.stopc.robertserver.ws.service.ContactDtoService;
import fr.gouv.stopc.robertserver.ws.service.IRestApiService;
import fr.gouv.stopc.robertserver.ws.utils.MessageConstants;
import fr.gouv.stopc.robertserver.ws.vo.ReportBatchRequestVo;
import io.micrometer.core.instrument.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import java.util.Objects;
@Service
@Slf4j
public class ReportControllerImpl implements IReportController {
private ReportControllerDelegate delegate;
private final ContactDtoService contactDtoService;
private final IRestApiService restApiService;
private ContactDtoService contactDtoService;
@Inject
public ReportControllerImpl(final ContactDtoService contactDtoService, final IRestApiService restApiService) {
public ReportControllerImpl(final ContactDtoService contactDtoService,
final IRestApiService restApiService, final ReportControllerDelegate delegate) {
this.contactDtoService = contactDtoService;
this.restApiService = restApiService;
}
private boolean areBothFieldsPresent(ReportBatchRequestVo reportBatchRequestVo) {
return !CollectionUtils.isEmpty(reportBatchRequestVo.getContacts())
&& StringUtils.isNotEmpty(reportBatchRequestVo.getContactsAsBinary());
}
private boolean areBothFieldsAbsent(ReportBatchRequestVo reportBatchRequestVo) {
return Objects.isNull(reportBatchRequestVo.getContacts())
&& StringUtils.isEmpty(reportBatchRequestVo.getContactsAsBinary());
this.delegate = delegate;
}
@Override
public ResponseEntity<ReportBatchResponseDto> reportContactHistory(ReportBatchRequestVo reportBatchRequestVo) throws RobertServerException {
if (areBothFieldsPresent(reportBatchRequestVo)) {
log.warn("Contacts and ContactsAsBinary are both present");
return ResponseEntity.badRequest().build();
} else if (Objects.isNull(reportBatchRequestVo.getContacts())) {
log.warn("Contacts are null. They could be empty([]) but not null");
return ResponseEntity.badRequest().build();
}else if (areBothFieldsAbsent(reportBatchRequestVo)) {
log.warn("Contacts and ContactsAsBinary are both absent");
return ResponseEntity.badRequest().build();
}
checkValidityToken(reportBatchRequestVo.getToken());
if ( ! delegate.isReportRequestValid(reportBatchRequestVo) )
return ResponseEntity.badRequest().build();
contactDtoService.saveContacts(reportBatchRequestVo.getContacts());
ReportBatchResponseDto reportBatchResponseDto = ReportBatchResponseDto.builder()
.message(MessageConstants.SUCCESSFUL_OPERATION.getValue())
.success(Boolean.TRUE)
.build();
ReportBatchResponseDto reportBatchResponseDto = ReportBatchResponseDto.builder().message(MessageConstants.SUCCESSFUL_OPERATION.getValue()).success(Boolean.TRUE).build();
return ResponseEntity.ok(reportBatchResponseDto);
}
private void checkValidityToken(String token) throws RobertServerException {
if (StringUtils.isEmpty(token)) {
log.warn("No token provided");
throw new RobertServerBadRequestException(MessageConstants.INVALID_DATA.getValue());
}
if (token.length() != 6 && token.length() != 36) {
log.warn("Token size is incorrect");
throw new RobertServerBadRequestException(MessageConstants.INVALID_DATA.getValue());
}
Optional<VerifyResponseDto> response = this.restApiService.verifyReportToken(token, getCodeType(token));
if (!response.isPresent() || !response.get().isValid()) {
log.warn("Verifying the token failed");
throw new RobertServerUnauthorizedException(MessageConstants.INVALID_AUTHENTICATION.getValue());
}
log.info("Verifying the token succeeded");
}
private String getCodeType(String token) {
// TODO: create enum for long and short codes
return token.length() == 6 ? "2" : "1";
return ResponseEntity.ok(reportBatchResponseDto);
}
}
package fr.gouv.stopc.robertserver.ws.controller.impl;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Date;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import fr.gouv.stopc.robertserver.ws.config.WsServerConfiguration;
import fr.gouv.stopc.robertserver.ws.controller.IReportControllerV4;
import fr.gouv.stopc.robertserver.ws.dto.ReportBatchResponseV4Dto;
import fr.gouv.stopc.robertserver.ws.exception.RobertServerException;
import fr.gouv.stopc.robertserver.ws.service.ContactDtoService;
import fr.gouv.stopc.robertserver.ws.utils.MessageConstants;
import fr.gouv.stopc.robertserver.ws.vo.ReportBatchRequestVo;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
@Service
@Slf4j
public class ReportControllerV4Impl implements IReportControllerV4 {
public static final SignatureAlgorithm signatureAlgo = SignatureAlgorithm.RS256;
private final WsServerConfiguration wsServerConfiguration;
private final ReportControllerDelegate delegate;
private final ContactDtoService contactDtoService;
private PrivateKey jwtPrivateKey;
public ReportControllerV4Impl(final ContactDtoService contactDtoService,
final WsServerConfiguration wsServerConfiguration,
final ReportControllerDelegate delegate) {
this.contactDtoService = contactDtoService;
this.wsServerConfiguration = wsServerConfiguration;
this.delegate = delegate;
}
@Override
public ResponseEntity<ReportBatchResponseV4Dto> reportContactHistory(ReportBatchRequestVo reportBatchRequestVo) throws RobertServerException {
if ( ! delegate.isReportRequestValid(reportBatchRequestVo) )
return ResponseEntity.badRequest().build();
contactDtoService.saveContacts(reportBatchRequestVo.getContacts());
String token = generateJWT(reportBatchRequestVo);
ReportBatchResponseV4Dto reportBatchResponseDto = ReportBatchResponseV4Dto.builder()
.message(MessageConstants.SUCCESSFUL_OPERATION.getValue())
.reportValidationToken(token)
.success(Boolean.TRUE)
.build();
return ResponseEntity.ok(reportBatchResponseDto);
}
private String generateJWT(ReportBatchRequestVo reportBatchRequestVo) throws RobertServerException {
try {
Date now = new Date();
Date expiration = new Date(now.getTime() + this.wsServerConfiguration.getJwtLifeTime() * 60000);
String token = Jwts.builder()
.setHeaderParam("type", "JWT")
.setIssuedAt(now)
.setExpiration(expiration)
.signWith(this.getJwtPrivateKey(), signatureAlgo)
.compact();
return token;
} catch (Exception e) {
log.error("JWT token generation failed!", e);
// Avoid to send an HTTP Error code to the client if only the report validation token generation fails
return "";
}
}
protected PrivateKey getJwtPrivateKey() throws NoSuchAlgorithmException, InvalidKeySpecException {
if (this.jwtPrivateKey != null)
return this.jwtPrivateKey;
if (this.wsServerConfiguration.getJwtUseTransientKey()) {
// In test mode, we generate a transient key
KeyPair keyPair = Keys.keyPairFor(signatureAlgo);
this.jwtPrivateKey = keyPair.getPrivate();
} else {
byte[] encoded = Decoders.BASE64.decode(this.wsServerConfiguration.getJwtPrivateKey());
KeyFactory keyFactory = KeyFactory.getInstance(signatureAlgo.getFamilyName());
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encoded);
this.jwtPrivateKey = keyFactory.generatePrivate(keySpec);
}
return this.jwtPrivateKey;
}
}
package fr.gouv.stopc.robertserver.ws.dto;
import javax.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class ReportBatchResponseV4Dto {
@NotNull
private Boolean success;
private String message;
private String reportValidationToken;
}
......@@ -25,4 +25,6 @@ public final class UriConstants {
public static final String API_V3 = "/v3";
public static final String API_V4 = "/v4";
}