Coverage Report - org.acegisecurity.ui.switchuser.SwitchUserProcessingFilter
 
Classes in this File Line Coverage Branch Coverage Complexity
SwitchUserProcessingFilter
81% 
100% 
2.526
 
 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.ui.switchuser;
 17  
 
 18  
 import org.acegisecurity.AccountExpiredException;
 19  
 import org.acegisecurity.AcegiMessageSource;
 20  
 import org.acegisecurity.Authentication;
 21  
 import org.acegisecurity.AuthenticationCredentialsNotFoundException;
 22  
 import org.acegisecurity.AuthenticationException;
 23  
 import org.acegisecurity.CredentialsExpiredException;
 24  
 import org.acegisecurity.DisabledException;
 25  
 import org.acegisecurity.GrantedAuthority;
 26  
 import org.acegisecurity.LockedException;
 27  
 
 28  
 import org.acegisecurity.context.SecurityContextHolder;
 29  
 
 30  
 import org.acegisecurity.event.authentication.AuthenticationSwitchUserEvent;
 31  
 
 32  
 import org.acegisecurity.providers.UsernamePasswordAuthenticationToken;
 33  
 
 34  
 import org.acegisecurity.ui.AuthenticationDetailsSource;
 35  
 import org.acegisecurity.ui.AuthenticationDetailsSourceImpl;
 36  
 
 37  
 import org.acegisecurity.userdetails.UserDetails;
 38  
 import org.acegisecurity.userdetails.UserDetailsService;
 39  
 import org.acegisecurity.userdetails.UsernameNotFoundException;
 40  
 
 41  
 import org.apache.commons.logging.Log;
 42  
 import org.apache.commons.logging.LogFactory;
 43  
 
 44  
 import org.springframework.beans.BeansException;
 45  
 import org.springframework.beans.factory.InitializingBean;
 46  
 
 47  
 import org.springframework.context.ApplicationEventPublisher;
 48  
 import org.springframework.context.ApplicationEventPublisherAware;
 49  
 import org.springframework.context.MessageSource;
 50  
 import org.springframework.context.MessageSourceAware;
 51  
 import org.springframework.context.support.MessageSourceAccessor;
 52  
 
 53  
 import org.springframework.util.Assert;
 54  
 
 55  
 import java.io.IOException;
 56  
 
 57  
 import java.util.ArrayList;
 58  
 import java.util.List;
 59  
 
 60  
 import javax.servlet.Filter;
 61  
 import javax.servlet.FilterChain;
 62  
 import javax.servlet.FilterConfig;
 63  
 import javax.servlet.ServletException;
 64  
 import javax.servlet.ServletRequest;
 65  
 import javax.servlet.ServletResponse;
 66  
 import javax.servlet.http.HttpServletRequest;
 67  
 import javax.servlet.http.HttpServletResponse;
 68  
 
 69  
 
 70  
 /**
 71  
  * Switch User processing filter responsible for user context switching.<p>This filter is similar to Unix 'su'
 72  
  * however for Acegi-managed web applications.  A common use-case for this feature is the ability to allow
 73  
  * higher-authority users (i.e. ROLE_ADMIN) to switch to a regular user (i.e. ROLE_USER).</p>
 74  
  *  <p>This filter assumes that the user performing the switch will be required to be logged in as normal (i.e.
 75  
  * ROLE_ADMIN user). The user will then access a page/controller that enables the administrator to specify who they
 76  
  * wish to become (see <code>switchUserUrl</code>). <br>
 77  
  * <b>Note: This URL will be required to have to appropriate security contraints configured so that  only users of that
 78  
  * role can access (i.e. ROLE_ADMIN).</b></p>
 79  
  *  <p>On successful switch, the user's  <code>SecurityContextHolder</code> will be updated to reflect the
 80  
  * specified user and will also contain an additinal {@link org.acegisecurity.ui.switchuser.SwitchUserGrantedAuthority
 81  
  * } which contains the original user.</p>
 82  
  *  <p>To 'exit' from a user context, the user will then need to access a URL (see <code>exitUserUrl</code>)  that
 83  
  * will switch back to the original user as identified by the <code>ROLE_PREVIOUS_ADMINISTRATOR</code>.</p>
 84  
  *  <p>To configure the Switch User Processing Filter, create a bean definition for the Switch User processing
 85  
  * filter and add to the filterChainProxy. <br>
 86  
  * Example:<pre>
 87  
  * &lt;bean id="switchUserProcessingFilter" class="org.acegisecurity.ui.switchuser.SwitchUserProcessingFilter">
 88  
  *    &lt;property name="authenticationDao" ref="jdbcDaoImpl" />
 89  
  *    &lt;property name="switchUserUrl">&lt;value>/j_acegi_switch_user&lt;/value>&lt;/property>
 90  
  *    &lt;property name="exitUserUrl">&lt;value>/j_acegi_exit_user&lt;/value>&lt;/property>
 91  
  *    &lt;property name="targetUrl">&lt;value>/index.jsp&lt;/value>&lt;/property>&lt;/bean></pre></p>
 92  
  *
 93  
  * @author Mark St.Godard
 94  
  * @version $Id: SwitchUserProcessingFilter.java 2637 2008-02-15 14:35:16Z luke_t $
 95  
  *
 96  
  * @see org.acegisecurity.ui.switchuser.SwitchUserGrantedAuthority
 97  
  */
 98  16
 public class SwitchUserProcessingFilter implements Filter, InitializingBean, ApplicationEventPublisherAware,
 99  
     MessageSourceAware {
 100  
     //~ Static fields/initializers =====================================================================================
 101  
 
 102  4
     private static final Log logger = LogFactory.getLog(SwitchUserProcessingFilter.class);
 103  
 
 104  
     public static final String ACEGI_SECURITY_SWITCH_USERNAME_KEY = "j_username";
 105  
     public static final String ROLE_PREVIOUS_ADMINISTRATOR = "ROLE_PREVIOUS_ADMINISTRATOR";
 106  
 
 107  
     //~ Instance fields ================================================================================================
 108  
 
 109  
     private ApplicationEventPublisher eventPublisher;
 110  16
     private AuthenticationDetailsSource authenticationDetailsSource = new AuthenticationDetailsSourceImpl();
 111  16
     protected MessageSourceAccessor messages = AcegiMessageSource.getAccessor();
 112  16
     private String exitUserUrl = "/j_acegi_exit_user";
 113  16
     private String switchUserUrl = "/j_acegi_switch_user";
 114  
     private String targetUrl;
 115  
     private SwitchUserAuthorityChanger switchUserAuthorityChanger;
 116  
     private UserDetailsService userDetailsService;
 117  
 
 118  
     //~ Methods ========================================================================================================
 119  
 
 120  
     public void afterPropertiesSet() throws Exception {
 121  2
         Assert.hasLength(switchUserUrl, "switchUserUrl must be specified");
 122  2
         Assert.hasLength(exitUserUrl, "exitUserUrl must be specified");
 123  2
         Assert.hasLength(targetUrl, "targetUrl must be specified");
 124  1
         Assert.notNull(userDetailsService, "authenticationDao must be specified");
 125  0
         Assert.notNull(messages, "A message source must be set");
 126  0
     }
 127  
 
 128  
     /**
 129  
      * Attempt to exit from an already switched user.
 130  
      *
 131  
      * @param request The http servlet request
 132  
      *
 133  
      * @return The original <code>Authentication</code> object or <code>null</code> otherwise.
 134  
      *
 135  
      * @throws AuthenticationCredentialsNotFoundException If no <code>Authentication</code> associated with this
 136  
      *         request.
 137  
      */
 138  
     protected Authentication attemptExitUser(HttpServletRequest request)
 139  
         throws AuthenticationCredentialsNotFoundException {
 140  
         // need to check to see if the current user has a SwitchUserGrantedAuthority
 141  2
         Authentication current = SecurityContextHolder.getContext().getAuthentication();
 142  
 
 143  2
         if (null == current) {
 144  1
             throw new AuthenticationCredentialsNotFoundException(messages.getMessage(
 145  
                     "SwitchUserProcessingFilter.noCurrentUser", "No current user associated with this request"));
 146  
         }
 147  
 
 148  
         // check to see if the current user did actual switch to another user
 149  
         // if so, get the original source user so we can switch back
 150  1
         Authentication original = getSourceAuthentication(current);
 151  
 
 152  1
         if (original == null) {
 153  0
             logger.error("Could not find original user Authentication object!");
 154  0
             throw new AuthenticationCredentialsNotFoundException(messages.getMessage(
 155  
                     "SwitchUserProcessingFilter.noOriginalAuthentication",
 156  
                     "Could not find original Authentication object"));
 157  
         }
 158  
 
 159  
         // get the source user details
 160  1
         UserDetails originalUser = null;
 161  1
         Object obj = original.getPrincipal();
 162  
 
 163  1
         if ((obj != null) && obj instanceof UserDetails) {
 164  0
             originalUser = (UserDetails) obj;
 165  
         }
 166  
 
 167  
         // publish event
 168  1
         if (this.eventPublisher != null) {
 169  0
             eventPublisher.publishEvent(new AuthenticationSwitchUserEvent(current, originalUser));
 170  
         }
 171  
 
 172  1
         return original;
 173  
     }
 174  
 
 175  
     /**
 176  
      * Attempt to switch to another user. If the user does not exist or is not active, return null.
 177  
      *
 178  
      * @param request The http request
 179  
      *
 180  
      * @return The new <code>Authentication</code> request if successfully switched to another user, <code>null</code>
 181  
      *         otherwise.
 182  
      *
 183  
      * @throws AuthenticationException
 184  
      * @throws UsernameNotFoundException If the target user is not found.
 185  
      * @throws LockedException DOCUMENT ME!
 186  
      * @throws DisabledException If the target user is disabled.
 187  
      * @throws AccountExpiredException If the target user account is expired.
 188  
      * @throws CredentialsExpiredException If the target user credentials are expired.
 189  
      */
 190  
     protected Authentication attemptSwitchUser(HttpServletRequest request)
 191  
         throws AuthenticationException {
 192  9
         UsernamePasswordAuthenticationToken targetUserRequest = null;
 193  
 
 194  9
         String username = request.getParameter(ACEGI_SECURITY_SWITCH_USERNAME_KEY);
 195  
 
 196  9
         if (username == null) {
 197  1
             username = "";
 198  
         }
 199  
 
 200  9
         if (logger.isDebugEnabled()) {
 201  0
             logger.debug("Attempt to switch to user [" + username + "]");
 202  
         }
 203  
 
 204  
         // load the user by name
 205  9
         UserDetails targetUser = this.userDetailsService.loadUserByUsername(username);
 206  
 
 207  
         // user not found
 208  7
         if (targetUser == null) {
 209  0
             throw new UsernameNotFoundException(messages.getMessage("SwitchUserProcessingFilter.usernameNotFound",
 210  
                     new Object[] {username}, "Username {0} not found"));
 211  
         }
 212  
 
 213  
         // account is expired
 214  7
         if (!targetUser.isAccountNonLocked()) {
 215  0
             throw new LockedException(messages.getMessage("SwitchUserProcessingFilter.locked", "User account is locked"));
 216  
         }
 217  
 
 218  
         // user is disabled
 219  7
         if (!targetUser.isEnabled()) {
 220  1
             throw new DisabledException(messages.getMessage("SwitchUserProcessingFilter.disabled", "User is disabled"));
 221  
         }
 222  
 
 223  
         // account is expired
 224  6
         if (!targetUser.isAccountNonExpired()) {
 225  1
             throw new AccountExpiredException(messages.getMessage("SwitchUserProcessingFilter.expired",
 226  
                     "User account has expired"));
 227  
         }
 228  
 
 229  
         // credentials expired
 230  5
         if (!targetUser.isCredentialsNonExpired()) {
 231  1
             throw new CredentialsExpiredException(messages.getMessage("SwitchUserProcessingFilter.credentialsExpired",
 232  
                     "User credentials have expired"));
 233  
         }
 234  
 
 235  
         // ok, create the switch user token
 236  4
         targetUserRequest = createSwitchUserToken(request, username, targetUser);
 237  
 
 238  4
         if (logger.isDebugEnabled()) {
 239  0
             logger.debug("Switch User Token [" + targetUserRequest + "]");
 240  
         }
 241  
 
 242  
         // publish event
 243  4
         if (this.eventPublisher != null) {
 244  0
             eventPublisher.publishEvent(new AuthenticationSwitchUserEvent(
 245  
                     SecurityContextHolder.getContext().getAuthentication(), targetUser));
 246  
         }
 247  
 
 248  4
         return targetUserRequest;
 249  
     }
 250  
 
 251  
     /**
 252  
      * Create a switch user token that contains an additional <tt>GrantedAuthority</tt> that contains the
 253  
      * original <code>Authentication</code> object.
 254  
      *
 255  
      * @param request The http servlet request.
 256  
      * @param username The username of target user
 257  
      * @param targetUser The target user
 258  
      *
 259  
      * @return The authentication token
 260  
      *
 261  
      * @see SwitchUserGrantedAuthority
 262  
      */
 263  
     private UsernamePasswordAuthenticationToken createSwitchUserToken(HttpServletRequest request, String username,
 264  
         UserDetails targetUser) {
 265  
         UsernamePasswordAuthenticationToken targetUserRequest;
 266  
 
 267  
         // grant an additional authority that contains the original Authentication object
 268  
         // which will be used to 'exit' from the current switched user.
 269  4
         Authentication currentAuth = SecurityContextHolder.getContext().getAuthentication();
 270  4
         GrantedAuthority switchAuthority = new SwitchUserGrantedAuthority(ROLE_PREVIOUS_ADMINISTRATOR, currentAuth);
 271  
 
 272  
         // get the original authorities        
 273  4
         ArrayList orig = new ArrayList();
 274  12
         for (int i = 0; i < targetUser.getAuthorities().length; i++) {
 275  8
                         orig.add(targetUser.getAuthorities()[i]);
 276  
                 }
 277  
 
 278  
         // Allow subclasses to change the authorities to be granted
 279  4
         if (switchUserAuthorityChanger != null) {
 280  1
             switchUserAuthorityChanger.modifyGrantedAuthorities(targetUser, currentAuth, orig);
 281  
         }
 282  
 
 283  
         // add the new switch user authority
 284  4
         List newAuths = new ArrayList(orig);
 285  4
         newAuths.add(switchAuthority);
 286  
 
 287  4
         GrantedAuthority[] authorities = {};
 288  4
         authorities = (GrantedAuthority[]) newAuths.toArray(authorities);
 289  
 
 290  
         // create the new authentication token
 291  4
         targetUserRequest = new UsernamePasswordAuthenticationToken(targetUser, targetUser.getPassword(), authorities);
 292  
 
 293  
         // set details
 294  4
         targetUserRequest.setDetails(authenticationDetailsSource.buildDetails((HttpServletRequest) request));
 295  
 
 296  4
         return targetUserRequest;
 297  
     }
 298  
 
 299  0
     public void destroy() {}
 300  
 
 301  
     /**
 302  
      *
 303  
      * @see javax.servlet.Filter#doFilter
 304  
      */
 305  
     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
 306  
         throws IOException, ServletException {
 307  4
         Assert.isInstanceOf(HttpServletRequest.class, request);
 308  4
         Assert.isInstanceOf(HttpServletResponse.class, response);
 309  
 
 310  4
         HttpServletRequest httpRequest = (HttpServletRequest) request;
 311  4
         HttpServletResponse httpResponse = (HttpServletResponse) response;
 312  
 
 313  
         // check for switch or exit request
 314  4
         if (requiresSwitchUser(httpRequest)) {
 315  
             // if set, attempt switch and store original
 316  2
             Authentication targetUser = attemptSwitchUser(httpRequest);
 317  
 
 318  
             // update the current context to the new target user
 319  2
             SecurityContextHolder.getContext().setAuthentication(targetUser);
 320  
 
 321  
             // redirect to target url
 322  2
             httpResponse.sendRedirect(httpResponse.encodeRedirectURL(httpRequest.getContextPath() + targetUrl));
 323  
 
 324  2
             return;
 325  2
         } else if (requiresExitUser(httpRequest)) {
 326  
             // get the original authentication object (if exists)
 327  2
             Authentication originalUser = attemptExitUser(httpRequest);
 328  
 
 329  
             // update the current context back to the original user
 330  1
             SecurityContextHolder.getContext().setAuthentication(originalUser);
 331  
 
 332  
             // redirect to target url
 333  1
             httpResponse.sendRedirect(httpResponse.encodeRedirectURL(httpRequest.getContextPath() + targetUrl));
 334  
 
 335  1
             return;
 336  
         }
 337  
 
 338  0
         chain.doFilter(request, response);
 339  0
     }
 340  
 
 341  
     /**
 342  
      * Find the original <code>Authentication</code> object from the current user's granted authorities. A
 343  
      * successfully switched user should have a <code>SwitchUserGrantedAuthority</code> that contains the original
 344  
      * source user <code>Authentication</code> object.
 345  
      *
 346  
      * @param current The current  <code>Authentication</code> object
 347  
      *
 348  
      * @return The source user <code>Authentication</code> object or <code>null</code> otherwise.
 349  
      */
 350  
     private Authentication getSourceAuthentication(Authentication current) {
 351  1
         Authentication original = null;
 352  
 
 353  
         // iterate over granted authorities and find the 'switch user' authority
 354  1
         GrantedAuthority[] authorities = current.getAuthorities();
 355  
 
 356  4
         for (int i = 0; i < authorities.length; i++) {
 357  
             // check for switch user type of authority
 358  3
             if (authorities[i] instanceof SwitchUserGrantedAuthority) {
 359  1
                 original = ((SwitchUserGrantedAuthority) authorities[i]).getSource();
 360  1
                 logger.debug("Found original switch user granted authority [" + original + "]");
 361  
             }
 362  
         }
 363  
 
 364  1
         return original;
 365  
     }
 366  
 
 367  0
     public void init(FilterConfig ignored) throws ServletException {}
 368  
 
 369  
     /**
 370  
      * Checks the request URI for the presence of <tt>exitUserUrl</tt>.
 371  
      *
 372  
      * @param request The http servlet request
 373  
      *
 374  
      * @return <code>true</code> if the request requires a exit user, <code>false</code> otherwise.
 375  
      *
 376  
      * @see SwitchUserProcessingFilter#exitUserUrl
 377  
      */
 378  
     protected boolean requiresExitUser(HttpServletRequest request) {
 379  3
         String uri = stripUri(request);
 380  
 
 381  3
         return uri.endsWith(request.getContextPath() + exitUserUrl);
 382  
     }
 383  
 
 384  
     /**
 385  
      * Checks the request URI for the presence of <tt>switchUserUrl</tt>.
 386  
      *
 387  
      * @param request The http servlet request
 388  
      *
 389  
      * @return <code>true</code> if the request requires a switch, <code>false</code> otherwise.
 390  
      *
 391  
      * @see SwitchUserProcessingFilter#switchUserUrl
 392  
      */
 393  
     protected boolean requiresSwitchUser(HttpServletRequest request) {
 394  6
         String uri = stripUri(request);
 395  
 
 396  6
         return uri.endsWith(request.getContextPath() + switchUserUrl);
 397  
     }
 398  
 
 399  
     public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher)
 400  
         throws BeansException {
 401  0
         this.eventPublisher = eventPublisher;
 402  0
     }
 403  
 
 404  
     public void setAuthenticationDetailsSource(AuthenticationDetailsSource authenticationDetailsSource) {
 405  0
         Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
 406  0
         this.authenticationDetailsSource = authenticationDetailsSource;
 407  0
     }
 408  
 
 409  
     /**
 410  
      * Set the URL to respond to exit user processing.
 411  
      *
 412  
      * @param exitUserUrl The exit user URL.
 413  
      */
 414  
     public void setExitUserUrl(String exitUserUrl) {
 415  5
         this.exitUserUrl = exitUserUrl;
 416  5
     }
 417  
 
 418  
     public void setMessageSource(MessageSource messageSource) {
 419  0
         this.messages = new MessageSourceAccessor(messageSource);
 420  0
     }
 421  
 
 422  
     /**
 423  
      * Set the URL to respond to switch user processing.
 424  
      *
 425  
      * @param switchUserUrl The switch user URL.
 426  
      */
 427  
     public void setSwitchUserUrl(String switchUserUrl) {
 428  6
         this.switchUserUrl = switchUserUrl;
 429  6
     }
 430  
 
 431  
     /**
 432  
      * Sets the URL to go to after a successful switch / exit user request.
 433  
      *
 434  
      * @param targetUrl The target url.
 435  
      */
 436  
     public void setTargetUrl(String targetUrl) {
 437  2
         this.targetUrl = targetUrl;
 438  2
     }
 439  
 
 440  
     /**
 441  
      * Sets the authentication data access object.
 442  
      *
 443  
      * @param userDetailsService The UserDetailsService to use
 444  
      */
 445  
     public void setUserDetailsService(UserDetailsService userDetailsService) {
 446  12
         this.userDetailsService = userDetailsService;
 447  12
     }
 448  
 
 449  
     /**
 450  
      * Strips any content after the ';' in the request URI
 451  
      *
 452  
      * @param request The http request
 453  
      *
 454  
      * @return The stripped uri
 455  
      */
 456  
     private static String stripUri(HttpServletRequest request) {
 457  9
         String uri = request.getRequestURI();
 458  9
         int idx = uri.indexOf(';');
 459  
 
 460  9
         if (idx > 0) {
 461  1
             uri = uri.substring(0, idx);
 462  
         }
 463  
 
 464  9
         return uri;
 465  
     }
 466  
 
 467  
     /**
 468  
      * @param switchUserAuthorityChanger to use to fine-tune the authorities granted to subclasses (may be null if
 469  
      * SwitchUserProcessingFilter shoudl not fine-tune the authorities)
 470  
      */
 471  
     public void setSwitchUserAuthorityChanger(SwitchUserAuthorityChanger switchUserAuthorityChanger) {
 472  1
         this.switchUserAuthorityChanger = switchUserAuthorityChanger;
 473  1
     }
 474  
 }