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                                             }