001    /*
002     * Copyright (c) 2002-2005 the original author or authors.
003     * 
004     * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005     * use this file except in compliance with the License. You may obtain a copy of
006     * the License at
007     * 
008     * http://www.apache.org/licenses/LICENSE-2.0
009     * 
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012     * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013     * License for the specific language governing permissions and limitations under
014     * the License.
015     */
016    package org.springframework.richclient.security.support;
017    
018    import java.util.Map;
019    
020    import org.apache.commons.logging.Log;
021    import org.apache.commons.logging.LogFactory;
022    import org.springframework.beans.factory.InitializingBean;
023    import org.springframework.context.ApplicationContext;
024    import org.springframework.richclient.application.Application;
025    import org.springframework.richclient.security.ApplicationSecurityManager;
026    import org.springframework.richclient.security.AuthenticationEvent;
027    import org.springframework.richclient.security.AuthenticationFailedEvent;
028    import org.springframework.richclient.security.LoginEvent;
029    import org.springframework.richclient.security.LogoutEvent;
030    import org.springframework.security.Authentication;
031    import org.springframework.security.AuthenticationManager;
032    import org.springframework.security.GrantedAuthority;
033    import org.springframework.security.SpringSecurityException;
034    import org.springframework.security.context.SecurityContextHolder;
035    import org.springframework.security.providers.AuthenticationProvider;
036    import org.springframework.security.providers.ProviderManager;
037    
038    /**
039     * Default implementation of ApplicationSecurityManager. It provides basic processing for
040     * login and logout actions and the event lifecycle.
041     * <p>
042     * Instances of this class should be configured with an instance of
043     * {@link org.springframework.security.AuthenticationManager} to be used to handle authentication
044     * (login) requests. This would be done like this:
045     * 
046     * <pre>
047     *         &lt;bean id=&quot;securityManager&quot;
048     *               class=&quot;org.springframework.richclient.security.support.DefaultApplicationSecurityManager&quot;&gt;
049     *            &lt;property name=&quot;authenticationManager&quot; ref=&quot;authenticationManager&quot;/&gt;
050     *         &lt;/bean&gt;
051     *         
052     *         &lt;bean id=&quot;authenticationManager&quot;
053     *           class=&quot;org.springframework.security.providers.ProviderManager&quot;&gt;
054     *           &lt;property name=&quot;providers&quot;&gt;
055     *               &lt;list&gt;
056     *                   &lt;ref bean=&quot;remoteAuthenticationProvider&quot; /&gt;
057     *               &lt;/list&gt;
058     *           &lt;/property&gt;
059     *       &lt;/bean&gt;
060     *       
061     *       &lt;bean id=&quot;remoteAuthenticationProvider&quot;
062     *           class=&quot;org.springframework.security.providers.rcp.RemoteAuthenticationProvider&quot;&gt;
063     *           &lt;property name=&quot;remoteAuthenticationManager&quot; ref=&quot;remoteAuthenticationManager&quot; /&gt;
064     *       &lt;/bean&gt;
065     *    
066     *       &lt;bean id=&quot;remoteAuthenticationManager&quot;
067     *           class=&quot;org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean&quot;&gt;
068     *           &lt;property name=&quot;serviceUrl&quot;&gt;
069     *               &lt;value&gt;http://localhost:8080/myserver/rootContext/RemoteAuthenticationManager&lt;/value&gt;
070     *           &lt;/property&gt;
071     *           &lt;property name=&quot;serviceInterface&quot;&gt;
072     *               &lt;value&gt;org.springframework.security.providers.rcp.RemoteAuthenticationManager&lt;/value&gt;
073     *           &lt;/property&gt;
074     *       &lt;/bean&gt;
075     * </pre>
076     * 
077     * If this is not done, then an attempt will be made to "auto-configure" by locating an
078     * appropriate authentication manager in the application context. In order, a search will
079     * be made for a bean that implements one of these classes:
080     * <ol>
081     * <li>ProviderManager</li>
082     * <li>AuthenticationProvider</li>
083     * <li>AuthenticationManager</li>
084     * </ol>
085     * The first instance to be located will be used to handle authentication requests.
086     * <p>
087     * 
088     * @author Larry Streepy
089     * 
090     */
091    public class DefaultApplicationSecurityManager implements ApplicationSecurityManager, InitializingBean {
092    
093        private final Log logger = LogFactory.getLog( getClass() );
094    
095        private AuthenticationManager authenticationManager = null;
096    
097        private Authentication currentAuthentication = null;
098    
099        /**
100         * Default constructor.
101         */
102        public DefaultApplicationSecurityManager() {
103            this( false );
104        }
105    
106        /**
107         * Constructor invoked when we are created as the default implementation by
108         * ApplicationServices. Since this bean won't be defined in the context under these
109         * circumstances, we need to perform some auto-configuration of our own.
110         * <p>
111         * Auto-configuration consists of trying to locate an AuthenticationManager (in one of
112         * several classes) in the application context. This auto-configuration is also
113         * attempted after the bean is constructed by the context if the authenticationManager
114         * property has not been set. See {@see #afterPropertiesSet()}.
115         * 
116         * @param autoConfigure pass true to perform auto-configuration
117         * @throws IllegalArgumentException If the auto-configuration fails
118         * @see #afterPropertiesSet()
119         */
120        public DefaultApplicationSecurityManager(boolean autoConfigure) {
121            if( autoConfigure ) {
122                afterPropertiesSet();
123            }
124        }
125    
126        /**
127         * Set the authentication manager to use.
128         * @param authenticationManager instance to use for authentication requests
129         */
130        public void setAuthenticationManager(AuthenticationManager authenticationManager) {
131            this.authenticationManager = authenticationManager;
132        }
133    
134        /**
135         * Get the authentication manager in use.
136         * @return authenticationManager instance used for authentication requests
137         */
138        public AuthenticationManager getAuthenticationManager() {
139            return authenticationManager;
140        }
141    
142        /**
143         * Process a login attempt and fire all related events. If the authentication fails,
144         * then a {@link AuthenticationFailedEvent} is published and the exception is
145         * rethrown. If the authentication succeeds, then an {@link AuthenticationEvent} is
146         * published, followed by a {@link LoginEvent}.
147         * 
148         * @param authentication token to use for the login attempt
149         * @return Authentication token resulting from a successful call to
150         *         {@link AuthenticationManager#authenticate(org.springframework.security.Authentication)}.
151         * @see org.springframework.richclient.security.ApplicationSecurityManager#doLogin(org.springframework.security.Authentication)
152         * @throws SpringSecurityException If the authentication attempt fails
153         */
154        public Authentication doLogin(Authentication authentication) {
155            final ApplicationContext appCtx = Application.instance().getApplicationContext();
156    
157            Authentication result = null;
158    
159            try {
160                result = getAuthenticationManager().authenticate( authentication );
161            } catch( SpringSecurityException e ) {
162                logger.info( "authentication failed: " + e.getMessage() );
163    
164                // Fire application event to advise of failed login
165                appCtx.publishEvent( new AuthenticationFailedEvent( authentication, e ) );
166    
167                // rethrow the exception
168                throw e;
169            }
170    
171            // Handle success or failure of the authentication attempt
172            if( logger.isDebugEnabled() ) {
173                logger.debug( "successful login - update context holder and fire event" );
174            }
175    
176            // Commit the successful Authentication object to the secure ContextHolder
177            SecurityContextHolder.getContext().setAuthentication( result );
178            setAuthentication( result );
179    
180            // Fire application events to advise of new login
181            appCtx.publishEvent( new AuthenticationEvent( result ) );
182            appCtx.publishEvent( new LoginEvent( result ) );
183    
184            return result;
185        }
186    
187        /**
188         * Return if a user is currently logged in, meaning that a previous call to doLogin
189         * resulted in a valid authentication request.
190         * @return true if a user is logged in
191         */
192        public boolean isUserLoggedIn() {
193            return getAuthentication() != null;
194        }
195    
196        /**
197         * Get the authentication token for the currently logged in user.
198         * @return authentication token, null if not logged in
199         */
200        public Authentication getAuthentication() {
201            return currentAuthentication;
202        }
203    
204        /**
205         * Set the authenticaiton token.
206         * @param authentication token to install as current.
207         */
208        protected void setAuthentication(Authentication authentication) {
209            currentAuthentication = authentication;
210        }
211    
212        /**
213         * Determine if the currently authenticated user has the role provided. Note that role
214         * comparisons are case sensitive.
215         * 
216         * @param role to check
217         * @return true if the user has the role requested
218         */
219        public boolean isUserInRole(String role) {
220            boolean inRole = false;
221    
222            Authentication authentication = getAuthentication();
223            if( authentication != null ) {
224                GrantedAuthority[] authorities = authentication.getAuthorities();
225                for( int i = 0; i < authorities.length; i++ ) {
226                    if( role.equals( authorities[i].getAuthority() ) ) {
227                        inRole = true;
228                        break;
229                    }
230                }
231            }
232            return inRole;
233        }
234    
235        /**
236         * Perform a logout. Set the current authentication token to null (in both the
237         * per-thread security context and the global context), then publish an
238         * {@link AuthenticationEvent} followed by a {@link LogoutEvent}.
239         * @return Authentication token that was in place prior to the logout.
240         * @see org.springframework.richclient.security.ApplicationSecurityManager#doLogout()
241         */
242        public Authentication doLogout() {
243            Authentication existing = getAuthentication();
244    
245            // Make the Authentication object null if a SecureContext exists
246            SecurityContextHolder.getContext().setAuthentication( null );
247            setAuthentication( null );
248    
249            // Fire application event to advise of logout
250            ApplicationContext appCtx = Application.instance().getApplicationContext();
251            appCtx.publishEvent( new AuthenticationEvent( null ) );
252            appCtx.publishEvent( new LogoutEvent( existing ) );
253    
254            return existing;
255        }
256    
257        /**
258         * Ensure that we have an authentication manager to work with. If one has not been
259         * specifically wired in, then look for beans to "auto-wire" in. Look for a bean of
260         * one of the following types (in order): {@link ProviderManager},
261         * {@link AuthenticationProvider}, and {@link AuthenticationManager}.
262         * 
263         * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
264         */
265        public void afterPropertiesSet() {
266            // Ensure that we have our authentication manager
267            if( authenticationManager == null ) {
268                if( logger.isDebugEnabled() ) {
269                    logger.debug( "No AuthenticationManager defined, look for one" );
270                }
271    
272                // Try the class types in sequence
273                Class[] types = new Class[] { ProviderManager.class, AuthenticationProvider.class,
274                        AuthenticationManager.class };
275    
276                for( int i = 0; i < types.length; i++ ) {
277                    if( tryToWire( types[i] ) ) {
278                        break;
279                    }
280                }
281            }
282    
283            // If we still don't have one, then that's it
284            if( authenticationManager == null ) {
285                throw new IllegalArgumentException( "authenticationManager must be defined" );
286            }
287        }
288    
289        /**
290         * Try to locate and "wire in" a suitable authentication manager.
291         * @param type The type of bean to look for
292         * @return true if we found and wired a suitable bean
293         */
294        protected boolean tryToWire(Class type) {
295            boolean success = false;
296            String className = type.getName();
297            Map map = Application.instance().getApplicationContext().getBeansOfType( type );
298            if( logger.isDebugEnabled() ) {
299                logger.debug( "Search for '" + className + "' found: " + map );
300            }
301    
302            if( map.size() == 1 ) {
303                // Got one - wire it in
304                Map.Entry entry = (Map.Entry) map.entrySet().iterator().next();
305                String name = (String) entry.getKey();
306                AuthenticationManager am = (AuthenticationManager) entry.getValue();
307    
308                setAuthenticationManager( am );
309                success = true;
310    
311                if( logger.isInfoEnabled() ) {
312                    logger.info( "Auto-configuration using '" + name + "' as authenticationManager" );
313                }
314            } else if( map.size() > 1 ) {
315                if( logger.isInfoEnabled() ) {
316                    logger.info( "Need a single '" + className + "', found: " + map.keySet() );
317                }
318            } else {
319                // Size 0, no potentials
320                if( logger.isInfoEnabled() ) {
321                    logger.info( "Auto-configuration did not find a suitable authenticationManager of type " + type );
322                }
323            }
324    
325            return success;
326        }
327    }