[java-identity-provider] branch maint-4 updated: IDP-995 - Administrative logout features

Scott Cantor cantor.2 at osu.edu
Wed Aug 3 19:49:06 UTC 2022


This is an automated email from the git hooks/post-receive script.

scantor pushed a commit to branch maint-4
in repository java-identity-provider.

View the commit online:
http://git.shibboleth.net/view/?p=java-identity-provider.git;a=commit;h=641d74995bab17a79098180f777e90bd9b4991f9

The following commit(s) were added to refs/heads/maint-4 by this push:
     new 641d74995 IDP-995 - Administrative logout features
641d74995 is described below

commit 641d74995bab17a79098180f777e90bd9b4991f9
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Wed Aug 3 15:49:03 2022 -0400

    IDP-995 - Administrative logout features
    
    https://shibboleth.atlassian.net/browse/IDP-995
    
    Switch key seperator to bang character.
    Add admin flow to interact with revocation cache.
---
 .../impl/DoRevocationCacheOperation.java           | 309 +++++++++++++++++++++
 .../revocation/impl/RevocationCacheCondition.java  |   4 +-
 .../net/shibboleth/idp/conf/admin-system.xml       |  17 ++
 .../net/shibboleth/idp/conf/mvc-beans.xml          |   1 +
 .../net/shibboleth/idp/conf/webflow-config.xml     |   1 +
 .../idp/flows/admin/revocation-beans.xml           |  41 +++
 .../shibboleth/idp/flows/admin/revocation-flow.xml |  45 +++
 .../src/main/resources/conf/admin/admin.properties |   8 +
 .../idp/profile/AbstractProfileAction.java         |  60 ++++
 9 files changed, 484 insertions(+), 2 deletions(-)

diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/revocation/impl/DoRevocationCacheOperation.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/revocation/impl/DoRevocationCacheOperation.java
new file mode 100644
index 000000000..b9d16e121
--- /dev/null
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/revocation/impl/DoRevocationCacheOperation.java
@@ -0,0 +1,309 @@
+/*
+ * Licensed to the University Corporation for Advanced Internet Development,
+ * Inc. (UCAID) under one or more contributor license agreements.  See the
+ * NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The UCAID licenses this file to You under the Apache
+ * License, Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * 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.
+ */
+
+package net.shibboleth.idp.authn.revocation.impl;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.util.Collections;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import javax.servlet.http.HttpServletResponse;
+
+import net.shibboleth.idp.profile.AbstractProfileAction;
+import net.shibboleth.idp.profile.context.SpringRequestContext;
+import net.shibboleth.utilities.java.support.annotation.constraint.NonnullAfterInit;
+import net.shibboleth.utilities.java.support.annotation.constraint.NotEmpty;
+import net.shibboleth.utilities.java.support.component.ComponentInitializationException;
+import net.shibboleth.utilities.java.support.component.ComponentSupport;
+import net.shibboleth.utilities.java.support.logic.Constraint;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.storage.RevocationCache;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.webflow.execution.RequestContext;
+
+import com.fasterxml.jackson.core.JsonFactory;
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.jasminb.jsonapi.models.errors.Error;
+import com.github.jasminb.jsonapi.models.errors.Errors;
+import com.google.common.base.Strings;
+
+/**
+ * Action that implements a JSON REST API for the {@link RevocationCache} interface.
+ * 
+ * <p>The API supports GET, PUT/POST, and DELETE at the moment, using jsonapi.org conventions.</p>
+ * 
+ * <dl>
+ *  <dt>GET</dt>
+ *  <dd>Return a revocation record.</dd>
+ *  
+ *  <dt>PUT/POST</dt>
+ *  <dd>Insert or update a revocation record.</dd>
+ *  
+ *  <dt>DELETE</dt>
+ *  <dd>Delete revocation record.</dd>
+ * </dl>
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link EventIds#IO_ERROR}
+ */
+public class DoRevocationCacheOperation extends AbstractProfileAction {
+    
+    /** Flow variable indicating ID of cache bean to access. */
+    @Nonnull @NotEmpty public static final String CACHE_ID = "revocationCacheId";
+
+    /** Flow variable indicating ID of account context. */
+    @Nonnull @NotEmpty public static final String CONTEXT = "context";
+
+    /** Flow variable indicating ID of account key. */
+    @Nonnull @NotEmpty public static final String KEY = "key";
+
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(DoRevocationCacheOperation.class);
+    
+    /** JSON object mapper. */
+    @NonnullAfterInit private ObjectMapper objectMapper;
+
+    /** Revocation context to operate on. */
+    @Nullable @NotEmpty private String context;
+    
+    /** Revocation key to operate on. */
+    @Nullable @NotEmpty private String key;
+
+    /** {@link AccountLockoutManager} to operate on. */
+    @Nullable private RevocationCache revocationCache;
+
+    /**
+     * Set the JSON {@link ObjectMapper} to use for serialization.
+     * 
+     * @param mapper object mapper
+     */
+    public void setObjectMapper(@Nonnull final ObjectMapper mapper) {
+        ComponentSupport.ifInitializedThrowUnmodifiabledComponentException(this);
+        ComponentSupport.ifDestroyedThrowDestroyedComponentException(this);
+        
+        objectMapper = Constraint.isNotNull(mapper, "ObjectMapper cannot be null");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (objectMapper == null) {
+            throw new ComponentInitializationException("ObjectMapper cannot be null");
+        }
+    }
+
+// Checkstyle: CyclomaticComplexity OFF
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(final ProfileRequestContext profileRequestContext) {
+        
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        } else if (getHttpServletRequest() == null || getHttpServletResponse() == null) {
+            log.warn("{} No HttpServletRequest or HttpServletResponse available", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        
+        try {
+            final SpringRequestContext springRequestContext =
+                    profileRequestContext.getSubcontext(SpringRequestContext.class);
+            if (springRequestContext == null) {
+                log.warn("{} Spring request context not found in profile request context", getLogPrefix());
+                sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+                        "Internal Server Error", "System misconfiguration.");
+                return false;
+            }
+    
+            final RequestContext requestContext = springRequestContext.getRequestContext();
+            if (requestContext == null) {
+                log.warn("{} Web Flow request context not found in Spring request context", getLogPrefix());
+                sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+                        "Internal Server Error", "System misconfiguration.");
+                return false;
+            }
+            
+            
+            final String id = getParameter(requestContext, CACHE_ID);
+            context = getParameter(requestContext, CONTEXT);
+            key = getParameter(requestContext, KEY);
+            
+            if (Strings.isNullOrEmpty(id) || Strings.isNullOrEmpty(context) || Strings.isNullOrEmpty(key)) {
+                sendError(HttpServletResponse.SC_NOT_FOUND,
+                        "Missing revocation cache ID, context, or key",
+                        "No revocation cache ID, context, key specified.");
+                return false;
+            }
+
+            revocationCache = getBean(requestContext, id, RevocationCache.class);
+            if (revocationCache == null) {
+                sendError(HttpServletResponse.SC_NOT_FOUND,
+                        "Invalid Revocation Cache", "Invalid revocation cache identifier in path.");
+                return false;
+            }
+
+        } catch (final IOException e) {
+            log.error("{} I/O error issuing API response", getLogPrefix(), e);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
+            return false;
+        }
+
+        return true;
+    }
+// Checkstyle: CyclomaticComplexity ON
+    
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(final ProfileRequestContext profileRequestContext) {
+        
+        try {
+            final String method = getHttpServletRequest().getMethod();
+            final HttpServletResponse response = getHttpServletResponse();
+            
+            response.setContentType("application/json");
+            response.setHeader("Cache-Control", "must-revalidate,no-cache,no-store");
+            
+            if ("GET".equals(method)) {
+                doGet();
+            } else if ("POST".equals(method) || "PUT".equals(method)) {
+                doPost();
+            } else if ("DELETE".equals(method)) {
+                doDelete();
+            } else {
+                log.warn("{} Invalid method: {}", getLogPrefix(), method);
+                sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED,
+                        "Unknown Operation", "Only GET, POST, and DELETE are supported.");
+            }
+            
+        } catch (final IOException e) {
+            log.error("{} I/O error responding to request", getLogPrefix(), e);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
+        }
+    }
+
+    /**
+     * Get a revocation record.
+     * 
+     * @throws IOException if an I/O error occurs
+     */
+    private void doGet() throws IOException {
+        try {
+            final String revocation = revocationCache.getRevocationRecord(context, key);
+            if (revocation != null) {
+                getHttpServletResponse().setStatus(HttpServletResponse.SC_OK);
+                final JsonFactory jsonFactory = new JsonFactory();
+                try (final JsonGenerator g = jsonFactory.createGenerator(
+                        getHttpServletResponse().getOutputStream()).useDefaultPrettyPrinter()) {
+                    g.setCodec(objectMapper);
+                    g.writeStartObject();
+                    g.writeObjectFieldStart("data");
+                    g.writeStringField("type", "revocation-records");
+                    g.writeStringField("id", revocationCache.getId() + '/' + context + '/' + key);
+                    g.writeObjectFieldStart("attributes");
+                    g.writeStringField("revocation", revocation);
+                }
+            } else {
+                getHttpServletResponse().setStatus(HttpServletResponse.SC_NOT_FOUND);
+            }
+        } catch (final IOException e) {
+            sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal Server Error",
+                    "Revocation cache error.");
+        }
+    }
+
+    /**
+     * Insert a revocation record.
+     * 
+     * @throws IOException if an I/O error occurs
+     */
+    private void doPost() throws IOException {
+        
+        final String value = getHttpServletRequest().getParameter("value");
+        final String duration = getHttpServletRequest().getParameter("duration");
+        
+        if (value == null || duration == null) {
+            sendError(HttpServletResponse.SC_BAD_REQUEST, "Bad Request", "Request missing value/duration parameters.");
+            return;
+        }
+        
+        final Long durationSeconds;
+        try {
+            durationSeconds = Long.valueOf(duration);
+        } catch (final NumberFormatException e) {
+            sendError(HttpServletResponse.SC_BAD_REQUEST, "Bad Request", "Duration parameter was not a long integer.");
+            return;
+        }
+        
+        if (revocationCache.revoke(context, key, value, Duration.ofSeconds(durationSeconds))) {
+            getHttpServletResponse().setStatus(HttpServletResponse.SC_ACCEPTED);
+        } else {
+            sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal Server Error",
+                    "Attempt to insert revocation record failed.");
+        }
+    }
+
+    /**
+     * Delete a revocation record.
+     * 
+     * @throws IOException if an I/O error occurs
+     */
+    private void doDelete() throws IOException {
+        if (revocationCache.unrevoke(context, key)) {
+            getHttpServletResponse().setStatus(HttpServletResponse.SC_NO_CONTENT);
+        } else {
+            getHttpServletResponse().setStatus(HttpServletResponse.SC_NOT_FOUND);
+        }
+    }
+
+    /**
+     * Output an error object.
+     * 
+     * @param status HTTP status
+     * @param title fixed error description
+     * @param detail human-readable error description
+     * 
+     * @throws IOException if unable to output the error
+     */
+    private void sendError(final int status, @Nonnull @NotEmpty final String title,
+            @Nonnull @NotEmpty final String detail) throws IOException {
+        
+        final HttpServletResponse response = getHttpServletResponse();
+        response.setContentType("application/json");
+        response.setHeader("Cache-Control", "must-revalidate,no-cache,no-store");
+        response.setStatus(status);
+        
+        final Error e = new Error();
+        final Errors errors = new Errors();
+        errors.setErrors(Collections.singletonList(e));
+        e.setStatus(Integer.toString(status));
+        e.setTitle(title);
+        e.setDetail(detail);
+        
+        objectMapper.writer().withDefaultPrettyPrinter().writeValue(response.getOutputStream(), errors);
+    }
+    
+}
\ No newline at end of file
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/revocation/impl/RevocationCacheCondition.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/revocation/impl/RevocationCacheCondition.java
index 1d821b39c..2eef5c438 100644
--- a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/revocation/impl/RevocationCacheCondition.java
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/revocation/impl/RevocationCacheCondition.java
@@ -55,10 +55,10 @@ public class RevocationCacheCondition extends AbstractInitializableComponent
     @Nonnull @NotEmpty public static final String REVOCATION_CONTEXT = "LoginFlowRevocation";
 
     /** Prefix of keys for principal-based revocation. */
