Merge pull request #208 from dBucik/ice

feat: IsEligible authproc filter and claim source
pull/1580/head
Dominik František Bučík 2022-07-08 12:25:55 +02:00 committed by GitHub
commit 7d5dc7abcd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 459 additions and 42 deletions

View File

@ -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.<br/><a class="mt-2 cw btn btn-primary btn-lg btn-block" href="%%TARGET%%">Znovu p\u0159ihl\u00E1sit</a>
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.<br/><a class="mt-2 cw btn btn-lg btn-primary btn-block" href="%%TARGET%%">Znovu p\u0159ihl\u00E1sit</a>
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

View File

@ -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.<a class="mt-2 cw btn btn-primary btn-lg btn-block" href="%%TARGET%%">Log in again</a>
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.<a class="mt-2 cw btn btn-primary btn-lg btn-block" href="%%TARGET%%">Log in again</a>
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

View File

@ -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" %>
<ls:header />
<div class="error_message" style="word-wrap: break-word;">
<h1><spring:message code="${outHeader}"/></h1>
<p><spring:message code="${outMessage}"/></p>
<c:if test="${hasTarget}">
<form method="POST" action="" class="mb-4">
<button class="btn btn-primary btn-block"><spring:message code="${outButton}"/></button>
</form>
</c:if>
<p><spring:message code="${outContactP}"/>${" "}<a href="mailto:${contactMail}">${contactMail}</a></p>
</div>
<ls:footer/>

View File

@ -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<String> cssLinks = new ArrayList<>();
pageContext.setAttribute("cssLinks", cssLinks);
%>
<t:header title="${title}" reqURL="${reqURL}" baseURL="${baseURL}" cssLinks="${cssLinks}" theme="${theme}"/>
</div> <%-- header --%>
<div id="content">
<div class="error_message" style="word-wrap: break-word;">
<h1><spring:message code="${outHeader}"/></h1>
<p><spring:message code="${outMessage}"/></p>
<c:if test="${hasTarget}">
<form method="POST" action="" class="mb-4">
<button class="btn btn-primary btn-block"><spring:message code="${outButton}"/></button>
</form>
</c:if>
<p><spring:message code="${outContactP}"/>${" "}<a href="mailto:${contactMail}">${contactMail}</a></p>
</div>
</div>
</div><!-- ENDWRAP -->
<t:footer baseURL="${baseURL}" theme="${theme}"/>

View File

@ -65,7 +65,7 @@
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_MAPPING}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_AUTHORIZATION}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_ENSURE_VO_MAPPING}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_IS_CESNET_ELIGIBLE_MAPPING}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_IS_ELIGIBLE_MAPPING}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_NOT_IN_MANDATORY_VOS_GROUPS}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_NOT_IN_PROD_VOS_GROUPS}**" />
<mvc:exclude-mapping path="#{T(cz.muni.ics.oidc.web.controllers.PerunUnapprovedController).UNAPPROVED_NOT_IN_TEST_VOS_GROUPS}**" />

View File

@ -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<String> list) {
ArrayNode res = JsonNodeFactory.instance.arrayNode();
if (list != null && !list.isEmpty()) {

View File

@ -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):
* <ul>
* <li><b>custom.claim.[claimName].source.attribute</b> - attribute containing the eligibility last seen timestamp passed via SAML authentication</li>
* <li><b>custom.claim.[claimName].source.validityPeriodMonths</b> - 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</li>
* </ul>
*
* @author Pavol Pluta <pavol.pluta1@gmail.com>
* @author Dominik Frantisek Bucik <bucik@ics.muni.cz>
*/
@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<String> 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;
}
}

View File

@ -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<String, String> params = new HashMap<>();

View File

@ -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):
* <ul>
* <li><b>filter.[name].samlAttribute</b> - mapping to isCesnetEligible attribute</li>
* <li><b>filter.[name].triggerScope</b> - scope that has to be requested to apply this filter</li>
* <li><b>filter.[name].validityPeriodMonths</b> - specify in months, how long the value can be old, if no value
* or invalid value has been provided, defaults to 12 months</li>
* </ul>
* @author Dominik Frantisek Bucik <bucik@ics.muni.cz>
*/
@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<String, String> 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);
}
}

View File

@ -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<String, Object> model,
@ -134,36 +147,45 @@ public class PerunUnapprovedController {
return "unapproved_spec";
}
@GetMapping(value = UNAPPROVED_IS_CESNET_ELIGIBLE_MAPPING)
public String showUnapprovedIsCesnetEligible(HttpServletRequest req, Map<String, Object> 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<String, Object> 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<String, Object> 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;
}
}