[java-plugin-shibd] 02/02: Implement an admin logout framework and a revocation cache.

Codeberg noreply at shibboleth.net
Wed Oct 21 19:56:06 UTC 2026


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

codeberg pushed a commit to branch main
in repository java-plugin-shibd.

View the commit online:
https://codeberg.org/Shibboleth/java-plugin-shibd/commit/f8d44bcdb2b579e75b2a180d7a7b34079e2acb95

commit f8d44bcdb2b579e75b2a180d7a7b34079e2acb95
Author: Scott Cantor <scott at restingparrotsoftware.com>
AuthorDate: Wed Oct 21 14:05:36 2026 -0400

    Implement an admin logout framework and a revocation cache.
---
 .../flows/sp/logout-admin/logout-admin-beans.xml   |  18 ++
 .../flows/sp/logout-admin/logout-admin-flow.xml    |  45 ++++
 .../shibboleth/sp/flows/LogoutAdminFlowTest.java   | 114 +++++++++
 .../main/java/net/shibboleth/sp/Application.java   |   9 +
 .../shibboleth/sp/BasicProtocolSupportService.java |  20 ++
 .../net/shibboleth/sp/ProtocolSupportService.java  |  10 +
 .../net/shibboleth/sp/impl/BasicApplication.java   |  59 +++++
 .../sp/impl/ProtocolSupportServiceManager.java     |   4 +-
 .../shibboleth/sp/profile/impl/RevokeSession.java  | 255 +++++++++++++++++++++
 9 files changed, 532 insertions(+), 2 deletions(-)

diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout-admin/logout-admin-beans.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout-admin/logout-admin-beans.xml
new file mode 100644
index 0000000..086dff0
--- /dev/null
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout-admin/logout-admin-beans.xml
@@ -0,0 +1,18 @@
+<?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 id="shibboleth.sp.profileId" class="java.lang.String" c:_0="http://shibboleth.net/ns/profiles/sp/logout-admin" />
+    <bean id="shibboleth.sp.loggingId" class="java.lang.String" c:_0="%{sp.service.logging.logout-admin:LogoutAdmin}" />
+
+    <bean id="shibboleth.metrics.ProfileCounter" class="java.lang.String" c:_0="net.shibboleth.sp.profiles.logout-admin" />
+
+    <bean id="RevokeSession" class="net.shibboleth.sp.profile.impl.RevokeSession" scope="prototype"
+        p:storageService="#{getObject('%{sp.revocation.StorageService:}'.trim())}" />
+
+</beans>
diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout-admin/logout-admin-flow.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout-admin/logout-admin-flow.xml
new file mode 100644
index 0000000..9b30abb
--- /dev/null
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/logout-admin/logout-admin-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="sp/application">
+
+    <action-state id="InitializeProfileRequestContext">
+        <evaluate expression="InitializeProfileRequestContext" />
+        <evaluate expression="'proceed'" />
+        
+        <!-- Branch to parent flow for authentication. -->
+        <transition on="proceed" to="AuthenticationSetup" />
+    </action-state>
+
+    <!-- Resume flow operation after set up by parent. -->
+    <action-state id="DoApplicationOperation">
+        <on-entry>
+            <evaluate expression="new net.shibboleth.shared.collection.ReflectionSafeIterator(opensamlProfileRequestContext.ensureSubcontext(T(net.shibboleth.sp.context.AgentRequestContext)).getApplication().getLogoutAdmins(opensamlProfileRequestContext).iterator())" result="flowScope.LogoutAdminIterator" />
+        </on-entry>
+        <evaluate expression="'proceed'" />
+        
+        <!-- Branch to child flow for actual work. -->
+        <transition on="proceed" to="CheckIterator" />
+    </action-state>
+
+    <decision-state id="CheckIterator">
+        <if test="LogoutAdminIterator.hasNext()" then="CallAdminFlow" else="RevokeSession" />
+    </decision-state>
+    
+    <subflow-state id="CallAdminFlow" subflow="sp/logout/admin/#{LogoutAdminIterator.next()}">
+        <input name="calledAsSubflow" value="true" />
+        <transition on="ReselectFlow" to="CheckIterator" />
+        <transition to="RevokeSession" />
+    </subflow-state>
+    
+    <action-state id="RevokeSession">
+        <evaluate expression="RevokeSession" />
+        <evaluate expression="'proceed'" />
+        
+        <transition on="proceed" to="EncodeAgentResponse" />
+    </action-state>
+    
+    <!-- The file really exists in this directory, but it's referenced from extending flow-directories -->
+    <bean-import resource="classpath:/META-INF/net/shibboleth/idp/flows/sp/logout-admin/logout-admin-beans.xml" />
+
+</flow>
diff --git a/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/LogoutAdminFlowTest.java b/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/LogoutAdminFlowTest.java
new file mode 100644
index 0000000..a52304f
--- /dev/null
+++ b/sp-conf-impl/src/test/java/net/shibboleth/sp/flows/LogoutAdminFlowTest.java
@@ -0,0 +1,114 @@
+/*
+ * Licensed 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.sp.flows;
+
+import java.io.IOException;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.EventIds;
+import org.springframework.webflow.executor.FlowExecutionResult;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.profile.SPConstants;
+import net.shibboleth.sp.profile.impl.RevokeSession;
+
+/**
+ * Unit test for the SP logout-admin flow (basics only).
+ */
+public class LogoutAdminFlowTest extends AbstractSPFlowTest {
+    
+    /** Flow ID. */
+    @Nonnull public static final String FLOW_ID = "sp/logout-admin";
+
+    protected LogoutAdminFlowTest() {
+        super(FLOW_ID);
+    }
+    
+    /**
+     * Test flow with no application specified.
+     * @throws IOException 
+     */
+    @Test
+    public void testNoApplication() throws IOException {
+        setDefaultAuth();
+        setRequest("POST", new DDF(null));
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        assertOutputMessageEvent(result, EventIds.INVALID_MESSAGE);
+    }
+    
+    /**
+     * Test flow with invalid application ID.
+     * @throws IOException 
+     */
+    @Test
+    public void testBadApplication() throws IOException {
+        setDefaultAuth();
+
+        final DDF input = new DDF(null).structure();
+        input.addmember(SPConstants.APPLICATION).string("foo");
+        setRequest("POST", input);
+        
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        assertOutputMessageEvent(result, EventIds.INVALID_MESSAGE);
+    }
+    
+    /**
+     * Test flow with no initiator subflows installed.
+     * @throws IOException 
+     */
+    @Test
+    public void testNoInput() throws IOException {
+        setDefaultAuth();
+        
+        final DDF input = new DDF(null).structure();
+        input.addmember(SPConstants.APPLICATION).string(APPLICATION_ID);
+        setRequest("POST", input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        assertOutputMessageEvent(result, EventIds.INVALID_MESSAGE);
+    }
+
+    /**
+     * Test flow with revocation.
+     * @throws IOException 
+     */
+    @Test
+    public void testSuccess() throws IOException {
+        setDefaultAuth();
+        
+        final DDF input = new DDF(null).structure();
+        input.addmember(RevokeSession.SESSION_ID).string("ABCDEF");
+        input.addmember(SPConstants.APPLICATION).string(APPLICATION_ID);
+        setRequest("POST", input);
+
+        final FlowExecutionResult result = flowExecutor.launchExecution(FLOW_ID, null, externalContext);
+        assertFlowExecutionResult(result, FLOW_ID);
+        assertFlowExecutionOutcome(result.getOutcome());
+        
+        final DDF output = assertOutputMessageSuccess(result);
+        assert output != null;
+        Assert.assertEquals(output.getmember(RevokeSession.STATUS).integer(), 1);
+    }
+
+}
\ No newline at end of file
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/Application.java b/sp-server-api/src/main/java/net/shibboleth/sp/Application.java
index 7fd0233..84a12bb 100644
--- a/sp-server-api/src/main/java/net/shibboleth/sp/Application.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/Application.java
@@ -114,6 +114,15 @@ public interface Application extends RelyingPartyConfiguration, RelyingPartyConf
      * @return ordered list of unprefixed flow IDs
      */
     @Nonnull List<String> getLogoutConsumers(@Nullable final ProfileRequestContext profileRequestContext);
+
+    /**
+     * Gets an ordered list of administrative logout flows to attempt for this application.
+     * 
+     * @param profileRequestContext profile request context
+     *  
+     * @return ordered list of unprefixed flow IDs
+     */
+    @Nonnull List<String> getLogoutAdmins(@Nullable final ProfileRequestContext profileRequestContext);
     
     /**
      * Gets the {@link StateManager} to use for thie application.
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/BasicProtocolSupportService.java b/sp-server-api/src/main/java/net/shibboleth/sp/BasicProtocolSupportService.java
index ce13d31..cf88960 100644
--- a/sp-server-api/src/main/java/net/shibboleth/sp/BasicProtocolSupportService.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/BasicProtocolSupportService.java
@@ -50,6 +50,9 @@ public class BasicProtocolSupportService extends AbstractIdentifiableInitializab
 
     /** Logout Consumer flow IDs. */
     @Nonnull private List<String> logoutConsumers;
+
+    /** Logout Admin flow IDs. */
+    @Nonnull private List<String> logoutAdmins;
     
     /** Non-metadata-driven verified configurations. */
     @Nonnull private Collection<ProfileConfiguration> profileConfigurations;
@@ -69,6 +72,7 @@ public class BasicProtocolSupportService extends AbstractIdentifiableInitializab
         tokenConsumers = CollectionSupport.emptyList();
         logoutInitiators = CollectionSupport.emptyList();
         logoutConsumers = CollectionSupport.emptyList();
+        logoutAdmins = CollectionSupport.emptyList();
         profileConfigurations = CollectionSupport.emptySet();
         metadataDrivenProfileConfigurations = CollectionSupport.emptySet();
         unverifiedProfileConfigurations = CollectionSupport.emptySet();
@@ -154,6 +158,22 @@ public class BasicProtocolSupportService extends AbstractIdentifiableInitializab
         
         logoutConsumers = CollectionSupport.copyToList(StringSupport.normalizeStringCollection(flows));
     }
