package at.asitplus.eidas.specific.modules.auth.eidas.v2.service; import java.math.BigInteger; import java.util.Collections; import java.util.List; import javax.annotation.Nonnull; import org.jetbrains.annotations.Nullable; import org.springframework.stereotype.Service; import com.google.common.collect.Streams; 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.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 lombok.AllArgsConstructor; import lombok.Getter; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @Slf4j @Service("registerSearchService") public class RegisterSearchService { 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 List resultsErnp = ernpClient.searchWithPersonIdentifier( eidasData.getPersonalIdentifier()); return RegisterStatusResults.fromZmrAndErnp(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 List resultsErnp = ernpClient.searchWithMds(eidasData.getGivenName(), eidasData.getFamilyName(), eidasData .getDateOfBirth()); return RegisterStatusResults.fromZmrAndErnp(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.debug("Selecting country-specific search processor: {}", ccSpecificProcessor.getName()); final ZmrRegisterResult resultsZmr = zmrClient.searchCountrySpecific(operationStatus.getZmrProcessId(), ccSpecificProcessor.generateSearchRequest(eidasData), eidasData.getCitizenCountryCode()); return RegisterStatusResults.fromZmr(resultsZmr); } else { // TODO: add search procesfor for ERnP searching return RegisterStatusResults.fromErnp(operationStatus, Collections.emptyList()); } } 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 zipcode Provided Zipcode * @param city Provided City * @param street Provided street * @return Results from ZMR or ERnP search */ public RegisterStatusResults searchWithResidence(RegisterOperationStatus operationStatus, SimpleEidasData eidasData, String zipcode, String city, String street) { final ZmrRegisterResult resultsZmr = zmrClient.searchWithResidenceData( operationStatus.getZmrProcessId(), eidasData.getGivenName(), eidasData.getFamilyName(), eidasData.getDateOfBirth(), zipcode, city, street); return RegisterStatusResults.fromZmr(resultsZmr); } /** * 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 */ public RegisterStatusResults step7aKittProcess(RegisterStatusResults registerResult, SimpleEidasData initialEidasData) throws WorkflowException { log.trace("Starting step7aKittProcess"); // TODO verify with which data this method gets called if (registerResult.getResultCount() != 1) { throw new WorkflowException("step7aKittProcess", "getResultCount() != 1"); } try { if (registerResult.getResultsZmr().size() == 1) { RegisterResult entryZmr = registerResult.getResultsZmr().get(0); ZmrRegisterResult updateZmr = zmrClient .update(registerResult.getOperationStatus().getZmrProcessId(), entryZmr, initialEidasData); return RegisterStatusResults.fromZmr(updateZmr); } else { RegisterResult entryErnp = registerResult.getResultsErnp().get(0); RegisterResult updateErnp = ernpClient.update(entryErnp, initialEidasData); return RegisterStatusResults.fromErnp(registerResult.operationStatus, Collections.singletonList(updateErnp)); } } catch (final EidasSAuthenticationException e) { throw new WorkflowException("kittMatchedIdentitiess", e.getMessage(), !(e instanceof ZmrCommunicationException), e); } } //TODO: check this method, because it's different to 'step7aKittProcess'??? /** * Automatic process to fix the register entries. * Called when the alternative eIDAS authn leads to a match in a register. * * @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); return RegisterStatusResults.fromZmr(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 RegisterResult updateAlt = ernpClient.update(entryErnp, altEidasData); return RegisterStatusResults.fromErnp(altSearchResult.operationStatus, Collections.singletonList(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 { /** * ZMR internal processId that is required for any further request in the same process. */ private BigInteger zmrProcessId; } /** * Response container for {@link RegisterSearchService} that holds a set of {@link RegisterResult}. * * @author tlenz */ @Getter @RequiredArgsConstructor public static class RegisterStatusResults { /** * 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(ZmrRegisterResult result) { return new RegisterStatusResults(new RegisterOperationStatus(result.getProcessId()), result.getPersonResult(), Collections.emptyList()); } static RegisterStatusResults fromZmrAndErnp(ZmrRegisterResult result, List resultsErnp) { return new RegisterStatusResults(new RegisterOperationStatus(result.getProcessId()), result.getPersonResult(), resultsErnp); } static RegisterStatusResults fromErnp(RegisterOperationStatus status, List resultsErnp) { return new RegisterStatusResults(status, Collections.emptyList(), resultsErnp); } } }