1
2
3
4
5
6
7
8
9
10
11
12
13
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 * <bean id="switchUserProcessingFilter" class="org.acegisecurity.ui.switchuser.SwitchUserProcessingFilter">
99 * <property name="authenticationDao" ref="jdbcDaoImpl" />
100 * <property name="switchUserUrl"><value>/j_acegi_switch_user</value></property>
101 * <property name="exitUserUrl"><value>/j_acegi_exit_user</value></property>
102 * <property name="targetUrl"><value>/index.jsp</value></property>
103 * </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
112
113 private static final Log logger = LogFactory.getLog(SwitchUserProcessingFilter.class);
114
115
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
121
122 private ApplicationEventPublisher eventPublisher;
123
124
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
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
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
166
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
179 UserDetails originalUser = null;
180 Object obj = original.getPrincipal();
181
182 if ((obj != null) && obj instanceof UserDetails) {
183 originalUser = (UserDetails) obj;
184 }
185
186
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
230 UserDetails targetUser = this.userDetailsService
231 .loadUserByUsername(username);
232
233
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
242 if (!targetUser.isAccountNonLocked()) {
243 throw new LockedException(messages.getMessage(
244 "SwitchUserProcessingFilter.locked",
245 "User account is locked"));
246 }
247
248
249 if (!targetUser.isEnabled()) {
250 throw new DisabledException(messages.getMessage(
251 "SwitchUserProcessingFilter.disabled",
252 "User is disabled"));
253 }
254
255
256 if (!targetUser.isAccountNonExpired()) {
257 throw new AccountExpiredException(messages.getMessage(
258 "SwitchUserProcessingFilter.expired",
259 "User account has expired"));
260 }
261
262
263 if (!targetUser.isCredentialsNonExpired()) {
264 throw new CredentialsExpiredException(messages
265 .getMessage(
266 "SwitchUserProcessingFilter.credentialsExpired",
267 "User credentials have expired"));
268 }
269
270
271 targetUserRequest = createSwitchUserToken(request,
272 username, targetUser);
273
274 if (logger.isDebugEnabled()) {
275 logger.debug("Switch User Token ["
276 + targetUserRequest + "]");
277 }
278
279
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
309
310 Authentication currentAuth = SecurityContextHolder.getContext()
311 .getAuthentication();
312 GrantedAuthority switchAuthority = new SwitchUserGrantedAuthority(ROLE_PREVIOUS_ADMINISTRATOR,
313 currentAuth);
314
315
316 List orig = Arrays.asList(targetUser.getAuthorities());
317
318
319 List newAuths = new ArrayList(orig);
320 newAuths.add(switchAuthority);
321
322 GrantedAuthority[] authorities = {};
323 authorities = (GrantedAuthority[]) newAuths.toArray(authorities);
324
325
326 targetUserRequest = new UsernamePasswordAuthenticationToken(targetUser,
327 targetUser.getPassword(), authorities);
328
329
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
351 if (requiresSwitchUser(httpRequest)) {
352
353 Authentication targetUser = attemptSwitchUser(httpRequest);
354
355
356 SecurityContextHolder.getContext()
357 .setAuthentication(targetUser);
358
359
360 httpResponse.sendRedirect(httpResponse
361 .encodeRedirectURL(httpRequest
362 .getContextPath() + targetUrl));
363
364 return;
365 } else if (requiresExitUser(httpRequest)) {
366
367 Authentication originalUser = attemptExitUser(httpRequest);
368
369
370 SecurityContextHolder.getContext()
371 .setAuthentication(originalUser);
372
373
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
409 GrantedAuthority[] authorities = current
410 .getAuthorities();
411
412 for (int i = 0; i < authorities.length;
413 i++) {
414
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 }