diff --git a/perun-oidc-server-webapp/src/main/resources/localization/messages_cs.properties b/perun-oidc-server-webapp/src/main/resources/localization/messages_cs.properties index 3ef721f53..dc80a800c 100644 --- a/perun-oidc-server-webapp/src/main/resources/localization/messages_cs.properties +++ b/perun-oidc-server-webapp/src/main/resources/localization/messages_cs.properties @@ -75,10 +75,6 @@ contact_p=V p\u0159\u00EDpad\u011B nejasnost\u00ED n\u00E1s kontaktujte na 403_informationPage=Pro v\u00EDce informac\u00ED o slu\u017Eb\u011B nav\u0161tivte 403_contactSupport=Pokud si mysl\u00EDte \u017Ee m\u00E1te m\u00EDt p\u0159\u00EDstup, kontaktujte administr\u00E1tora: 403_subject=Probl\u00E9m s p\u0159ihl\u00E1\u0161en\u00EDm do slu\u017Eby -403_isCesnetEligible_notSet_hdr=P\u0159\u00EDstup zam\u00EDtnut -403_isCesnetEligible_notSet_msg=P\u0159\u00EDstup ke slu\u017Eb\u011B zam\u00EDtnut, proto\u017Ee V\u00E1\u0161 \u00FA\u010Det nen\u00ED z \u010Desk\u00E9 akademick\u00E9 instituce. P\u0159ihlaste se, pros\u00EDm, pomoc\u00ED sv\u00E9ho \u00FA\u010Dtu u akademick\u00E9 instituce.
Znovu p\u0159ihl\u00E1sit -403_isCesnetEligible_expired_hdr=P\u0159\u00EDstup zam\u00EDtnut -403_isCesnetEligible_expired_msg=P\u0159\u00EDstup ke slu\u017Eb\u011B zam\u00EDtnut, proto\u017Ee plynula doba 12 m\u011Bs\u00EDc\u016F od Va\u0161eho posledn\u00EDho p\u0159ihl\u00E1\u0161en\u00ED \u00FA\u010Dtem z \u010Desk\u00E9 akademick\u00E9 instituce. P\u0159ihlaste se, pros\u00EDm, pomoc\u00ED sv\u00E9ho \u00FA\u010Dtu u akademick\u00E9 instituce.
Znovu p\u0159ihl\u00E1sit 403_ensure_vo_hdr=P\u0159\u00EDstup zam\u00EDtnut 403_ensure_vo_msg=Nem\u00E1te dostate\u010Dn\u00E1 pr\u00E1va pro p\u0159\u00EDstup ke slu\u017Eb\u011B 403_authorization_hdr=P\u0159\u00EDstup zam\u00EDtnut @@ -92,6 +88,11 @@ contact_p=V p\u0159\u00EDpad\u011B nejasnost\u00ED n\u00E1s kontaktujte na 403_not_logged_in_hdr=P\u0159\u00EDstup zam\u00EDtnut 403_not_logged_in_msg=Zd\u00E1 se, \u017Ee p\u0159ihl\u00E1\u0161en\u00ED selhalo. Zkuste, pros\u00EDm, zav\u0159\u00EDt V\u00E1\u0161 prohl\u00ED\u017Ee\u010D a p\u0159ihl\u00E1sit se znovu. +403_is_eligible_default_header_text=P\u0159\u00EDstup zam\u00EDtnut +403_is_eligible_default_text=P\u0159\u00EDstup ke slu\u017Eb\u011B byl zam\u00EDtnut, proto\u017Ee V\u00E1\u0161 \u00FA\u010Det nespl\u0148uje pomd\u00EDnky p\u0159\u00EDstupu. P\u0159ihlaste se, pros\u00EDme, pomoc\u00ED jin\u00E9ho \u00FA\u010Dtu. +403_is_eligible_default_button_text=Pokra\u010Dovat +403_is_eligible_default_contact_text=Pokud si mysl\u00EDte, \u017Ee pou\u017E\u00EDv\u00E1te spr\u00E1vn\u00FD \u00FA\u010Det a p\u0159\u00EDstup je V\u00E1m odm\u00EDtnut nepr\u00E1vem, pros\u00EDme kontakujte n\u00E1s na + #GO TO REGISTRATION go_to_registration_title=Je vy\u017Eadov\u00E1na Va\u0161e aktivita go_to_registration_header1=Pro p\u0159\u00EDstup ke slu\u017Eb\u011B diff --git a/perun-oidc-server-webapp/src/main/resources/localization/messages_en.properties b/perun-oidc-server-webapp/src/main/resources/localization/messages_en.properties index 057597046..9ee693a3e 100644 --- a/perun-oidc-server-webapp/src/main/resources/localization/messages_en.properties +++ b/perun-oidc-server-webapp/src/main/resources/localization/messages_en.properties @@ -74,10 +74,6 @@ contact_p=In case of any questions, do not hesitate to contact us at 403_informationPage=For more information about this service please visit this 403_contactSupport=If you think you should have an access contact service operator at 403_subject=Problem with login to service: -403_isCesnetEligible_notSet_hdr=Access denied -403_isCesnetEligible_notSet_msg=Your account is not from Czech academic institution. Please log in with your account from academic institution.Log in again -403_isCesnetEligible_expired_hdr=Access denied -403_isCesnetEligible_expired_msg=Your last login, from Czech academic institution, has been registered 12 months ago. Please sign in with your account from academic institution.Log in again 403_ensure_vo_hdr=Access denied 403_ensure_vo_msg=You don't meet the prerequisites to access the service. 403_authorization_hdr=Access denied @@ -91,6 +87,11 @@ contact_p=In case of any questions, do not hesitate to contact us at 403_not_logged_in_hdr=Access denied 403_not_logged_in_msg=It appears the login process has failed. Please close your browser and try to log in again. +403_is_eligible_default_header_text=Access denied +403_is_eligible_default_text=Your account does not meet the criteria for accessing the service. Please log in with other account. +403_is_eligible_default_button_text=Continue with other account. +403_is_eligible_default_contact_text=If you think you have used an account which meets the criteria, and you are still prevented from logging in to the service, please contact us at + #GO TO REGISTRATION go_to_registration_title=Your activity is necessary go_to_registration_header1=Your activity is necessary to access the diff --git a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/lsaai/unapproved_is_eligible.jsp b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/lsaai/unapproved_is_eligible.jsp new file mode 100644 index 000000000..e44920617 --- /dev/null +++ b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/lsaai/unapproved_is_eligible.jsp @@ -0,0 +1,21 @@ +<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> +<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%> +<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> +<%@ taglib prefix="ls" tagdir="/WEB-INF/tags/lsaai" %> + + + +
+

