diff options
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.java | 857 |
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..6a732a0d --- /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_ERNPCLIENT_SSL_KEYSTORE_TYPE), + basicConfig.getBasicConfiguration(Constants.CONIG_PROPS_EIDAS_ERNPCLIENT_SSL_KEYSTORE_PATH), + basicConfig.getBasicConfiguration(Constants.CONIG_PROPS_EIDAS_ERNPCLIENT_SSL_KEYSTORE_PASSWORD), + basicConfig.getBasicConfiguration(Constants.CONIG_PROPS_EIDAS_ERNPCLIENT_SSL_KEYSTORE_NAME)); + + // Set key information + config.setSslKeyAlias( + basicConfig.getBasicConfiguration(Constants.CONIG_PROPS_EIDAS_ERNPCLIENT_SSL_KEYS_ALIAS)); + config.setSslKeyPassword( + basicConfig.getBasicConfiguration(Constants.CONIG_PROPS_EIDAS_ERNPCLIENT_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; + + } + +} |