[java-plugin-shibd] branch main updated: WIP flow for session cache mgmt.

Codeberg noreply at shibboleth.net
Mon Dec 1 20:49:01 UTC 2025


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/2ba3b6a882c81c40e56b31d36088d49803989490

The following commit(s) were added to refs/heads/main by this push:
     new 2ba3b6a  WIP flow for session cache mgmt.
2ba3b6a is described below

commit 2ba3b6a882c81c40e56b31d36088d49803989490
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Mon Dec 1 15:48:50 2025 -0500

    WIP flow for session cache mgmt.
---
 .../flows/sp/session-cache/session-cache-beans.xml |  18 +
 .../flows/sp/session-cache/session-cache-flow.xml  |  26 +
 .../shibboleth/idp/module/conf/sp/sp.properties    |   8 +-
 .../sp/profile/impl/DoSessionCacheOperation.java   | 542 +++++++++++++++++++++
 4 files changed, 589 insertions(+), 5 deletions(-)

diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-cache/session-cache-beans.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-cache/session-cache-beans.xml
new file mode 100644
index 0000000..bfc56f5
--- /dev/null
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-cache/session-cache-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/parse-request-map" />
+    <bean id="shibboleth.sp.loggingId" class="java.lang.String" c:_0="%{sp.service.logging.storage:SPAgent.Storage}" />
+
+    <bean id="DoSessionCacheOperation"
+        class="net.shibboleth.sp.profile.impl.DoSessionCacheOperation" scope="prototype"
+        p:maxStorageTimeout="%{sp.session.maxStorageTimeout:P1D}"
+        p:storageService-ref="#{'%{sp.session.storageService}'.trim()}" />
+
+</beans>
diff --git a/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-cache/session-cache-flow.xml b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-cache/session-cache-flow.xml
new file mode 100644
index 0000000..8d1c7d2
--- /dev/null
+++ b/sp-conf-impl/src/main/resources/META-INF/net/shibboleth/idp/flows/sp/session-cache/session-cache-flow.xml
@@ -0,0 +1,26 @@
+<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/abstract">
+
+    <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="DoOperation">
+        <evaluate expression="DoSessionCacheOperation" />
+        <evaluate expression="'proceed'" />
+        
+        <!-- Branch to parent flow to send response. -->
+        <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/session-cache/session-cache-beans.xml" />
+
+</flow>
diff --git a/sp-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/sp.properties b/sp-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/sp.properties
index 005759d..158b33d 100644
--- a/sp-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/sp.properties
+++ b/sp-conf-impl/src/main/resources/net/shibboleth/idp/module/conf/sp/sp.properties
@@ -10,11 +10,6 @@ sp.issuer = https://sp.example.org
 #sp.service.agents.failFast = false
 sp.service.agents.checkInterval = PT5M
 
-# Set to StorageService to use for remoted storage data if in use.
-#sp.storageService =
-# Set to DataSealer to use for remoted data encryption.
-#sp.dataSealer = shibboleth.DataSealer
-
 # Default precedence/set of Session Initiator and Token Consumer flows to attempt
 #sp.application.sessionInitiators = 
 #sp.application.tokenConsumers =
@@ -54,6 +49,9 @@ sp.service.agents.checkInterval = PT5M
 #sp.discoveryURL =
 #sp.discoveryURLFunction =
 
+# Settings for Hub-mediated session storage
+#sp.session.storageService =
+#sp.session.maxStorageTimeout = P1D
 
 ###############################
 # Agent Authentication Settings
