package at.gv.egiz.eaaf.core.impl.idp.auth.services; import java.io.IOException; import java.io.InputStream; import java.net.URISyntaxException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.PostConstruct; import javax.servlet.http.HttpServletRequest; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.text.StringEscapeUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.type.CollectionType; import com.fasterxml.jackson.databind.type.TypeFactory; import at.gv.egiz.eaaf.core.api.IRequest; import at.gv.egiz.eaaf.core.api.IStatusMessenger; import at.gv.egiz.eaaf.core.api.data.EaafConstants; import at.gv.egiz.eaaf.core.api.gui.ModifyableGuiBuilderConfiguration; import at.gv.egiz.eaaf.core.api.idp.IConfiguration; import at.gv.egiz.eaaf.core.api.utils.IPendingRequestIdGenerationStrategy; import at.gv.egiz.eaaf.core.exceptions.EaafException; import at.gv.egiz.eaaf.core.exceptions.TaskExecutionException; import at.gv.egiz.eaaf.core.impl.data.ErrorConfig; import at.gv.egiz.eaaf.core.impl.gui.AbstractGuiFormBuilderConfiguration; import at.gv.egiz.eaaf.core.impl.idp.controller.ProtocolFinalizationController; import at.gv.egiz.eaaf.core.impl.utils.DefaultYamlMapper; import at.gv.egiz.eaaf.core.impl.utils.FileUtils; import at.gv.egiz.eaaf.core.impl.utils.ServletUtils; import lombok.Builder; import lombok.Getter; import lombok.var; import lombok.extern.slf4j.Slf4j; @Slf4j public abstract class TicketErrorService implements IErrorService { private static final String CONFIG_PROP_ERROR_HANDLING_CONFIG_PATH = "core.error.handling.config"; private static final String TECH_LOG_MSG = "errorCode={0} Message={1}"; private static final String TICKET_LOG_MSG = "Ticket={2} errorCode={0} Message={1}"; private final List errorConfigList = new ArrayList<>(); @Autowired IConfiguration basicConfig; @Autowired ResourceLoader resourceLoader; @Autowired IPendingRequestIdGenerationStrategy requestIdValidationStragegy; @Autowired(required = false) Set modelExtensionHandlers; @Override public String getExternalCodeFromInternal(String internalCode) { ErrorConfig errorConfig = findByInternalCode(internalCode); return StringUtils.isNotEmpty(errorConfig.getExternalCode()) ? errorConfig.getExternalCode() : errorConfig.getUseInternalAsExternal() ? internalCode : IStatusMessenger.CODES_EXTERNAL_ERROR_GENERIC; } @Override public IHandleData createHandleData(Throwable throwable, IRequest protocolRequest) throws EaafException { String internalErrorId = extractInternalErrorCode(throwable); ErrorConfig errorFlowConfig = findByInternalCode(internalErrorId); ActionType errorHandlingFlow = errorFlowConfig.getActionType(); return HandleData.builder() .throwable(throwable) .internalErrorCode(internalErrorId) .actionType(errorHandlingFlow) .logLevel(LogLevel.fromString(errorFlowConfig.getLogLevel())) .supportTicket(ActionType.TICKET.equals(errorHandlingFlow) ? generateSupportTicket() : null) .errorIdTokenForRedirect( protocolRequest != null ? requestIdValidationStragegy.generateExternalPendingRequestId() : null) .additionalGuiModelElements(readAdditionalGuiModelInfos(protocolRequest)) .build(); } @Override public void displayErrorData(ModifyableGuiBuilderConfiguration c, IErrorService.IHandleData errorData, HttpServletRequest httpReq) throws EaafException { if (!(errorData instanceof TicketErrorService.HandleData)) { throw new EaafException(IStatusMessenger.CODES_INTERNAL_ERROR_GENERIC); } var ed = (TicketErrorService.HandleData) errorData; // inject all additional model parameters errorData.getAdditionalGuiModelElements().entrySet().forEach( el -> c.putCustomParameter( AbstractGuiFormBuilderConfiguration.PARAM_GROUP_MSG, el.getKey(), el.getValue())); // set SupportTicket c.putCustomParameter(AbstractGuiFormBuilderConfiguration.PARAM_GROUP_MSG, PARAM_GUI_TICKET, ed.getSupportTicket()); // set redirect to SP path if (StringUtils.isNotEmpty(ed.getErrorIdTokenForRedirect())) { c.putCustomParameterWithOutEscaption( AbstractGuiFormBuilderConfiguration.PARAM_GROUP_MSG, PARAM_GUI_REDIRECT, generateRedirect(httpReq, ed.getErrorIdTokenForRedirect())); } } /** * Generate a application-specific support-ticket. * * @return Support ticket for error screen */ protected abstract String generateSupportTicket(); @Nonnull private ErrorConfig findByInternalCode(@Nonnull String seekedInternalCode) { return errorConfigList.stream() .filter(c -> c.getInternalCode() != null && c.getInternalCode().contains(seekedInternalCode)) .findFirst() .orElse( errorConfigList.stream() .filter(c -> c.getDefaultConfig() != null && c.getDefaultConfig().equals(true)) .findFirst() .orElse(ErrorConfig.builder() .action(ActionType.TICKET.toString()) .externalCode(IStatusMessenger.CODES_INTERNAL_ERROR_GENERIC) .logLevel("ERROR") .defaultConfig(true) .build()) ); } private Map readAdditionalGuiModelInfos(IRequest protocolRequest) { if (protocolRequest != null) { log.trace("Searching for additional GUI model elements to inject ... "); return modelExtensionHandlers.stream() .map(el -> el.elementsForErrorModel(protocolRequest)) .flatMap(m -> m.entrySet().stream()) .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, Map.Entry::getValue)); } return Collections.emptyMap(); } private String extractInternalErrorCode(Throwable throwable) { Throwable originalException; if (throwable instanceof TaskExecutionException && ((TaskExecutionException) throwable).getOriginalException() != null) { originalException = ((TaskExecutionException) throwable).getOriginalException(); } else { originalException = throwable; } if (!(originalException instanceof EaafException)) { return IStatusMessenger.CODES_INTERNAL_ERROR_GENERIC; } else { return ((EaafException) originalException).getErrorId(); } } private String generateRedirect(HttpServletRequest httpReq, String errorTokenId) { String redirectUrl = ServletUtils.getBaseUrl(httpReq); redirectUrl += ProtocolFinalizationController.ENDPOINT_ERROR_REDIRECT + "?" + EaafConstants.PARAM_HTTP_ERROR_CODE + "=" + StringEscapeUtils .escapeHtml4(errorTokenId); return redirectUrl; } @PostConstruct private void initialize() throws EaafException { final String errorConfPath = basicConfig.getBasicConfiguration(CONFIG_PROP_ERROR_HANDLING_CONFIG_PATH); log.info("Initializing error-handling service from configuration: {}", errorConfPath); if (StringUtils.isEmpty(errorConfPath)) { log.error("Error: Path to error handling config is not known"); throw new EaafException("internal.configuration.00", new Object[]{CONFIG_PROP_ERROR_HANDLING_CONFIG_PATH}); } try { final byte[] raw = readFromFile(errorConfPath); ObjectMapper mapper = DefaultYamlMapper.getYamlMapper(); final TypeFactory typeFactory = mapper.getTypeFactory(); final CollectionType javaType = typeFactory.constructCollectionType(List.class, ErrorConfig.class); errorConfigList.addAll(mapper.readValue(raw, javaType)); log.info("Found #{} configuration-elements for Error Handling", errorConfigList.size()); } catch (Exception e) { log.error("Error reading Configurations file", e); throw new EaafException("internal.configuration.01", new Object[]{CONFIG_PROP_ERROR_HANDLING_CONFIG_PATH, "Error reading Configurations file"}); } // log currently loaded GUI model extension handlers if (modelExtensionHandlers != null) { modelExtensionHandlers.forEach(el -> log.info( "Register Consent Attribute-Parameter handler: {} into Consent-RequestedAttributes Handler", el.getClass().getName())); } else { modelExtensionHandlers = Collections.emptySet(); } } private byte[] readFromFile(final String filePath) throws URISyntaxException, IOException { final String fullFilePath = FileUtils.makeAbsoluteUrl(filePath, basicConfig.getConfigurationRootDirectory()); final Resource ressource = resourceLoader.getResource(fullFilePath); final InputStream is = ressource.getInputStream(); final byte[] result = IOUtils.toByteArray(is); is.close(); return result; } @Builder static class HandleData implements IHandleData { @Getter private String errorIdTokenForRedirect; @Getter private String supportTicket; @Getter private final Throwable throwable; @Getter private String internalErrorCode; @Getter private ActionType actionType; @Getter private LogLevel logLevel; @Getter private Map additionalGuiModelElements; public String getPreFormatedErrorMessage() { if (supportTicket != null) { return MessageFormat.format(TICKET_LOG_MSG, internalErrorCode, throwable.getMessage(), supportTicket); } else { return MessageFormat.format(TECH_LOG_MSG, internalErrorCode, throwable.getMessage()); } } } }