+
+    /** {@inheritDoc} */
+    @Nonnull @Unmodifiable @NotLive public List<String> getLogoutAdmins() {
+        return logoutAdmins;
+    }
+    
+    /**
+     * Sets the ordered list of admin logout flow IDs to install.
+     * 
+     * @param flows ordered list of unprefixed admin logout flow IDs
+     */
+    public void setLogoutAdmins(@Nullable final List<String> flows) {
+        checkSetterPreconditions();
+        
+        logoutAdmins = CollectionSupport.copyToList(StringSupport.normalizeStringCollection(flows));
+    }
     
     /** {@inheritDoc} */
     @Nonnull @Unmodifiable @NotLive public Collection<ProfileConfiguration> getDefaultProfileConfigurations(
diff --git a/sp-server-api/src/main/java/net/shibboleth/sp/ProtocolSupportService.java b/sp-server-api/src/main/java/net/shibboleth/sp/ProtocolSupportService.java
index 7c428f6..1413a29 100644
--- a/sp-server-api/src/main/java/net/shibboleth/sp/ProtocolSupportService.java
+++ b/sp-server-api/src/main/java/net/shibboleth/sp/ProtocolSupportService.java
@@ -76,6 +76,16 @@ public interface ProtocolSupportService extends IdentifiedComponent, Initializab
      */
     @Nonnull @Unmodifiable @NotLive List<String> getLogoutConsumers();
     
+    /**
+     * Gets a default-ordered list of admin logout flows supported by this protocol plugin.
+     *  
+     * <p>The default order should reflect what "most" deployers would expect to use when adding
+     * support for this plugin to their system.</p>
+     * 
+     * @return ordered list of unprefixed flow IDs
+     */
+    @Nonnull @Unmodifiable @NotLive List<String> getLogoutAdmins();
+
     /**
      * Gets the collection of default {@link ProfileConfiguration} objects supplied by this protocol plugin.
      * 
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicApplication.java b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicApplication.java
index 32097bf..b139b53 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicApplication.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/BasicApplication.java
@@ -113,6 +113,9 @@ public class BasicApplication extends BasicRelyingPartyConfiguration implements
 
     /** Logout consumer list lookup strategy. */
     @NonnullAfterInit private Function<ProfileRequestContext,List<String>> logoutConsumerLookupStrategy;
+
+    /** Logout admin list lookup strategy. */
+    @NonnullAfterInit private Function<ProfileRequestContext,List<String>> logoutAdminLookupStrategy;
     
     /** State management. */
     @Nullable private StateManager stateManager;
@@ -463,6 +466,50 @@ public class BasicApplication extends BasicRelyingPartyConfiguration implements
         
         logoutConsumerLookupStrategy = Constraint.isNotNull(strategy, "Logout consumers lookup strategy cannot be null");
     }
