aboutsummaryrefslogtreecommitdiff
path: root/modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/clients/ernp/ErnpRestClient.java
diff options
context:
space:
mode:
Diffstat (limited to 'modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/clients/ernp/ErnpRestClient.java')
-rw-r--r--modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/clients/ernp/ErnpRestClient.java857
1 files changed, 857 insertions, 0 deletions
diff --git a/modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/clients/ernp/ErnpRestClient.java b/modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/clients/ernp/ErnpRestClient.java
new file mode 100644
index 00000000..4c4e3d87
--- /dev/null
+++ b/modules/authmodule-eIDAS-v2/src/main/java/at/asitplus/eidas/specific/modules/auth/eidas/v2/clients/ernp/ErnpRestClient.java
@@ -0,0 +1,857 @@
+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.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nonnull;
+import javax.annotation.PostConstruct;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.client.HttpClient;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.http.client.ClientHttpResponse;
+import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import org.springframework.lang.NonNull;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+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.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.Constants;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.RegisterResult;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.SimpleEidasData;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.SimpleEidasData.SimpleEidasDataBuilder;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.ernp.api.DefaultApi;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.ernp.invoker.ApiClient;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.ernp.model.Aendern;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.ernp.model.AendernResponse;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.ernp.model.Anlegen;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.ernp.model.AnlegenResponse;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.ernp.model.Eidas;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.ernp.model.PartialDate;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.ernp.model.Person;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.ernp.model.PersonAendern;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.ernp.model.PersonAnlegen;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.ernp.model.PersonSuchen;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.ernp.model.Personendaten;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.ernp.model.PersonendatenErgebnis;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.ernp.model.SuchEidas;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.ernp.model.Suchdaten;
+import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.ernp.model.SuchenResponse;
+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.base._20040201_.ServiceFault;
+import at.gv.bmi.namespace.zmr_su.zmr._20040201.EidasSuchdatenType;
+import at.gv.bmi.namespace.zmr_su.zmr._20040201.PersonSuchenRequest;
+import at.gv.egiz.eaaf.core.api.idp.IConfiguration;
+import at.gv.egiz.eaaf.core.exceptions.EaafAuthenticationException;
+import at.gv.egiz.eaaf.core.exceptions.EaafException;
+import at.gv.egiz.eaaf.core.impl.credential.EaafKeyStoreFactory;
+import at.gv.egiz.eaaf.core.impl.http.HttpClientConfiguration;
+import at.gv.egiz.eaaf.core.impl.http.HttpClientConfiguration.ClientAuthMode;
+import at.gv.egiz.eaaf.core.impl.http.IHttpClientFactory;
+import at.gv.egiz.eaaf.core.impl.utils.TransactionIdUtils;
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Getter;
+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_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 =
+ "Proces ERnP response during '{}' operation failes with msg: {}";
+
+ //private static final String LOGMSG_ERNP_REST_ERROR =
+ // "ERnP anwser for transaction: {0} with code: {1} and message: {2}";
+
+ 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 PROCESS_ADD_IDENITY = "Add new person";
+
+ private static final String FRIENDLYNAME_HTTP_CLIENT = "ERnP Client";
+
+ // HTTP header-names from ERnP response
+ private static final String ERNP_RESPONSE_HEADER_SERVER_ID = "Server-Request-Id";
+
+
+ @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
+ final GenericRequestParams generic = buildGenericRequestParameters("stepId");
+
+ // build search request
+ final SuchEidas eidasInfos = new SuchEidas();
+ eidasInfos.setArt(Constants.eIDAS_ATTRURN_PERSONALIDENTIFIER);
+ eidasInfos.setWert(personIdentifier);
+ eidasInfos.setStaatscode2(citizenCountryCode);
+
+ final PersonSuchen personSuchen = new PersonSuchen();
+ personSuchen.setSuchoptionen(generateSearchParameters());
+ personSuchen.setBegruendung(PROCESS_SEARCH_PERSONAL_IDENTIFIER);
+ final Suchdaten searchInfos = new Suchdaten();
+ searchInfos.setEidas(Arrays.asList(eidasInfos));
+ personSuchen.setSuchdaten(searchInfos);
+
+ // request ERnP
+ log.trace("Requesting ERnP for '{}' operation", PROCESS_SEARCH_PERSONAL_IDENTIFIER);
+ 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 (final EidasSAuthenticationException e) {
+ throw 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 {
+ try {
+ // build generic request metadata
+ final GenericRequestParams generic = buildGenericRequestParameters("stepMDS");
+
+ // build search request
+ final Suchdaten searchInfos = new Suchdaten();
+ searchInfos.setFamilienname(familyName);
+ searchInfos.setVorname(givenName);
+ searchInfos.setGeburtsdatum(buildErnpBirthday(dateOfBirth));
+
+ final PersonSuchen personSuchen = new PersonSuchen();
+ personSuchen.setSuchoptionen(generateSearchParameters());
+ personSuchen.setBegruendung(PROCESS_SEARCH_MDS_ONLY);
+ personSuchen.setSuchdaten(searchInfos);
+
+ // request ERnP
+ log.trace("Requesting ERnP for '{}' operation", PROCESS_SEARCH_MDS_ONLY);
+ final SuchenResponse resp = ernpClient.suchen(generic.getClientBehkz(), generic.clientName,
+ generic.getClientRequestTime(), generic.getClientRequestId(), personSuchen);
+
+ // parse ZMR response
+ return processErnpResponse(resp, citizenCountryCode, false, PROCESS_SEARCH_MDS_ONLY);
+
+ } catch (RestClientException e) {
+ log.warn(LOGMSG_ERNP_ERROR, PROCESS_SEARCH_MDS_ONLY, e.getMessage());
+ throw new EidasSAuthenticationException(ERROR_MATCHING_11, new Object[] { e.getMessage() }, e);
+
+ } catch (final EidasSAuthenticationException e) {
+ throw e;
+
+ } catch (final Exception e) {
+ log.warn(LOGMSG_ERNP_RESP_PROCESS, PROCESS_SEARCH_MDS_ONLY, e.getMessage());
+ throw new EidasSAuthenticationException(ERROR_MATCHING_99, new Object[] { e.getMessage() }, e);
+ }
+ }
+
+ @Override
+ public ErnpRegisterResult searchCountrySpecific(PersonSuchenRequest personSearchDao,
+ String citizenCountryCode) throws EidasSAuthenticationException {
+ String countrySearchMsg = MessageFormat.format(PROCESS_SEARCH_COUNTRY_SPECIFIC, citizenCountryCode);
+
+ try {
+ // build generic request metadata
+ final GenericRequestParams generic = buildGenericRequestParameters("stepCC");
+
+ // build search request
+ final PersonSuchen personSuchen = new PersonSuchen();
+ personSuchen.setSuchoptionen(generateSearchParameters());
+ personSuchen.setBegruendung(countrySearchMsg);
+ personSuchen.setSuchdaten(mapCountrySpecificSearchData(personSearchDao));
+
+ // request ERnP
+ log.trace("Requesting ERnP for '{}' operation", countrySearchMsg);
+ final SuchenResponse resp = ernpClient.suchen(generic.getClientBehkz(), generic.clientName,
+ generic.getClientRequestTime(), generic.getClientRequestId(), personSuchen);
+
+ // parse ZMR response
+ return processErnpResponse(resp, citizenCountryCode, true, countrySearchMsg);
+
+ } catch (RestClientException e) {
+ log.warn(LOGMSG_ERNP_ERROR, countrySearchMsg, e.getMessage());
+ throw new EidasSAuthenticationException(ERROR_MATCHING_11, new Object[] { e.getMessage() }, e);
+
+ } catch (final EidasSAuthenticationException e) {
+ throw e;
+
+ } catch (final Exception e) {
+ log.warn(LOGMSG_ERNP_RESP_PROCESS, countrySearchMsg, e.getMessage());
+ throw new EidasSAuthenticationException(ERROR_MATCHING_99, new Object[] { e.getMessage() }, e);
+
+ }
+ }
+
+ @Override
+ public ErnpRegisterResult update(RegisterResult registerResult, SimpleEidasData eidData)
+ throws EidasSAuthenticationException {
+ try {
+ //search person with register result, because update needs information from search response
+ Person ernpPersonToKitt = searchPersonForUpdate(registerResult);
+
+ // select elements that have to be updated
+ Collection<? extends Eidas> eidasDocumentToAdd =
+ selectEidasDocumentsToAdd(ernpPersonToKitt, eidData);
+ SimpleEidasData mdsToUpdate = selectMdsInformationToUpdate(ernpPersonToKitt, eidData);
+
+ if (eidasDocumentToAdd.isEmpty() && mdsToUpdate == null) {
+ log.info("Find no eIDAS document or MDS for update during: {}. Nothing todo on ERnP side",
+ PROCESS_KITT_GENERAL);
+ return new ErnpRegisterResult(Arrays.asList(registerResult));
+
+ } else {
+ log.info("Find #{} eIDAS documents for update during: {}", eidasDocumentToAdd.size(), PROCESS_KITT_GENERAL);
+
+ // update entry based on selected update info's and results from search response
+ return updatePersonInErnp(ernpPersonToKitt, eidasDocumentToAdd, mdsToUpdate, eidData.getCitizenCountryCode());
+
+ }
+
+ } catch (RestClientException e) {
+ log.warn(LOGMSG_ERNP_ERROR, PROCESS_KITT_GENERAL, e.getMessage());
+ throw new EidasSAuthenticationException(ERROR_MATCHING_11, new Object[] { e.getMessage() }, e);
+
+ } catch (final EidasSAuthenticationException e) {
+ throw e;
+
+ } catch (final Exception e) {
+ log.warn(LOGMSG_ERNP_RESP_PROCESS, PROCESS_KITT_GENERAL, e.getMessage());
+ throw new EidasSAuthenticationException(ERROR_MATCHING_99, new Object[] { e.getMessage() }, e);
+
+ }
+ }
+
+ @Override
+ public ErnpRegisterResult add(SimpleEidasData eidData) throws EidasSAuthenticationException {
+ try {
+ // build generic request metadata
+ final GenericRequestParams generic = buildGenericRequestParameters("stepNew");
+
+ // build update request
+ PersonAnlegen ernpReq = new PersonAnlegen();
+ ernpReq.setBegruendung(PROCESS_ADD_IDENITY);
+
+ // inject person data
+ Personendaten person = new Personendaten();
+ person.setFamilienname(eidData.getFamilyName());
+ person.setVorname(eidData.getGivenName());
+ person.setGeburtsdatum(buildErnpBirthday(eidData.getDateOfBirth()));
+ ernpReq.setPersonendaten(person);
+
+ buildNewEidasDocumens(ernpReq, eidData);
+
+ // request ERnP
+ log.trace("Requesting ERnP for '{}' operation", PROCESS_ADD_IDENITY);
+ AnlegenResponse ernpResp = ernpClient.anlegen(generic.getClientBehkz(), generic.clientName,
+ generic.getClientRequestTime(), generic.getClientRequestId(), ernpReq);
+ log.trace("Receive response from ERnP for '{}' operation", PROCESS_ADD_IDENITY);
+
+ return new ErnpRegisterResult(Arrays.asList(
+ mapErnpResponseToRegisterResult(ernpResp.getPerson(), eidData.getCitizenCountryCode())));
+
+ } catch (RestClientException e) {
+ log.warn(LOGMSG_ERNP_ERROR, PROCESS_ADD_IDENITY, e.getMessage());
+ throw new EidasSAuthenticationException(ERROR_MATCHING_11, new Object[] { e.getMessage() }, e);
+
+ } catch (final Exception e) {
+ log.warn(LOGMSG_ERNP_RESP_PROCESS, PROCESS_ADD_IDENITY, e.getMessage());
+ throw new EidasSAuthenticationException(ERROR_MATCHING_99, new Object[] { e.getMessage() }, e);
+
+ }
+ }
+
+ @Override
+ public ErnpRegisterResult searchWithResidenceData(String givenName, String familyName, String dateOfBirth,
+ String zipcode, String city, String street) {
+ log.warn("Matching with residence information is prohibited by design! This requests will be ignored");
+ return new ErnpRegisterResult(Collections.emptyList());
+
+ }
+
+ @PostConstruct
+ private void initialize() throws EaafException {
+ // 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() {
+ 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() {
+ final Suchoptionen options = new Suchoptionen();
+ options.setZmr(false);
+ options.setHistorisch(HistorischEnum.AKTUELLUNDHISTORISCH);
+ options.setSucheMitNamensteilen(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
+ || 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",
+ resp.getPerson().size(), processStepFiendlyname);
+
+ if (forceSinglePersonMatch) {
+ return new ErnpRegisterResult(processSearchPersonResponseSingleResult(
+ resp.getPerson(), citizenCountryCode, processStepFiendlyname));
+
+ } else {
+ return new ErnpRegisterResult(processSearchPersonResponse(
+ resp.getPerson(), citizenCountryCode));
+
+ }
+ }
+ }
+
+ @Nonnull
+ private List<RegisterResult> processSearchPersonResponse(
+ @Nonnull List<Person> list,
+ @Nonnull String citizenCountryCode) throws EaafAuthenticationException {
+ return list.stream()
+ .map(el -> mapErnpResponseToRegisterResult(el, citizenCountryCode))
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+
+ }
+
+ @NonNull
+ private List<RegisterResult> processSearchPersonResponseSingleResult(
+ @Nonnull List<Person> 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,
+ "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) {
+ // build result
+ return RegisterResult.builder()
+ .pseudonym(selectAllEidasDocument(person, citizenCountryCode,
+ Constants.eIDAS_ATTRURN_PERSONALIDENTIFIER))
+ .familyName(person.getPersonendaten().getFamilienname())
+ .givenName(person.getPersonendaten().getVorname())
+ .dateOfBirth(getTextualBirthday(person.getPersonendaten().getGeburtsdatum()))
+ .bpk(person.getPersonendaten().getBpkZp())
+ .placeOfBirth(selectSingleEidasDocument(person, citizenCountryCode,
+ Constants.eIDAS_ATTRURN_PLACEOFBIRTH))
+ .birthName(selectSingleEidasDocument(person, citizenCountryCode,
+ Constants.eIDAS_ATTRURN_BIRTHNAME))
+ .build();
+
+ }
+
+ private Suchdaten mapCountrySpecificSearchData(PersonSuchenRequest personSearchDao) {
+ final Suchdaten searchInfos = new Suchdaten();
+ searchInfos.setFamilienname(personSearchDao.getNatuerlichePerson().getPersonenName().getFamilienname());
+ searchInfos.setVorname(personSearchDao.getNatuerlichePerson().getPersonenName().getVorname());
+ searchInfos.setGeburtsdatum(buildErnpBirthday(personSearchDao.getNatuerlichePerson().getGeburtsdatum()));
+
+ // map all eIDAS documents into ERnP format
+ searchInfos.setEidas(personSearchDao.getEidasSuchdaten().stream()
+ .map(el -> buildErnpEidasDocument(el))
+ .collect(Collectors.toList()));
+
+ return searchInfos;
+
+ }
+
+ private ErnpRegisterResult updatePersonInErnp(Person ernpPersonToKitt,
+ Collection<? extends Eidas> eidasDocumentToAdd, SimpleEidasData mdsToUpdate, String citizenCountryCode)
+ throws ServiceFault {
+ // build generic request metadata
+ final GenericRequestParams generic = buildGenericRequestParameters("stepKittUpdate");
+
+ // build update request
+ PersonAendern ernpReq = new PersonAendern();
+ ernpReq.setBegruendung(PROCESS_KITT_IDENITIES_UPDATE);
+
+ // set reference elements for person update
+ ernpReq.setEntityId(ernpPersonToKitt.getEntityId());
+ ernpReq.setVersion(ernpPersonToKitt.getVersion());
+
+ // add new eIDAS attributes
+ if (!eidasDocumentToAdd.isEmpty()) {
+ log.debug("Find eIDAS Documents to update. Injection update entries into ERnP request ... ");
+ ernpReq.setAnlegen(new Anlegen());
+ eidasDocumentToAdd.stream().forEach(el -> ernpReq.getAnlegen().addEidasItem(el));
+
+ }
+
+ // update MDS if required
+ if (mdsToUpdate != null) {
+ log.debug("Find MDS to update. Injection update entries into ERnP request ... ");
+ ernpReq.setAendern(generateMdsChangeRequest(ernpPersonToKitt, mdsToUpdate));
+
+ }
+
+ // request ERnP
+ log.trace("Requesting ERnP for '{}' operation", PROCESS_KITT_IDENITIES_UPDATE);
+ AendernResponse ernpResp = ernpClient.aendern(generic.getClientBehkz(), generic.clientName,
+ generic.getClientRequestTime(), generic.getClientRequestId(), ernpReq);
+ log.trace("Receive response from ERnP for '{}' operation", PROCESS_KITT_IDENITIES_UPDATE);
+
+ return new ErnpRegisterResult(Arrays.asList(
+ mapErnpResponseToRegisterResult(ernpResp.getPerson(), citizenCountryCode)));
+
+ }
+
+ private Collection<? extends Eidas> selectEidasDocumentsToAdd(
+ Person ernpPersonToKitt, SimpleEidasData eidData) {
+
+ //TODO: maybe we should re-factor SimpleEidasData to a generic data-model to facilitate arbitrary eIDAS attributes
+ Set<Eidas> result = new HashSet<>();
+ addEidasDocumentIfNotAvailable(result, ernpPersonToKitt, eidData.getCitizenCountryCode(),
+ Constants.eIDAS_ATTRURN_PERSONALIDENTIFIER, eidData.getPseudonym(), true);
+ addEidasDocumentIfNotAvailable(result, ernpPersonToKitt, eidData.getCitizenCountryCode(),
+ Constants.eIDAS_ATTRURN_PLACEOFBIRTH, eidData.getPlaceOfBirth(), false);
+ addEidasDocumentIfNotAvailable(result, ernpPersonToKitt, eidData.getCitizenCountryCode(),
+ Constants.eIDAS_ATTRURN_BIRTHNAME, eidData.getBirthName(), false);
+
+ return result;
+
+ }
+
+ private void addEidasDocumentIfNotAvailable(Set<Eidas> result,
+ Person ernpPersonToKitt, String citizenCountryCode,
+ String attrName, String attrValue, boolean allowMoreThanOneEntry) {
+
+ if (StringUtils.isEmpty(attrValue)) {
+ log.trace("No eIDAS document: {}. Nothing todo for KITT process ... ", attrName);
+ return;
+
+ }
+
+ // check if eIDAS attribute is already includes an eIDAS-Document
+ boolean alreadyExist = ernpPersonToKitt.getEidas().stream()
+ .filter(el -> el.getWert().equals(attrValue)
+ && el.getArt().equals(attrName)
+ && el.getStaatscode2().equals(citizenCountryCode))
+ .findAny()
+ .isPresent();
+
+ if (!alreadyExist) {
+ // check eIDAS documents already contains a document with this pair of country-code and attribute-name
+ Optional<Eidas> oneDocWithNameExists = ernpPersonToKitt.getEidas().stream()
+ .filter(el -> el.getStaatscode2().equals(citizenCountryCode)
+ && el.getArt().equals(attrName))
+ .findAny();
+
+ if (!allowMoreThanOneEntry && oneDocWithNameExists.isPresent()
+ && !oneDocWithNameExists.get().getWert().equals(attrValue)) {
+ log.warn("eIDAS document: {} already exists for country: {} but attribute-value does not match. "
+ + "Skip update process because no multi-value allowed for this ... ",
+ attrName, citizenCountryCode);
+
+ } else {
+
+ Eidas eidasDocToAdd = new Eidas();
+ eidasDocToAdd.setStaatscode2(citizenCountryCode);
+ eidasDocToAdd.setArt(attrName);
+ eidasDocToAdd.setWert(attrValue);
+ log.info("Add eIDAS document: {} for country: {} to ERnP person", attrName, citizenCountryCode);
+ result.add(eidasDocToAdd);
+
+ }
+
+ } else {
+ log.debug("eIDAS document: {} already exists for country: {}. Skip update process for this ... ",
+ attrName, citizenCountryCode);
+
+ }
+ }
+
+ private Person searchPersonForUpdate(RegisterResult registerResult) throws WorkflowException {
+ // build generic request metadata
+ final GenericRequestParams generic = buildGenericRequestParameters("stepKittSearch");
+
+ // build search request
+ final Suchdaten searchInfos = new Suchdaten();
+ searchInfos.setBpkZp(registerResult.getBpk());
+ searchInfos.setFamilienname(registerResult.getFamilyName());
+ searchInfos.setVorname(registerResult.getGivenName());
+ searchInfos.setGeburtsdatum(buildErnpBirthday(registerResult.getDateOfBirth()));
+
+ final PersonSuchen personSuchen = new PersonSuchen();
+ personSuchen.setSuchoptionen(generateSearchParameters());
+ personSuchen.setBegruendung(PROCESS_KITT_IDENITIES_GET);
+ personSuchen.setSuchdaten(searchInfos);
+
+ // request ERnP
+ log.trace("Requesting ERnP for '{}' operation", PROCESS_KITT_IDENITIES_GET);
+ final SuchenResponse resp = ernpClient.suchen(generic.getClientBehkz(), generic.clientName,
+ generic.getClientRequestTime(), generic.getClientRequestId(), personSuchen);
+
+ // perform shot validation of ERnP response
+ if (resp.getPerson() == null || resp.getPerson().size() != 1) {
+ log.error("ERnP result contains NO 'Person' or 'Person' is empty");
+ throw new WorkflowException(PROCESS_KITT_IDENITIES_GET,
+ "Find NO data-set with already matchted eID during ERnP KITT process");
+
+ } else {
+ log.debug("Find person for '{}' operation", PROCESS_KITT_IDENITIES_GET);
+ return resp.getPerson().get(0);
+
+ }
+ }
+
+ private void buildNewEidasDocumens(PersonAnlegen ernpReq, SimpleEidasData eidData) {
+ ernpReq.addEidasItem(buildNewEidasDocument(eidData.getCitizenCountryCode(),
+ Constants.eIDAS_ATTRURN_PERSONALIDENTIFIER, eidData.getPseudonym()));
+
+ if (StringUtils.isNotEmpty(eidData.getPlaceOfBirth())) {
+ ernpReq.addEidasItem(buildNewEidasDocument(eidData.getCitizenCountryCode(),
+ Constants.eIDAS_ATTRURN_PLACEOFBIRTH, eidData.getPlaceOfBirth()));
+
+ }
+
+ if (StringUtils.isNotEmpty(eidData.getBirthName())) {
+ ernpReq.addEidasItem(buildNewEidasDocument(eidData.getCitizenCountryCode(),
+ Constants.eIDAS_ATTRURN_BIRTHNAME, eidData.getBirthName()));
+
+ }
+ }
+
+ private Eidas buildNewEidasDocument(String citizenCountryCode, String eidasAttrName,
+ String eidasAddrValue) {
+ Eidas el = new Eidas();
+ el.setArt(eidasAttrName);
+ el.setWert(eidasAddrValue);
+ el.setStaatscode2(citizenCountryCode);
+ return el;
+ }
+
+ private SimpleEidasData selectMdsInformationToUpdate(Person ernpPersonToKitt, SimpleEidasData eidData) {
+ PersonendatenErgebnis person = ernpPersonToKitt.getPersonendaten();
+ SimpleEidasDataBuilder builder = SimpleEidasData.builder()
+ .givenName(eidData.getGivenName())
+ .familyName(eidData.getFamilyName())
+ .dateOfBirth(eidData.getDateOfBirth());
+
+ boolean findMatch = person.getVorname().equals(eidData.getGivenName())
+ && person.getFamilienname().equals(eidData.getFamilyName())
+ && getTextualBirthday(person.getGeburtsdatum()).equals(eidData.getDateOfBirth());
+ return findMatch ? null : builder.build();
+
+ }
+
+ private Aendern generateMdsChangeRequest(Person ernpPersonToKitt, SimpleEidasData mdsToUpdate) {
+ Aendern el = new Aendern();
+ Personendaten person = new Personendaten();
+ person.setEntityId(ernpPersonToKitt.getPersonendaten().getEntityId());
+ el.setPersonendaten(person);
+ person.setFamilienname(mdsToUpdate.getFamilyName());
+ person.setVorname(mdsToUpdate.getGivenName());
+ person.setGeburtsdatum(buildErnpBirthday(mdsToUpdate.getDateOfBirth()));
+ return el;
+
+ }
+
+ /**
+ * Map an AT specific Date String 'yyyy-MM-dd' to ERnP birthday representation.
+ *
+ * <p>
+ * <b>Info:</b> {@link LocalDate} can not be used, because '1940-00-00' is also
+ * a valid birthday.
+ * </p>
+ *
+ * @param dateOfBirth in 'yyyy-MM-dd' format
+ * @return ERnP birthday representation
+ */
+ private PartialDate buildErnpBirthday(String dateOfBirth) {
+ String[] elements = dateOfBirth.split("-");
+ Assert.isTrue(elements.length == 3, "Find invalid dateOfBirth element: " + dateOfBirth);
+
+ PartialDate result = new PartialDate();
+ result.setJahr(Integer.valueOf(elements[0]));
+ result.setMonat(Integer.valueOf(elements[1]));
+ result.setTag(Integer.valueOf(elements[2]));
+ return result;
+
+ }
+
+ /**
+ * Map eIDAS search-data from ZMR model into ERnP model.
+ *
+ * @param daten eIDAS document as ZMR model
+ * @return the same eIDAS document as an ERnP model
+ */
+ private SuchEidas buildErnpEidasDocument(EidasSuchdatenType daten) {
+ return new SuchEidas()
+ .art(daten.getEidasArt())
+ .wert(daten.getEidasWert())
+ .staatscode2(daten.getStaatscode2());
+ }
+
+
+ /**
+ * Build AT specific Date String 'yyyy-MM-dd' from ERnP birthday representation.
+ *
+ * <p>
+ * <b>Info:</b> {@link LocalDate} can not be used, because '1940-00-00' is also
+ * a valid birthday on ERnP site.
+ * </p>
+ *
+ * @param geburtsdatum ERnP birthday representation
+ * @return birthday in 'yyyy-MM-dd' format
+ */
+ private String getTextualBirthday(PartialDate geburtsdatum) {
+ return MessageFormat.format("{0}-{1}-{2}",
+ String.valueOf(geburtsdatum.getJahr()),
+ String.format("%02d", geburtsdatum.getMonat()),
+ String.format("%02d", geburtsdatum.getTag()));
+
+ }
+
+
+ /**
+ * Get all eIDAS document with the specified country code and document type.
+ *
+ * @param person Person information from ERnP
+ * @param citizenCountryCode Country code of the eIDAS attribute
+ * @param eidasAttrurnPersonalidentifier eIDAS attribute identifier
+ * @return {@link List} of eIDAS attribute values or an empty list if's not
+ * found
+ */
+ @NonNull
+ private List<String> selectAllEidasDocument(Person person, String citizenCountryCode,
+ 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();
+
+ }
+
+ }
+
+ /**
+ * Get the first eIDAS document with the specified country code and document
+ * type.
+ *
+ * @param person Person information from ERnP
+ * @param citizenCountryCode Country code of the eIDAS attribute
+ * @param eidasAttrurnPersonalidentifier eIDAS attribute identifier
+ * @return Value of this eIDAS attribute or <code>null</code> if's not found
+ */
+ @Nullable
+ private String selectSingleEidasDocument(Person person, String citizenCountryCode,
+ String eidasAttrurnPersonalidentifier) {
+ 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());
+ final ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
+ final RestTemplate springClient = new RestTemplate(requestFactory);
+ springClient.setErrorHandler(buildErrorHandler());
+ springClient.getMessageConverters().add(0, buildCustomJacksonObjectMapper());
+ return springClient;
+
+ }
+
+ private HttpMessageConverter<?> buildCustomJacksonObjectMapper() {
+ final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
+ converter.setSupportedMediaTypes(Collections.singletonList(MediaType.APPLICATION_JSON));
+ converter.getObjectMapper().setSerializationInclusion(Include.NON_NULL);
+
+ converter.getObjectMapper().registerModule(new JavaTimeModule());
+ converter.getObjectMapper().configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
+ return converter;
+
+ }
+
+ @Nonnull
+ private ResponseErrorHandler buildErrorHandler() {
+ return new ResponseErrorHandler() {
+
+ @Override
+ public boolean hasError(ClientHttpResponse response) throws IOException {
+ return response.getStatusCode().is4xxClientError()
+ || response.getStatusCode().is5xxServerError();
+
+ }
+
+ @Override
+ public void handleError(ClientHttpResponse response) throws IOException {
+ // TODO: opimize errorHandling based on response info's from real ERnP
+
+ List<String> serverId = response.getHeaders().getOrEmpty(ERNP_RESPONSE_HEADER_SERVER_ID);
+ log.warn("Receive http-error: {} from ERnP with serverTransactionId {}",
+ response.getRawStatusCode(), serverId.isEmpty() ? "'not set'" : serverId.get(0));
+ log.warn(" Full ERnP response-body: {}", IOUtils.toString(response.getBody(), "UTF-8"));
+ throw new ErnpRestCommunicationException(response.getRawStatusCode());
+
+ }
+ };
+ }
+
+ @Nonnull
+ private HttpClientConfiguration buildHttpClientConfiguration() throws EaafException {
+ final HttpClientConfiguration config = new HttpClientConfiguration(FRIENDLYNAME_HTTP_CLIENT);
+ config.setAuthMode(ClientAuthMode.SSL.getMode());
+
+ // Set keystore configuration
+ config.buildKeyStoreConfig(
+ basicConfig.getBasicConfiguration(Constants.CONIG_PROPS_EIDAS_COMMON_CLIENT_SSL_KEYSTORE_TYPE),
+ basicConfig.getBasicConfiguration(Constants.CONIG_PROPS_EIDAS_COMMON_CLIENT_SSL_KEYSTORE_PATH),
+ basicConfig.getBasicConfiguration(Constants.CONIG_PROPS_EIDAS_COMMON_CLIENT_SSL_KEYSTORE_PASSWORD),
+ basicConfig.getBasicConfiguration(Constants.CONIG_PROPS_EIDAS_COMMON_CLIENT_SSL_KEYSTORE_NAME));
+
+ // Set key information
+ config.setSslKeyAlias(
+ 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
+
+ return config;
+ }
+
+ @AllArgsConstructor
+ @Getter
+ public static class ErnpRegisterResult {
+ private final List<RegisterResult> personResult;
+
+ }
+
+ private GenericRequestParams buildGenericRequestParameters(String operationIdentifier) {
+ return GenericRequestParams.builder()
+ .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();
+
+ }
+
+ @Builder
+ @Getter
+ private static class GenericRequestParams {
+ String clientBehkz;
+ String clientName;
+ OffsetDateTime clientRequestTime;
+ String clientRequestId;
+
+ }
+
+}