/* * Copyright 2021 A-SIT Plus GmbH * AT-specific eIDAS Connector has been developed in a cooperation between EGIZ, * A-SIT Plus GmbH, A-SIT, and Graz University of Technology. * * Licensed under the EUPL, Version 1.2 or - as soon they will be approved by * the European Commission - subsequent versions of the EUPL (the "License"); * You may not use this work except in compliance with the License. * You may obtain a copy of the License at: * https://joinup.ec.europa.eu/news/understanding-eupl-v12 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * This product combines work with different licenses. See the "NOTICE" text * file for details on the various modules and licenses. * The "NOTICE" text file is part of the distribution. Any derivative works * that you distribute must include a readable copy of the "NOTICE" text file. */ package at.asitplus.eidas.specific.modules.auth.eidas.v2.tasks; import static at.asitplus.eidas.specific.modules.auth.eidas.v2.Constants.CONTEXT_FLAG_ADVANCED_MATCHING_FAILED; import static at.asitplus.eidas.specific.modules.auth.eidas.v2.Constants.CONTEXT_FLAG_ADVANCED_MATCHING_FAILED_REASON; import static at.asitplus.eidas.specific.modules.auth.eidas.v2.Constants.TRANSITION_TO_GENERATE_GUI_QUERY_AUSTRIAN_RESIDENCE_TASK; import static at.asitplus.eidas.specific.modules.auth.eidas.v2.Constants.TRANSITION_TO_REQUESTING_NEW_ERNP_ENTRY_TASK; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import java.util.Enumeration; import java.util.Set; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.NotNull; import org.springframework.stereotype.Component; import com.google.common.collect.Sets; import at.asitplus.eidas.specific.core.MsEidasNodeConstants.MatchingStates; import at.asitplus.eidas.specific.modules.auth.eidas.v2.Constants; import at.asitplus.eidas.specific.modules.auth.eidas.v2.controller.AdresssucheController; import at.asitplus.eidas.specific.modules.auth.eidas.v2.controller.AdresssucheController.AdresssucheOutput; import at.asitplus.eidas.specific.modules.auth.eidas.v2.controller.AdresssucheController.AdresssucheOutput.AdresssucheOutputBuilder; import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.MatchedPersonResult; import at.asitplus.eidas.specific.modules.auth.eidas.v2.dao.SimpleEidasData; import at.asitplus.eidas.specific.modules.auth.eidas.v2.exception.ManualFixNecessaryException; import at.asitplus.eidas.specific.modules.auth.eidas.v2.exception.WorkflowException; import at.asitplus.eidas.specific.modules.auth.eidas.v2.service.RegisterSearchService; import at.asitplus.eidas.specific.modules.auth.eidas.v2.service.RegisterSearchService.RegisterStatusResults; import at.asitplus.eidas.specific.modules.auth.eidas.v2.utils.MatchingTaskUtils; import at.gv.egiz.eaaf.core.api.idp.process.ExecutionContext; import at.gv.egiz.eaaf.core.exceptions.EaafException; import at.gv.egiz.eaaf.core.exceptions.EaafStorageException; import at.gv.egiz.eaaf.core.exceptions.TaskExecutionException; import at.gv.egiz.eaaf.core.impl.idp.controller.tasks.AbstractLocaleAuthServletTask; import lombok.extern.slf4j.Slf4j; /** * Task receives the response of {@link GenerateAustrianResidenceGuiTask} and handles it. * This corresponds to Steps 17B, 18, 19 in the eIDAS Matching Concept. * Input: * * Output: * * Transitions: * * * @author amarsalek * @author ckollmann * @author tlenz */ @Slf4j @Component("ReceiveAustrianResidenceGuiResponseTask") public class ReceiveAustrianResidenceGuiResponseTask extends AbstractLocaleAuthServletTask { private static final String MSG_PROP_21 = "module.eidasauth.matching.21"; private static final String MSG_PROP_22 = "module.eidasauth.matching.22"; public static final String HTTP_PARAM_NO_RESIDENCE = "noResidence"; public static final Set ALL_EXECUTIONCONTEXT_PARAMETERS = Sets.newHashSet( CONTEXT_FLAG_ADVANCED_MATCHING_FAILED, CONTEXT_FLAG_ADVANCED_MATCHING_FAILED_REASON, TRANSITION_TO_REQUESTING_NEW_ERNP_ENTRY_TASK, TRANSITION_TO_GENERATE_GUI_QUERY_AUSTRIAN_RESIDENCE_TASK); private final RegisterSearchService registerSearchService; public ReceiveAustrianResidenceGuiResponseTask(RegisterSearchService registerSearchService) { this.registerSearchService = registerSearchService; } @Override protected void executeWithLocale(ExecutionContext executionContext, HttpServletRequest request, HttpServletResponse response) throws TaskExecutionException { log.trace("Starting ReceiveAustrianResidenceGuiResponseTask"); try { //return to AuswahlScreen if HTTP_PARAM_NO_RESIDENCE was selected final boolean forwardWithOutMandate = parseFlagFromHttpRequest(request, HTTP_PARAM_NO_RESIDENCE, false); if (forwardWithOutMandate) { log.debug("User selects 'no residence' button. Switch back to 'insert-into-ERnP' selection ... "); executionContext.put(TRANSITION_TO_REQUESTING_NEW_ERNP_ENTRY_TASK, true); executionContext.put(TRANSITION_TO_GENERATE_GUI_QUERY_AUSTRIAN_RESIDENCE_TASK, false); return; } else { executionContext.put(TRANSITION_TO_REQUESTING_NEW_ERNP_ENTRY_TASK, false); } //load search parameters from HTML form AdresssucheOutput input = parseHtmlInput(request); if (validateHtmlInput(input)) { // HTML form should ensure that mandatory fields are set => this should never happen log.warn("HTML form contains no residence information. Switch back to 'input residence inputs' ... "); executionContext.put(TRANSITION_TO_GENERATE_GUI_QUERY_AUSTRIAN_RESIDENCE_TASK, true); executionContext.put(CONTEXT_FLAG_ADVANCED_MATCHING_FAILED_REASON, MSG_PROP_21); executionContext.put(CONTEXT_FLAG_ADVANCED_MATCHING_FAILED, true); return; } MatchingTaskUtils.getDetailedMatchingStatistic(pendingReq).incrementAddressSearch(); // get pre-processed information SimpleEidasData eidasData = MatchingTaskUtils.getInitialEidasData(pendingReq); RegisterStatusResults initialSearchResult = MatchingTaskUtils.getIntermediateMatchingResult(pendingReq); // search in register RegisterStatusResults residencyResult = registerSearchService.searchWithResidence(initialSearchResult.getOperationStatus(), eidasData, input); // validate matching response from registers if (residencyResult.getResultCount() != 1) { log.info("Find {} match by using residence information. Forward user to 'input residence infos' ... ", residencyResult.getResultCount() == 0 ? "no" : "more-than-one"); executionContext.put(TRANSITION_TO_GENERATE_GUI_QUERY_AUSTRIAN_RESIDENCE_TASK, true); executionContext.put(CONTEXT_FLAG_ADVANCED_MATCHING_FAILED_REASON, MSG_PROP_22); executionContext.put(CONTEXT_FLAG_ADVANCED_MATCHING_FAILED, true); } else { log.debug("Find single match by using residence information. Starting data validation ... "); compareSearchResultWithInitialData(residencyResult, eidasData); executionContext.put(TRANSITION_TO_GENERATE_GUI_QUERY_AUSTRIAN_RESIDENCE_TASK, false); } // store pending request before next step requestStoreage.storePendingRequest(pendingReq); } catch (WorkflowException e) { throw new TaskExecutionException(pendingReq, "Search with residency data failed", e); } catch (EaafException e) { log.error("Search with residency data failed", e); throw new TaskExecutionException(pendingReq, "Search with residency data failed", e); } } private boolean validateHtmlInput(AdresssucheOutput input) { return StringUtils.isEmpty(input.getMunicipality()) && StringUtils.isEmpty(input.getNumber()) && StringUtils.isEmpty(input.getPostleitzahl()) && StringUtils.isEmpty(input.getStreet()) && StringUtils.isEmpty(input.getVillage()); } private void compareSearchResultWithInitialData(RegisterStatusResults residencyResult, SimpleEidasData eidasData) throws TaskExecutionException, EaafStorageException { try { if (!eidasData.equalsRegisterData(residencyResult.getResult())) { // update register information RegisterStatusResults updateResult = registerSearchService.step7aKittProcess(residencyResult, eidasData); // store updated result to re-used in CreateIdentityLink step, because there we need bPK and MDS MatchingTaskUtils.setMatchingState(pendingReq, MatchingStates.BY_ADDRESS); MatchingTaskUtils.storeFinalMatchingResult(pendingReq, MatchedPersonResult.generateFormMatchingResult( updateResult.getResult(), eidasData.getCitizenCountryCode())); } else { log.warn("Suspect state FOUND. Matching by residence was neccessary but NO register-update are required!"); // no update required. Data can be used as it is. MatchingTaskUtils.setMatchingState(pendingReq, MatchingStates.BY_ADDRESS); MatchingTaskUtils.storeFinalMatchingResult(pendingReq, MatchedPersonResult.generateFormMatchingResult( residencyResult.getResult(), eidasData.getCitizenCountryCode())); } } catch (WorkflowException e) { log.warn("Kitt operation after successful residence matching FAILED.", e); throw new TaskExecutionException(pendingReq, "Search failed", new ManualFixNecessaryException(eidasData)); } } private @NotNull AdresssucheOutput parseHtmlInput(HttpServletRequest request) { Enumeration reqParamNames = request.getParameterNames(); AdresssucheOutputBuilder resultBuilder = AdresssucheOutput.builder(); while (reqParamNames.hasMoreElements()) { final String paramName = reqParamNames.nextElement(); String escaped = URLDecoder.decode(request.getParameter(paramName), StandardCharsets.UTF_8); if (AdresssucheController.PARAM_MUNIPICALITY.equalsIgnoreCase(paramName)) { resultBuilder.municipality(escaped); } else if (AdresssucheController.PARAM_NUMBER.equalsIgnoreCase(paramName)) { resultBuilder.number(escaped); } else if (AdresssucheController.PARAM_POSTLEITZAHL.equalsIgnoreCase(paramName)) { resultBuilder.postleitzahl(escaped); } else if (AdresssucheController.PARAM_STREET.equalsIgnoreCase(paramName)) { resultBuilder.street(escaped); } else if (AdresssucheController.PARAM_VILLAGE.equalsIgnoreCase(paramName)) { resultBuilder.village(escaped); } } return resultBuilder.build(); } }