-    @Nonnull @NotEmpty public static final String PRINCIPAL_REVOCATION_PREFIX = "prin:";
+    @Nonnull @NotEmpty public static final String PRINCIPAL_REVOCATION_PREFIX = "prin!";
 
     /** Prefix of keys for address-based revocation. */
-    @Nonnull @NotEmpty public static final String ADDRESS_REVOCATION_PREFIX = "addr:";
+    @Nonnull @NotEmpty public static final String ADDRESS_REVOCATION_PREFIX = "addr!";
 
     /** Class logger. */
     @Nonnull private final Logger log = LoggerFactory.getLogger(RevocationCacheCondition.class);
diff --git a/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/admin-system.xml b/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/admin-system.xml
index 3e52c1841..cf4826a9c 100644
--- a/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/admin-system.xml
+++ b/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/admin-system.xml
@@ -160,6 +160,23 @@
         </property>
     </bean>
 
+    <bean parent="shibboleth.AdminFlow"
+            c:id="http://shibboleth.net/ns/profiles/revocation"
+            p:loggingId="%{idp.revocation.logging:Revocation}"
+            p:policyName="%{idp.revocation.accessPolicy:AccessDenied}"
+            p:nonBrowserSupported="%{idp.revocation.nonBrowserSupported:false}"
+            p:authenticated="%{idp.revocation.authenticated:false}"
+            p:resolveAttributes="%{idp.revocation.resolveAttributes:false}">
+        <property name="postAuthenticationFlows">
+            <bean parent="shibboleth.CommaDelimStringArray"
+                c:_0="#{'%{idp.revocation.postAuthenticationFlows:}'.trim()}" />
+        </property>
+        <property name="defaultAuthenticationMethodsByString">
+            <bean parent="shibboleth.CommaDelimStringArray"
+                c:_0="#{'%{idp.revocation.defaultAuthenticationMethods:}'.trim()}" />
+        </property>
+    </bean>
+
     <bean parent="shibboleth.AdminFlow"
             c:id="http://shibboleth.net/ns/profiles/storage"
             p:loggingId="%{idp.storage.logging:Storage}"
