From 84bb45ecb417f6a92b7d8884048bd7901c6b1b42 Mon Sep 17 00:00:00 2001 From: Thomas <> Date: Thu, 3 Feb 2022 20:28:50 +0100 Subject: fix(ernp): fix some bugs in ERnP client implementation --- .../specific/modules/auth/eidas/v2/Constants.java | 2 - .../auth/eidas/v2/clients/ernp/ErnpRestClient.java | 265 ++++++++++++--------- .../exception/ErnpRestCommunicationException.java | 24 ++ 3 files changed, 180 insertions(+), 111 deletions(-) create mode 100644 eidas_modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/exception/ErnpRestCommunicationException.java (limited to 'eidas_modules/authmodule-eIDAS-v2/src/main/java/at') diff --git a/eidas_modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/Constants.java b/eidas_modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/Constants.java index d48d69bf..6da30299 100644 --- a/eidas_modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/Constants.java +++ b/eidas_modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/Constants.java @@ -166,8 +166,6 @@ public class Constants { public static final String CONIG_PROPS_EIDAS_ERNPCLIENT = CONIG_PROPS_EIDAS_PREFIX + ".ernpclient"; public static final String CONIG_PROPS_EIDAS_ERNPCLIENT_ENDPOINT = CONIG_PROPS_EIDAS_ERNPCLIENT + ".endpoint"; - public static final String CONIG_PROPS_EIDAS_ERNPCLIENT_DEBUG_TRACEMESSAGES = CONIG_PROPS_EIDAS_ERNPCLIENT - + ".debug.logfullmessages"; public static final String CONIG_PROPS_EIDAS_ERNPCLIENT_TIMEOUT_CONNECTION = CONIG_PROPS_EIDAS_ERNPCLIENT + ".timeout.connection"; public static final String CONIG_PROPS_EIDAS_ERNPCLIENT_TIMEOUT_RESPONSE = CONIG_PROPS_EIDAS_ERNPCLIENT diff --git a/eidas_modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/clients/ernp/ErnpRestClient.java b/eidas_modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/clients/ernp/ErnpRestClient.java index a651385f..605f1d0f 100644 --- a/eidas_modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/clients/ernp/ErnpRestClient.java +++ b/eidas_modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/clients/ernp/ErnpRestClient.java @@ -2,6 +2,7 @@ package at.asitplus.eidas.specific.modules.auth.eidas.v2.clients.ernp; import java.io.IOException; import java.text.MessageFormat; +import java.time.LocalDate; import java.time.OffsetDateTime; import java.util.Arrays; import java.util.Collections; @@ -12,9 +13,11 @@ import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.PostConstruct; +import org.apache.commons.lang3.StringUtils; import org.apache.http.client.HttpClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; @@ -23,10 +26,10 @@ import org.springframework.http.converter.json.MappingJackson2HttpMessageConvert import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.web.client.ResponseErrorHandler; +import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import com.fasterxml.jackson.annotation.JsonInclude.Include; -import com.fasterxml.jackson.databind.ObjectMapper; import at.asitplus.eidas.specific.modules.auth.eidas.v2.Constants; import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.RegisterResult; @@ -42,6 +45,7 @@ import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.ernp.model.SuchenRes import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.ernp.model.Suchoptionen; import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.ernp.model.Suchoptionen.HistorischEnum; import at.asitplus.eidas.specific.modules.auth.eidas.v2.exception.EidasSAuthenticationException; +import at.asitplus.eidas.specific.modules.auth.eidas.v2.exception.ErnpRestCommunicationException; import at.asitplus.eidas.specific.modules.auth.eidas.v2.exception.WorkflowException; import at.asitplus.eidas.specific.modules.auth.eidas.v2.utils.VersionHolder; import at.gv.bmi.namespace.zmr_su.zmr._20040201.PersonSuchenRequest; @@ -60,17 +64,18 @@ import lombok.extern.slf4j.Slf4j; /** * Implements an ERnP client that uses REST API for communication. - * + * * @author tlenz * */ @Slf4j public class ErnpRestClient implements IErnpClient { - private static final String ERROR_MATCHING_01 = "module.eidasauth.matching.01"; - private static final String ERROR_MATCHING_02 = "module.eidasauth.matching.02"; + private static final String ERROR_MATCHING_11 = "module.eidasauth.matching.11"; + private static final String ERROR_MATCHING_12 = "module.eidasauth.matching.12"; private static final String ERROR_MATCHING_99 = "module.eidasauth.matching.99"; - + + private static final String LOGMSG_MISSING_CONFIG = "Missing configuration with key: {0}"; private static final String LOGMSG_ERNP_ERROR = "Receive an error from ERnP during '{}' operation with msg: {}"; private static final String LOGMSG_ERNP_RESP_PROCESS = @@ -78,69 +83,73 @@ public class ErnpRestClient implements IErnpClient { private static final String LOGMSG_ERNP_SOAP_ERROR = "ERnP anwser for transaction: {0} with code: {1} and message: {2}"; - - private static final String PROCESS_SEARCH_PERSONAL_IDENTIFIER = + + private static final String PROCESS_SEARCH_PERSONAL_IDENTIFIER = "Searching " + Constants.eIDAS_ATTR_PERSONALIDENTIFIER; private static final String PROCESS_SEARCH_MDS_ONLY = "Searching with MDS only"; private static final String PROCESS_SEARCH_COUNTRY_SPECIFIC = "Searching {0} specific"; - + private static final String PROCESS_KITT_GENERAL = "KITT general-processing"; private static final String PROCESS_KITT_IDENITIES_GET = "KITT get-latest-version"; private static final String PROCESS_KITT_IDENITIES_UPDATE = "KITT update dataset"; - + private static final String FRIENDLYNAME_HTTP_CLIENT = "ERnP Client"; - - private static final String PATTERN_BIRTHDAY_STRING = "{0}-{1}-{2}"; - - - @Autowired IConfiguration basicConfig; - @Autowired EaafKeyStoreFactory keyStoreFactory; - @Autowired IHttpClientFactory httpClientFactory; - @Autowired VersionHolder versionHolder; - + + @Autowired + IConfiguration basicConfig; + @Autowired + EaafKeyStoreFactory keyStoreFactory; + @Autowired + IHttpClientFactory httpClientFactory; + @Autowired + VersionHolder versionHolder; + private DefaultApi ernpClient; - @Override public ErnpRegisterResult searchWithPersonIdentifier(String personIdentifier, String citizenCountryCode) throws EidasSAuthenticationException { - + try { - + // build generic request metadata - GenericRequestParams generic = buildGenericRequestParameters("step1"); + final GenericRequestParams generic = buildGenericRequestParameters("step1"); - // build search request - SuchEidas eidasInfos = new SuchEidas(); + // build search request + final SuchEidas eidasInfos = new SuchEidas(); eidasInfos.setArt(Constants.eIDAS_ATTRURN_PERSONALIDENTIFIER); eidasInfos.setWert(personIdentifier); eidasInfos.setStaatscode2(citizenCountryCode); - - PersonSuchen personSuchen = new PersonSuchen(); + + final PersonSuchen personSuchen = new PersonSuchen(); personSuchen.setSuchoptionen(generateSearchParameters()); - personSuchen.setBegruendung(PROCESS_SEARCH_PERSONAL_IDENTIFIER); - Suchdaten searchInfos = new Suchdaten(); - searchInfos.setEidas(eidasInfos); + personSuchen.setBegruendung(PROCESS_SEARCH_PERSONAL_IDENTIFIER); + final Suchdaten searchInfos = new Suchdaten(); + searchInfos.setEidas(eidasInfos); personSuchen.setSuchdaten(searchInfos); - + // request ERnP log.trace("Requesting ERnP for '{}' operation", PROCESS_SEARCH_PERSONAL_IDENTIFIER); - SuchenResponse resp = ernpClient.suchen(generic.getClientBehkz(), generic.clientName, + final SuchenResponse resp = ernpClient.suchen(generic.getClientBehkz(), generic.clientName, generic.getClientRequestTime(), generic.getClientRequestId(), personSuchen); - + // parse ZMR response return processErnpResponse(resp, citizenCountryCode, true, PROCESS_SEARCH_PERSONAL_IDENTIFIER); + + } catch (RestClientException e) { + log.warn(LOGMSG_ERNP_ERROR, PROCESS_SEARCH_PERSONAL_IDENTIFIER, e.getMessage()); + throw new EidasSAuthenticationException(ERROR_MATCHING_11, new Object[] { e.getMessage() }, e); - } catch (EidasSAuthenticationException e) { + } catch (final EidasSAuthenticationException e) { throw e; - - } catch (Exception e) { + + } catch (final Exception e) { log.warn(LOGMSG_ERNP_RESP_PROCESS, PROCESS_SEARCH_PERSONAL_IDENTIFIER, e.getMessage()); throw new EidasSAuthenticationException(ERROR_MATCHING_99, new Object[] { e.getMessage() }, e); } - + } - + @Override public ErnpRegisterResult searchWithMds(String givenName, String familyName, String dateOfBirth, String citizenCountryCode) throws EidasSAuthenticationException { @@ -164,54 +173,66 @@ public class ErnpRestClient implements IErnpClient { String zipcode, String city, String street) { return new ErnpRegisterResult(Collections.emptyList()); } - + @PostConstruct private void initialize() throws EaafException { - // set-up the Ernp client - ernpClient = new DefaultApi(new ApiClient(buildRestClient())); - // validate additional Ernp communication parameters valdiateAdditionalConfigParameters(); - + + // set-up the Ernp client + final ApiClient baseClient = new ApiClient(buildRestClient()); + baseClient.setBasePath(basicConfig.getBasicConfiguration( + Constants.CONIG_PROPS_EIDAS_ERNPCLIENT_ENDPOINT)); + ernpClient = new DefaultApi(baseClient); + } - + private void valdiateAdditionalConfigParameters() { - // TODO Auto-generated method stub - + checkConfigurationValue(Constants.CONIG_PROPS_EIDAS_ERNPCLIENT_ENDPOINT); + checkConfigurationValue(Constants.CONIG_PROPS_EIDAS_ERNPCLIENT_REQ_ORGANIZATION_NR); + + } + + private void checkConfigurationValue(String key) { + if (StringUtils.isEmpty(basicConfig.getBasicConfiguration(key))) { + throw new RuntimeException(MessageFormat.format(LOGMSG_MISSING_CONFIG, key)); + + } } private Suchoptionen generateSearchParameters() { - Suchoptionen options = new Suchoptionen(); + final Suchoptionen options = new Suchoptionen(); options.setZmr(false); options.setHistorisch(HistorischEnum.AKTUELLUNDHISTORISCH); options.setSucheMitNamensteilen(false); - options.setSuchwizard(false); + options.setSuchwizard(false); return options; - + } @Nonnull private ErnpRegisterResult processErnpResponse(SuchenResponse resp, @Nonnull String citizenCountryCode, - boolean forceSinglePersonMatch, @Nonnull String processStepFiendlyname) throws EaafAuthenticationException { - if (resp.getPerson() == null + boolean forceSinglePersonMatch, @Nonnull String processStepFiendlyname) + throws EaafAuthenticationException { + if (resp.getPerson() == null || resp.getPerson().isEmpty()) { log.debug("ERnP result contains NO 'Person' or 'Person' is empty"); return new ErnpRegisterResult(Collections.emptyList()); - - } else { - log.debug("Get #{} person results from '{}' operation", + + } else { + log.debug("Get #{} person results from '{}' operation", resp.getPerson().size(), processStepFiendlyname); - + if (forceSinglePersonMatch) { return new ErnpRegisterResult(processSearchPersonResponseSingleResult( resp.getPerson(), citizenCountryCode, processStepFiendlyname)); - + } else { return new ErnpRegisterResult(processSearchPersonResponse( resp.getPerson(), citizenCountryCode)); - - } - } + + } + } } @Nonnull @@ -224,22 +245,22 @@ public class ErnpRestClient implements IErnpClient { .collect(Collectors.toList()); } - + @NonNull private List processSearchPersonResponseSingleResult( @Nonnull List persons, @Nonnull String citizenCountryCode, String processStepFiendlyname) throws EaafAuthenticationException { if (persons.size() > 1) { log.error("Find more-than-one ERnP entry with search criteria that has to be unique"); - throw new WorkflowException(processStepFiendlyname, + throw new WorkflowException(processStepFiendlyname, "Find more-than-one ERnP entry with search criteria that has to be unique", true); - + } else { return Arrays.asList(mapErnpResponseToRegisterResult(persons.get(0), citizenCountryCode)); } } - + @Nonnull private RegisterResult mapErnpResponseToRegisterResult(@Nonnull Person person, @Nonnull String citizenCountryCode) { @@ -254,15 +275,28 @@ public class ErnpRestClient implements IErnpClient { .placeOfBirth(selectSingleEidasDocument(person, citizenCountryCode, Constants.eIDAS_ATTRURN_PLACEOFBIRTH)) .birthName(selectSingleEidasDocument(person, citizenCountryCode, - Constants.eIDAS_ATTRURN_BIRTHNAME)) + Constants.eIDAS_ATTRURN_BIRTHNAME)) .build(); } - + + /** + * Build AT specific Date String 'yyyy-MM-dd' from ERnP birthday representation. + * + *

+ * Info: {@link LocalDate} can not be used, because '1940-00-00' is also + * a valid birthday on ERnP site. + *

+ * + * @param geburtsdatum ERnP birthday representation + * @return birthday in 'yyyy-MM-dd' format + */ private String buildTextualBirthday(PartialDate geburtsdatum) { - return MessageFormat.format(PATTERN_BIRTHDAY_STRING, - geburtsdatum.getJahr(), geburtsdatum.getMonat(), geburtsdatum.getTag()); - + return MessageFormat.format("{0}-{1}-{2}", + String.valueOf(geburtsdatum.getJahr()), + String.format("%02d", geburtsdatum.getMonat()), + String.format("%02d", geburtsdatum.getTag())); + } /** @@ -276,12 +310,18 @@ public class ErnpRestClient implements IErnpClient { */ @NonNull private List selectAllEidasDocument(Person person, String citizenCountryCode, - String eidasAttrurnPersonalidentifier) { - return person.getEidas().stream() - .filter(el -> eidasAttrurnPersonalidentifier.equals(el.getArt()) - && el.getStaatscode2().equals(citizenCountryCode)) - .map(el -> el.getWert()) - .collect(Collectors.toList()); + String eidasAttrurnPersonalidentifier) { + if (person.getEidas() != null) { + return person.getEidas().stream() + .filter(el -> eidasAttrurnPersonalidentifier.equals(el.getArt()) + && el.getStaatscode2().equals(citizenCountryCode)) + .map(el -> el.getWert()) + .collect(Collectors.toList()); + + } else { + return Collections.emptyList(); + + } } @@ -297,15 +337,20 @@ public class ErnpRestClient implements IErnpClient { @Nullable private String selectSingleEidasDocument(Person person, String citizenCountryCode, String eidasAttrurnPersonalidentifier) { - return person.getEidas().stream() - .filter(el -> eidasAttrurnPersonalidentifier.equals(el.getArt()) - && el.getStaatscode2().equals(citizenCountryCode)) - .findFirst() - .map(el -> el.getWert()) - .orElse(null); + if (person.getEidas() != null) { + return person.getEidas().stream() + .filter(el -> eidasAttrurnPersonalidentifier.equals(el.getArt()) + && el.getStaatscode2().equals(citizenCountryCode)) + .findFirst() + .map(el -> el.getWert()) + .orElse(null); + + } else { + return null; + } } - + private RestTemplate buildRestClient() throws EaafException { log.debug("Building REST-Client for ERnP communication ... "); final HttpClient httpClient = httpClientFactory.getHttpClient(buildHttpClientConfiguration()); @@ -316,42 +361,44 @@ public class ErnpRestClient implements IErnpClient { return springClient; } - + private HttpMessageConverter buildCustomJacksonObjectMapper() { - MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); - ObjectMapper objectMapper = new ObjectMapper(); - objectMapper.setSerializationInclusion(Include.NON_NULL); - converter.setObjectMapper(objectMapper); + final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(); + converter.setSupportedMediaTypes(Collections.singletonList(MediaType.ALL)); + converter.getObjectMapper() + .setSerializationInclusion(Include.NON_NULL); return converter; - + } - + @Nonnull private ResponseErrorHandler buildErrorHandler() { return new ResponseErrorHandler() { @Override public boolean hasError(ClientHttpResponse response) throws IOException { - return response.getStatusCode().is4xxClientError() - || response.getStatusCode().is5xxServerError(); - + return response.getStatusCode().is4xxClientError() + || response.getStatusCode().is5xxServerError(); + } @Override - public void handleError(ClientHttpResponse response) throws IOException { - //TODO: implement errorHandling based on response infos - + public void handleError(ClientHttpResponse response) throws IOException { + // TODO: implement errorHandling based on response infos + if (response.getStatusCode().series() == HttpStatus.Series.SERVER_ERROR) { log.warn("Receive http-server-error: {} from ERnP", response.getRawStatusCode()); - + } else if (response.getStatusCode().series() == HttpStatus.Series.CLIENT_ERROR) { log.warn("Receive http-client-error: {} from ERnP", response.getRawStatusCode()); - + } + + throw new ErnpRestCommunicationException(response.getRawStatusCode()); } }; } - + @Nonnull private HttpClientConfiguration buildHttpClientConfiguration() throws EaafException { final HttpClientConfiguration config = new HttpClientConfiguration(FRIENDLYNAME_HTTP_CLIENT); @@ -369,39 +416,39 @@ public class ErnpRestClient implements IErnpClient { basicConfig.getBasicConfiguration(Constants.CONIG_PROPS_EIDAS_COMMON_CLIENT_SSL_KEYS_ALIAS)); config.setSslKeyPassword( basicConfig.getBasicConfiguration(Constants.CONIG_PROPS_EIDAS_COMMON_CLIENT_SSL_KEY_PASSWORD)); - + // Set connection parameters - //TODO: update EAAF-components to allow custom HTTP Connection-Timeouts - + // TODO: update EAAF-components to allow custom HTTP Connection-Timeouts + return config; } - @AllArgsConstructor @Getter public static class ErnpRegisterResult { private final List personResult; - + } private GenericRequestParams buildGenericRequestParameters(String operationIdentifier) { return GenericRequestParams.builder() - .clientBehkz(basicConfig.getBasicConfiguration(Constants.CONIG_PROPS_EIDAS_ZMRCLIENT_REQ_ORGANIZATION_NR)) + .clientBehkz(basicConfig.getBasicConfiguration( + Constants.CONIG_PROPS_EIDAS_ZMRCLIENT_REQ_ORGANIZATION_NR)) .clientName(MessageFormat.format(Constants.CLIENT_INFO, versionHolder.getVersion())) .clientRequestTime(OffsetDateTime.now()) .clientRequestId(TransactionIdUtils.getTransactionId() + "_" + operationIdentifier) - .build(); - + .build(); + } - + @Builder @Getter - private static class GenericRequestParams { + private static class GenericRequestParams { String clientBehkz; - String clientName; - OffsetDateTime clientRequestTime; + String clientName; + OffsetDateTime clientRequestTime; String clientRequestId; - + } } diff --git a/eidas_modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/exception/ErnpRestCommunicationException.java b/eidas_modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/exception/ErnpRestCommunicationException.java new file mode 100644 index 00000000..72a5ece6 --- /dev/null +++ b/eidas_modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/exception/ErnpRestCommunicationException.java @@ -0,0 +1,24 @@ +package at.asitplus.eidas.specific.modules.auth.eidas.v2.exception; + +import java.io.IOException; + +import lombok.Getter; + +/** + * ERnP exception in case of a REST communication error. + * + * @author tlenz + * + */ +public class ErnpRestCommunicationException extends IOException { + private static final long serialVersionUID = -3178689122077228976L; + + @Getter + int httpStatusCode; + + public ErnpRestCommunicationException(int rawStatusCode) { + super("ERnP service answers with an error and HTTP status-code: " + rawStatusCode); + this.httpStatusCode = rawStatusCode; + } + +} -- cgit v1.2.3