Coverage Report - org.acegisecurity.providers.ProviderManager
 
Classes in this File Line Coverage Branch Coverage Complexity
ProviderManager
91% 
93% 
2.429
 
 1  
 /* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
 2  
  *
 3  
  * Licensed under the Apache License, Version 2.0 (the "License");
 4  
  * you may not use this file except in compliance with the License.
 5  
  * You may obtain a copy of the License at
 6  
  *
 7  
  *     http://www.apache.org/licenses/LICENSE-2.0
 8  
  *
 9  
  * Unless required by applicable law or agreed to in writing, software
 10  
  * distributed under the License is distributed on an "AS IS" BASIS,
 11  
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12  
  * See the License for the specific language governing permissions and
 13  
  * limitations under the License.
 14  
  */
 15  
 
 16  
 package org.acegisecurity.providers;
 17  
 
 18  
 import org.acegisecurity.AbstractAuthenticationManager;
 19  
 import org.acegisecurity.AccountExpiredException;
 20  
 import org.acegisecurity.AcegiMessageSource;
 21  
 import org.acegisecurity.Authentication;
 22  
 import org.acegisecurity.AuthenticationException;
 23  
 import org.acegisecurity.AuthenticationServiceException;
 24  
 import org.acegisecurity.BadCredentialsException;
 25  
 import org.acegisecurity.CredentialsExpiredException;
 26  
 import org.acegisecurity.DisabledException;
 27  
 import org.acegisecurity.LockedException;
 28  
 
 29  
 import org.acegisecurity.concurrent.ConcurrentLoginException;
 30  
 import org.acegisecurity.concurrent.ConcurrentSessionController;
 31  
 import org.acegisecurity.concurrent.NullConcurrentSessionController;
 32  
 
 33  
 import org.acegisecurity.event.authentication.AbstractAuthenticationEvent;
 34  
 import org.acegisecurity.event.authentication.AuthenticationFailureBadCredentialsEvent;
 35  
 import org.acegisecurity.event.authentication.AuthenticationFailureConcurrentLoginEvent;
 36  
 import org.acegisecurity.event.authentication.AuthenticationFailureCredentialsExpiredEvent;
 37  
 import org.acegisecurity.event.authentication.AuthenticationFailureDisabledEvent;
 38  
 import org.acegisecurity.event.authentication.AuthenticationFailureExpiredEvent;
 39  
 import org.acegisecurity.event.authentication.AuthenticationFailureLockedEvent;
 40  
 import org.acegisecurity.event.authentication.AuthenticationFailureProviderNotFoundEvent;
 41  
 import org.acegisecurity.event.authentication.AuthenticationFailureProxyUntrustedEvent;
 42  
 import org.acegisecurity.event.authentication.AuthenticationFailureServiceExceptionEvent;
 43  
 import org.acegisecurity.event.authentication.AuthenticationSuccessEvent;
 44  
 
 45  
 import org.acegisecurity.providers.cas.ProxyUntrustedException;
 46  
 
 47  
 import org.acegisecurity.userdetails.UsernameNotFoundException;
 48  
 
 49  
 import org.apache.commons.logging.Log;
 50  
 import org.apache.commons.logging.LogFactory;
 51  
 
 52  
 import org.springframework.beans.factory.InitializingBean;
 53  
 
 54  
 import org.springframework.context.ApplicationEvent;
 55  
 import org.springframework.context.ApplicationEventPublisher;
 56  
 import org.springframework.context.ApplicationEventPublisherAware;
 57  
 import org.springframework.context.MessageSource;
 58  
 import org.springframework.context.MessageSourceAware;
 59  
 import org.springframework.context.support.MessageSourceAccessor;
 60  
 
 61  
 import org.springframework.util.Assert;
 62  
 
 63  
 import java.lang.reflect.Constructor;
 64  
 import java.lang.reflect.InvocationTargetException;
 65  
 
 66  
 import java.util.Iterator;
 67  
 import java.util.List;
 68  
 import java.util.Properties;
 69  
 
 70  
 
 71  
 /**
 72  
  * Iterates an {@link Authentication} request through a list of {@link AuthenticationProvider}s.
 73  
  *
 74  
  * Can optionally be configured with a {@link ConcurrentSessionController} to limit the number of sessions a user can
 75  
  * have.
 76  
  * <p>
 77  
  * <code>AuthenticationProvider</code>s are tried in order until one provides a non-null response.
 78  
  * A non-null response indicates the provider had authority to decide on the authentication request and no further
 79  
  * providers are tried. If an <code>AuthenticationException</code> is thrown by a provider, it is retained until
 80  
  * subsequent providers are tried. If a subsequent provider successfully authenticates the request, the earlier
 81  
  * authentication exception is disregarded and the successful authentication will be used. If no subsequent provider
 82  
  * provides a non-null response, or a new <code>AuthenticationException</code>, the last
 83  
  * <code>AuthenticationException</code> received will be used. If no provider returns a non-null response, or indicates
 84  
  * it can even process an <code>Authentication</code>, the <code>ProviderManager</code> will throw a
 85  
  * <code>ProviderNotFoundException</code>.</p>
 86  
  *
 87  
  * <p>If a valid <code>Authentication</code> is returned by an <code>AuthenticationProvider</code>, the
 88  
  * <code>ProviderManager</code> will publish an {@link
 89  
  * org.acegisecurity.event.authentication.AuthenticationSuccessEvent}. If an <code>AuthenticationException</code> is
 90  
  * detected, the final <code>AuthenticationException</code> thrown will be used to publish an appropriate failure
 91  
  * event. By default <code>ProviderManager</code> maps common exceptions to events, but this can be fine-tuned by
 92  
  * providing a new <code>exceptionMappings</code><code>java.util.Properties</code> object. In the properties object,
 93  
  * each of the keys represent the fully qualified classname of the exception, and each of the values represent the
 94  
  * name of an event class which subclasses {@link
 95  
  * org.acegisecurity.event.authentication.AbstractAuthenticationFailureEvent} and provides its constructor.</p>
 96  
  *
 97  
  * @see ConcurrentSessionController
 98  
  */
 99  
 public class ProviderManager extends AbstractAuthenticationManager implements InitializingBean,
 100  
     ApplicationEventPublisherAware, MessageSourceAware {
 101  
     //~ Static fields/initializers =====================================================================================
 102  
 
 103  25
     private static final Log logger = LogFactory.getLog(ProviderManager.class);
 104  1
     private static final Properties DEFAULT_EXCEPTION_MAPPINGS = new Properties();
 105  
 
 106  
     //~ Instance fields ================================================================================================
 107  
 
 108  
     private ApplicationEventPublisher applicationEventPublisher;
 109  21
     private ConcurrentSessionController sessionController = new NullConcurrentSessionController();
 110  
     private List providers;
 111  21
     protected MessageSourceAccessor messages = AcegiMessageSource.getAccessor();
 112  21
     private Properties exceptionMappings = new Properties();
 113  21
     private Properties additionalExceptionMappings = new Properties();
 114  
 
 115  
     static {
 116  1
         DEFAULT_EXCEPTION_MAPPINGS.put(AccountExpiredException.class.getName(),
 117  
                 AuthenticationFailureExpiredEvent.class.getName());
 118  1
         DEFAULT_EXCEPTION_MAPPINGS.put(AuthenticationServiceException.class.getName(),
 119  
                 AuthenticationFailureServiceExceptionEvent.class.getName());
 120  1
         DEFAULT_EXCEPTION_MAPPINGS.put(LockedException.class.getName(),
 121  
                 AuthenticationFailureLockedEvent.class.getName());
 122  1
         DEFAULT_EXCEPTION_MAPPINGS.put(CredentialsExpiredException.class.getName(),
 123  
                 AuthenticationFailureCredentialsExpiredEvent.class.getName());
 124  1
         DEFAULT_EXCEPTION_MAPPINGS.put(DisabledException.class.getName(),
 125  
                 AuthenticationFailureDisabledEvent.class.getName());
 126  1
         DEFAULT_EXCEPTION_MAPPINGS.put(BadCredentialsException.class.getName(),
 127  
                 AuthenticationFailureBadCredentialsEvent.class.getName());
 128  1
         DEFAULT_EXCEPTION_MAPPINGS.put(UsernameNotFoundException.class.getName(),
 129  
                 AuthenticationFailureBadCredentialsEvent.class.getName());
 130  1
         DEFAULT_EXCEPTION_MAPPINGS.put(ConcurrentLoginException.class.getName(),
 131  
                 AuthenticationFailureConcurrentLoginEvent.class.getName());
 132  1
         DEFAULT_EXCEPTION_MAPPINGS.put(ProviderNotFoundException.class.getName(),
 133  
                 AuthenticationFailureProviderNotFoundEvent.class.getName());
 134  1
         DEFAULT_EXCEPTION_MAPPINGS.put(ProxyUntrustedException.class.getName(),
 135  
                 AuthenticationFailureProxyUntrustedEvent.class.getName());
 136  1
     }
 137  
 
 138  21
     public ProviderManager() {
 139  21
         exceptionMappings.putAll(DEFAULT_EXCEPTION_MAPPINGS);
 140  21
     }
 141  
 
 142  
     //~ Methods ========================================================================================================
 143  
 
 144  
     public void afterPropertiesSet() throws Exception {
 145  18
         checkIfValidList(this.providers);
 146  17
         Assert.notNull(this.messages, "A message source must be set");
 147  17
         exceptionMappings.putAll(additionalExceptionMappings);
 148  17
         doAddExtraDefaultExceptionMappings(exceptionMappings);
 149  17
     }
 150  
 
 151  
     private void checkIfValidList(List listToCheck) {
 152  37
         if ((listToCheck == null) || (listToCheck.size() == 0)) {
 153  2
             throw new IllegalArgumentException("A list of AuthenticationManagers is required");
 154  
         }
 155  35
     }
 156  
 
 157  
     /**
 158  
      * Provided so subclasses can add extra exception mappings during startup if no exception mappings are
 159  
      * injected by the IoC container.
 160  
      *
 161  
      * @param exceptionMappings the properties object, which already has entries in it
 162  
      * @deprecated This method has been removed from the 2.0 series; please use the
 163  
      *  {@link #additionalExceptionMappings} property instead to inject additional exception
 164  
      *  to event mappings
 165  
      */
 166  17
     protected void doAddExtraDefaultExceptionMappings(Properties exceptionMappings) {}
 167  
 
 168  
     /**
 169  
      * Attempts to authenticate the passed {@link Authentication} object.<p>The list of {@link
 170  
      * AuthenticationProvider}s will be successively tried until an <code>AuthenticationProvider</code> indicates it
 171  
      * is  capable of authenticating the type of <code>Authentication</code> object passed. Authentication will then
 172  
      * be attempted with that <code>AuthenticationProvider</code>.</p>
 173  
      *  <p>If more than one <code>AuthenticationProvider</code> supports the passed <code>Authentication</code>
 174  
      * object, only the first <code>AuthenticationProvider</code> tried will determine the result. No subsequent
 175  
      * <code>AuthenticationProvider</code>s will be tried.</p>
 176  
      *
 177  
      * @param authentication the authentication request object.
 178  
      *
 179  
      * @return a fully authenticated object including credentials.
 180  
      *
 181  
      * @throws AuthenticationException if authentication fails.
 182  
      */
 183  
     public Authentication doAuthentication(Authentication authentication)
 184  
         throws AuthenticationException {
 185  13
         Iterator iter = providers.iterator();
 186  
 
 187  13
         Class toTest = authentication.getClass();
 188  
 
 189  13
         AuthenticationException lastException = null;
 190  
 
 191  19
         while (iter.hasNext()) {
 192  14
             AuthenticationProvider provider = (AuthenticationProvider) iter.next();
 193  
 
 194  14
             if (provider.supports(toTest)) {
 195  13
                 logger.debug("Authentication attempt using " + provider.getClass().getName());
 196  
 
 197  13
                 Authentication result = null;
 198  
 
 199  
                 try {
 200  13
                     result = provider.authenticate(authentication);
 201  9
                     copyDetails(authentication, result);
 202  9
                     sessionController.checkAuthenticationAllowed(result);
 203  4
                 } catch (AuthenticationException ae) {
 204  4
                     lastException = ae;
 205  4
                     result = null;
 206  9
                 }
 207  
 
 208  13
                 if (result != null) {
 209  8
                     sessionController.registerSuccessfulAuthentication(result);
 210  8
                     publishEvent(new AuthenticationSuccessEvent(result));
 211  
 
 212  8
                     return result;
 213  
                 }
 214  
             }
 215  6
         }
 216  
 
 217  5
         if (lastException == null) {
 218  1
             lastException = new ProviderNotFoundException(messages.getMessage("ProviderManager.providerNotFound",
 219  
                         new Object[] {toTest.getName()}, "No AuthenticationProvider found for {0}"));
 220  
         }
 221  
 
 222  
         // Publish the event
 223  5
         String className = exceptionMappings.getProperty(lastException.getClass().getName());
 224  5
         AbstractAuthenticationEvent event = null;
 225  
 
 226  5
         if (className != null) {
 227  
             try {
 228  5
                 Class clazz = getClass().getClassLoader().loadClass(className);
 229  5
                 Constructor constructor = clazz.getConstructor(new Class[] {
 230  
                             Authentication.class, AuthenticationException.class
 231  
                         });
 232  5
                 Object obj = constructor.newInstance(new Object[] {authentication, lastException});
 233  5
                 Assert.isInstanceOf(AbstractAuthenticationEvent.class, obj, "Must be an AbstractAuthenticationEvent");
 234  5
                 event = (AbstractAuthenticationEvent) obj;
 235  0
             } catch (ClassNotFoundException ignored) {}
 236  0
             catch (NoSuchMethodException ignored) {}
 237  0
             catch (IllegalAccessException ignored) {}
 238  0
             catch (InstantiationException ignored) {}
 239  5
             catch (InvocationTargetException ignored) {}
 240  
         }
 241  
 
 242  5
         if (event != null) {
 243  5
             publishEvent(event);
 244  
         } else {
 245  0
             if (logger.isDebugEnabled()) {
 246  0
                 logger.debug("No event was found for the exception " + lastException.getClass().getName());
 247  
             }
 248  
         }
 249  
 
 250  
         // Throw the exception
 251  5
         throw lastException;
 252  
     }
 253  
 
 254  
     /**
 255  
      * Copies the authentication details from a source Authentication object to a destination one, provided the
 256  
      * latter does not already have one set.
 257  
      *
 258  
      * @param source source authentication
 259  
      * @param dest the destination authentication object
 260  
      */
 261  
     private void copyDetails(Authentication source, Authentication dest) {
 262  9
         if ((dest instanceof AbstractAuthenticationToken) && (dest.getDetails() == null)) {
 263  6
             AbstractAuthenticationToken token = (AbstractAuthenticationToken) dest;
 264  
 
 265  6
             token.setDetails(source.getDetails());
 266  
         }
 267  9
     }
 268  
 
 269  
     public List getProviders() {
 270  1
         return this.providers;
 271  
     }
 272  
 
 273  
     /**
 274  
      * The configured {@link ConcurrentSessionController} is returned or the {@link
 275  
      * NullConcurrentSessionController} if a specific one has not been set.
 276  
      *
 277  
      * @return {@link ConcurrentSessionController} instance
 278  
      */
 279  
     public ConcurrentSessionController getSessionController() {
 280  3
         return sessionController;
 281  
     }
 282  
 
 283  
     public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
 284  16
         this.applicationEventPublisher = applicationEventPublisher;
 285  16
     }
 286  
 
 287  
     public void setMessageSource(MessageSource messageSource) {
 288  1
         this.messages = new MessageSourceAccessor(messageSource);
 289  1
     }
 290  
 
 291  
     /**
 292  
      * Sets the {@link AuthenticationProvider} objects to be used for authentication.
 293  
      *
 294  
      * @param newList
 295  
      *
 296  
      * @throws IllegalArgumentException DOCUMENT ME!
 297  
      */
 298  
     public void setProviders(List newList) {
 299  19
         checkIfValidList(newList);
 300  
 
 301  18
         Iterator iter = newList.iterator();
 302  
 
 303  36
         while (iter.hasNext()) {
 304  19
             Object currentObject = iter.next();
 305  19
             Assert.isInstanceOf(AuthenticationProvider.class, currentObject,
 306  
                     "Can only provide AuthenticationProvider instances");
 307  18
         }
 308  
 
 309  17
         this.providers = newList;
 310  17
     }
 311  
 
 312  
     /**
 313  
      * Set the {@link ConcurrentSessionController} to be used for limiting user's sessions.  The {@link
 314  
      * NullConcurrentSessionController} is used by default
 315  
      *
 316  
      * @param sessionController {@link ConcurrentSessionController}
 317  
      */
 318  
     public void setSessionController(ConcurrentSessionController sessionController) {
 319  1
         this.sessionController = sessionController;
 320  1
     }
 321  
 
 322  
     private void publishEvent(ApplicationEvent event) {
 323  13
         if (applicationEventPublisher != null) {
 324  13
             applicationEventPublisher.publishEvent(event);
 325  
         }
 326  13
     }
 327  
     
 328  
     /**
 329  
      * Sets additional exception to event mappings. These are automatically merged with the default
 330  
      * exception to event mappings that <code>ProviderManager</code> defines.
 331  
      * 
 332  
      * @param additionalExceptionMappings where keys are the fully-qualified string name of the
 333  
      * exception class and the values are the fully-qualified string name of the event class to fire
 334  
      */
 335  
         public void setAdditionalExceptionMappings(
 336  
                         Properties additionalExceptionMappings) {
 337  0
                 this.additionalExceptionMappings = additionalExceptionMappings;
 338  0
         }
 339  
     
 340  
 }