diff --git a/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/mvc-beans.xml b/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/mvc-beans.xml
index 49bce3584..543d6f490 100644
--- a/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/mvc-beans.xml
+++ b/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/mvc-beans.xml
@@ -19,6 +19,7 @@
             <list>
                 <value>admin/metrics</value>
                 <value>admin/lockout</value>
+                <value>admin/revocation</value>
                 <value>admin/storage</value>
             </list>
         </property>
diff --git a/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/webflow-config.xml b/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/webflow-config.xml
index 733e86f46..7097ca797 100644
--- a/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/webflow-config.xml
+++ b/idp-conf-impl/src/main/resources/net/shibboleth/idp/conf/webflow-config.xml
@@ -30,6 +30,7 @@
                 <entry key="admin/lockout" value="classpath:/net/shibboleth/idp/flows/admin/lockout-flow.xml" />
                 <entry key="admin/mdquery" value="classpath:/net/shibboleth/idp/flows/admin/mdquery-flow.xml" />
                 <entry key="admin/metrics" value="classpath:/net/shibboleth/idp/flows/admin/metrics-flow.xml" />
+                <entry key="admin/revocation" value="classpath:/net/shibboleth/idp/flows/admin/revocation-flow.xml" />
                 <entry key="admin/storage" value="classpath:/net/shibboleth/idp/flows/admin/storage-flow.xml" />
                 <entry key="admin/unlock-keys" value="classpath:/net/shibboleth/idp/flows/admin/unlock-keys-flow.xml" />
                         
