View Javadoc

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