diff --git a/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/DoSessionCacheOperation.java b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/DoSessionCacheOperation.java
new file mode 100644
index 0000000..fe84111
--- /dev/null
+++ b/sp-server-impl/src/main/java/net/shibboleth/sp/profile/impl/DoSessionCacheOperation.java
@@ -0,0 +1,542 @@
+/*
+ * 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.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.time.Instant;
+
+import javax.annotation.Nonnull;
+
+import org.opensaml.profile.action.ActionSupport;
+import org.opensaml.profile.action.EventIds;
+import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.storage.StorageRecord;
+import org.opensaml.storage.StorageService;
+import org.opensaml.storage.VersionMismatchException;
+import org.slf4j.Logger;
+
+import net.shibboleth.shared.annotation.constraint.NonnullAfterInit;
+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.component.ComponentInitializationException;
+import net.shibboleth.shared.logic.Constraint;
+import net.shibboleth.shared.primitive.LoggerFactory;
+import net.shibboleth.shared.security.IdentifierGenerationStrategy;
+import net.shibboleth.shared.security.IdentifierGenerationStrategy.ProviderType;
+import net.shibboleth.sp.ddf.DDF;
+import net.shibboleth.sp.profile.AbstractAgentAction;
+
+/**
+ * Action that implements a remote API for SP agents to mamage sessions remotely.
+ * 
+ * <p>This is implememnted internally with a {@link StorageService} but implements
+ * the operations somewhat more efficiently with more awareness of the purpose.</p>
+ * 
+ * @event {@link EventIds#PROCEED_EVENT_ID}
+ * @event {@link EventIds#INVALID_PROFILE_CTX}
+ * @event {@link EventIds#INVALID_MESSAGE}
+ * @event {@link EventIds#IO_ERROR}
+ * @event {@link EventIds#MESSAGE_PROC_ERROR}
+ * @event {@link #INVALID_SESSION}
+ * @event {@link #EXPIRED_SESSION}
+ * @event {@link #VERSION_MISMATCH}
+ */
+public class DoSessionCacheOperation extends AbstractAgentAction {
+
+    /** Storage context for sessions. */
+    @Nonnull @NotEmpty public static final String STORAGE_CONTEXT = "net.shibboleth.sp.sessions";
+
+    /** Custom event for invalid session. */
+    @Nonnull @NotEmpty public static final String INVALID_SESSION = "InvalidSession";
+
+    /** Custom event for expired session. */
+    @Nonnull @NotEmpty public static final String EXPIRED_SESSION = "ExpiredSession";
+    
+    /** Custom event for version mismatch on conditional update. */
+    @Nonnull @NotEmpty public static final String VERSION_MISMATCH = "VersionMismatch";
+    
+    /** Input member for operation. */
+    @Nonnull @NotEmpty public static final String OP = "op";
+
+    /** Input member for session data. */
+    @Nonnull @NotEmpty public static final String SESSION = "session";
+
+    /** Member for session timeout. */
+    @Nonnull @NotEmpty public static final String STORAGE_TIMEOUT = "storage_timeout";
+    
+    /** Input member for session key. */
+    @Nonnull @NotEmpty public static final String KEY = "key";
+    
+    /** Member for session lifetime. */
+    @Nonnull @NotEmpty public static final String LIFETIME = "lifetime";
+
+    /** Member for session timeout. */
+    @Nonnull @NotEmpty public static final String TIMEOUT = "timeout";
+
+    /** Member for session creation timestamp. */
+    @Nonnull @NotEmpty public static final String TS = "ts";
+    
+    /** Member for session version. */
+    @Nonnull @NotEmpty public static final String VERSION = "ver";
+    
+    /** Class logger. */
+    @Nonnull private Logger log = LoggerFactory.getLogger(DoSessionCacheOperation.class);
+    
+    /** {@link StorageService} to operate on. */
+    @NonnullAfterInit private StorageService storageService;
+
+    /** Identifier generation. */
+    @NonnullAfterInit private IdentifierGenerationStrategy identifierStrategy;
+    
+    /** Upper bound on agent-supplied storage timeout. */
+    @Nonnull private Duration maxStorageTimeout;
+    
+    /** Input message. */
+    @NonnullBeforeExec private DDF input;
+    
+    /** Constructor. */
+    public DoSessionCacheOperation() {
+        maxStorageTimeout = Duration.ofDays(1);
+    }
+    
+    /**
+     * Sets the {@link StorageService} to use.
+     * 
+     * @param storage storage service
+     */
+    public void setStorageService(@Nonnull final StorageService storage) {
+        checkSetterPreconditions();
+        
+        storageService = Constraint.isNotNull(storage, "StorageService cannot be null");
+    }
+    
+    /**
+     * Set {@link IdentifierGenerationStrategy} to use.
+     * 
+     * <p>Defaults to a secure random source that produces 16 byte values.</p>
+     * 
+     * @param strategy identifier generator strategy
+     */
+    public void setIdentifierGenerationStrategy(@Nonnull final IdentifierGenerationStrategy strategy) {
+        checkSetterPreconditions();
+        
+        identifierStrategy = Constraint.isNotNull(strategy, "IdentifierGenerationStrategy cannot be null");
+    }
+    
+    /**
+     * Sets the maximum storage life for session 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 timeout must be positive");
+        maxStorageTimeout = timeout;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    protected void doInitialize() throws ComponentInitializationException {
+        super.doInitialize();
+        
+        if (storageService == null) {
+            throw new ComponentInitializationException("StorageService cannot be null");
+        }
+        
+        if (identifierStrategy == null) {
+            identifierStrategy = IdentifierGenerationStrategy.getInstance(ProviderType.SECURE);
+        }
+    }
+    
+    /** {@inheritDoc} */
+    @Override
+    protected boolean doPreExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        if (!super.doPreExecute(profileRequestContext)) {
+            return false;
+        }
+        
+        input = ensureAgentRequestContext().getInput();
+        if (input == null || !input.isstruct()) {
+            log.warn("{} Invalid or missing input message", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            return false;
+        }
+
+        return true;
+    }
+
+    /** {@inheritDoc} */
+    @Override protected void doExecute(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        try {
+            final String op = input.getmember(OP).string();
+            
+            if ("C".equals(op)) {
+                doCreate(profileRequestContext);
+            } else if ("R".equals(op)) {
+                doRead(profileRequestContext);
+            } else if ("U".equals(op)) {
+                doUpdate(profileRequestContext);
+            } else if ("T".equals(op)) {
+                doTouch(profileRequestContext);
+            } else if ("D".equals(op)) {
+                doDelete(profileRequestContext);
+            } else {
+                log.warn("{} Invalid operation: {}", getLogPrefix(), op);
+                ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            }
+        } catch (final IOException e) {
+            log.error("{} I/O error responding to request", getLogPrefix(), e);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
+        }
+    }
+    
+    /**
+     * Perform create operation.
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @throws IOException if an error is raised
+     */
+    private void doCreate(@Nonnull final ProfileRequestContext profileRequestContext) throws IOException {
+
+        // Input:
+        // "storage_timeout" - the storage timeout in seconds
+        // "session" - structure to store
+        
+        // Output:
+        // "key" - session key
+        
+        DDF data = input.getmember(SESSION);
+        if (!data.isstruct()) {
+            log.warn("{} Missing required '{}' structure member", getLogPrefix(), SESSION);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            return;
+        }
+        
+        Long exp = Instant.now().plusSeconds(getStorageTimeout()).toEpochMilli();
+        
+        // TODO: Encode/encrypt/etc.?
+        String value;
+        try (final ByteArrayOutputStream sink = new ByteArrayOutputStream()) {
+            data.serialize(sink);
+            value = sink.toString(StandardCharsets.UTF_8);
+        }
+        
+        assert value != null;
+                
+        int attempts = 0;
+        do {
+            final String key = identifierStrategy.generateIdentifier(false);
+            if (storageService.create(STORAGE_CONTEXT, ensureAgent().getId() + '!' + key, value, exp)) {
+                log.debug("{} Created session record ({}), expiration ({})", getLogPrefix(), key, Instant.ofEpochMilli(exp));
+                DDF out = new DDF(null);
+                out.addmember(KEY).string(key);
+                ensureAgentRequestContext().setOutput(out);
+                return;
+            } else {
+                log.warn("{} Session record ({}) exists", getLogPrefix(), key);
+            }
+        } while (++attempts < 3);
+        
+        log.warn("{} Unable to generate usable session key after 3 attempts", getLogPrefix());
+        ActionSupport.buildEvent(profileRequestContext, EventIds.MESSAGE_PROC_ERROR);
+    }
+    
+    /**
+     * Perform read operation.
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @throws IOException if an error is raised
+     */
+    private void doRead(@Nonnull final ProfileRequestContext profileRequestContext) throws IOException {
+        
+        // Input:
+        // "key" - session key
+        // "storage_timeout" - the storage timeout in seconds
+        // "lifetime" - session lifetime policy to apply
+        // "timeout" - session timeout policy to apply
+        
+        // Output:
+        // "session" - session structure
+
+        final String key = input.getmember(KEY).string();
+        if (key == null) {
+            log.warn("{} Missing required '{}' structure member", getLogPrefix(), KEY);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            return;
+        }
+        
+        final StorageRecord<?> record = storageService.read(STORAGE_CONTEXT, ensureAgent().getId() + '!' + key);
+        if (record == null) {
+            // Send back an empty response.
+            log.debug("{} No session found ({})", getLogPrefix(), key);
+            final DDF output = new DDF();
+            ensureAgentRequestContext().setOutput(output);
+            return;
+        }
+        
+        Long lastAccess = record.getExpiration();
+        if (lastAccess == null) {
+            log.error("{} Session record ({}) had no expiration", getLogPrefix(), key);
+            ActionSupport.buildEvent(profileRequestContext, INVALID_SESSION);
+            return;
+        }
+        
+        final Instant now = Instant.now();
+        
+        final Integer timeout = input.getmember(TIMEOUT).integer();
+        if (timeout != null && timeout > 0) {
+            // Recover last access time by backdating record expiration.
+            lastAccess -= getStorageTimeout();
+            if (Instant.ofEpochMilli(lastAccess).plusSeconds(timeout).isBefore(now)) {
+                log.info("{} Session record ({}) timed out, last use was {}, timeout policy was {}",
+                        getLogPrefix(), key, Instant.ofEpochMilli(lastAccess), timeout);
+                storageService.delete(STORAGE_CONTEXT, ensureAgent().getId() + '!' + key);
+                ActionSupport.buildEvent(profileRequestContext, EXPIRED_SESSION);
+                return;
+            }
+        }
+        
+        DDF sessionData;
+        try (final ByteArrayInputStream source =
+                new ByteArrayInputStream(record.getValue().getBytes(StandardCharsets.UTF_8))) {
+            sessionData = DDF.deserialize(source);
+        }
+        assert sessionData != null;
+        
+        // We have a deserialized session, need to enforce policies if specified.
+        
+        final Integer lifetime = input.getmember(LIFETIME).integer();
+        if (lifetime != null && lifetime > 0) {
+            final Long ts = sessionData.getmember(TS).longinteger();
+            if (ts == null) {
+                log.error("{} Session record ({}) missing creation timestamp", getLogPrefix(), key);
+                storageService.delete(STORAGE_CONTEXT, ensureAgent().getId() + '!' + key);
+                ActionSupport.buildEvent(profileRequestContext, INVALID_SESSION);
+                return;
+            }
+            
+            if (Instant.ofEpochSecond(ts).plusSeconds(lifetime).isBefore(now)) {
+                log.info("{} Session record ({}) expired, created {}, lifetime policy was {}",
+                        getLogPrefix(), key, Instant.ofEpochSecond(ts), lifetime);
+                storageService.delete(STORAGE_CONTEXT, ensureAgent().getId() + '!' + key);
+                ActionSupport.buildEvent(profileRequestContext, EXPIRED_SESSION);
+                return;
+            }
+        }
+                
+        final DDF output = new DDF().structure();
+        output.add(sessionData);
+        ensureAgentRequestContext().setOutput(output);
+    }    
+
+    /**
+     * Perform update operation.
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @throws IOException if an error is raised
+     */
+    private void doUpdate(@Nonnull final ProfileRequestContext profileRequestContext) throws IOException {
+
+        // Input:
+        // "key" - session key
+        // "ver" - existing assumed session version
+        // "storage_timeout" - the storage timeout in seconds
+        // "session" - structure to store
+        
+        // Output:
+        // "ver" - new version if update succeeded
+        
+        final String key = input.getmember(KEY).string();
+        Long version = input.getmember(VERSION).longinteger();
+        if (key == null || version == null || version <= 0) {
+            log.warn("{} Missing required '{}' or '{}' structure member", getLogPrefix(), KEY, VERSION);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            return;
+        }
+
+        final DDF data = input.getmember(SESSION);
+        if (!data.isstruct()) {
+            log.warn("{} Missing required '{}' structure member", getLogPrefix(), SESSION);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            return;
+        }
+        
+        final Long st = getStorageTimeout();
+        final Long exp = Instant.now().plusSeconds(st).toEpochMilli();
+        
+        // TODO: Encode/encrypt/etc.?
+        String value;
+        try (final ByteArrayOutputStream sink = new ByteArrayOutputStream()) {
+            data.serialize(sink);
+            value = sink.toString(StandardCharsets.UTF_8);
+        }
+        
+        assert value != null;
+        
+        try {
+            version = storageService.updateWithVersion(version, STORAGE_CONTEXT, ensureAgent().getId() + '!' + key,
+                    value, exp);
+            if (version != null) {
+                log.debug("{} Updated session ({}) to version ({})", getLogPrefix(), key, version);
+                final DDF output = new DDF().structure();
+                output.addmember(VERSION).longinteger(version);
+                ensureAgentRequestContext().setOutput(output);
+            } else {
+                // Send back an empty response.
+                log.debug("{} No session found ({})", getLogPrefix(), key);
+                final DDF output = new DDF();
+                ensureAgentRequestContext().setOutput(output);
+            }
+        } catch (final VersionMismatchException e) {
+            log.info("{} Version mismatch for session ({}). requested update with older version ({})", getLogPrefix(),
+                    key, version);
+            ActionSupport.buildEvent(profileRequestContext, VERSION_MISMATCH);
+        }
+    }
+    
+    /**
+     * Perform touch operation to update expiration (and thus time of last access).
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @throws IOException if an error is raised
+     */
+    private void doTouch(@Nonnull final ProfileRequestContext profileRequestContext) throws IOException {
+
+        // Input:
+        // "key" - session key
+        // "storage_timeout" - the storage timeout in seconds
+        // "timeout" - session timeout policy to apply
+        
+        // Output:
+        // "ver" - current version, indicates record was found and updated 
+        
+        final String key = input.getmember(KEY).string();
+        if (key == null) {
+            log.warn("{} Missing required '{}' structure member", getLogPrefix(), KEY);
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            return;
+        }
+        
+        // Read back record to check for timeout.
+        
+        final StorageRecord<?> record = storageService.read(STORAGE_CONTEXT, ensureAgent().getId() + '!' + key);
+        if (record == null) {
+            // Send back an empty response.
+            log.debug("{} No session found ({})", getLogPrefix(), key);
+            final DDF output = new DDF();
+            ensureAgentRequestContext().setOutput(output);
+            return;
+        }
+        
+        Long lastAccess = record.getExpiration();
+        if (lastAccess == null) {
+            log.error("{} Session record ({}) had no expiration", getLogPrefix(), key);
+            ActionSupport.buildEvent(profileRequestContext, INVALID_SESSION);
+            return;
+        }
+        
+        final Long st = getStorageTimeout();
+        final Instant now = Instant.now();
+        
+        final Integer timeout = input.getmember(TIMEOUT).integer();
+        if (timeout != null && timeout > 0) {
+            // Recover last access time by backdating record expiration.
+            lastAccess -= st;
+            if (Instant.ofEpochMilli(lastAccess).plusSeconds(timeout).isBefore(now)) {
+                log.info("{} Session record ({}) timed out, last use was {}, timeout policy was {}",
+                        getLogPrefix(), key, Instant.ofEpochMilli(lastAccess), timeout);
+                storageService.delete(STORAGE_CONTEXT, ensureAgent().getId() + '!' + key);
+                ActionSupport.buildEvent(profileRequestContext, EXPIRED_SESSION);
+                return;
+            }
+        }
+        
+        // Bump the expiration and return the version as a success indicator.
+        
+        if (storageService.updateExpiration(STORAGE_CONTEXT, ensureAgent().getId() + '!' + key,
+                now.plusSeconds(st).toEpochMilli())) {
+            final DDF output = new DDF();
+            output.addmember(VERSION).longinteger(record.getVersion());
+            ensureAgentRequestContext().setOutput(output);
+        } else {
+            // Record disappeared, so send back an empty response.
+            log.debug("{} Session record ({}) disappeared before update?", getLogPrefix(), key);
+            final DDF output = new DDF();
+            ensureAgentRequestContext().setOutput(output);
+        }        
+    }
+    
+    /**
+     * Perform delete operation.
+     * 
+     * @param profileRequestContext profile request context
+     * 
+     * @throws IOException if an error is raised
+     */
+    private void doDelete(@Nonnull final ProfileRequestContext profileRequestContext) throws IOException {
+
+        // Input:
+        // "key" - session key to delete
+        
+        // Output: None
+        
+        final String key = input.getmember(KEY).string();
+        if (key == null) {
+            log.warn("{} Key required for delete operation", getLogPrefix());
+            ActionSupport.buildEvent(profileRequestContext, EventIds.INVALID_MESSAGE);
+            return;
+        }
+        
+        if (storageService.delete(STORAGE_CONTEXT, ensureAgent().getId() + '!' + key)) {
+            log.debug("{} Deleted session record ({})", getLogPrefix(), key);
+        } else {
+            log.debug("{} No session record ({})", getLogPrefix(), key);
+        }
+    }
+
+    /**
+     * Get Agent-supplied storage timeout in seconds, bounded by configured maximum.
+     * 
+     * @return effective timeout in seconds
+     */
+    @Nonnull Long getStorageTimeout() {
+        
+        Long st = input.getmember(STORAGE_TIMEOUT).longinteger();
+        if (st != null) {
+            if (st <= maxStorageTimeout.toSeconds()) {
+                return st;
+            }
+            
+            log.warn("{} Agent-supplied {} value exceeded configured maximum ({})", getLogPrefix(), STORAGE_TIMEOUT,
+                    maxStorageTimeout);
+        }
+        
+        return maxStorageTimeout.toSeconds();
+    }
+
+}
\ 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