package at.asitplus.eidas.specific.modules.auth.eidas.v2.service; import java.io.Serializable; import java.math.BigInteger; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nonnull; import org.jetbrains.annotations.Nullable; import org.springframework.lang.NonNull; import org.springframework.stereotype.Service; import com.google.common.collect.Streams; import at.asitplus.eidas.specific.modules.auth.eidas.v2.clients.ernp.ErnpRestClient.ErnpRegisterResult; import at.asitplus.eidas.specific.modules.auth.eidas.v2.clients.ernp.IErnpClient; import at.asitplus.eidas.specific.modules.auth.eidas.v2.clients.zmr.IZmrClient; import at.asitplus.eidas.specific.modules.auth.eidas.v2.clients.zmr.ZmrSoapClient.ZmrRegisterResult; import at.asitplus.eidas.specific.modules.auth.eidas.v2.controller.AdresssucheController.AdresssucheOutput; 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.exception.EidasSAuthenticationException; import at.asitplus.eidas.specific.modules.auth.eidas.v2.exception.WorkflowException; import at.asitplus.eidas.specific.modules.auth.eidas.v2.exception.ZmrCommunicationException; import at.asitplus.eidas.specific.modules.auth.eidas.v2.handler.CountrySpecificDetailSearchProcessor; import at.gv.bmi.namespace.zmr_su.zmr._20040201.PersonSuchenRequest; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j @Service("registerSearchService") public class RegisterSearchService { private static final Object ZMR = "ZMR"; private static final Object ERNP = "ERnP"; private static final Object ERNP_KITT = "ERnP(ZMR-Kitt)"; private static final String LOG_MSG_RESULTS = "Matching operation: {} results: " + ZMR + ": {} | " + ERNP + ": {} | " + ERNP_KITT + ": {}"; private static final String LOG_MSG_RESULTS_CLEARING = "Post-processing of register results find duplicated entries. " + "Remove {} entries from " + ERNP + " result."; private static final String LOG_MSG_RESULTS_CLEARING_KITT = "Post-processing of register results find duplicated entries. " + "Remove {} entries from " + ERNP_KITT + " result."; private static final String LOG_MSG_KITT = "Matching operation kitts entry on: {}"; private final IZmrClient zmrClient; private final IErnpClient ernpClient; private final List handlers; /** * Service that combines ZMR and ERnP register search operations. * * @param handlers Available country-specific search processors * @param zmrClient ZMR client * @param ernpClient ERnP client */ public RegisterSearchService(List handlers, IZmrClient zmrClient, IErnpClient ernpClient) { this.zmrClient = zmrClient; this.ernpClient = ernpClient; this.handlers = handlers; log.info("Init with #{} search services for country-specific details", handlers.size()); } /** * Search with Person Identifier (eIDAS Pseudonym) in ZMR and ERnP. * * @param eidasData Received eIDAS data * @throws WorkflowException In case of a register interaction error */ @Nonnull public RegisterStatusResults searchWithPersonIdentifier(SimpleEidasData eidasData) throws WorkflowException { return searchWithPersonIdentifier(null, eidasData); } /** * Search with Person Identifier (eIDAS Pseudonym) in ZMR and ERnP. * * @param operationStatus Current register-operation status that contains processing informations * @param eidasData Received eIDAS data * @throws WorkflowException In case of a register interaction error */ @Nonnull public RegisterStatusResults searchWithPersonIdentifier(@Nullable RegisterOperationStatus operationStatus, @Nonnull SimpleEidasData eidasData) throws WorkflowException { try { final ZmrRegisterResult resultsZmr = zmrClient.searchWithPersonIdentifier( operationStatus != null ? operationStatus.getZmrProcessId() : null, eidasData.getPseudonym(), eidasData.getCitizenCountryCode()); final ErnpRegisterResult resultsErnp = ernpClient.searchWithPersonIdentifier( eidasData.getPseudonym(), eidasData.getCitizenCountryCode()); log.info(LOG_MSG_RESULTS, "seachByPersonalId", resultsZmr.getPersonResult().size(), resultsErnp.getPersonResult().size(), resultsErnp.getZmrPersonResult().size()); return RegisterStatusResults.fromZmrAndErnp(operationStatus, resultsZmr, resultsErnp); } catch (final EidasSAuthenticationException e) { throw new WorkflowException("searchWithPersonalIdentifier", e.getMessage(), !(e instanceof ZmrCommunicationException), e); } } /** * Search with MDS (Given Name, Family Name, Date of Birth) in ZMR and ERnP. * * @param operationStatus Current register-operation status that contains processing informations * @param eidasData Received eIDAS data * @throws WorkflowException In case of a register interaction error */ @Nonnull public RegisterStatusResults searchWithMds(RegisterOperationStatus operationStatus, SimpleEidasData eidasData) throws WorkflowException { try { final ZmrRegisterResult resultsZmr = zmrClient.searchWithMds(operationStatus.getZmrProcessId(), eidasData.getGivenName(), eidasData.getFamilyName(), eidasData.getDateOfBirth(), eidasData.getCitizenCountryCode()); final ErnpRegisterResult resultsErnp = ernpClient.searchWithMds(eidasData.getGivenName(), eidasData.getFamilyName(), eidasData.getDateOfBirth(), eidasData.getCitizenCountryCode()); log.info(LOG_MSG_RESULTS, "seachByMDS", resultsZmr.getPersonResult().size(), resultsErnp.getPersonResult().size(), resultsErnp.getZmrPersonResult().size()); return RegisterStatusResults.fromZmrAndErnp(operationStatus, resultsZmr, resultsErnp); } catch (final EidasSAuthenticationException e) { throw new WorkflowException("searchWithMDSOnly", e.getMessage(), !(e instanceof ZmrCommunicationException), e); } } /** * Search with country-specific parameters based on information from available * {@link CountrySpecificDetailSearchProcessor} implementations. * * @param operationStatus Current register-operation status that contains processing informations * @param eidasData Receive eIDAS eID information * @return Results from ZMR or ERnP search * @throws WorkflowException In case of a register interaction error */ @Nonnull public RegisterStatusResults searchWithCountrySpecifics(RegisterOperationStatus operationStatus, SimpleEidasData eidasData) throws WorkflowException { try { @Nullable final CountrySpecificDetailSearchProcessor ccSpecificProcessor = findSpecificProcessor(eidasData); if (ccSpecificProcessor != null) { log.info("Selecting country-specific search processor: {}", ccSpecificProcessor.getName()); PersonSuchenRequest ccSpecificSearchReq = ccSpecificProcessor.generateSearchRequest(eidasData); // search in ZMR final ZmrRegisterResult resultsZmr = zmrClient.searchCountrySpecific(operationStatus.getZmrProcessId(), ccSpecificSearchReq, eidasData.getCitizenCountryCode()); //search in ERnP ErnpRegisterResult resultErnp = ernpClient.searchCountrySpecific( ccSpecificSearchReq, eidasData.getCitizenCountryCode()); log.info(LOG_MSG_RESULTS, "seachByCountrySpecifics", resultsZmr.getPersonResult().size(), resultErnp.getPersonResult().size(), resultErnp.getZmrPersonResult().size()); return RegisterStatusResults.fromZmrAndErnp(operationStatus, resultsZmr, resultErnp); } else { return RegisterStatusResults.fromEmpty(operationStatus); } } catch (final EidasSAuthenticationException e) { throw new WorkflowException("searchWithCountrySpecifics", e.getMessage(), !(e instanceof ZmrCommunicationException), e); } } /** * Search with residence infos. * * @param operationStatus Current register-operation status that contains processing informations * @param eidasData Receive eIDAS eID information * @param address Address information provided by user * @return Results from ZMR or ERnP search * @throws WorkflowException In case of a register interaction error */ public RegisterStatusResults searchWithResidence(RegisterOperationStatus operationStatus, SimpleEidasData eidasData, AdresssucheOutput address) throws WorkflowException { try { final ZmrRegisterResult resultsZmr = zmrClient.searchWithResidenceData( operationStatus.getZmrProcessId(), eidasData.getGivenName(), eidasData.getFamilyName(), eidasData.getDateOfBirth(), eidasData.getCitizenCountryCode(), address); /* ERnP search is not used here, * because we only search for people with Austrian residence and they are in ZMR only */ log.info(LOG_MSG_RESULTS, "seachByResidence", resultsZmr.getPersonResult().size(), 0, 0); return RegisterStatusResults.fromZmr(operationStatus, resultsZmr); } catch (final EidasSAuthenticationException e) { throw new WorkflowException("searchWithResidenceInformation", e.getMessage(), !(e instanceof ZmrCommunicationException), e); } } /** * Automatic process to fix the register entries. * Called when the initial eIDAS authn leads to a match in a register. * * @param registerResult Result of last register search * @param initialEidasData Received eidas data from initial authn * @return */ @NonNull public RegisterStatusResults step7aKittProcess(RegisterStatusResults registerResult, SimpleEidasData initialEidasData) throws WorkflowException { log.trace("Starting step7aKittProcess"); // check if only one single result was found if (registerResult.getResultCount() != 1) { throw new WorkflowException("step7aKittProcess", "getResultCount() != 1"); } // perform updated operation in respect to register results try { if (registerResult.getResultsZmr().size() == 1) { RegisterResult entryZmr = registerResult.getResultsZmr().get(0); ZmrRegisterResult updateZmr = zmrClient .update(registerResult.getOperationStatus().getZmrProcessId(), entryZmr, initialEidasData); log.info(LOG_MSG_KITT, ZMR); return RegisterStatusResults.fromZmr(registerResult.operationStatus, updateZmr); } else { RegisterResult entryErnp = registerResult.getResultsErnp().get(0); ErnpRegisterResult updateErnp = ernpClient.update(entryErnp, initialEidasData); log.info(LOG_MSG_KITT, ERNP); return RegisterStatusResults.fromErnp(registerResult.operationStatus, updateErnp); } } catch (final EidasSAuthenticationException e) { throw new WorkflowException("kittMatchedIdentitiess", e.getMessage(), !(e instanceof ZmrCommunicationException), e); } } /** * Automatic process to fix the register entries. * Called when the alternative eIDAS authn leads to a match in a register. * *

This method perform two additional operations: *

*

* * @param initialSearchResult Register results from initial authentication * @param initialEidasData Received eIDAS data from initial authentication * @param altSearchResult Register results from alternative authentication * @param altEidasData Received eIDAS data from alternative authentication * @return */ public RegisterStatusResults step7bKittProcess( RegisterStatusResults initialSearchResult, SimpleEidasData initialEidasData, RegisterStatusResults altSearchResult, SimpleEidasData altEidasData) throws WorkflowException { log.trace("Starting step7bKittProcess"); // check if alternative authentication ends in a single result if (altSearchResult.getResultCount() != 1) { throw new WorkflowException("step7bKittProcess", "getResultCount() != 1"); } // check if alternative authentication result is part of initialSearchResults if (!Streams.concat(initialSearchResult.getResultsZmr().stream(), initialSearchResult.getResultsErnp().stream()) .filter(el -> { try { return altSearchResult.getResult().getBpk().equals(el.getBpk()); } catch (WorkflowException e1) { //can not appear because it's already validated above. return false; } }) .findFirst() .isPresent()) { throw new WorkflowException("step7bKittProcess", "Register result from alternativ authentication does not fit into intermediate state"); } // perform KITT operations try { if (altSearchResult.getResultsZmr().size() == 1) { RegisterResult entryZmr = altSearchResult.getResultsZmr().get(0); // update ZMR entry by using eIDAS information from initial authentication zmrClient.update(altSearchResult.getOperationStatus().getZmrProcessId(), entryZmr, initialEidasData); // update ZMR entry by using eIDAS information from alternative authentication ZmrRegisterResult updateAlt = zmrClient.update( altSearchResult.getOperationStatus().getZmrProcessId(), entryZmr, altEidasData); log.info(LOG_MSG_KITT, ZMR); return RegisterStatusResults.fromZmr(altSearchResult.getOperationStatus(), updateAlt); } else { RegisterResult entryErnp = altSearchResult.getResultsErnp().get(0); // update ZMR entry by using eIDAS information from initial authentication ernpClient.update(entryErnp, initialEidasData); // update ZMR entry by using eIDAS information from alternative authentication ErnpRegisterResult updateAlt = ernpClient.update(entryErnp, altEidasData); log.info(LOG_MSG_KITT, ERNP); return RegisterStatusResults.fromErnp(altSearchResult.getOperationStatus(), updateAlt); } } catch (final EidasSAuthenticationException e) { throw new WorkflowException("kittMatchedIdentitiess", e.getMessage(), !(e instanceof ZmrCommunicationException), e); } } @Nullable private CountrySpecificDetailSearchProcessor findSpecificProcessor(SimpleEidasData eidasData) { final String citizenCountry = eidasData.getCitizenCountryCode(); for (final CountrySpecificDetailSearchProcessor processor : handlers) { if (processor.canHandle(citizenCountry, eidasData)) { log.debug("Found suitable search handler for {} by using: {}", citizenCountry, processor.getName()); return processor; } } return null; } /** * Register releated information that are needed for any request. * * @author tlenz */ @AllArgsConstructor @Getter public static class RegisterOperationStatus implements Serializable { private static final long serialVersionUID = -1037357883275379796L; /** * ZMR internal processId that is required for any further request in the same process. */ private BigInteger zmrProcessId; /** * Flag that indicates if ERnP entries by user decision is allowed. */ private final boolean allowErnpEntryByUser; /** * Build {@link RegisterOperationStatus} based on an existing status 'before' * and an 'allowErnpEntry' flag of current process. * * @param before Status from process-step before * @param allowErnpEntry does current result allow new ERnP entry by user * @return */ public static RegisterOperationStatus buildFromExisting(@Nonnull RegisterOperationStatus before, boolean allowErnpEntry) { return new RegisterOperationStatus(before.getZmrProcessId(), before.isAllowErnpEntryByUser() && allowErnpEntry); } } /** * Response container for {@link RegisterSearchService} that holds a set of {@link RegisterResult}. * * @author tlenz */ @Getter @RequiredArgsConstructor public static class RegisterStatusResults implements Serializable { private static final long serialVersionUID = -2489125033838373511L; /** * Operation status for this result. */ private final RegisterOperationStatus operationStatus; /** * Current ZMR search result. */ private final List resultsZmr; /** * Current ERnP search result. */ private final List resultsErnp; /** * Get sum of ZMR and ERnP results. * * @return number of results */ public int getResultCount() { return resultsZmr.size() + resultsErnp.size(); } /** * Verifies that there is only one match and returns the bpk. * * @return bpk bpk of the match * @throws WorkflowException if multiple results have been found */ public String getBpk() throws WorkflowException { if (getResultCount() != 1) { throw new WorkflowException("readRegisterResults", "getResultCount() != 1"); } return getResult().getBpk(); } /** * Returns the results, if there is exactly one, throws exception otherwise. * * @return The result * @throws WorkflowException Results does not contain exactly one result */ public RegisterResult getResult() throws WorkflowException { if (getResultCount() != 1) { throw new WorkflowException("readRegisterResults", "getResultCount() != 1"); } if (resultsZmr.size() == 1) { return resultsZmr.get(0); } else { return resultsErnp.get(0); } } static RegisterStatusResults fromZmr(RegisterOperationStatus status, ZmrRegisterResult result) { return new RegisterStatusResults(RegisterOperationStatus.buildFromExisting(status, true), result.getPersonResult(), Collections.emptyList()); } static RegisterStatusResults fromZmrAndErnp(RegisterOperationStatus status, ZmrRegisterResult result, ErnpRegisterResult resultErnp) { /* * Post-processing of ERnP entries to remove entities that included twice. * In case of KITT on register side that KITTS an ERnP to ZMR entry, * the same entity can part of both responses. */ Set existingZmrPersons = result.getPersonResult().stream() .map(el -> el.getBpk()) .collect(Collectors.toSet()); // check active ERnP entries List ernpCleared = resultErnp.getPersonResultStream() .filter(el -> !existingZmrPersons.contains(el.getBpk())) .collect(Collectors.toList()); if (ernpCleared.size() < resultErnp.getPersonResult().size()) { log.warn(LOG_MSG_RESULTS_CLEARING, resultErnp.getPersonResult().size() - ernpCleared.size()); } // check ERnP to ZMR kitt entries and join results from ZMR client List zmrCleared = Streams.concat( resultErnp.getZmrPersonResultStream() .filter(el -> !existingZmrPersons.contains(el.getBpk())), result.getPersonResult().stream()) .collect(Collectors.toList()); if (zmrCleared.size() < result.getPersonResult().size() + resultErnp.getZmrPersonResult().size()) { log.info(LOG_MSG_RESULTS_CLEARING_KITT, result.getPersonResult().size() + resultErnp.getZmrPersonResult().size() - zmrCleared.size()); } return new RegisterStatusResults( status != null ? RegisterOperationStatus.buildFromExisting(status, resultErnp.isAllowErnpEntryByUser()) : new RegisterOperationStatus(result.getProcessId(), resultErnp.isAllowErnpEntryByUser()), zmrCleared, ernpCleared); } static RegisterStatusResults fromErnp(RegisterOperationStatus status, ErnpRegisterResult updateErnp) { return new RegisterStatusResults( RegisterOperationStatus.buildFromExisting(status, updateErnp.isAllowErnpEntryByUser()), Collections.emptyList(), updateErnp.getPersonResult()); } static RegisterStatusResults fromEmpty(RegisterOperationStatus status) { return new RegisterStatusResults( RegisterOperationStatus.buildFromExisting(status, true), Collections.emptyList(), Collections.emptyList()); } } }