Merge pull request #184 from fpapon/SHIRO-669

[SHIRO-669] Included a boolean flag in FirstSuccessfulStrategy to bre…
diff --git a/core/src/main/java/org/apache/shiro/authc/pam/FirstSuccessfulStrategy.java b/core/src/main/java/org/apache/shiro/authc/pam/FirstSuccessfulStrategy.java
index 6a99ff0..87cb5cc 100644
--- a/core/src/main/java/org/apache/shiro/authc/pam/FirstSuccessfulStrategy.java
+++ b/core/src/main/java/org/apache/shiro/authc/pam/FirstSuccessfulStrategy.java
@@ -37,6 +37,17 @@
  */
 public class FirstSuccessfulStrategy extends AbstractAuthenticationStrategy {
 
+    private boolean stopAfterFirstSuccess;
+
+    public void setStopAfterFirstSuccess (boolean stopAfterFirstSuccess ) {
+
+        this.stopAfterFirstSuccess  = stopAfterFirstSuccess ;
+    }
+
+    public boolean getStopAfterFirstSuccess() {
+        return stopAfterFirstSuccess ;
+    }
+
     /**
      * Returns {@code null} immediately, relying on this class's {@link #merge merge} implementation to return
      * only the first {@code info} object it encounters, ignoring all subsequent ones.
@@ -45,6 +56,22 @@
         return null;
     }
 
+
+    /**
+     * Throws ShortCircuitIterationException if stopAfterFirstSuccess is set and authentication is 
+     * successful with a previously consulted realm. 
+     * Returns the <code>aggregate</code> method argument, without modification
+     * otherwise.
+     */
+    public AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException {
+        if (getStopAfterFirstSuccess() && aggregate != null && isEmpty(aggregate.getPrincipals())) {
+            throw new ShortCircuitIterationException();
+        }
+        return aggregate;
+    }
+
+    
+
     private static boolean isEmpty(PrincipalCollection pc) {
         return pc == null || pc.isEmpty();
     }
diff --git a/core/src/main/java/org/apache/shiro/authc/pam/ModularRealmAuthenticator.java b/core/src/main/java/org/apache/shiro/authc/pam/ModularRealmAuthenticator.java
index 41ebe41..02bd3aa 100644
--- a/core/src/main/java/org/apache/shiro/authc/pam/ModularRealmAuthenticator.java
+++ b/core/src/main/java/org/apache/shiro/authc/pam/ModularRealmAuthenticator.java
@@ -207,7 +207,13 @@
 
         for (Realm realm : realms) {
 
-            aggregate = strategy.beforeAttempt(realm, token, aggregate);
+            try {
+                aggregate = strategy.beforeAttempt(realm, token, aggregate);
+            } catch (ShortCircuitIterationException shortCircuitSignal) {
+                // Break from continuing with subsequnet realms on receiving 
+                // short circuit signal from strategy
+                break;
+            }
 
             if (realm.supports(token)) {
 
diff --git a/core/src/main/java/org/apache/shiro/authc/pam/ShortCircuitIterationException.java b/core/src/main/java/org/apache/shiro/authc/pam/ShortCircuitIterationException.java
new file mode 100644
index 0000000..0522b24
--- /dev/null
+++ b/core/src/main/java/org/apache/shiro/authc/pam/ShortCircuitIterationException.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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 org.apache.shiro.authc.pam;
+
+import org.apache.shiro.authc.AuthenticationException;
+
+
+/**
+ * Exception thrown during the authentication process using
+ * {@link org.apache.shiro.authc.pam.FirstSuccessfulStrategy}, with 
+ * <code>stopAfterFirstSuccess</code> set.  
+ * This is a signal to short circuit the authentication from proceeding 
+ * with subsequent {@link org.apache.shiro.realm.Realm Realm}s 
+ * after a first successful authentication.
+ *
+ * @see org.apache.shiro.authc.pam.AuthenticationStrategy
+ * @see org.apache.shiro.authc.pam.FirstSuccessfulStrategy
+ * @since 1.4.1
+ */
+public class ShortCircuitIterationException extends AuthenticationException {
+
+    /**
+     * Creates a new ShortCircuitIterationException.
+     */
+    public ShortCircuitIterationException() {
+        super();
+    }
+
+    /**
+     * Constructs a new ShortCircuitIterationException.
+     *
+     * @param message the reason for the exception
+     */
+    public ShortCircuitIterationException(String message) {
+        super(message);
+    }
+
+    /**
+     * Constructs a new ShortCircuitIterationException.
+     *
+     * @param cause the underlying Throwable that caused this exception to be thrown.
+     */
+    public ShortCircuitIterationException(Throwable cause) {
+        super(cause);
+    }
+
+    /**
+     * Constructs a new ShortCircuitIterationException.
+     *
+     * @param message the reason for the exception
+     * @param cause   the underlying Throwable that caused this exception to be thrown.
+     */
+    public ShortCircuitIterationException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/core/src/test/java/org/apache/shiro/authc/pam/FirstSuccessfulStrategyTest.java b/core/src/test/java/org/apache/shiro/authc/pam/FirstSuccessfulStrategyTest.java
index ce95416..68fe395 100644
--- a/core/src/test/java/org/apache/shiro/authc/pam/FirstSuccessfulStrategyTest.java
+++ b/core/src/test/java/org/apache/shiro/authc/pam/FirstSuccessfulStrategyTest.java
@@ -1,35 +1,34 @@
 /*
- * Copyright (c) 2019 Nova Ordis LLC
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF 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
  *
- * 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
  *
- *    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.
+ * 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 org.apache.shiro.authc.pam;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNull;
-
 import org.apache.shiro.authc.AuthenticationInfo;
 import org.apache.shiro.authc.MergableAuthenticationInfo;
 import org.apache.shiro.authc.SimpleAuthenticationInfo;
 import org.apache.shiro.subject.PrincipalCollection;
 import org.apache.shiro.subject.SimplePrincipalCollection;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
 import org.junit.Before;
 import org.junit.Test;
 
-/**
- * Created by lei.li4 on 2019/1/23
- */
+
 public class FirstSuccessfulStrategyTest {
 
     private FirstSuccessfulStrategy strategy;
@@ -37,6 +36,7 @@
     @Before
     public void setUp() {
         strategy = new FirstSuccessfulStrategy();
+        strategy.setStopAfterFirstSuccess(true);
     }
 
     @Test
@@ -89,6 +89,19 @@
         AuthenticationInfo authInfo = new SimpleAuthenticationInfo();
         AuthenticationInfo mergeResult = strategy.merge(authInfo, aggregate);
         assertEquals(authInfo, mergeResult);
+        AuthenticationInfo info = strategy.beforeAllAttempts(null, null);
+        assertNull(info);
+    }
+
+    @Test 
+    public void testBeforeAttemptNull() {
+        assertNull(strategy.beforeAttempt(null, null, null));
+    }
+
+    @Test (expected=ShortCircuitIterationException.class)
+    public void testBeforeAttemptStopAfterFirstSuccess() {
+        AuthenticationInfo aggregate = new SimpleAuthenticationInfo();
+        strategy.beforeAttempt(null, null, aggregate);
     }
 
 }