diff --git a/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/admin/revocation-beans.xml b/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/admin/revocation-beans.xml
new file mode 100644
index 000000000..e3396293a
--- /dev/null
+++ b/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/admin/revocation-beans.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans
+    xmlns="http://www.springframework.org/schema/beans"
+    xmlns:c="http://www.springframework.org/schema/c"
+    xmlns:context="http://www.springframework.org/schema/context"
+    xmlns:p="http://www.springframework.org/schema/p"
+    xmlns:util="http://www.springframework.org/schema/util"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+    default-init-method="initialize"
+    default-destroy-method="destroy">
+
+    <bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer"
+        p:placeholderPrefix="%{" p:placeholderSuffix="}" />
+
+    <!-- Profile ID for flow. -->    
+    <bean id="shibboleth.AdminProfileId" class="java.lang.String" c:_0="http://shibboleth.net/ns/profiles/revocation" />
+    
+    <!-- Default operation/resource suppliers for access checks. -->
+
+    <bean id="shibboleth.AdminOperationLookupStrategy" parent="shibboleth.ContextFunctions.Expression"
+        c:outputType="#{T(java.lang.String)}"
+        c:expression="#custom.getMethod()"
+        p:customObject-ref="shibboleth.HttpServletRequest" />
+        
+    <bean id="shibboleth.AdminResourceLookupStrategy" parent="shibboleth.ContextFunctions.Expression"
+        c:outputType="#{T(java.lang.String)}"
+        c:expression="#input.getSubcontext(T(net.shibboleth.idp.profile.context.SpringRequestContext)).getRequestContext().getFlowScope().get('context')" />
+    
+    <!--  Work beans. -->
+    
+    <bean id="DoRevocationCacheOperation" class="net.shibboleth.idp.authn.revocation.impl.DoRevocationCacheOperation" scope="prototype"
+        p:httpServletRequest-ref="shibboleth.HttpServletRequest"
+        p:httpServletResponse-ref="shibboleth.HttpServletResponse"
+        p:objectMapper-ref="shibboleth.JSONObjectMapper" />
+        
+    <bean id="RecordResponseComplete" class="net.shibboleth.idp.profile.impl.RecordResponseComplete" scope="prototype" />
+        
+</beans>
diff --git a/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/admin/revocation-flow.xml b/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/admin/revocation-flow.xml
new file mode 100644
index 000000000..4ff31b96c
--- /dev/null
+++ b/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/admin/revocation-flow.xml
@@ -0,0 +1,45 @@
+<flow xmlns="http://www.springframework.org/schema/webflow"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+    xsi:schemaLocation="http://www.springframework.org/schema/webflow http://www.springframework.org/schema/webflow/spring-webflow.xsd"
+    parent="admin.abstract">
+
+    <on-start>
+        <!-- Extract PATH_INFO containing revocation cache and context/key. -->
+        <evaluate expression="flowRequestContext.getActiveFlow().getId()" result="flowScope.flowId" />
+        <evaluate expression="externalContext.getNativeRequest().getPathInfo().length() gt flowId.length() + 2 ? externalContext.getNativeRequest().getPathInfo().substring(flowId.length() + 2) : ''" result="flowScope.pathInfo" />
+        <evaluate expression="pathInfo.split('/')" result="flowScope.pathInfoArray" />
+        <evaluate expression="pathInfoArray.length gt 0 ? T(net.shibboleth.utilities.java.support.net.URISupport).doURLDecode(pathInfoArray[0]) : null" result="flowScope.revocationCacheId" />
+        <evaluate expression="pathInfoArray.length gt 1 ? T(net.shibboleth.utilities.java.support.net.URISupport).doURLDecode(pathInfoArray[1]) : null" result="flowScope.context" />
+        <evaluate expression="pathInfoArray.length gt 2 ? T(net.shibboleth.utilities.java.support.net.URISupport).doURLDecode(pathInfoArray[2]) : null" result="flowScope.key" />
+    </on-start>
+
+    <!-- Start action. -->
+        
+    <action-state id="InitializeProfileRequestContext">
+        <evaluate expression="InitializeProfileRequestContext" />
+        <evaluate expression="FlowStartPopulateAuditContext" />
+        <evaluate expression="'proceed'" />
+        
+        <!-- Branch to determine if authentication is required. -->
+        <transition on="proceed" to="DoAdminPreamble" />
+    </action-state>
+
+    <!-- Resume actual flow processing. -->
+
+    <action-state id="DoProfileWork">
+        <evaluate expression="CheckAccess" />
+        <evaluate expression="WriteAuditLog" />
+        <evaluate expression="DoRevocationCacheOperation" />
+        <evaluate expression="RecordResponseComplete" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="end" />
+    </action-state>
+    
+    <!-- Terminus -->
+
+    <end-state id="end" />
+    
+    <bean-import resource="revocation-beans.xml" />
+
+</flow>
diff --git a/idp-conf/src/main/resources/conf/admin/admin.properties b/idp-conf/src/main/resources/conf/admin/admin.properties
index 1e4f3a90e..344cb7902 100644
--- a/idp-conf/src/main/resources/conf/admin/admin.properties
+++ b/idp-conf/src/main/resources/conf/admin/admin.properties
@@ -56,6 +56,14 @@
 #idp.lockout.resolveAttributes = false
 #idp.lockout.postAuthenticationFlows =
 