+
+    /** {@inheritDoc} */
+    @Nonnull public List<String> getLogoutAdmins(@Nullable final ProfileRequestContext profileRequestContext) {
+        checkComponentActive();
+        
+        final List<String> ret = logoutAdminLookupStrategy.apply(profileRequestContext);
+        if (ret != null && !ret.isEmpty()) {
+            return ret;
+        }
+
+        if (parent != null && allowInheritance) {
+            return parent.getLogoutAdmins(profileRequestContext);
+        }
+        
+        return CollectionSupport.emptyList(); 
+    }
+    
+    /**
+     * Sets the ordered collection of unprefixed admin logout flow IDs to use for this application.
+     * 
+     * @param admins ordered collection of unprefixed admin logout flow IDs
+     */
+    public void setLogoutAdmins(@Nullable final Collection<String> admins) {
+        checkSetterPreconditions();
+        
+        if (admins != null) {
+            logoutAdminLookupStrategy = FunctionSupport.constant(
+                    CollectionSupport.copyToList(StringSupport.normalizeStringCollection(admins)));
+        } else {
+            logoutAdminLookupStrategy = FunctionSupport.constant(CollectionSupport.emptyList());
+        }
+    }
+    
+    /**
+     * Sets the lookup strategy for the ordered collection of unprefixed admin logout flow IDs
+     * to use for this application.
+     * 
+     * @param strategy lookup strategy
+     */
+    public void setLogoutAdminsLookupStrategy(@Nonnull final Function<ProfileRequestContext,List<String>> strategy) {
+        checkSetterPreconditions();
+        
+        logoutAdminLookupStrategy = Constraint.isNotNull(strategy, "Logout admins lookup strategy cannot be null");
+    }
     
     /** {@inheritDoc} */
     @Nonnull public StateManager getStateManager() {
@@ -723,6 +770,9 @@ public class BasicApplication extends BasicRelyingPartyConfiguration implements
             if (logoutConsumerLookupStrategy == null) {
                 logoutConsumerLookupStrategy = FunctionSupport.constant(CollectionSupport.emptyList());
             }
+            if (logoutAdminLookupStrategy == null) {
+                logoutAdminLookupStrategy = FunctionSupport.constant(CollectionSupport.emptyList());
+            }
             return;
         }
         
@@ -773,6 +823,15 @@ public class BasicApplication extends BasicRelyingPartyConfiguration implements
             }
             logoutConsumerLookupStrategy = FunctionSupport.constant(CollectionSupport.copyToList(consumers));
         }
