/*
* 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:
*
* - {@link Constants#DATA_SIMPLE_EIDAS} initial login data from user
* - {@link Constants#DATA_INTERMEDIATE_RESULT} results from search in registers with personIdentifier
*
* Output:
*
* - {@link Constants#DATA_PERSON_MATCH_RESULT} if one register result found
*
* Transitions:
*
* - {@link GenerateOtherLoginMethodGuiTask} if no results from search with residency data in registers
* - {@link CreateIdentityLinkTask} if one exact match between initial register search (with MDS) and results
* from search with residency data in registers exists
* - {@link GenerateOtherLoginMethodGuiTask} if a user input error has happened
*
*
* @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();
}
}