+#idp.revocation.logging = Revocation
+#idp.revocation.accessPolicy = AccessDenied
+#idp.revocation.authenticated = false
+#idp.revocation.nonBrowserSupported = false
+#idp.revocation.defaultAuthenticationMethods =
+#idp.revocation.resolveAttributes = false
+#idp.revocation.postAuthenticationFlows =
+
 #idp.storage.logging = Storage
 #idp.storage.accessPolicy = AccessDenied
 #idp.storage.authenticated = false
diff --git a/idp-profile-api/src/main/java/net/shibboleth/idp/profile/AbstractProfileAction.java b/idp-profile-api/src/main/java/net/shibboleth/idp/profile/AbstractProfileAction.java
index 2fa778398..1b33efdf5 100644
--- a/idp-profile-api/src/main/java/net/shibboleth/idp/profile/AbstractProfileAction.java
+++ b/idp-profile-api/src/main/java/net/shibboleth/idp/profile/AbstractProfileAction.java
@@ -45,6 +45,7 @@ import org.springframework.context.MessageSourceAware;
 import org.springframework.context.MessageSourceResolvable;
 import org.springframework.context.NoSuchMessageException;
 import org.springframework.webflow.core.collection.AttributeMap;
+import org.springframework.webflow.core.collection.MutableAttributeMap;
 import org.springframework.webflow.execution.Action;
 import org.springframework.webflow.execution.Event;
 import org.springframework.webflow.execution.RequestContext;
@@ -254,6 +255,65 @@ public abstract class AbstractProfileAction
         return null;
     }
 
+    /**
+     * Return a casted parameter of a given name from the flow's flow or conversation scope (in that order). 
+     * 
+     * @param <T> the parameter type
+     * 
+     * @param profileRequestContext profile request context
+     * @param name parameter name
+     * 
+     * @return the parameter or null
+     * 
+     * @since 4.3.0
+     */
+    @Nullable protected <T> T getParameter(@Nonnull final ProfileRequestContext profileRequestContext,
+            @Nonnull @NotEmpty final String name) {
+        
+        final SpringRequestContext springRequestContext =
+                profileRequestContext.getSubcontext(SpringRequestContext.class);
+        if (springRequestContext == null) {
+            log.warn("{} Spring request context not found in profile request context", getLogPrefix());
+            return null;
+        }
+
+        final RequestContext requestContext = springRequestContext.getRequestContext();
+        if (requestContext == null) {
+            log.warn("{} Web Flow request context not found in Spring request context", getLogPrefix());
+            return null;
+        }
+
+        return getParameter(requestContext, name);
+    }
+
+    /**
+     * Return a casted parameter of a given name from the flow's flow or conversation scope (in that order). 
+     * 
+     * @param <T> the parameter type
+     * 
+     * @param flowRequestContext the active flow's request context
+     * @param name parameter name
+     * 
+     * @return the parameter or null
+     * 
+     * @since 4.3.0
+     */
+    @Nullable protected <T> T getParameter(@Nonnull final RequestContext flowRequestContext,
+            @Nonnull @NotEmpty final String name) {
+        
+        MutableAttributeMap<Object> scope = flowRequestContext.getFlowScope();
+        if (scope != null && scope.contains(name)) {
+            return (T) scope.get(name);
+        }
+        
+        scope = flowRequestContext.getConversationScope();
+        if (scope != null && scope.contains(name)) {
+            return (T) scope.get(name);
+        }
+        
+        return null;
+    }
+    
     /** {@inheritDoc} */
     @Override
     public void setMessageSource(final MessageSource source) {

-- 
To stop receiving notification emails like this one, please contact
the administrator of this repository.


More information about the commits mailing list