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.LocalDateTime; 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 java.util.stream.Stream; import javax.annotation.Nonnull; import javax.annotation.PostConstruct; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.Pair; import org.apache.http.client.HttpClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpRequest; import org.springframework.http.MediaType; import org.springframework.http.client.ClientHttpRequestExecution; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.ClientHttpRequestInterceptor; 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.ErnpPersonRegisterResult; 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.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.Personendaten.GeburtsbundeslandEnum; 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.asitplus.eidas.specific.modules.core.eidas.EidasConstants; 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.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 " + EidasConstants.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"; // ERnP person type that indicates mark a person as ZMR entry private static final String ERNP_RESPONSE_OPERATION_ZMR_FORWARD = "PersonUebernehmen"; private static final String HEADER_TXID = "txid"; private static final String HEADER_PVP_TXID = "pvp-txid"; private static final String HEADER_MSG_NOT_SET = "NOT-set"; @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(); // build search request final SuchEidas eidasInfos = new SuchEidas(); eidasInfos.setArt(EidasConstants.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 (final 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(); // 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 (final 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 { final String countrySearchMsg = MessageFormat.format(PROCESS_SEARCH_COUNTRY_SPECIFIC, citizenCountryCode); try { // build generic request metadata final GenericRequestParams generic = buildGenericRequestParameters(); // 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 (final 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 final Person ernpPersonToKitt = searchPersonForUpdate(registerResult); // select elements that have to be updated final Collection eidasDocumentToAdd = selectEidasDocumentsToAdd(ernpPersonToKitt, eidData); final boolean mdsValidInErnp = isMdsInErnpValid(ernpPersonToKitt, eidData); if (eidasDocumentToAdd.isEmpty() && mdsValidInErnp) { log.info("Find no eIDAS document or MDS for update during: {}. Nothing todo on ERnP side", PROCESS_KITT_GENERAL); return new ErnpRegisterResult(Arrays.asList(new ErnpPersonRegisterResult(registerResult, false))); } 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, mdsValidInErnp, eidData); } } catch (final 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(); // build update request final PersonAnlegen ernpReq = new PersonAnlegen(); ernpReq.setBegruendung(PROCESS_ADD_IDENITY); // inject person data final 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); final 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 (final 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 processSearchPersonResponseSingleResult( resp.getPerson(), citizenCountryCode, processStepFiendlyname); } else { return new ErnpRegisterResult(processSearchPersonResponse(resp.getPerson(), citizenCountryCode)); } } } @Nonnull private List processSearchPersonResponse( @Nonnull List list, @Nonnull String citizenCountryCode) throws EaafAuthenticationException { final List ernpResult = list.stream() .map(el -> mapErnpResponseToRegisterResult(el, citizenCountryCode)) .filter(Objects::nonNull) .collect(Collectors.toList()); log.info("Get #{} ERnP results after post-processing", ernpResult.size()); return ernpResult; } @NonNull private ErnpRegisterResult processSearchPersonResponseSingleResult( @Nonnull List persons, @Nonnull String citizenCountryCode, String processStepFiendlyname) throws EaafAuthenticationException { // process ERnP response and check state of entities List activePersons = processSearchPersonResponse(persons, citizenCountryCode); // check final result if (activePersons.isEmpty()) { log.info("ERnP entry, which was selected by matching, looks already closed. " + "Disallow new ERnP entries by user selection"); return new ErnpRegisterResult(Collections.emptyList(), false); } else if (activePersons.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 new ErnpRegisterResult(activePersons); } } /** * Process a single Person data-set from ERnP. * * @param personEl Person data-set from ERnP * @param citizenCountryCode Country-Code of the citizen * @return {@link Pair} of Simplified register result and 'isZMREntry' flag, or * null if the person data-set is not active anymore * @throws EaafAuthenticationException In case of a validation error */ @Nullable private ErnpPersonRegisterResult mapErnpResponseToRegisterResult(@Nonnull Person person, @Nonnull String citizenCountryCode) { if (checkIfPersonIsActive(person)) { // build result return new ErnpPersonRegisterResult( RegisterResult.builder() .pseudonym(selectAllEidasDocument(person, citizenCountryCode, EidasConstants.eIDAS_ATTRURN_PERSONALIDENTIFIER)) .familyName(person.getPersonendaten().getFamilienname()) .givenName(person.getPersonendaten().getVorname()) .dateOfBirth(getTextualBirthday(person.getPersonendaten().getGeburtsdatum())) .bpk(person.getPersonendaten().getBpkZp()) .placeOfBirth(selectSingleEidasDocument(person, citizenCountryCode, EidasConstants.eIDAS_ATTRURN_PLACEOFBIRTH)) .birthName(selectSingleEidasDocument(person, citizenCountryCode, EidasConstants.eIDAS_ATTRURN_BIRTHNAME)) .build(), isPersonMovedToZmr(person)); } else { log.debug("Entity is not valid anymore. Skip it ... "); return null; } } private boolean checkIfPersonIsActive(Person person) { if (person.getGueltigBis() != null) { final LocalDateTime validTo = person.getGueltigBis().toLocalDateTime(); final LocalDateTime now = LocalDateTime.now(); if (isPersonMovedToZmr(person)) { log.debug("Entity has a 'validTo' element, but it's marked as {}. Use it as a ZMR entry", ERNP_RESPONSE_OPERATION_ZMR_FORWARD); } else if (validTo.isBefore(now)) { log.warn("Enity was valid to: {}, but now its: {}. Ignore that entry", validTo, now); return false; } else { log.debug("Entity has a 'validTo' element, but it is in the future."); } } else { log.trace("Entity has no 'validTo' element. Therefore it should be valid"); } return true; } /** * Check if ERnP person is marked as KITT to ZMR entry. * *

