Making it possible for the ModularRealmAuthenticator to not check all realms for authc via AuthenticationStrategy.
The FirstSuccessfulStrategy takes advantage of this.

This is useful when you have multiple realms configured and do NOT want to eat the cost of checking each realm.

git-svn-id: https://svn.apache.org/repos/asf/shiro/branches/first-successful-authc-strategy@1298115 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/core/src/main/java/org/apache/shiro/authc/pam/AbstractAuthenticationStrategy.java b/core/src/main/java/org/apache/shiro/authc/pam/AbstractAuthenticationStrategy.java
index 55b7a20..6051171 100644
--- a/core/src/main/java/org/apache/shiro/authc/pam/AbstractAuthenticationStrategy.java
+++ b/core/src/main/java/org/apache/shiro/authc/pam/AbstractAuthenticationStrategy.java
@@ -67,6 +67,13 @@
     }
 
     /**
+     * Base implementation always returns true, to be consistent with versions before Shiro 1.3.
+     */
+    public boolean continueAfterAttempt( AuthenticationInfo singleRealmInfo, AuthenticationInfo aggregateInfo, Throwable t ) {
+        return true;
+    }
+
+    /**
      * Merges the specified <code>info</code> argument into the <code>aggregate</code> argument and then returns an
      * aggregate for continued use throughout the login process.
      * <p/>
diff --git a/core/src/main/java/org/apache/shiro/authc/pam/AuthenticationStrategy.java b/core/src/main/java/org/apache/shiro/authc/pam/AuthenticationStrategy.java
index 0b2410e..b4fe520 100644
--- a/core/src/main/java/org/apache/shiro/authc/pam/AuthenticationStrategy.java
+++ b/core/src/main/java/org/apache/shiro/authc/pam/AuthenticationStrategy.java
@@ -99,6 +99,23 @@
             throws AuthenticationException;
 
     /**
+     * Method invoked by the ModularAuthenticator after a realm is consulted for authentication in order to check if other
+     * realms should also be consulted.
+     *
+     * <p>This method returns returns true if other realms should be consulted for authentication, false if no other
+     * realms should be consulted.</p>
+     *
+     * @param singleRealmInfo the info returned from a single realm.
+     * @param aggregateInfo   the aggregate info representing all realms in a multi-realm environment.
+     * @param t               the Throwable thrown by the Realm during the attempt, or {@code null} if the method returned normally.
+     * @return true if other realms should be consulted for authentication, false otherwise.
+     * @throws AuthenticationException an exception thrown by the Strategy implementation if it wishes the login process
+     *                                 for the associated subject (user) to stop immediately.
+     * @since 1.3
+     */
+    boolean continueAfterAttempt( AuthenticationInfo singleRealmInfo, AuthenticationInfo aggregateInfo, Throwable t );
+
+    /**
      * Method invoked by the ModularAuthenticator signifying that all of its configured Realms have been consulted
      * for account data, allowing post-proccessing after all realms have completed.
      *
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 fc66714..ae2ef21 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
@@ -45,6 +45,10 @@
         return null;
     }
 
+    public boolean continueAfterAttempt( AuthenticationInfo singleRealmInfo, AuthenticationInfo aggregateInfo, Throwable t ) {
+        return !( aggregateInfo != null && aggregateInfo == singleRealmInfo );
+    }
+
     /**
      * Returns the specified {@code aggregate} instance if is non null and valid (that is, has principals and they are
      * not empty) immediately, or, if it is null or not valid, the {@code info} argument is returned instead.
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 3632431..c7a9295 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
@@ -227,6 +227,12 @@
 
                 aggregate = strategy.afterAttempt(realm, token, info, aggregate, t);
 
+                // check if we should check the next realm, or just stop here.
+                if(!strategy.continueAfterAttempt( info, aggregate, t )) {
+                    log.trace( "Will not consult any other realms for authentication, last realm [{}].", realm );
+                    break;
+                }
+
             } else {
                 log.debug("Realm [{}] does not support token {}.  Skipping realm.", realm, token);
             }
diff --git a/core/src/test/groovy/org/apache/shiro/authc/pam/ModularRealmAuthenticatorTest.groovy b/core/src/test/groovy/org/apache/shiro/authc/pam/ModularRealmAuthenticatorTest.groovy
index 9e240a5..49bd080 100644
--- a/core/src/test/groovy/org/apache/shiro/authc/pam/ModularRealmAuthenticatorTest.groovy
+++ b/core/src/test/groovy/org/apache/shiro/authc/pam/ModularRealmAuthenticatorTest.groovy
@@ -22,6 +22,7 @@
 import org.apache.shiro.subject.PrincipalCollection
 import org.apache.shiro.authc.*
 import static org.easymock.EasyMock.*
+import static org.easymock.EasyMock.same
 
 /**
  * Unit tests for the {@link ModularRealmAuthenticator} implementation.
@@ -132,11 +133,13 @@
         expect(realm1.supports(same(token))).andReturn true
         expect(realm1.getAuthenticationInfo(same(token))).andReturn realm1Info
         expect(strategy.afterAttempt(same(realm1), same(token), same(realm1Info), same(aggregate), isNull(Throwable))).andReturn aggregate
+        expect(strategy.continueAfterAttempt(same(realm1Info), same(aggregate), isNull(Throwable))).andReturn true
 
         expect(strategy.beforeAttempt(same(realm2), same(token), same(aggregate))).andReturn aggregate
         expect(realm2.supports(same(token))).andReturn true
         expect(realm2.getAuthenticationInfo(same(token))).andReturn realm2Info
         expect(strategy.afterAttempt(same(realm2), same(token), same(realm2Info), same(aggregate), isNull(Throwable))).andReturn aggregate
+        expect(strategy.continueAfterAttempt(same(realm2Info), same(aggregate), isNull(Throwable))).andReturn true
 
         expect(strategy.afterAllAttempts(same(token), same(aggregate))).andReturn aggregate
 
@@ -170,11 +173,13 @@
         expect(realm1.supports(same(token))).andReturn true
         expect(realm1.getAuthenticationInfo(same(token))).andReturn realm1Info
         expect(strategy.afterAttempt(same(realm1), same(token), same(realm1Info), same(aggregate), isNull(Throwable))).andReturn aggregate
+        expect(strategy.continueAfterAttempt(same(realm1Info), same(aggregate), isNull(Throwable))).andReturn true
 
         expect(strategy.beforeAttempt(same(realm2), same(token), same(aggregate))).andReturn aggregate
         expect(realm2.supports(same(token))).andReturn true
         expect(realm2.getAuthenticationInfo(same(token))).andThrow authcException
         expect(strategy.afterAttempt(same(realm2), same(token), isNull(AuthenticationInfo), same(aggregate), same(authcException))).andReturn aggregate
+        expect(strategy.continueAfterAttempt(isNull(AuthenticationInfo), same(aggregate), same(authcException))).andReturn true
 
         expect(strategy.afterAllAttempts(same(token), same(aggregate))).andReturn aggregate
 
@@ -190,6 +195,47 @@
         verify realm1, realm1Info, realm2, token, aggregate, strategy
     }
 
+    void testMultiRealmAuthenticationStrategyDoesNotContinue() {
+
+      def realm1 = createStrictMock(Realm)
+      def realm2 = createStrictMock(Realm)
+      def realm2Info = createStrictMock(AuthenticationInfo)
+      def realm3 = createStrictMock(Realm)
+      def realms = [realm1, realm2, realm3]
+      def token = createStrictMock(AuthenticationToken)
+      def strategy = createStrictMock(AuthenticationStrategy)
+      def aggregate = createStrictMock(AuthenticationInfo)
+
+      expect(strategy.beforeAllAttempts(same(realms), same(token))).andReturn aggregate
+
+      expect(strategy.beforeAttempt(same(realm1), same(token), same(aggregate))).andReturn aggregate
+      expect(realm1.supports(same(token))).andReturn true
+      expect(realm1.getAuthenticationInfo(same(token))).andReturn null
+      expect(strategy.afterAttempt(same(realm1), same(token), isNull(AuthenticationInfo), same(aggregate), isNull(Throwable))).andReturn aggregate
+      expect(strategy.continueAfterAttempt(isNull(AuthenticationInfo), same(aggregate), isNull(Throwable))).andReturn true
+
+      expect(strategy.beforeAttempt(same(realm2), same(token), same(aggregate))).andReturn aggregate
+      expect(realm2.supports(same(token))).andReturn true
+      expect(realm2.getAuthenticationInfo(same(token))).andReturn realm2Info
+      expect(strategy.afterAttempt(same(realm2), same(token), same(realm2Info), same(aggregate),  isNull(Throwable))).andReturn aggregate
+      expect(strategy.continueAfterAttempt(same(realm2Info), same(aggregate), isNull(Throwable))).andReturn false
+
+      // realm 3 should never be called
+
+      expect(strategy.afterAllAttempts(same(token), same(aggregate))).andReturn aggregate
+
+
+      replay realm1, realm2, realm2Info, realm3, token, strategy, aggregate
+
+      ModularRealmAuthenticator mra = new ModularRealmAuthenticator()
+      mra.setAuthenticationStrategy(strategy)
+      mra.realms = realms
+
+      assertSame aggregate, mra.doAuthenticate(token)
+
+      verify realm1, realm2, realm2Info, realm3, token, strategy, aggregate
+
+    }
     void testOnLogout() {
 
         def realm = createStrictMock(LogoutAwareRealm)
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
new file mode 100644
index 0000000..cb2dc7a
--- /dev/null
+++ b/core/src/test/java/org/apache/shiro/authc/pam/FirstSuccessfulStrategyTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.AuthenticationInfo;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.easymock.EasyMock.*;
+import static org.junit.Assert.*;
+
+/**
+ * Tests for {@code FirstSuccessfulStrategy}
+ * @since 1.3
+ */
+public class FirstSuccessfulStrategyTest
+{
+
+    private FirstSuccessfulStrategy strategy;
+
+    @Before
+    public void setUp() {
+        strategy = new FirstSuccessfulStrategy();
+    }
+
+    @Test
+    public void beforeAllAttempts() {
+        AuthenticationInfo info = strategy.beforeAllAttempts(null, null);
+        assertNull( info );
+    }
+
+    @Test
+    public void afterAttempt() {
+        
+        AuthenticationInfo authInfo = createNiceMock(AuthenticationInfo.class);
+        AuthenticationInfo otherAuthInfo = createNiceMock(AuthenticationInfo.class);
+        
+        // same auth info for both the singleRealmInfo and the aggregate
+        assertFalse(strategy.continueAfterAttempt(authInfo, authInfo, null));
+        
+        // both null
+        assertTrue(strategy.continueAfterAttempt(null, null, null));
+        
+        // singleRealm not null, aggregate null (not valid condition, but make sure it returns true)
+        assertTrue(strategy.continueAfterAttempt(authInfo, null, null));
+
+        // single realm null, aggregate not null (the ModularRealmAuthenticator will not get into this state)
+        assertTrue(strategy.continueAfterAttempt(null, authInfo, null));
+        
+        // single realm and aggregate have different authInfo
+        assertTrue(strategy.continueAfterAttempt(authInfo, otherAuthInfo, null));
+    }
+}