+
+        if (logoutAdminLookupStrategy == null) {
+            final ArrayList<String> admins = new ArrayList<>();
+            for (final ProtocolSupportService service : protocolServiceManager.getComponents()) {
+                assert service != null;
+                admins.addAll(StringSupport.normalizeStringCollection(service.getLogoutAdmins()));
+            }
+            logoutAdminLookupStrategy = FunctionSupport.constant(CollectionSupport.copyToList(admins));
+        }
     }
     
     /** {@inheritDoc} */
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/ProtocolSupportServiceManager.java b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/ProtocolSupportServiceManager.java
index f3a7dbc..b31f7e7 100644
--- a/sp-server-impl/src/main/java/net/shibboleth/sp/impl/ProtocolSupportServiceManager.java
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/impl/ProtocolSupportServiceManager.java
@@ -53,9 +53,9 @@ public class ProtocolSupportServiceManager extends IdentifiedComponentManager<Pr
         for (final ProtocolSupportService service : getComponents()) {
             assert service != null;
             log.info("Installed SP Hub support for protocol {}: initiators ({}), consumers ({}), "
-                        + "logout-initiators ({}), logout-consumers ({})", service.getId(),
+                        + "logout-initiators ({}), logout-consumers ({}), logout-admins ({})", service.getId(),
                         service.getSessionInitiators(), service.getTokenConsumers(),
-                        service.getLogoutInitiators(), service.getLogoutConsumers());
+                        service.getLogoutInitiators(), service.getLogoutConsumers(), service.getLogoutAdmins());
         }
     }
 
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/RevokeSession.java b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/RevokeSession.java
new file mode 100644
index 0000000..809878d
--- /dev/null
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/RevokeSession.java
@@ -0,0 +1,255 @@
+/*
+ * Licensed 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.sp.profile.impl;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.util.function.Function;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.EventContext;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.profile.context.navigate.CurrentOrPreviousEventLookup;
+import org.opensaml.storage.StorageService;
+import org.slf4j.Logger;
+
+import net.shibboleth.idp.authn.AuthnEventIds;
+import net.shibboleth.shared.annotation.constraint.NonnullBeforeExec;
+import net.shibboleth.shared.annotation.constraint.NotEmpty;
+import net.shibboleth.shared.annotation.constraint.Positive;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.sp.context.AgentRequestContext;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.profile.AbstractApplicationAction;
+import net.shibboleth.sp.profile.SPConstants;
+
+/**
+ * A profile action to optionally record the revocation of a session in a revocation cache
+ * and signal back an administrative logout result to the Agent.
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_MESSAGE}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * 
+ * @pre <pre>AgentRequestContext.getIntput().isstruct()</pre>
+ * @pre <pre>AgentRequestContext.getOutput().isstruct()</pre>
+ * @post optionally a revocation record is stored
+ */
+public class RevokeSession extends AbstractApplicationAction {
+    
+    /** Storage context for revocation. */
+    @Nonnull @NotEmpty public static final String STORAGE_CONTEXT = "net.shibboleth.sp.sessions.revocation";
+    
+    /** Input member for session ID. */
+    @Nonnull @NotEmpty public static final String SESSION_ID = "session.id";
+
+    /** Input member for revocation lifetime. */
+    @Nonnull @NotEmpty public static final String LIFETIME = "lifetime";
+    
+    /** Output "status" member. */
+    @Nonnull @NotEmpty public static final String STATUS = "status";
+
+    /** Success "status" value. */
+    public static final int STATUS_SUCCESS = 1;
+
+    /** Partial "status" value. */
+    public static final int STATUS_PARTIAL = 0;
+
+    /** Failed "status" value. */
+    public static final int STATUS_FAILED = -1;
+    
+    /** Class logger. */
+    @Nonnull private final Logger log = LoggerFactory.getLogger(RevokeSession.class);
+
+    /** Strategy function for access to {@link EventContext} to check. */
+    @Nonnull private Function<ProfileRequestContext,EventContext> eventContextLookupStrategy;
+    
+    /** Storage for revocation list. */
+    @Nullable private StorageService storageService;
+    
+    /** Upper bound on agent-supplied storage lifetime. */
+    @Nonnull private Duration maxStorageLifetime;
+    
+    /** Input message. */
+    @NonnullBeforeExec private DDF input;
+    
+    /** Session ID from message. */
+    @NonnullBeforeExec private String key;
+    
+    /** Cached output object. */
+    @NonnullBeforeExec private DDF output;
+    
+    /** Constructor. */
+    public RevokeSession() {
+        eventContextLookupStrategy = new CurrentOrPreviousEventLookup();
+        maxStorageLifetime = Duration.ofDays(1);
+    }
+    
+    /**
+     * Set lookup strategy for {@link EventContext} to base resulting status on.
+     * 
+     * @param strategy  lookup strategy
+     */
+    public void setEventContextLookupStrategy(@Nonnull final Function<ProfileRequestContext,EventContext> strategy) {
+        checkSetterPreconditions();
+        
+        eventContextLookupStrategy = Constraint.isNotNull(strategy, "EventContext lookup strategy cannot be null");
+    }
+    
+    /**
+     * Set storage service for revocation list.
+     * 
+     * @param service revocation storage
+     */
+    public void setStorageService(@Nullable final StorageService service) {
+        checkSetterPreconditions();
+        
+        storageService = service;
+    }
+    
+    /**
+     * Sets the maximum storage life for revocation records that an Agent may request.
+     * 
+     * <p>Defaults to 1 day.</p>
+     * 
+     * @param timeout maximum storage lifetime
+     */
+    public void setMaxStorageTimeout(@Nonnull @Positive final Duration timeout) {
+        Constraint.isFalse(timeout == null || timeout.isZero() || timeout.isNegative(),
+                "Max storage lifetime must be positive");
+        maxStorageLifetime = timeout;
+    }    
+        
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(ProfileRequestContext profileRequestContext) {
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+                
+        final AgentRequestContext agentContext = ensureAgentRequestContext();
+        
+        input = ensureAgentRequestContext().getInput();
+        if (input == null || !input.isstruct()) {
+            log.warn("{} Invalid or missing input message", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            return false;
+        }
+        
+        key = input.getmember(SESSION_ID).string();
+        if (key == null || key.isEmpty()) {
+            log.warn("{} Invalid or missing {} parameter", getLogPrefix(), SESSION_ID);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            return false;
+        }
+        
+        output = agentContext.getOutput();
+        if (output == null) {
+            log.error("{} Output message structure was missing", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_PROFILE_CTX);
+            return false;
+        }
+        
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+                
+        // If revocation fails, we just signal failure using the status field.
+        if (!doRevocation(profileRequestContext)) {
+            output.addmember(STATUS).integer(STATUS_FAILED);
+            return;
+        }
+        
+        // Otherwise signal status based on active event if any.
+        final EventContext eventCtx = eventContextLookupStrategy.apply(profileRequestContext);
+        final Object event = eventCtx != null ? eventCtx.getEvent() : null;
+        if (event != null) {
+            if (EventIds.PROCEED_EVENT_ID.equals(event.toString()) ||
+                    AuthnEventIds.NO_POTENTIAL_FLOW.equals(event.toString())) {
+                output.addmember(STATUS).integer(STATUS_SUCCESS);
+            } else {
+                output.addmember(STATUS).integer(STATUS_PARTIAL);
+            }
+            output.addmember(SPConstants.EVENT).string(event.toString());
+        } else {
+            output.addmember(STATUS).integer(STATUS_SUCCESS);
+        }
+    }
+
+    /**
+     * Store a revocation record if configured to do so.
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @return true iff revocation succeeded or was not configured
+     */
+    private boolean doRevocation(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        final StorageService localStorage = storageService;
+        if (localStorage == null) {
+            return true;
+        }
+        
+        try {
+            if (localStorage.create(getStorageContext(), key, "1", getStorageLifetime())) {
+                log.debug("{} Added revocation record for session ({})", getLogPrefix(), key);
+                return true;
+            }
+            log.error("{} Detected existing revocation record for session ({})", getLogPrefix(), key);
+        } catch (final IOException e) {
+            log.error("{} Error creating revocation record for session ({})", getLogPrefix(), key, e);
+        }
+        
+        return false;
+    }
+    
+    /**
+     * Get Agent-supplied storage lifetime in seconds, bounded by configured maximum.
+     * 
+     * @return effective lifetime in seconds
+     */
+    @Nonnull private Long getStorageLifetime() {
+        
+        Long st = input.getmember(LIFETIME).longinteger();
+        if (st != null) {
+            if (st <= maxStorageLifetime.toSeconds()) {
+                return st;
+            }
+            
+            log.warn("{} Agent-supplied {} value exceeded configured maximum ({})", getLogPrefix(), LIFETIME,
+                    maxStorageLifetime);
+        }
+        
+        return maxStorageLifetime.toSeconds();
+    }
+    
+    /**
+     * Compute agent-specific storage context. 
+     *
+     * @return storage context with agent qualifier
+     */
+    @Nonnull private String getStorageContext() {
+        return STORAGE_CONTEXT + '!' + ensureAgent().getId() + '!' + ensureApplication().getApplicationId();
+    }
+    
+}
\ No newline at end of file

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


More information about the commits mailing list