001: /* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
002: *
003: * Licensed under the Apache License, Version 2.0 (the "License");
004: * you may not use this file except in compliance with the License.
005: * You may obtain a copy of the License at
006: *
007: * http://www.apache.org/licenses/LICENSE-2.0
008: *
009: * Unless required by applicable law or agreed to in writing, software
010: * distributed under the License is distributed on an "AS IS" BASIS,
011: * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
012: * See the License for the specific language governing permissions and
013: * limitations under the License.
014: */
015:
016: package org.acegisecurity.providers;
017:
018: import org.acegisecurity.AbstractAuthenticationManager;
019: import org.acegisecurity.AccountExpiredException;
020: import org.acegisecurity.AcegiMessageSource;
021: import org.acegisecurity.Authentication;
022: import org.acegisecurity.AuthenticationException;
023: import org.acegisecurity.AuthenticationServiceException;
024: import org.acegisecurity.BadCredentialsException;
025: import org.acegisecurity.CredentialsExpiredException;
026: import org.acegisecurity.DisabledException;
027: import org.acegisecurity.LockedException;
028:
029: import org.acegisecurity.concurrent.ConcurrentLoginException;
030: import org.acegisecurity.concurrent.ConcurrentSessionController;
031: import org.acegisecurity.concurrent.NullConcurrentSessionController;
032:
033: import org.acegisecurity.event.authentication.AbstractAuthenticationEvent;
034: import org.acegisecurity.event.authentication.AuthenticationFailureBadCredentialsEvent;
035: import org.acegisecurity.event.authentication.AuthenticationFailureConcurrentLoginEvent;
036: import org.acegisecurity.event.authentication.AuthenticationFailureCredentialsExpiredEvent;
037: import org.acegisecurity.event.authentication.AuthenticationFailureDisabledEvent;
038: import org.acegisecurity.event.authentication.AuthenticationFailureExpiredEvent;
039: import org.acegisecurity.event.authentication.AuthenticationFailureLockedEvent;
040: import org.acegisecurity.event.authentication.AuthenticationFailureProviderNotFoundEvent;
041: import org.acegisecurity.event.authentication.AuthenticationFailureProxyUntrustedEvent;
042: import org.acegisecurity.event.authentication.AuthenticationFailureServiceExceptionEvent;
043: import org.acegisecurity.event.authentication.AuthenticationSuccessEvent;
044:
045: import org.acegisecurity.providers.cas.ProxyUntrustedException;
046:
047: import org.acegisecurity.userdetails.UsernameNotFoundException;
048:
049: import org.apache.commons.logging.Log;
050: import org.apache.commons.logging.LogFactory;
051:
052: import org.springframework.beans.factory.InitializingBean;
053:
054: import org.springframework.context.ApplicationEvent;
055: import org.springframework.context.ApplicationEventPublisher;
056: import org.springframework.context.ApplicationEventPublisherAware;
057: import org.springframework.context.MessageSource;
058: import org.springframework.context.MessageSourceAware;
059: import org.springframework.context.support.MessageSourceAccessor;
060:
061: import org.springframework.util.Assert;
062:
063: import java.lang.reflect.Constructor;
064: import java.lang.reflect.InvocationTargetException;
065:
066: import java.util.Iterator;
067: import java.util.List;
068: import java.util.Properties;
069:
070: /**
071: * Iterates an {@link Authentication} request through a list of {@link AuthenticationProvider}s.
072: *
073: * Can optionally be configured with a {@link ConcurrentSessionController} to limit the number of sessions a user can
074: * have.
075: * <p>
076: * <code>AuthenticationProvider</code>s are tried in order until one provides a non-null response.
077: * A non-null response indicates the provider had authority to decide on the authentication request and no further
078: * providers are tried. If an <code>AuthenticationException</code> is thrown by a provider, it is retained until
079: * subsequent providers are tried. If a subsequent provider successfully authenticates the request, the earlier
080: * authentication exception is disregarded and the successful authentication will be used. If no subsequent provider
081: * provides a non-null response, or a new <code>AuthenticationException</code>, the last
082: * <code>AuthenticationException</code> received will be used. If no provider returns a non-null response, or indicates
083: * it can even process an <code>Authentication</code>, the <code>ProviderManager</code> will throw a
084: * <code>ProviderNotFoundException</code>.</p>
085: *
086: * <p>If a valid <code>Authentication</code> is returned by an <code>AuthenticationProvider</code>, the
087: * <code>ProviderManager</code> will publish an {@link
088: * org.acegisecurity.event.authentication.AuthenticationSuccessEvent}. If an <code>AuthenticationException</code> is
089: * detected, the final <code>AuthenticationException</code> thrown will be used to publish an appropriate failure
090: * event. By default <code>ProviderManager</code> maps common exceptions to events, but this can be fine-tuned by
091: * providing a new <code>exceptionMappings</code><code>java.util.Properties</code> object. In the properties object,
092: * each of the keys represent the fully qualified classname of the exception, and each of the values represent the
093: * name of an event class which subclasses {@link
094: * org.acegisecurity.event.authentication.AbstractAuthenticationFailureEvent} and provides its constructor.</p>
095: *
096: * @see ConcurrentSessionController
097: */
098: public class ProviderManager extends AbstractAuthenticationManager
099: implements InitializingBean, ApplicationEventPublisherAware,
100: MessageSourceAware {
101: //~ Static fields/initializers =====================================================================================
102:
103: private static final Log logger = LogFactory
104: .getLog(ProviderManager.class);
105: private static final Properties DEFAULT_EXCEPTION_MAPPINGS = new Properties();
106:
107: //~ Instance fields ================================================================================================
108:
109: private ApplicationEventPublisher applicationEventPublisher;
110: private ConcurrentSessionController sessionController = new NullConcurrentSessionController();
111: private List providers;
112: protected MessageSourceAccessor messages = AcegiMessageSource
113: .getAccessor();
114: private Properties exceptionMappings = new Properties();
115:
116: static {
117: DEFAULT_EXCEPTION_MAPPINGS.put(AccountExpiredException.class
118: .getName(), AuthenticationFailureExpiredEvent.class
119: .getName());
120: DEFAULT_EXCEPTION_MAPPINGS.put(
121: AuthenticationServiceException.class.getName(),
122: AuthenticationFailureServiceExceptionEvent.class
123: .getName());
124: DEFAULT_EXCEPTION_MAPPINGS.put(LockedException.class.getName(),
125: AuthenticationFailureLockedEvent.class.getName());
126: DEFAULT_EXCEPTION_MAPPINGS.put(
127: CredentialsExpiredException.class.getName(),
128: AuthenticationFailureCredentialsExpiredEvent.class
129: .getName());
130: DEFAULT_EXCEPTION_MAPPINGS.put(DisabledException.class
131: .getName(), AuthenticationFailureDisabledEvent.class
132: .getName());
133: DEFAULT_EXCEPTION_MAPPINGS.put(BadCredentialsException.class
134: .getName(),
135: AuthenticationFailureBadCredentialsEvent.class
136: .getName());
137: DEFAULT_EXCEPTION_MAPPINGS.put(UsernameNotFoundException.class
138: .getName(),
139: AuthenticationFailureBadCredentialsEvent.class
140: .getName());
141: DEFAULT_EXCEPTION_MAPPINGS.put(ConcurrentLoginException.class
142: .getName(),
143: AuthenticationFailureConcurrentLoginEvent.class
144: .getName());
145: DEFAULT_EXCEPTION_MAPPINGS.put(ProviderNotFoundException.class
146: .getName(),
147: AuthenticationFailureProviderNotFoundEvent.class
148: .getName());
149: DEFAULT_EXCEPTION_MAPPINGS.put(ProxyUntrustedException.class
150: .getName(),
151: AuthenticationFailureProxyUntrustedEvent.class
152: .getName());
153: }
154:
155: public ProviderManager() {
156: exceptionMappings.putAll(DEFAULT_EXCEPTION_MAPPINGS);
157: }
158:
159: //~ Methods ========================================================================================================
160:
161: public void afterPropertiesSet() throws Exception {
162: checkIfValidList(this .providers);
163: Assert.notNull(this .messages, "A message source must be set");
164: doAddExtraDefaultExceptionMappings(exceptionMappings);
165: }
166:
167: private void checkIfValidList(List listToCheck) {
168: if ((listToCheck == null) || (listToCheck.size() == 0)) {
169: throw new IllegalArgumentException(
170: "A list of AuthenticationManagers is required");
171: }
172: }
173:
174: /**
175: * Provided so subclasses can add extra exception mappings during startup if no exception mappings are
176: * injected by the IoC container.
177: *
178: * @param exceptionMappings the properties object, which already has entries in it
179: */
180: protected void doAddExtraDefaultExceptionMappings(
181: Properties exceptionMappings) {
182: }
183:
184: /**
185: * Attempts to authenticate the passed {@link Authentication} object.<p>The list of {@link
186: * AuthenticationProvider}s will be successively tried until an <code>AuthenticationProvider</code> indicates it
187: * is capable of authenticating the type of <code>Authentication</code> object passed. Authentication will then
188: * be attempted with that <code>AuthenticationProvider</code>.</p>
189: * <p>If more than one <code>AuthenticationProvider</code> supports the passed <code>Authentication</code>
190: * object, only the first <code>AuthenticationProvider</code> tried will determine the result. No subsequent
191: * <code>AuthenticationProvider</code>s will be tried.</p>
192: *
193: * @param authentication the authentication request object.
194: *
195: * @return a fully authenticated object including credentials.
196: *
197: * @throws AuthenticationException if authentication fails.
198: */
199: public Authentication doAuthentication(Authentication authentication)
200: throws AuthenticationException {
201: Iterator iter = providers.iterator();
202:
203: Class toTest = authentication.getClass();
204:
205: AuthenticationException lastException = null;
206:
207: while (iter.hasNext()) {
208: AuthenticationProvider provider = (AuthenticationProvider) iter
209: .next();
210:
211: if (provider.supports(toTest)) {
212: logger.debug("Authentication attempt using "
213: + provider.getClass().getName());
214:
215: Authentication result = null;
216:
217: try {
218: result = provider.authenticate(authentication);
219: copyDetails(authentication, result);
220: sessionController
221: .checkAuthenticationAllowed(result);
222: } catch (AuthenticationException ae) {
223: lastException = ae;
224: result = null;
225: }
226:
227: if (result != null) {
228: sessionController
229: .registerSuccessfulAuthentication(result);
230: publishEvent(new AuthenticationSuccessEvent(result));
231:
232: return result;
233: }
234: }
235: }
236:
237: if (lastException == null) {
238: lastException = new ProviderNotFoundException(messages
239: .getMessage("ProviderManager.providerNotFound",
240: new Object[] { toTest.getName() },
241: "No AuthenticationProvider found for {0}"));
242: }
243:
244: // Publish the event
245: String className = exceptionMappings.getProperty(lastException
246: .getClass().getName());
247: AbstractAuthenticationEvent event = null;
248:
249: if (className != null) {
250: try {
251: Class clazz = getClass().getClassLoader().loadClass(
252: className);
253: Constructor constructor = clazz
254: .getConstructor(new Class[] {
255: Authentication.class,
256: AuthenticationException.class });
257: Object obj = constructor.newInstance(new Object[] {
258: authentication, lastException });
259: Assert.isInstanceOf(AbstractAuthenticationEvent.class,
260: obj, "Must be an AbstractAuthenticationEvent");
261: event = (AbstractAuthenticationEvent) obj;
262: } catch (ClassNotFoundException ignored) {
263: } catch (NoSuchMethodException ignored) {
264: } catch (IllegalAccessException ignored) {
265: } catch (InstantiationException ignored) {
266: } catch (InvocationTargetException ignored) {
267: }
268: }
269:
270: if (event != null) {
271: publishEvent(event);
272: } else {
273: if (logger.isDebugEnabled()) {
274: logger.debug("No event was found for the exception "
275: + lastException.getClass().getName());
276: }
277: }
278:
279: // Throw the exception
280: throw lastException;
281: }
282:
283: /**
284: * Copies the authentication details from a source Authentication object to a destination one, provided the
285: * latter does not already have one set.
286: *
287: * @param source source authentication
288: * @param dest the destination authentication object
289: */
290: private void copyDetails(Authentication source, Authentication dest) {
291: if ((dest instanceof AbstractAuthenticationToken)
292: && (dest.getDetails() == null)) {
293: AbstractAuthenticationToken token = (AbstractAuthenticationToken) dest;
294:
295: token.setDetails(source.getDetails());
296: }
297: }
298:
299: public List getProviders() {
300: return this .providers;
301: }
302:
303: /**
304: * The configured {@link ConcurrentSessionController} is returned or the {@link
305: * NullConcurrentSessionController} if a specific one has not been set.
306: *
307: * @return {@link ConcurrentSessionController} instance
308: */
309: public ConcurrentSessionController getSessionController() {
310: return sessionController;
311: }
312:
313: public void setApplicationEventPublisher(
314: ApplicationEventPublisher applicationEventPublisher) {
315: this .applicationEventPublisher = applicationEventPublisher;
316: }
317:
318: public void setMessageSource(MessageSource messageSource) {
319: this .messages = new MessageSourceAccessor(messageSource);
320: }
321:
322: /**
323: * Sets the {@link AuthenticationProvider} objects to be used for authentication.
324: *
325: * @param newList
326: *
327: * @throws IllegalArgumentException DOCUMENT ME!
328: */
329: public void setProviders(List newList) {
330: checkIfValidList(newList);
331:
332: Iterator iter = newList.iterator();
333:
334: while (iter.hasNext()) {
335: Object currentObject = iter.next();
336: Assert
337: .isInstanceOf(AuthenticationProvider.class,
338: currentObject,
339: "Can only provide AuthenticationProvider instances");
340: }
341:
342: this .providers = newList;
343: }
344:
345: /**
346: * Set the {@link ConcurrentSessionController} to be used for limiting user's sessions. The {@link
347: * NullConcurrentSessionController} is used by default
348: *
349: * @param sessionController {@link ConcurrentSessionController}
350: */
351: public void setSessionController(
352: ConcurrentSessionController sessionController) {
353: this .sessionController = sessionController;
354: }
355:
356: private void publishEvent(ApplicationEvent event) {
357: if (applicationEventPublisher != null) {
358: applicationEventPublisher.publishEvent(event);
359: }
360: }
361: }
|