+

+ +
+ +
+
+

${" "}${contactMail}

+
+ + diff --git a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unapproved_is_eligible.jsp b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unapproved_is_eligible.jsp new file mode 100644 index 000000000..02dc5db2a --- /dev/null +++ b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/views/unapproved_is_eligible.jsp @@ -0,0 +1,35 @@ +<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8" trimDirectiveWhitespaces="true" %> +<%@ page import="java.util.ArrayList" %> +<%@ page import="java.util.List" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> +<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %> +<%@ taglib prefix="t" tagdir="/WEB-INF/tags/common"%> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> + +<% + +List cssLinks = new ArrayList<>(); + +pageContext.setAttribute("cssLinks", cssLinks); + +%> + + + + <%-- header --%> + +
+
+

+

+ +
+ +
+
+

${" "}${contactMail}

+
+
+ + + diff --git a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/web-context.xml b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/web-context.xml index cec77325e..f7792e795 100644 --- a/perun-oidc-server-webapp/src/main/webapp/WEB-INF/web-context.xml +++ b/perun-oidc-server-webapp/src/main/webapp/WEB-INF/web-context.xml @@ -65,7 +65,7 @@ - + diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/ClaimUtils.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/ClaimUtils.java index 9c4b41d97..423d23104 100644 --- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/ClaimUtils.java +++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/ClaimUtils.java @@ -65,6 +65,23 @@ public class ClaimUtils { } } + public static int fillIntegerPropertyOrDefaultVal(String suffix, ClaimSourceInitContext ctx, int defaultVal) { + return fillIntegerPropertyOrDefaultVal(ctx.getProperty(suffix, NO_VALUE), defaultVal); + } + + private static int fillIntegerPropertyOrDefaultVal(String prop, int defaultVal) { + if (StringUtils.hasText(prop)) { + try { + return Integer.parseInt(prop); + } catch (NumberFormatException e) { + log.warn("Caught {}", e.getClass().getSimpleName(), e); + return defaultVal; + } + } else { + return defaultVal; + } + } + public static ArrayNode listToArrayNode(List list) { ArrayNode res = JsonNodeFactory.instance.arrayNode(); if (list != null && !list.isEmpty()) { diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/sources/IsEligibleClaimSource.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/sources/IsEligibleClaimSource.java new file mode 100644 index 000000000..ed81b184c --- /dev/null +++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/claims/sources/IsEligibleClaimSource.java @@ -0,0 +1,98 @@ +package cz.muni.ics.oidc.server.claims.sources; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeFactory; +import cz.muni.ics.oauth2.model.SamlAuthenticationDetails; +import cz.muni.ics.oidc.server.claims.ClaimSource; +import cz.muni.ics.oidc.server.claims.ClaimSourceInitContext; +import cz.muni.ics.oidc.server.claims.ClaimSourceProduceContext; +import cz.muni.ics.oidc.server.claims.ClaimUtils; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Collections; +import java.util.Set; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; + +/** + * This source checks if the timestamp is within defined months subtracted from the current timestamp. + * If so, returns TRUE, FALSE otherwise. + * + * Configuration (replace [claimName] with the name of the claim): + *
    + *
  • custom.claim.[claimName].source.attribute - attribute containing the eligibility last seen timestamp passed via SAML authentication
  • + *
  • custom.claim.[claimName].source.validityPeriodMonths - amount of months that we subtract from the current timestamp and compare it with the eligibility timestamp. If not provided, default 12 months is used
  • + *
+ * + * @author Pavol Pluta + * @author Dominik Frantisek Bucik + */ +@Slf4j +public class IsEligibleClaimSource extends ClaimSource { + + private static final String TIMESTAMP_FORMAT = "yyyy-MM-dd HH:mm:ss"; + private static final int DEFAULT_VALIDITY_PERIOD = 12; // 12 months + + private static final String SOURCE_ATTR_NAME = "attribute"; + private static final String VALIDITY_PERIOD_MONTHS = "validityPeriodMonths"; + + private final String sourceAttr; + private final int validityPeriodMonths; + + public IsEligibleClaimSource(ClaimSourceInitContext ctx) { + super(ctx); + + this.sourceAttr = ClaimUtils.fillStringMandatoryProperty(SOURCE_ATTR_NAME, ctx, getClaimName()); + this.validityPeriodMonths = ClaimUtils.fillIntegerPropertyOrDefaultVal(VALIDITY_PERIOD_MONTHS, ctx, DEFAULT_VALIDITY_PERIOD); + log.debug("{} - SAML attribute: '{}', validity period in months: '{}'", getClaimName(), sourceAttr, validityPeriodMonths); + } + + @Override + public Set getAttrIdentifiers() { + return Collections.singleton(sourceAttr); + } + + @Override + public JsonNode produceValue(ClaimSourceProduceContext pctx) { + SamlAuthenticationDetails details = pctx.getSamlAuthenticationDetails(); + if (details == null || details.getAttributes() == null || details.getAttributes().isEmpty()) { + log.warn("{} - no attribute set to get the source attribute from, returning FALSE", getClaimName()); + return JsonNodeFactory.instance.booleanNode(false); + } + String[] attrValue = details.getAttributes().getOrDefault(sourceAttr, new String[] {}); + if (attrValue == null || attrValue.length == 0) { + log.warn("{} - no attribute to construct value from, returning FALSE", getClaimName()); + return JsonNodeFactory.instance.booleanNode(false); + } else { + if (attrValue.length > 1) { + log.warn("{} - configured source attribute '{}' has more than one value, will use just the first one", getClaimName(), sourceAttr); + } + String timestamp = attrValue[0]; + return JsonNodeFactory.instance.booleanNode(isEligible(timestamp)); + } + } + + private boolean isEligible(String eligibleLastSeenValue) { + if (!StringUtils.hasText(eligibleLastSeenValue)) { + return false; + } + LocalDate eligibleLastSeenTimestamp; + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(TIMESTAMP_FORMAT); + eligibleLastSeenTimestamp = LocalDate.parse(eligibleLastSeenValue, formatter); + } catch (DateTimeParseException e) { + log.warn("{} - could not parse value '{}' as timestamp in format '{}'. Returning NOT ELIGIBLE.", + getClaimName(), eligibleLastSeenValue, TIMESTAMP_FORMAT); + return false; + } + + LocalDate now = LocalDateTime.now().toLocalDate(); + boolean isValid = !eligibleLastSeenTimestamp.isBefore(now.minusMonths(validityPeriodMonths)); + log.debug("{} - timestamp '{}' is time {} the defined period of '{} months'", + getClaimName(), eligibleLastSeenTimestamp, isValid ? "within" : "out of", DEFAULT_VALIDITY_PERIOD); + return isValid; + } + +} diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/FiltersUtils.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/FiltersUtils.java index cad8be75d..adc2cc6c0 100644 --- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/FiltersUtils.java +++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/FiltersUtils.java @@ -357,15 +357,23 @@ public class FiltersUtils { public static String fillStringMandatoryProperty(String propertyName, String filterName, AuthProcFilterInitContext params) { - String filled = params.getProperty(propertyName); + String filled = fillStringProperty(propertyName, params, null); - if (!StringUtils.hasText(filled)) { + if (filled == null) { throw new IllegalArgumentException("No value configured for '" + propertyName + "' in filter " + filterName); } return filled; } + public static String fillStringProperty(String propertyName, AuthProcFilterInitContext params, String defaultValue) { + String filled = params.getProperty(propertyName); + if (!StringUtils.hasText(filled)) { + return defaultValue; + } + return filled; + } + private static void redirectToRegistrationForm(String base, HttpServletResponse response, String clientIdentifier, Facility facility, PerunUser user) { Map params = new HashMap<>(); diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/IsEligibleFilter.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/IsEligibleFilter.java new file mode 100644 index 000000000..7562fcabf --- /dev/null +++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/server/filters/impl/IsEligibleFilter.java @@ -0,0 +1,206 @@ +package cz.muni.ics.oidc.server.filters.impl; + +import cz.muni.ics.oidc.BeanUtil; +import cz.muni.ics.oidc.exceptions.ConfigurationException; +import cz.muni.ics.oidc.server.configurations.PerunOidcConfig; +import cz.muni.ics.oidc.server.filters.AuthProcFilter; +import cz.muni.ics.oidc.server.filters.AuthProcFilterCommonVars; +import cz.muni.ics.oidc.server.filters.AuthProcFilterConstants; +import cz.muni.ics.oidc.server.filters.AuthProcFilterInitContext; +import cz.muni.ics.oidc.server.filters.FiltersUtils; +import cz.muni.ics.oidc.web.controllers.ControllerUtils; +import cz.muni.ics.oidc.web.controllers.PerunUnapprovedController; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; +import lombok.extern.slf4j.Slf4j; +import org.apache.http.HttpHeaders; +import org.springframework.security.saml.SAMLCredential; +import org.springframework.util.StringUtils; + +/** + * This filter verifies that user attribute isCesnetEligible is not older than given time frame. + * In case the value is older, denies access to the service and forces user to use verified identity. + * Otherwise, user can to access the service. + * + * Configuration (replace [name] part with the name defined for the filter): + *
    + *
  • filter.[name].samlAttribute - mapping to isCesnetEligible attribute
  • + *
  • filter.[name].triggerScope - scope that has to be requested to apply this filter
  • + *
  • filter.[name].validityPeriodMonths - specify in months, how long the value can be old, if no value + * or invalid value has been provided, defaults to 12 months
  • + *
+ * @author Dominik Frantisek Bucik + */ +@Slf4j +public class IsEligibleFilter extends AuthProcFilter { + + public static final String APPLIED = "APPLIED_" + IsEligibleFilter.class.getSimpleName(); + + public final static String DEFAULT_HEADER_TRANSLATION_KEY = "403_is_eligible_default_header_text"; + public final static String DEFAULT_TEXT_TRANSLATION_KEY = "403_is_eligible_default_text"; + public final static String DEFAULT_BUTTON_TRANSLATION_KEY = "403_is_eligible_default_button_text"; + public final static String DEFAULT_CONTACT_TRANSLATION_KEY = "403_is_eligible_default_contact_text"; + + public static final String HEADER_TRANSLATION = "header_translation"; + public static final String TEXT_TRANSLATION = "text_translation"; + public static final String BUTTON_TRANSLATION = "button_translation"; + public static final String CONTACT_TRANSLATION = "contact_translation"; + + /* CONFIGURATION PROPERTIES */ + private static final String SAML_ATTRIBUTE = "samlAttribute"; + private static final String TRIGGER_SCOPE = "triggerScope"; + private static final String VALIDITY_PERIOD = "validityPeriodMonths"; + private static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; + + private static final String OLD_VALUE_HEADER_TRANSLATION_KEY = "old_value_header_translation_key"; + private static final String OLD_VALUE_TEXT_TRANSLATION_KEY = "old_value_text_translation_key"; + private static final String OLD_VALUE_BUTTON_TRANSLATION_KEY = "old_value_button_translation_key"; + private static final String OLD_VALUE_CONTACT_TRANSLATION_KEY = "old_value_contact_translation_key"; + + private static final String NO_VALUE_HEADER_TRANSLATION_KEY = "no_value_header_translation_key"; + private static final String NO_VALUE_TEXT_TRANSLATION_KEY = "no_value_text_translation_key"; + private static final String NO_VALUE_BUTTON_TRANSLATION_KEY = "no_value_button_translation_key"; + private static final String NO_VALUE_CONTACT_TRANSLATION_KEY = "no_value_contact_translation_key"; + + /* END OF CONFIGURATION PROPERTIES */ + + private final String eligibleLastSeenSAMLAttributeName; + private final String triggerScope; + private final int validityPeriod; + + private final String oldValueHeaderTranslationKey; + private final String oldValueTextTranslationKey; + private final String oldValueButtonTranslationKey; + private final String oldValueContactTranslationKey; + + private final String noValueHeaderTranslationKey; + private final String noValueTextTranslationKey; + private final String noValueButtonTranslationKey; + private final String noValueContactTranslationKey; + + private final PerunOidcConfig config; + private final String filterName; + + public IsEligibleFilter(AuthProcFilterInitContext ctx) throws ConfigurationException { + super(ctx); + this.filterName = ctx.getFilterName(); + + BeanUtil beanUtil = ctx.getBeanUtil(); + this.config = beanUtil.getBean(PerunOidcConfig.class); + this.eligibleLastSeenSAMLAttributeName = FiltersUtils.fillStringMandatoryProperty(SAML_ATTRIBUTE, filterName, ctx); + this.triggerScope = FiltersUtils.fillStringMandatoryProperty(TRIGGER_SCOPE, filterName, ctx); + int validityPeriodParam = 12; + if (ctx.hasProperty(VALIDITY_PERIOD)) { + try { + validityPeriodParam = Integer.parseInt(ctx.getProperty(VALIDITY_PERIOD)); + } catch (NumberFormatException ignored) { + //no problem, we have default value + } + } + + this.oldValueHeaderTranslationKey = FiltersUtils.fillStringProperty( + OLD_VALUE_HEADER_TRANSLATION_KEY, ctx, DEFAULT_HEADER_TRANSLATION_KEY); + this.oldValueTextTranslationKey = FiltersUtils.fillStringProperty( + OLD_VALUE_TEXT_TRANSLATION_KEY, ctx, DEFAULT_TEXT_TRANSLATION_KEY); + this.oldValueButtonTranslationKey = FiltersUtils.fillStringProperty( + OLD_VALUE_BUTTON_TRANSLATION_KEY, ctx, DEFAULT_BUTTON_TRANSLATION_KEY); + this.oldValueContactTranslationKey = FiltersUtils.fillStringProperty( + OLD_VALUE_CONTACT_TRANSLATION_KEY, ctx, DEFAULT_CONTACT_TRANSLATION_KEY); + + this.noValueHeaderTranslationKey = FiltersUtils.fillStringProperty( + NO_VALUE_HEADER_TRANSLATION_KEY, ctx, DEFAULT_HEADER_TRANSLATION_KEY); + this.noValueTextTranslationKey = FiltersUtils.fillStringProperty( + NO_VALUE_TEXT_TRANSLATION_KEY, ctx, DEFAULT_TEXT_TRANSLATION_KEY); + this.noValueButtonTranslationKey = FiltersUtils.fillStringProperty( + NO_VALUE_BUTTON_TRANSLATION_KEY, ctx, DEFAULT_BUTTON_TRANSLATION_KEY); + this.noValueContactTranslationKey = FiltersUtils.fillStringProperty( + NO_VALUE_CONTACT_TRANSLATION_KEY, ctx, DEFAULT_CONTACT_TRANSLATION_KEY); + + this.validityPeriod = validityPeriodParam; + + } + + @Override + protected String getSessionAppliedParamName() { + return APPLIED + filterName; + } + + @Override + protected boolean process(HttpServletRequest req, HttpServletResponse res, AuthProcFilterCommonVars params) { + if (!FiltersUtils.isScopePresent(req.getParameter(AuthProcFilterConstants.PARAM_SCOPE), triggerScope)) { + log.debug("{} - skip execution: scope '{}' is not present in request", filterName, triggerScope); + return true; + } + + SAMLCredential samlCredential = FiltersUtils.getSamlCredential(req); + if (samlCredential == null) { + log.debug("{} - skip execution: no SAML credential to fetch attribute from is available", filterName); + return true; + } + + String eligibleLastSeenTimestamp = samlCredential.getAttributeAsString(eligibleLastSeenSAMLAttributeName); + + String headerKey = noValueHeaderTranslationKey; + String textKey = noValueTextTranslationKey; + String buttonKey = noValueButtonTranslationKey; + String contactKey = noValueContactTranslationKey; + + if (StringUtils.hasText(eligibleLastSeenTimestamp)) { + LocalDateTime timeStamp; + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_TIME_FORMAT); + timeStamp = LocalDateTime.parse(eligibleLastSeenTimestamp, formatter); + } catch (DateTimeParseException e) { + log.warn("{} - could not parse timestamp from attribute '{}' with value '{}'", + filterName, eligibleLastSeenSAMLAttributeName, eligibleLastSeenTimestamp); + log.debug("{} - skip execution - have no timestamp to compare to", filterName); + log.trace("{} - details:", filterName, e); + return true; + } + + LocalDateTime now = LocalDateTime.now(); + if (now.minusMonths(validityPeriod).isBefore(timeStamp)) { + log.debug("{} - attribute '{}' value is valid", filterName, eligibleLastSeenSAMLAttributeName); + return true; + } else { + headerKey = oldValueHeaderTranslationKey; + textKey = oldValueTextTranslationKey; + buttonKey = oldValueButtonTranslationKey; + contactKey = oldValueContactTranslationKey; + } + } + + HttpSession sess = req.getSession(true); + sess.setAttribute(HEADER_TRANSLATION, headerKey); + sess.setAttribute(TEXT_TRANSLATION, textKey); + sess.setAttribute(BUTTON_TRANSLATION, buttonKey); + sess.setAttribute(CONTACT_TRANSLATION, contactKey); + + log.debug("{} - attribute '{}' value is invalid, stop user at this point", filterName, eligibleLastSeenTimestamp); + this.redirect(req, res); + return false; + } + + private void redirect(HttpServletRequest req, HttpServletResponse res) { + Map params = new HashMap<>(); + + String targetURL = FiltersUtils.buildRequestURL(req, + Collections.singletonMap(AuthProcFilterConstants.PARAM_PROMPT, "login")); + params.put(AuthProcFilterConstants.PARAM_TARGET, targetURL); + + String redirectUrl = ControllerUtils.createRedirectUrl(config.getConfigBean().getIssuer(), + PerunUnapprovedController.UNAPPROVED_IS_ELIGIBLE_MAPPING, params); + log.debug("{} - redirecting user to unapproved: URL '{}'", filterName, redirectUrl); + res.reset(); + res.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); + res.setHeader(HttpHeaders.LOCATION, redirectUrl); + } + +} diff --git a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/PerunUnapprovedController.java b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/PerunUnapprovedController.java index ae1114a99..eed9e1d6b 100644 --- a/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/PerunUnapprovedController.java +++ b/perun-oidc-server/src/main/java/cz/muni/ics/oidc/web/controllers/PerunUnapprovedController.java @@ -3,8 +3,15 @@ package cz.muni.ics.oidc.web.controllers; import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.PARAM_CLIENT_ID; import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.PARAM_HEADER; import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.PARAM_MESSAGE; -import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.PARAM_REASON; import static cz.muni.ics.oidc.server.filters.AuthProcFilterConstants.PARAM_TARGET; +import static cz.muni.ics.oidc.server.filters.impl.IsEligibleFilter.BUTTON_TRANSLATION; +import static cz.muni.ics.oidc.server.filters.impl.IsEligibleFilter.CONTACT_TRANSLATION; +import static cz.muni.ics.oidc.server.filters.impl.IsEligibleFilter.DEFAULT_BUTTON_TRANSLATION_KEY; +import static cz.muni.ics.oidc.server.filters.impl.IsEligibleFilter.DEFAULT_CONTACT_TRANSLATION_KEY; +import static cz.muni.ics.oidc.server.filters.impl.IsEligibleFilter.DEFAULT_HEADER_TRANSLATION_KEY; +import static cz.muni.ics.oidc.server.filters.impl.IsEligibleFilter.DEFAULT_TEXT_TRANSLATION_KEY; +import static cz.muni.ics.oidc.server.filters.impl.IsEligibleFilter.HEADER_TRANSLATION; +import static cz.muni.ics.oidc.server.filters.impl.IsEligibleFilter.TEXT_TRANSLATION; import cz.muni.ics.oauth2.model.ClientDetailsEntity; import cz.muni.ics.oauth2.service.ClientDetailsEntityService; @@ -13,13 +20,19 @@ import cz.muni.ics.oidc.web.WebHtmlClasses; import cz.muni.ics.openid.connect.view.HttpCodeView; import java.util.Map; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.servlet.http.HttpSession; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; +import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; import org.springframework.stereotype.Controller; +import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.SessionAttribute; /** * Ctonroller for the unapproved page. @@ -32,7 +45,7 @@ public class PerunUnapprovedController { public static final String UNAPPROVED_MAPPING = "/unapproved"; public static final String UNAPPROVED_SPECIFIC_MAPPING = "/unapproved_spec"; - public static final String UNAPPROVED_IS_CESNET_ELIGIBLE_MAPPING = "/unapprovedIce"; + public static final String UNAPPROVED_IS_ELIGIBLE_MAPPING = "/unapprovedNotEligible"; public static final String UNAPPROVED_ENSURE_VO_MAPPING = "/unapprovedEnsureVo"; public static final String UNAPPROVED_AUTHORIZATION = "/unapprovedAuthorization"; public static final String UNAPPROVED_NOT_IN_TEST_VOS_GROUPS = "/unapprovedNotInTestVosGroups"; @@ -40,11 +53,9 @@ public class PerunUnapprovedController { public static final String UNAPPROVED_NOT_IN_MANDATORY_VOS_GROUPS = "/unapprovedNotInMandatoryVosGroups"; public static final String UNAPPROVED_NOT_LOGGED_IN = "/unapprovedNotLoggedIn"; - public static final String REASON_NOT_SET = "notSet"; - public static final String REASON_EXPIRED = "expired"; - private static final String OUT_HEADER = "outHeader"; private static final String OUT_MESSAGE = "outMessage"; + private static final String OUT_BUTTON = "outButton"; private static final String OUT_CONTACT_P = "outContactP"; private static final String ENSURE_VO_HDR = "403_ensure_vo_hdr"; @@ -53,11 +64,6 @@ public class PerunUnapprovedController { private static final String AUTHORIZATION_HDR = "403_authorization_hdr"; private static final String AUTHORIZATION_MSG = "403_authorization_msg"; - private static final String ICE_NOT_SET_HDR = "403_isCesnetEligible_notSet_hdr"; - private static final String ICE_NOT_SET_MSG = "403_isCesnetEligible_notSet_msg"; - private static final String ICE_EXPIRED_HDR = "403_isCesnetEligible_expired_hdr"; - private static final String ICE_EXPIRED_MSG = "403_isCesnetEligible_expired_msg"; - private static final String NOT_IN_TEST_VOS_GROUPS_HDR = "403_not_in_test_vos_groups_hdr"; private static final String NOT_IN_TEST_VOS_GROUPS_MSG = "403_not_in_test_vos_groups_msg"; @@ -72,6 +78,10 @@ public class PerunUnapprovedController { private static final String CONTACT_LANG_PROP_KEY = "contact_p"; private static final String CONTACT_MAIL = "contactMail"; + private static final String HAS_TARGET = "hasTarget"; + private static final String REASON = "reason"; + + public static final String TARGET = "target"; @Autowired private ClientDetailsEntityService clientService; @@ -82,6 +92,9 @@ public class PerunUnapprovedController { @Autowired private WebHtmlClasses htmlClasses; + @Autowired + private SecurityContextLogoutHandler logoutHandler; + @GetMapping(value = UNAPPROVED_MAPPING) public String showUnapproved(HttpServletRequest req, Map model, @@ -134,36 +147,45 @@ public class PerunUnapprovedController { return "unapproved_spec"; } - @GetMapping(value = UNAPPROVED_IS_CESNET_ELIGIBLE_MAPPING) - public String showUnapprovedIsCesnetEligible(HttpServletRequest req, Map model, - @RequestParam(value = PARAM_TARGET) String target, - @RequestParam(value = PARAM_REASON) String reason) { - + @GetMapping(value = UNAPPROVED_IS_ELIGIBLE_MAPPING) + public String showUnapprovedIsEligible(HttpServletRequest req, + Map model, + @RequestParam(value = PARAM_TARGET, required = false) String target) + { ControllerUtils.setPageOptions(model, req, htmlClasses, perunOidcConfig); - String header; - String message; - - if (REASON_EXPIRED.equals(reason)) { - header = ICE_EXPIRED_HDR; - message = ICE_EXPIRED_MSG; - } else if (REASON_NOT_SET.equals(reason)){ - header = ICE_NOT_SET_HDR; - message = ICE_NOT_SET_MSG; - } else { - model.put(HttpCodeView.CODE, HttpStatus.NOT_FOUND); - return HttpCodeView.VIEWNAME; - } + HttpSession sess = req.getSession(); + String header = loadSessionTranslationKey(sess, HEADER_TRANSLATION, DEFAULT_HEADER_TRANSLATION_KEY); + String message = loadSessionTranslationKey(sess, TEXT_TRANSLATION, DEFAULT_TEXT_TRANSLATION_KEY); + String button = loadSessionTranslationKey(sess, BUTTON_TRANSLATION, DEFAULT_BUTTON_TRANSLATION_KEY); + String contactP = loadSessionTranslationKey(sess, CONTACT_TRANSLATION, DEFAULT_CONTACT_TRANSLATION_KEY); model.put(OUT_HEADER, header); model.put(OUT_MESSAGE, message); - model.put(OUT_CONTACT_P, CONTACT_LANG_PROP_KEY); + model.put(OUT_BUTTON, button); + model.put(OUT_CONTACT_P, contactP); model.put(CONTACT_MAIL, perunOidcConfig.getEmailContact()); + model.put(HAS_TARGET, StringUtils.hasText(target)); + req.getSession(true).setAttribute(TARGET, target); if (perunOidcConfig.getTheme().equalsIgnoreCase("lsaai")) { - return "lsaai/unapproved_spec"; + return "lsaai/unapproved_is_eligible"; + } + return "unapproved_is_eligible"; + } + + @PostMapping(value = UNAPPROVED_IS_ELIGIBLE_MAPPING) + public String showUnapprovedIsEligibleHandle(HttpServletRequest req, + HttpServletResponse res, + Map model, + @SessionAttribute(PARAM_TARGET) String target) + { + if (!StringUtils.hasText(target)) { + return showUnapprovedIsEligible(req, model, null); + } else { + logoutHandler.logout(req, res, null); + return "redirect:" + target; } - return "unapproved_spec"; } @GetMapping(value = UNAPPROVED_ENSURE_VO_MAPPING) @@ -253,4 +275,12 @@ public class PerunUnapprovedController { return "unapproved_spec"; } + + private String loadSessionTranslationKey(HttpSession sess, String key, String fallbackValue) { + if (sess != null && StringUtils.hasText((String) sess.getAttribute(key))) { + return (String) sess.getAttribute(key); + } + return fallbackValue; + } + }