* If person is marked as ZMR person then it has the same quality as a ZMR * match. *

* * @param person ERnP person result * @return true if the person should be in ERnP, otherwise * false */ private boolean isPersonMovedToZmr(Person person) { return person.getLetzteOperation() != null && ERNP_RESPONSE_OPERATION_ZMR_FORWARD.equals(person.getLetzteOperation().getVorgang()); } 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 eidasDocumentToAdd, boolean mdsValidInErnp, SimpleEidasData eidData) throws ServiceFault { // build generic request metadata final GenericRequestParams generic = buildGenericRequestParameters(); // build update request final 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 (!mdsValidInErnp) { log.debug("Find MDS to update. Injection update entries into ERnP request ... "); ernpReq.setAendern(generateMdsChangeRequest(ernpPersonToKitt, eidData)); } // request ERnP log.trace("Requesting ERnP for '{}' operation", PROCESS_KITT_IDENITIES_UPDATE); final 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(Collections.singletonList( mapErnpResponseToRegisterResult(ernpResp.getPerson(), eidData.getCitizenCountryCode()))); } private Collection selectEidasDocumentsToAdd( Person ernpPersonToKitt, SimpleEidasData eidData) { // TODO: maybe we should re-factor SimpleEidasData to a generic data-model to // facilitate arbitrary eIDAS attributes final Set result = new HashSet<>(); addEidasDocumentIfNotAvailable(result, ernpPersonToKitt, eidData.getCitizenCountryCode(), EidasConstants.eIDAS_ATTRURN_PERSONALIDENTIFIER, eidData.getPseudonym(), true); addEidasDocumentIfNotAvailable(result, ernpPersonToKitt, eidData.getCitizenCountryCode(), EidasConstants.eIDAS_ATTRURN_PLACEOFBIRTH, eidData.getPlaceOfBirth(), false); addEidasDocumentIfNotAvailable(result, ernpPersonToKitt, eidData.getCitizenCountryCode(), EidasConstants.eIDAS_ATTRURN_BIRTHNAME, eidData.getBirthName(), false); return result; } private void addEidasDocumentIfNotAvailable(Set 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; } // get all eIDAS documents from current ERnP entry or an empty list of no exists final List currentEidasDocs = ernpPersonToKitt.getEidas() != null ? ernpPersonToKitt.getEidas() : Collections.emptyList(); // check if eIDAS attribute is already includes an eIDAS-Document final boolean alreadyExist = currentEidasDocs.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 final Optional oneDocWithNameExists = currentEidasDocs.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 { final 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(); // 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(), EidasConstants.eIDAS_ATTRURN_PERSONALIDENTIFIER, eidData.getPseudonym())); if (StringUtils.isNotEmpty(eidData.getPlaceOfBirth())) { ernpReq.addEidasItem(buildNewEidasDocument(eidData.getCitizenCountryCode(), EidasConstants.eIDAS_ATTRURN_PLACEOFBIRTH, eidData.getPlaceOfBirth())); } if (StringUtils.isNotEmpty(eidData.getBirthName())) { ernpReq.addEidasItem(buildNewEidasDocument(eidData.getCitizenCountryCode(), EidasConstants.eIDAS_ATTRURN_BIRTHNAME, eidData.getBirthName())); } } private Eidas buildNewEidasDocument(String citizenCountryCode, String eidasAttrName, String eidasAddrValue) { final Eidas el = new Eidas(); el.setArt(eidasAttrName); el.setWert(eidasAddrValue); el.setStaatscode2(citizenCountryCode); return el; } private boolean isMdsInErnpValid(Person ernpPersonToKitt, SimpleEidasData eidData) { final PersonendatenErgebnis person = ernpPersonToKitt.getPersonendaten(); return person.getVorname().equalsIgnoreCase(eidData.getGivenName()) && person.getFamilienname().equalsIgnoreCase(eidData.getFamilyName()) && getTextualBirthday(person.getGeburtsdatum()).equalsIgnoreCase(eidData.getDateOfBirth()); } private Aendern generateMdsChangeRequest(Person ernpPersonToKitt, SimpleEidasData eidData) { final PersonendatenErgebnis input = ernpPersonToKitt.getPersonendaten(); // copy full Person result because ERnP does not support delta updates final Personendaten person = makeCopyOfPerson(input); // change MDS to eIDAS information in required if (!input.getFamilienname().equalsIgnoreCase(eidData.getFamilyName())) { person.setFamilienname(eidData.getFamilyName()); } if (!input.getVorname().equalsIgnoreCase(eidData.getGivenName())) { person.setVorname(eidData.getGivenName()); } if (!getTextualBirthday(input.getGeburtsdatum()).equalsIgnoreCase(eidData.getDateOfBirth())) { person.setGeburtsdatum(buildErnpBirthday(eidData.getDateOfBirth())); } final Aendern el = new Aendern(); el.setPersonendaten(person); return el; } private Personendaten makeCopyOfPerson(PersonendatenErgebnis input) { final Personendaten person = new Personendaten(); person.setEntityId(input.getEntityId()); person.setFamilienname(input.getFamilienname()); person.setGeburtsdatum(input.getGeburtsdatum()); person.setGeburtsort(input.getGeburtsort()); person.setGeburtsstaat(input.getGeburtsstaat()); person.setNameVorEhe(input.getNameVorEhe()); person.setVorname(input.getVorname()); if (input.getGeburtsbundesland() != null) { person.setGeburtsbundesland( GeburtsbundeslandEnum.fromValue(input.getGeburtsbundesland().getValue())); } if (input.getGeschlecht() != null) { person.setGeschlecht( Personendaten.GeschlechtEnum.fromValue(input.getGeschlecht().getValue())); } return person; } /** * Map an AT specific Date String 'yyyy-MM-dd' to ERnP birthday representation. * *

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

* * @param dateOfBirth in 'yyyy-MM-dd' format * @return ERnP birthday representation */ private PartialDate buildErnpBirthday(String dateOfBirth) { final String[] elements = dateOfBirth.split("-"); Assert.isTrue(elements.length == 3, "Find invalid dateOfBirth element: " + dateOfBirth); final 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. * *

* 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 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 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 null 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()); springClient.setInterceptors(Collections.singletonList(buildTransactionIdInterceptor())); return springClient; } private ClientHttpRequestInterceptor buildTransactionIdInterceptor() { return new ClientHttpRequestInterceptor() { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { ClientHttpResponse response = execution.execute(request, body); log.info("response for SOAP client: {} with {}:{} and {}:{}", FRIENDLYNAME_HTTP_CLIENT, HEADER_TXID, extractHeaderValue(response.getHeaders(), HEADER_TXID), HEADER_PVP_TXID, extractHeaderValue(response.getHeaders(), HEADER_PVP_TXID)); return response; } private String extractHeaderValue(HttpHeaders headers, String headerName) { List value = headers.get(headerName); return value == null || value.isEmpty() ? HEADER_MSG_NOT_SET : value.get(0); } }; } 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 final List 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; } private GenericRequestParams buildGenericRequestParameters() { 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()) .build(); } @Getter public static class ErnpRegisterResult { /** * Flag that indicates if ERnP entries by user decision is allowed. */ private final boolean allowErnpEntryByUser; private final List fullErnpResults; /** * Build reduced ERnP register result. * *

* New ERnP entries are allowed by default *

* * @param list {@link List} of ERnP entities */ public ErnpRegisterResult(List list) { this(list, true); } /** * Build reduced ERnP register result. * * @param list {@link List} of ERnP entities * @param allowNewErnpEntries true to allow new ERnP entries by * user decision, otherwise false */ public ErnpRegisterResult(List list, boolean allowNewErnpEntries) { fullErnpResults = list; allowErnpEntryByUser = allowNewErnpEntries; } /** * Get all active ERnP results. * * @return ERnP entities */ public Stream getPersonResultStream() { return fullErnpResults.stream() .filter(el -> !el.isZmrEntryNow()); } /** * Get all active ERnP results. * * @return ERnP entities */ public List getPersonResult() { return getPersonResultStream() .collect(Collectors.toList()); } /** * Get all ERnP results that are kitted to ZMR entries. * * @return entities that are in ZMR now */ public Stream getZmrPersonResultStream() { return fullErnpResults.stream() .filter(el -> el.isZmrEntryNow()); } /** * Get all ERnP results that are kitted to ZMR entries. * * @return entities that are in ZMR now */ public List getZmrPersonResult() { return getZmrPersonResultStream() .collect(Collectors.toList()); } } @Builder @Getter private static class GenericRequestParams { String clientBehkz; String clientName; OffsetDateTime clientRequestTime; String clientRequestId; } }