[java-identity-provider] branch main updated: IDP-2047 - Get list of locked accounts

Scott Cantor cantor.2 at osu.edu
Fri Aug 4 19:43:54 UTC 2023


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

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

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

The following commit(s) were added to refs/heads/main by this push:
     new ae737d05b IDP-2047 - Get list of locked accounts
ae737d05b is described below

commit ae737d05b512aad63587b464c6bf0d0cc48899d2
Author: Scott Cantor <cantor.2 at osu.edu>
AuthorDate: Fri Aug 4 15:42:59 2023 -0400

    IDP-2047 - Get list of locked accounts
    
    https://shibboleth.atlassian.net/browse/IDP-2047
    
    Implemented enumeration-capable lockout manager.
    Refactored some of the internals.
    Added an inexact query param to REST API.
---
 .../authn/EnumeratableAccountLockoutManager.java   |  43 +++++
 .../idp/authn/impl/DoLockoutManagerOperation.java  |  99 +++++++----
 .../impl/StorageBackedAccountLockoutManager.java   | 190 ++++++++++++++++-----
 .../StorageBackedAccountLockoutManagerTest.java    |   3 +-
 .../shibboleth/idp/flows/admin/lockout-flow.xml    |   1 +
 5 files changed, 258 insertions(+), 78 deletions(-)

diff --git a/idp-authn-api/src/main/java/net/shibboleth/idp/authn/EnumeratableAccountLockoutManager.java b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/EnumeratableAccountLockoutManager.java
new file mode 100644
index 000000000..8de82cb3a
--- /dev/null
+++ b/idp-authn-api/src/main/java/net/shibboleth/idp/authn/EnumeratableAccountLockoutManager.java
@@ -0,0 +1,43 @@
+/*
+ * 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.idp.authn;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+import org.opensaml.profile.context.ProfileRequestContext;
+
+import net.shibboleth.idp.authn.context.LockoutManagerContext;
+
+/**
+ * An extension to {@link AccountLockoutManager} that allows for enumeration over
+ * partial matches of a key.
+ * 
+ * @since 5.0.0
+ */
+public interface EnumeratableAccountLockoutManager extends AccountLockoutManager {
+    
+    /**
+     * Return iterable collection of locked out keys that match a supplied partial key (i.e., are prefixed by it).
+     * 
+     * <p>The key MUST be supplied via a {@link LockoutManagerContext} subcontext of the input context.</p>
+     * 
+     * @param profileRequestContext current profile request context 
+     * 
+     * @return the locked out keys, or a null if an error occurs
+     */
+    @Nullable Iterable<String> enumerate(@Nonnull final ProfileRequestContext profileRequestContext);
+
+}
\ No newline at end of file
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/DoLockoutManagerOperation.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/DoLockoutManagerOperation.java
index a904cc33c..2034c1457 100644
--- a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/DoLockoutManagerOperation.java
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/DoLockoutManagerOperation.java
@@ -15,6 +15,7 @@
 package net.shibboleth.idp.authn.impl;
 
 import java.io.IOException;
+import java.util.Map;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -23,7 +24,6 @@ import org.opensaml.profile.action.ActionSupport;
 import org.opensaml.profile.action.EventIds;
 import org.opensaml.profile.context.ProfileRequestContext;
 import org.slf4j.Logger;
-import org.springframework.beans.BeansException;
 import org.springframework.webflow.execution.RequestContext;
 
 import com.fasterxml.jackson.core.JsonFactory;
@@ -36,6 +36,7 @@ import com.google.common.base.Strings;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletResponse;
 import net.shibboleth.idp.authn.AccountLockoutManager;
+import net.shibboleth.idp.authn.EnumeratableAccountLockoutManager;
 import net.shibboleth.idp.authn.context.LockoutManagerContext;
 import net.shibboleth.idp.profile.AbstractProfileAction;
 import net.shibboleth.idp.profile.context.SpringRequestContext;
@@ -75,6 +76,9 @@ public class DoLockoutManagerOperation extends AbstractProfileAction {
     /** Flow variable indicating ID of account key. */
     @Nonnull @NotEmpty public static final String KEY = "key";
 
+    /** Flow variable indicating whether key should be inexactly matched. */
+    @Nonnull @NotEmpty public static final String INEXACT = "inexact";
+
     /** Class logger. */
     @Nonnull private Logger log = LoggerFactory.getLogger(DoLockoutManagerOperation.class);
     
@@ -87,6 +91,9 @@ public class DoLockoutManagerOperation extends AbstractProfileAction {
     /** Account key to operate on. */
     @NonnullBeforeExec @NotEmpty private String key;
     
+    /** Enumerating on inexact matches? */
+    private boolean inexact;
+    
     /** {@link AccountLockoutManager} to operate on. */
     @NonnullBeforeExec private AccountLockoutManager lockoutManager;
 
@@ -110,6 +117,7 @@ public class DoLockoutManagerOperation extends AbstractProfileAction {
         }
     }
 
+// Checkstyle: CyclomaticComplexity|ReturnCount OFF
     /** {@inheritDoc} */
     @Override
     protected boolean doPreExecute(final @Nonnull ProfileRequestContext profileRequestContext) {
@@ -153,6 +161,16 @@ public class DoLockoutManagerOperation extends AbstractProfileAction {
                         "Missing Account Key", "No account key specified.");
                 return false;
             }
+            
+            final String flag = (String) requestContext.getFlowScope().get(INEXACT);
+            if (flag != null) {
+                inexact = Boolean.valueOf(flag);
+                if (inexact && !(lockoutManager instanceof EnumeratableAccountLockoutManager)) {
+                    sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+                            "Invalid Lockout Manager", "Lockout manager specified does not support inexact lookup.");
+                    return false;
+                }
+            }
 
         } catch (final IOException e) {
             log.error("{} I/O error issuing API response", getLogPrefix(), e);
@@ -162,8 +180,9 @@ public class DoLockoutManagerOperation extends AbstractProfileAction {
 
         return true;
     }
+// Checkstyle: ReturnCount ON
 
-// Checkstyle: CyclomaticComplexity OFF
+// Checkstyle: MethodLength OFF
     /** {@inheritDoc} */
     @Override protected void doExecute(final @Nonnull ProfileRequestContext profileRequestContext) {
 
@@ -178,18 +197,48 @@ public class DoLockoutManagerOperation extends AbstractProfileAction {
             
             if ("GET".equals(request.getMethod())) {
                 try {
-                    final boolean lockout = getLockoutManager().check(profileRequestContext);
-                    response.setStatus(HttpServletResponse.SC_OK);
-                    final JsonFactory jsonFactory = new JsonFactory();
-                    try (final JsonGenerator g = jsonFactory.createGenerator(
-                            response.getOutputStream()).useDefaultPrettyPrinter()) {
-                        g.setCodec(objectMapper);
-                        g.writeStartObject();
-                        g.writeObjectFieldStart("data");
-                        g.writeStringField("type", "lockout-statuses");
-                        g.writeStringField("id", managerId + '/' + key);
-                        g.writeObjectFieldStart("attributes");
-                        g.writeBooleanField("lockout", lockout);
+                    if (inexact) {
+                        final Iterable<String> lockedKeys =
+                                ((EnumeratableAccountLockoutManager) getLockoutManager()).enumerate(
+                                        profileRequestContext);
+                        if (lockedKeys != null) {
+                            response.setStatus(HttpServletResponse.SC_OK);
+                            final JsonFactory jsonFactory = new JsonFactory();
+                            try (final JsonGenerator g = jsonFactory.createGenerator(
+                                    response.getOutputStream()).useDefaultPrettyPrinter()) {
+                                g.setCodec(objectMapper);
+                                g.writeStartObject();
+                                g.writeObjectFieldStart("data");
+                                g.writeStringField("id", managerId + '/' + key);
+                                g.writeStringField("type", "lockout-keys");
+                                g.writeArrayFieldStart("data");
+                                for (final String k : lockedKeys) {
+                                    g.writeString(k);
+                                }
+                                g.writeEndArray();
+                                g.writeEndObject();
+                                g.writeEndObject();
+                            }
+                        } else {
+                            sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal Server Error",
+                                    "Lockout manager error.");
+                        }
+                    } else {
+                        final boolean lockout = getLockoutManager().check(profileRequestContext);
+                        response.setStatus(HttpServletResponse.SC_OK);
+                        final JsonFactory jsonFactory = new JsonFactory();
+                        try (final JsonGenerator g = jsonFactory.createGenerator(
+                                response.getOutputStream()).useDefaultPrettyPrinter()) {
+                            g.setCodec(objectMapper);
+                            g.writeStartObject();
+                            g.writeObjectFieldStart("data");
+                            g.writeStringField("type", "lockout-statuses");
+                            g.writeStringField("id", managerId + '/' + key);
+                            g.writeObjectFieldStart("attributes");
+                            g.writeBooleanField("lockout", lockout);
+                            g.writeEndObject();
+                            g.writeEndObject();
+                        }
                     }
                 } catch (final IOException e) {
                     sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal Server Error",
@@ -231,7 +280,7 @@ public class DoLockoutManagerOperation extends AbstractProfileAction {
             ActionSupport.buildEvent(profileRequestContext, EventIds.IO_ERROR);
         }
     }
-// Checkstyle: CyclomaticComplexity ON
+// Checkstyle: CyclomaticComplexity|MethodLength ON
 
     /**
      * Helper method to get the manager bean to operate on.
@@ -242,24 +291,14 @@ public class DoLockoutManagerOperation extends AbstractProfileAction {
      */
     @Nullable private AccountLockoutManager setupLockoutManager(@Nonnull final RequestContext requestContext) {
         
-        final String mgrId = this.managerId = (String) requestContext.getFlowScope().get(MANAGER_ID);
-        if (mgrId == null) {
+        managerId = (String) requestContext.getFlowScope().get(MANAGER_ID);
+        if (managerId == null) {
             log.warn("{} No {} flow variable found in request", getLogPrefix(), MANAGER_ID);
             return null;
         }
-        
-        try {
-            assert mgrId != null;
-            final Object bean = requestContext.getActiveFlow().getApplicationContext().getBean(mgrId);
-            if (bean != null && bean instanceof AccountLockoutManager) {
-                return (AccountLockoutManager) bean;
-            }
-        } catch (final BeansException e) {
-            
-        }
-        
-        log.warn("{} No bean of the correct type found named {}", getLogPrefix(), mgrId);
-        return null;
+
+        assert managerId != null;
+        return getBean(requestContext, managerId, AccountLockoutManager.class);
     }
 
     /** Null safe getter.
diff --git a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/StorageBackedAccountLockoutManager.java b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/StorageBackedAccountLockoutManager.java
index adbdb057f..b47abc476 100644
--- a/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/StorageBackedAccountLockoutManager.java
+++ b/idp-authn-impl/src/main/java/net/shibboleth/idp/authn/impl/StorageBackedAccountLockoutManager.java
@@ -17,18 +17,21 @@ package net.shibboleth.idp.authn.impl;
 import java.io.IOException;
 import java.time.Duration;
 import java.time.Instant;
+import java.util.ArrayList;
+import java.util.Collection;
 import java.util.function.Function;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 import org.opensaml.profile.context.ProfileRequestContext;
+import org.opensaml.storage.EnumeratableStorageService;
 import org.opensaml.storage.StorageCapabilities;
 import org.opensaml.storage.StorageRecord;
 import org.opensaml.storage.StorageService;
 import org.slf4j.Logger;
 
-import net.shibboleth.idp.authn.AccountLockoutManager;
+import net.shibboleth.idp.authn.EnumeratableAccountLockoutManager;
 import net.shibboleth.idp.authn.context.AuthenticationContext;
 import net.shibboleth.idp.authn.context.LockoutManagerContext;
 import net.shibboleth.idp.authn.context.UsernamePasswordContext;
@@ -46,11 +49,11 @@ import net.shibboleth.shared.servlet.HttpServletSupport;
 import jakarta.servlet.http.HttpServletRequest;
 
 /**
- * Implementation of {@link AccountLockoutManager} interface that relies on a {@link StorageService}
+ * Implementation of {@link EnumeratableAccountLockoutManager} interface that relies on a {@link StorageService}
  * to track lockout state.
  */
 public class StorageBackedAccountLockoutManager extends AbstractIdentifiableInitializableComponent
-        implements AccountLockoutManager {
+        implements EnumeratableAccountLockoutManager {
     
     /** Class logger. */
     @Nonnull private Logger log = LoggerFactory.getLogger(StorageBackedAccountLockoutManager.class);
@@ -218,11 +221,138 @@ public class StorageBackedAccountLockoutManager extends AbstractIdentifiableInit
 
     /** {@inheritDoc} */
     public boolean check(@Nonnull final ProfileRequestContext profileRequestContext) {
-        final String key = getLockoutKeyStrategy().apply(profileRequestContext);
+        
+        final String key;
+        final boolean managerOp;
+        final LockoutManagerContext managerCtx = profileRequestContext.getSubcontext(LockoutManagerContext.class);
+        if (managerCtx != null) {
+            key = managerCtx.getKey();
+            managerOp = true;
+        } else {
+            key = getLockoutKeyStrategy().apply(profileRequestContext);
+            managerOp = false;
+        }
+        
+        if (key == null) {
+            log.warn("No lockout key returned for request");
+            return false;
+        }
+
+        final long lockoutDuration = lockoutDurationLookupStrategy.apply(profileRequestContext).toMillis();
+        final long counterInterval = counterIntervalLookupStrategy.apply(profileRequestContext).toMillis();
+        final int maxAttempts = maxAttemptsLookupStrategy.apply(profileRequestContext);
+
+        return doCheck(profileRequestContext, key, maxAttempts, lockoutDuration, counterInterval,
+                extendLockoutDuration && !managerOp);
+    }
+
+    /** {@inheritDoc} */
+    public boolean increment(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        final String key;
+        final LockoutManagerContext managerCtx = profileRequestContext.getSubcontext(LockoutManagerContext.class);
+        if (managerCtx != null) {
+            key = managerCtx.getKey();
+        } else {
+            key = getLockoutKeyStrategy().apply(profileRequestContext);
+        }
+        
         if (key == null) {
             log.warn("No lockout key returned for request");
             return false;
         }
+        
+        final long lockoutDuration = lockoutDurationLookupStrategy.apply(profileRequestContext).toMillis();
+        final long counterInterval = counterIntervalLookupStrategy.apply(profileRequestContext).toMillis();
+        
+        return doIncrement(profileRequestContext, key, 10, lockoutDuration, counterInterval);
+    }
+
+    /** {@inheritDoc} */
+    public boolean clear(@Nonnull final ProfileRequestContext profileRequestContext) {
+        
+        final String key;
+        final LockoutManagerContext managerCtx = profileRequestContext.getSubcontext(LockoutManagerContext.class);
+        if (managerCtx != null) {
+            key = managerCtx.getKey();
+        } else {
+            key = getLockoutKeyStrategy().apply(profileRequestContext);
+        }
+
+        try {
+            if (key != null) {
+                log.debug("Clearing lockout state for '{}'", key);
+                storageService.delete(ensureId(), key);
+                return true;
+            }
+            log.warn("No lockout key returned for request");
+        } catch (final IOException e) {
+            log.error("Error deleting lockout entry", e);
+        }
+        return false;
+    }
+
+    /** {@inheritDoc} */
+    @Nullable public Iterable<String> enumerate(@Nonnull final ProfileRequestContext profileRequestContext) {
+
+        final String partialKey;
+        final LockoutManagerContext managerCtx = profileRequestContext.getSubcontext(LockoutManagerContext.class);
+        if (managerCtx != null) {
+            partialKey = managerCtx.getKey();
+        } else {
+            partialKey = null;
+        }
+        
+        if (partialKey == null) {
+            log.warn("No lockout key provided for request");
+            return null;
+        }
+
+        if (storageService instanceof EnumeratableStorageService ess) {
+            final Iterable<String> keys;
+            try {
+                keys = ess.getContextKeys(ensureId());
+            } catch (final IOException e) {
+                log.error("Error enumerating lockout storage context", e);
+                return null;
+            }
+        
+            final long lockoutDuration = lockoutDurationLookupStrategy.apply(profileRequestContext).toMillis();
+            final long counterInterval = counterIntervalLookupStrategy.apply(profileRequestContext).toMillis();
+            final int maxAttempts = maxAttemptsLookupStrategy.apply(profileRequestContext);
+            
+            final Collection<String> results = new ArrayList<>();
+            
+            for (final String key : keys) {
+                if (key.startsWith(partialKey)) {
+                    if (doCheck(profileRequestContext, key, maxAttempts, lockoutDuration, counterInterval, false)) {
+                        results.add(key);
+                    }
+                }
+            }
+            
+            return results;
+        } else {
+            throw new UnsupportedOperationException("Underlying storage service does not support enumeration of keys");
+        }
+        
+    }
+    
+    /**
+     * Helper method to perform a check operation against a specific key.
+     * 
+     * @param profileRequestContext current profile request context
+     * @param key input key to check
+     * @param maxAttempts maximum allowable attempts before lockout
+     * @param lockoutDuration duration of lockout
+     * @param counterInterval interval before disregarding attempts
+     * @param increment whether to increment the counter if already locked out
+     * 
+     * @return true iff the designated key is locked out
+     */
+// Checkstyle: ParameterNum OFF
+    protected boolean doCheck(@Nonnull final ProfileRequestContext profileRequestContext, @Nonnull final String key,
+            final int maxAttempts, final long lockoutDuration, final long counterInterval, final boolean increment) {
         // Read back account state. No state obviously means no lockout, but in the case of errors
         // that does fail open. Of course, in-memory won't fail...
         StorageRecord<?> sr = null;
@@ -240,18 +370,16 @@ public class StorageBackedAccountLockoutManager extends AbstractIdentifiableInit
         try {
             // Read counter and check if we've exceeded the limit.
             final int counter = Integer.parseInt(sr.getValue());
-            if (counter >= maxAttemptsLookupStrategy.apply(profileRequestContext)) {
+            if (counter >= maxAttempts) {
                 // Recover time of last attempt from the record expiration and find the time elapsed since.
                 // If that's under the lockout duration, we're locked out.
-                final long lockoutDuration = lockoutDurationLookupStrategy.apply(profileRequestContext).toMillis();
-                final long counterInterval = counterIntervalLookupStrategy.apply(profileRequestContext).toMillis();
                 final Long exp = Constraint.isNotNull(sr.getExpiration(), "Stored expiration canot be null");
                 final long lastAttempt = exp - Math.max(lockoutDuration, counterInterval);
                 final long timeDifference = System.currentTimeMillis() - lastAttempt;
                 if (timeDifference <= lockoutDuration) {
                     log.info("Lockout threshold reached for '{}', invalid count is {}", key, counter);
-                    if (extendLockoutDuration) {
-                        doIncrement(profileRequestContext, key, 10);
+                    if (increment) {
+                        doIncrement(profileRequestContext, key, 10, lockoutDuration, counterInterval);
                     }
                     return true;
                 }
@@ -265,34 +393,7 @@ public class StorageBackedAccountLockoutManager extends AbstractIdentifiableInit
 
         return false;
     }
-
-    /** {@inheritDoc} */
-    public boolean increment(@Nonnull final ProfileRequestContext profileRequestContext) {
-        // Work is done by helper method to track storage retries.
-        final String key = getLockoutKeyStrategy().apply(profileRequestContext);
-        if (key == null) {
-            log.warn("No lockout key returned for request");
-            return false;
-        }
-        
-        return doIncrement(profileRequestContext, key, 10);
-    }
-
-    /** {@inheritDoc} */
-    public boolean clear(@Nonnull final ProfileRequestContext profileRequestContext) {
-        try {
-            final String key = getLockoutKeyStrategy().apply(profileRequestContext);
-            if (key != null) {
-                log.debug("Clearing lockout state for '{}'", key);
-                storageService.delete(ensureId(), key);
-                return true;
-            }
-            log.warn("No lockout key returned for request");
-        } catch (final IOException e) {
-            log.error("Error deleting lockout entry", e);
-        }
-        return false;
-    }
+// Checkstyle: ParameterNum ON
     
 // Checkstyle: CyclomaticComplexity|MethodLength OFF
     /**
@@ -301,11 +402,14 @@ public class StorageBackedAccountLockoutManager extends AbstractIdentifiableInit
      * @param profileRequestContext current profile request context
      * @param key account lockout key
      * @param retries number of additional retries to allow
+     * @param lockoutDuration duration of lockout
+     * @param counterInterval interval before disregarding attempts
      * 
      * @return true iff successful
      */
     protected boolean doIncrement(@Nonnull final ProfileRequestContext profileRequestContext,
-            @Nonnull @NotEmpty final String key, final int retries) {
+            @Nonnull @NotEmpty final String key, final int retries, final long lockoutDuration,
+            final long counterInterval) {
 
         if (retries <= 0) {
             log.error("Account lockout increment attempts for '{}' exceeded retry limit", key);
@@ -333,8 +437,6 @@ public class StorageBackedAccountLockoutManager extends AbstractIdentifiableInit
         }
         
         final long now = System.currentTimeMillis();
-        final long lockoutDuration = lockoutDurationLookupStrategy.apply(profileRequestContext).toMillis();
-        final long counterInterval = counterIntervalLookupStrategy.apply(profileRequestContext).toMillis();
         
         // Compute last access time by backing off from record expiration.
         long lastAccess = now;
@@ -374,7 +476,7 @@ public class StorageBackedAccountLockoutManager extends AbstractIdentifiableInit
             }
         }
         
-        return doIncrement(profileRequestContext, key, retries-1);
+        return doIncrement(profileRequestContext, key, retries-1, lockoutDuration, counterInterval);
     }
 // Checkstyle: CyclomaticComplexity|MethodLength ON
     
@@ -417,12 +519,6 @@ public class StorageBackedAccountLockoutManager extends AbstractIdentifiableInit
                 return null;
             }
             
-            final LockoutManagerContext lockoutManagerContext =
-                    profileRequestContext.getSubcontext(LockoutManagerContext.class);
-            if (lockoutManagerContext != null) {
-                return lockoutManagerContext.getKey();
-            }
-
             if (getHttpServletRequest() == null) {
                 return null;
             }
diff --git a/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/StorageBackedAccountLockoutManagerTest.java b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/StorageBackedAccountLockoutManagerTest.java
index 3a2d71c78..af066dcc9 100644
--- a/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/StorageBackedAccountLockoutManagerTest.java
+++ b/idp-authn-impl/src/test/java/net/shibboleth/idp/authn/impl/StorageBackedAccountLockoutManagerTest.java
@@ -32,6 +32,7 @@ import net.shibboleth.shared.component.ComponentInitializationException;
 import net.shibboleth.shared.testing.ConstantSupplier;
 
 /** {@link StorageBackedAccountLockoutManager} unit test. */
+ at SuppressWarnings("javadoc")
 public class StorageBackedAccountLockoutManagerTest extends BaseAuthenticationContextTest {
 
     private StorageBackedAccountLockoutManager manager;    
@@ -114,4 +115,4 @@ public class StorageBackedAccountLockoutManagerTest extends BaseAuthenticationCo
         Assert.assertFalse(manager.check(prc));
     }
 
-}
+}
\ No newline at end of file
diff --git a/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/admin/lockout-flow.xml b/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/admin/lockout-flow.xml
index 9a1356025..c9ef89925 100644
--- a/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/admin/lockout-flow.xml
+++ b/idp-conf-impl/src/main/resources/net/shibboleth/idp/flows/admin/lockout-flow.xml
@@ -6,6 +6,7 @@
     <on-start>
         <!-- Extract PATH_INFO containing lockout manager and key. -->
         <evaluate expression="flowRequestContext.getActiveFlow().getId()" result="flowScope.flowId" />
+        <evaluate expression="externalContext.getNativeRequest().getParameter('inexact')" result="flowScope.inexact" />
         <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.shared.net.URISupport).doURLDecode(pathInfoArray[0]) : null" result="flowScope.lockoutManagerId" />

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


More information about the commits mailing list