diff --git a/core/src/main/java/org/acegisecurity/ui/switchuser/SwitchUserGrantedAuthority.java b/core/src/main/java/org/acegisecurity/ui/switchuser/SwitchUserGrantedAuthority.java new file mode 100644 index 0000000000..5295d196e7 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/ui/switchuser/SwitchUserGrantedAuthority.java @@ -0,0 +1,59 @@ +/* Copyright 2004, 2005 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sf.acegisecurity.ui.switchuser; + +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.GrantedAuthorityImpl; + + +/** + * Custom GrantedAuthority used by {@link + * net.sf.acegisecurity.ui.switchuser.SwitchUserProcessingFilter} + * + *

+ * Stores the Authentication object of the original user to be + * used later when 'exiting' from a user switch. + *

+ * + * @author Mark St.Godard + * @version $Id$ + * + * @see net.sf.acegisecurity.ui.switchuser.SwitchUserProcessingFilter + */ +public class SwitchUserGrantedAuthority extends GrantedAuthorityImpl { + //~ Instance fields ======================================================== + + private Authentication source; + + //~ Constructors =========================================================== + + public SwitchUserGrantedAuthority(String role, Authentication source) { + super(role); + this.source = source; + } + + //~ Methods ================================================================ + + /** + * Returns the original user associated with a successful user switch. + * + * @return The original Authentication object of the switched + * user. + */ + public Authentication getSource() { + return source; + } +} diff --git a/core/src/main/java/org/acegisecurity/ui/switchuser/SwitchUserProcessingFilter.java b/core/src/main/java/org/acegisecurity/ui/switchuser/SwitchUserProcessingFilter.java new file mode 100644 index 0000000000..8c1dfb452e --- /dev/null +++ b/core/src/main/java/org/acegisecurity/ui/switchuser/SwitchUserProcessingFilter.java @@ -0,0 +1,462 @@ +/* Copyright 2004, 2005 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sf.acegisecurity.ui.switchuser; + +import net.sf.acegisecurity.AccountExpiredException; +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.AuthenticationCredentialsNotFoundException; +import net.sf.acegisecurity.AuthenticationException; +import net.sf.acegisecurity.CredentialsExpiredException; +import net.sf.acegisecurity.DisabledException; +import net.sf.acegisecurity.GrantedAuthority; +import net.sf.acegisecurity.UserDetails; +import net.sf.acegisecurity.context.SecurityContextHolder; +import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken; +import net.sf.acegisecurity.providers.dao.AuthenticationDao; +import net.sf.acegisecurity.providers.dao.UsernameNotFoundException; +import net.sf.acegisecurity.ui.WebAuthenticationDetails; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.beans.factory.InitializingBean; + +import org.springframework.util.Assert; + +import java.io.IOException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + + +/** + * Switch User processing filter responsible for user context switching. + * + *

+ * This filter is similar to Unix 'su' however for Acegi-managed web + * applications. A common use-case for this feature is the ability to allow + * higher-authority users (i.e. ROLE_ADMIN) to switch to a regular user (i.e. + * ROLE_USER). + *

+ * + *

+ * This filter assumes that the user performing the switch will be required to + * be logged in as normal (i.e. ROLE_ADMIN user). The user will then access a + * page/controller that enables the administrator to specify who they wish to + * become (see switchUserUrl).
+ * Note: This URL will be required to have to appropriate security + * contraints configured so that only users of that role can access (i.e. + * ROLE_ADMIN). + *

+ * + *

+ * On successful switch, the user's SecureContextHolder will be + * updated to reflect the specified user and will also contain an additinal + * {@link net.sf.acegisecurity.ui.switchuser.SwitchUserGrantedAuthority } + * which contains the original user. + *

+ * + *

+ * To 'exit' from a user context, the user will then need to access a URL (see + * exitUserUrl) that will switch back to the original user as + * identified by the SWITCH_USER_GRANTED_AUTHORITY. + *

+ * + *

+ * To configure the Switch User Processing Filter, create a bean definition for + * the Switch User processing filter and add to the filterChainProxy.
+ * Example: + *

+ * <bean id="switchUserProcessingFilter" class="net.sf.acegisecurity.ui.switchuser.SwitchUserProcessingFilter">
+ *    <property name="authenticationDao" ref="jdbcDaoImpl" />
+ *    <property name="switchUserUrl"><value>/j_acegi_switch_user</value></property>
+ *    <property name="exitUserUrl"><value>/j_acegi_exit_user</value></property>
+ *    <property name="targetUrl"><value>/index.jsp</value></property>
+ * </bean>
+ * 
+ *

+ * + * @author Mark St.Godard + * @version $Id$ + * + * @see net.sf.acegisecurity.ui.switchuser.SwitchUserGrantedAuthority + */ +public class SwitchUserProcessingFilter implements InitializingBean, Filter { + //~ Static fields/initializers ============================================= + + private static final Log logger = LogFactory.getLog(SwitchUserProcessingFilter.class); + + // ~ Static fields/initializers + // ============================================= + public static final String ACEGI_SECURITY_SWITCH_USERNAME_KEY = "j_username"; + public static final String SWITCH_USER_GRANTED_AUTHORITY = "PREVIOUS_ADMINISTRATOR"; + + //~ Instance fields ======================================================== + + // ~ Instance fields + // ======================================================== + private AuthenticationDao authenticationDao; + private String exitUserUrl; + private String switchUserUrl; + private String targetUrl; + + //~ Methods ================================================================ + + /** + * Sets the authentication data access object. + * + * @param authenticationDao The authentication dao + */ + public void setAuthenticationDao(AuthenticationDao authenticationDao) { + this.authenticationDao = authenticationDao; + } + + /** + * This filter by default responds to /j_acegi_exit_user. + * + * @return the default exit user url + */ + public String getDefaultExitUserUrl() { + return "/j_acegi_exit_user"; + } + + // ~ Methods + // ================================================================ + + /** + * This filter by default responds to /j_acegi_switch_user. + * + * @return the default switch user url + */ + public String getDefaultSwitchUserUrl() { + return "/j_acegi_switch_user"; + } + + /** + * Set the URL to respond to exit user processing. + * + * @param exitUserUrl The exit user URL. + */ + public void setExitUserUrl(String exitUserUrl) { + this.exitUserUrl = exitUserUrl; + } + + /** + * Set the URL to respond to switch user processing. + * + * @param switchUserUrl The switch user URL. + */ + public void setSwitchUserUrl(String switchUserUrl) { + this.switchUserUrl = switchUserUrl; + } + + /** + * Sets the URL to go to after a successful switch / exit user request. + * + * @param targetUrl The target url. + */ + public void setTargetUrl(String targetUrl) { + this.targetUrl = targetUrl; + } + + public void afterPropertiesSet() throws Exception { + Assert.hasLength(switchUserUrl, "switchUserUrl must be specified"); + Assert.hasLength(exitUserUrl, "exitUserUrl must be specified"); + Assert.hasLength(targetUrl, "targetUrl must be specified"); + Assert.notNull(authenticationDao, "authenticationDao must be specified"); + } + + public void destroy() {} + + /** + * @see javax.servlet.Filter#doFilter + */ + public void doFilter(ServletRequest request, ServletResponse response, + FilterChain chain) throws IOException, ServletException { + if (!(request instanceof HttpServletRequest)) { + throw new ServletException("Can only process HttpServletRequest"); + } + + if (!(response instanceof HttpServletResponse)) { + throw new ServletException("Can only process HttpServletResponse"); + } + + HttpServletRequest httpRequest = (HttpServletRequest) request; + HttpServletResponse httpResponse = (HttpServletResponse) response; + + // check for switch or exit request + if (requiresSwitchUser(httpRequest)) { + // if set, attempt switch and store original + Authentication targetUser = attemptSwitchUser(httpRequest); + + // update the current context to the new target user + SecurityContextHolder.getContext().setAuthentication(targetUser); + + // redirect to target url + httpResponse.sendRedirect(httpResponse.encodeRedirectURL(targetUrl)); + + return; + } else if (requiresExitUser(httpRequest)) { + // get the original authentication object (if exists) + Authentication originalUser = attemptExitUser(httpRequest); + + // update the current context back to the original user + SecurityContextHolder.getContext().setAuthentication(originalUser); + + // redirect to target url + httpResponse.sendRedirect(httpResponse.encodeRedirectURL(targetUrl)); + + return; + } + + chain.doFilter(request, response); + } + + public void init(FilterConfig filterConfig) throws ServletException {} + + /** + * Attempt to exit from an already switched user. + * + * @param request The http servlet request + * + * @return The original Authentication object or + * null otherwise. + * + * @throws AuthenticationCredentialsNotFoundException If no + * Authentication associated with this request. + */ + protected Authentication attemptExitUser(HttpServletRequest request) + throws AuthenticationCredentialsNotFoundException { + // need to check to see if the current user has a SwitchUserGrantedAuthority + Authentication current = SecurityContextHolder.getContext() + .getAuthentication(); + + if (null == current) { + throw new AuthenticationCredentialsNotFoundException( + "No current user associated with this request!"); + } + + // check to see if the current user did actual switch to another user + // if so, get the original source user so we can switch back + Authentication original = getSourceAuthentication(current); + + if (original == null) { + logger.error("Could not find original user Authentication object!"); + throw new AuthenticationCredentialsNotFoundException( + "Could not find original Authentication object!"); + } + + return original; + } + + /** + * Attempt to switch to another user. If the user does not exist or is not + * active, return null. + * + * @param request The http request + * + * @return The new Authentication request if successfully + * switched to another user, null otherwise. + * + * @throws AuthenticationException + * @throws UsernameNotFoundException If the target user is not found. + * @throws DisabledException If the target user is disabled. + * @throws AccountExpiredException If the target user account is expired. + * @throws CredentialsExpiredException If the target user credentials are + * expired. + */ + protected Authentication attemptSwitchUser(HttpServletRequest request) + throws AuthenticationException { + UsernamePasswordAuthenticationToken targetUserRequest = null; + + String username = request.getParameter(ACEGI_SECURITY_SWITCH_USERNAME_KEY); + + if (username == null) { + username = ""; + } + + if (logger.isDebugEnabled()) { + logger.debug("Attempt to switch to user [" + username + "]"); + } + + // load the user by name + UserDetails targetUser = this.authenticationDao.loadUserByUsername(username); + + // user not found + if (targetUser == null) { + throw new UsernameNotFoundException("User [" + username + + "] cannot be found!"); + } + + // user is disabled + if (!targetUser.isEnabled()) { + throw new DisabledException("User is disabled"); + } + + // account is expired + if (!targetUser.isAccountNonExpired()) { + throw new AccountExpiredException("User account has expired"); + } + + // credentials expired + if (!targetUser.isCredentialsNonExpired()) { + throw new CredentialsExpiredException("User credentials expired"); + } + + // ok, create the switch user token + targetUserRequest = createSwitchUserToken(request, username, targetUser); + + if (logger.isDebugEnabled()) { + logger.debug("Switch User Token [" + targetUserRequest + "]"); + } + + return targetUserRequest; + } + + /** + * Checks the request URI for the presence of exitUserUrl. + * + * @param request The http servlet request + * + * @return true if the request requires a exit user, + * false otherwise. + * + * @see SwitchUserProcessingFilter#exitUserUrl + */ + protected boolean requiresExitUser(HttpServletRequest request) { + String uri = stripUri(request); + + return uri.endsWith(request.getContextPath() + exitUserUrl); + } + + /** + * Checks the request URI for the presence of switchUserUrl. + * + * @param request The http servlet request + * + * @return true if the request requires a switch, + * false otherwise. + * + * @see SwitchUserProcessingFilter#switchUserUrl + */ + protected boolean requiresSwitchUser(HttpServletRequest request) { + String uri = stripUri(request); + + return uri.endsWith(request.getContextPath() + switchUserUrl); + } + + /** + * Strips any content after the ';' in the request URI + * + * @param request The http request + * + * @return The stripped uri + */ + private static String stripUri(HttpServletRequest request) { + String uri = request.getRequestURI(); + int idx = uri.indexOf(';'); + + if (idx > 0) { + uri = uri.substring(0, idx); + } + + return uri; + } + + /** + * Find the original Authentication object from the current + * user's granted authorities. A successfully switched user should have a + * SwitchUserGrantedAuthority that contains the original + * source user Authentication object. + * + * @param current The current Authentication object + * + * @return The source user Authentication object or + * null otherwise. + */ + private Authentication getSourceAuthentication(Authentication current) { + Authentication original = null; + + // iterate over granted authorities and find the 'switch user' authority + GrantedAuthority[] authorities = current.getAuthorities(); + + for (int i = 0; i < authorities.length; i++) { + // check for switch user type of authority + if (authorities[i] instanceof SwitchUserGrantedAuthority) { + original = ((SwitchUserGrantedAuthority) authorities[i]) + .getSource(); + logger.debug("Found original switch user granted authority [" + + original + "]"); + } + } + + return original; + } + + /** + * Create a switch user token that contains an additional + * GrantedAuthority that contains the original + * Authentication object. + * + * @param request The http servlet request. + * @param username The username of target user + * @param targetUser The target user + * + * @return The authentication token + * + * @see SwitchUserGrantedAuthority + */ + private UsernamePasswordAuthenticationToken createSwitchUserToken( + HttpServletRequest request, String username, UserDetails targetUser) { + UsernamePasswordAuthenticationToken targetUserRequest; + + // grant an additional authority that contains the original Authentication object + // which will be used to 'exit' from the current switched user. + Authentication currentAuth = SecurityContextHolder.getContext() + .getAuthentication(); + GrantedAuthority switchAuthority = new SwitchUserGrantedAuthority(SWITCH_USER_GRANTED_AUTHORITY, + currentAuth); + + // get the original authorities + List orig = Arrays.asList(targetUser.getAuthorities()); + + // add the new switch user authority + List newAuths = new ArrayList(orig); + newAuths.add(switchAuthority); + + GrantedAuthority[] authorities = {}; + authorities = (GrantedAuthority[]) newAuths.toArray(authorities); + + // create the new authentication token + targetUserRequest = new UsernamePasswordAuthenticationToken(username, + targetUser.getPassword(), authorities); + + // set details + targetUserRequest.setDetails(new WebAuthenticationDetails(request)); + + return targetUserRequest; + } +} diff --git a/core/src/test/java/org/acegisecurity/ui/switchuser/SwitchUserProcessingFilterTests.java b/core/src/test/java/org/acegisecurity/ui/switchuser/SwitchUserProcessingFilterTests.java new file mode 100644 index 0000000000..11bc2b14e6 --- /dev/null +++ b/core/src/test/java/org/acegisecurity/ui/switchuser/SwitchUserProcessingFilterTests.java @@ -0,0 +1,447 @@ +/* Copyright 2004, 2005 Acegi Technology Pty Limited + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.sf.acegisecurity.ui.switchuser; + +import junit.framework.TestCase; + +import net.sf.acegisecurity.AccountExpiredException; +import net.sf.acegisecurity.Authentication; +import net.sf.acegisecurity.AuthenticationException; +import net.sf.acegisecurity.CredentialsExpiredException; +import net.sf.acegisecurity.DisabledException; +import net.sf.acegisecurity.GrantedAuthority; +import net.sf.acegisecurity.GrantedAuthorityImpl; +import net.sf.acegisecurity.UserDetails; +import net.sf.acegisecurity.context.SecurityContextHolder; +import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken; +import net.sf.acegisecurity.providers.dao.AuthenticationDao; +import net.sf.acegisecurity.providers.dao.User; +import net.sf.acegisecurity.providers.dao.UsernameNotFoundException; +import net.sf.acegisecurity.util.MockFilterChain; + +import org.springframework.dao.DataAccessException; + +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; + + +/** + * Tests {@link net.sf.acegisecurity.ui.switchuser.SwitchUserProcessingFilter}. + * + * @author Mark St.Godard + * @version $Id$ + */ +public class SwitchUserProcessingFilterTests extends TestCase { + //~ Constructors =========================================================== + + public SwitchUserProcessingFilterTests() { + super(); + } + + public SwitchUserProcessingFilterTests(String arg0) { + super(arg0); + } + + //~ Methods ================================================================ + + public final void setUp() throws Exception { + super.setUp(); + } + + public static void main(String[] args) { + junit.textui.TestRunner.run(SwitchUserProcessingFilterTests.class); + } + + public void testAttemptSwitchToUnknownUser() throws Exception { + // set current user + UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken("dano", + "hawaii50"); + SecurityContextHolder.getContext().setAuthentication(auth); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter(SwitchUserProcessingFilter.ACEGI_SECURITY_SWITCH_USERNAME_KEY, + "user-that-doesnt-exist"); + + SwitchUserProcessingFilter filter = new SwitchUserProcessingFilter(); + filter.setAuthenticationDao(new MockAuthenticationDaoUserJackLord()); + + try { + Authentication result = filter.attemptSwitchUser(request); + + fail("Should not be able to switch to unknown user"); + } catch (UsernameNotFoundException expected) {} + } + + public void testAttemptSwitchToUserThatIsDisabled() + throws Exception { + // set current user + UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken("dano", + "hawaii50"); + SecurityContextHolder.getContext().setAuthentication(auth); + + MockHttpServletRequest request = new MockHttpServletRequest(); + + // this user is disabled + request.addParameter(SwitchUserProcessingFilter.ACEGI_SECURITY_SWITCH_USERNAME_KEY, + "mcgarrett"); + + SwitchUserProcessingFilter filter = new SwitchUserProcessingFilter(); + filter.setAuthenticationDao(new MockAuthenticationDaoUserJackLord()); + + try { + Authentication result = filter.attemptSwitchUser(request); + + fail("Should not be able to switch to disabled user"); + } catch (DisabledException expected) { + // user should be disabled + } + } + + public void testAttemptSwitchToUserWithAccountExpired() + throws Exception { + // set current user + UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken("dano", + "hawaii50"); + SecurityContextHolder.getContext().setAuthentication(auth); + + MockHttpServletRequest request = new MockHttpServletRequest(); + + // this user is disabled + request.addParameter(SwitchUserProcessingFilter.ACEGI_SECURITY_SWITCH_USERNAME_KEY, + "wofat"); + + SwitchUserProcessingFilter filter = new SwitchUserProcessingFilter(); + filter.setAuthenticationDao(new MockAuthenticationDaoUserJackLord()); + + try { + Authentication result = filter.attemptSwitchUser(request); + + fail("Should not be able to switch to user with expired account"); + } catch (AccountExpiredException expected) { + // expected user account expired + } + } + + public void testAttemptSwitchToUserWithExpiredCredentials() + throws Exception { + // set current user + UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken("dano", + "hawaii50"); + SecurityContextHolder.getContext().setAuthentication(auth); + + MockHttpServletRequest request = new MockHttpServletRequest(); + + // this user is disabled + request.addParameter(SwitchUserProcessingFilter.ACEGI_SECURITY_SWITCH_USERNAME_KEY, + "steve"); + + SwitchUserProcessingFilter filter = new SwitchUserProcessingFilter(); + filter.setAuthenticationDao(new MockAuthenticationDaoUserJackLord()); + + try { + Authentication result = filter.attemptSwitchUser(request); + + fail("Should not be able to switch to user with expired account"); + } catch (CredentialsExpiredException expected) { + // user credentials expired + } + } + + public void testAttemptSwitchUser() throws Exception { + // set current user + UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken("dano", + "hawaii50"); + SecurityContextHolder.getContext().setAuthentication(auth); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.addParameter(SwitchUserProcessingFilter.ACEGI_SECURITY_SWITCH_USERNAME_KEY, + "jacklord"); + + SwitchUserProcessingFilter filter = new SwitchUserProcessingFilter(); + filter.setAuthenticationDao(new MockAuthenticationDaoUserJackLord()); + + Authentication result = filter.attemptSwitchUser(request); + assertTrue(result != null); + } + + public void testBadConfigMissingAuthenticationDao() { + SwitchUserProcessingFilter filter = new SwitchUserProcessingFilter(); + filter.setSwitchUserUrl("/j_acegi_switch_user"); + filter.setExitUserUrl("/j_acegi_exit_user"); + filter.setTargetUrl("/main.jsp"); + + try { + filter.afterPropertiesSet(); + fail("Expect to fail due to missing 'authenticationDao'"); + } catch (Exception expected) { + // expected exception + } + } + + public void testBadConfigMissingExitUserUrl() { + SwitchUserProcessingFilter filter = new SwitchUserProcessingFilter(); + filter.setAuthenticationDao(new MockAuthenticationDaoUserJackLord()); + filter.setSwitchUserUrl("/j_acegi_switch_user"); + filter.setTargetUrl("/main.jsp"); + + try { + filter.afterPropertiesSet(); + fail("Expect to fail due to missing 'exitUserUrl'"); + } catch (Exception expected) { + // expected exception + } + } + + public void testBadConfigMissingSwitchUserUrl() { + SwitchUserProcessingFilter filter = new SwitchUserProcessingFilter(); + filter.setAuthenticationDao(new MockAuthenticationDaoUserJackLord()); + filter.setExitUserUrl("/j_acegi_exit_user"); + filter.setTargetUrl("/main.jsp"); + + try { + filter.afterPropertiesSet(); + fail("Expect to fail due to missing 'switchUserUrl'"); + } catch (Exception expected) { + // expected exception + } + } + + public void testBadConfigMissingTargetUrl() { + SwitchUserProcessingFilter filter = new SwitchUserProcessingFilter(); + filter.setAuthenticationDao(new MockAuthenticationDaoUserJackLord()); + filter.setSwitchUserUrl("/j_acegi_switch_user"); + filter.setExitUserUrl("/j_acegi_exit_user"); + + try { + filter.afterPropertiesSet(); + fail("Expect to fail due to missing 'targetUrl'"); + } catch (Exception expected) { + // expected exception + } + } + + public void testDefaultExitProcessUrl() { + SwitchUserProcessingFilter filter = new SwitchUserProcessingFilter(); + assertEquals("/j_acegi_exit_user", filter.getDefaultExitUserUrl()); + } + + public void testDefaultProcessesFilterUrlWithPathParameter() { + MockHttpServletRequest request = createMockSwitchRequest(); + SwitchUserProcessingFilter filter = new SwitchUserProcessingFilter(); + filter.setSwitchUserUrl("/j_acegi_switch_user"); + + request.setRequestURI( + "/webapp/j_acegi_switch_user;jsessionid=8JHDUD723J8"); + assertTrue(filter.requiresSwitchUser(request)); + } + + public void testDefaultSwitchProcessUrl() { + SwitchUserProcessingFilter filter = new SwitchUserProcessingFilter(); + assertEquals("/j_acegi_switch_user", filter.getDefaultSwitchUserUrl()); + } + + public void testExitRequestUserJackLordToDano() throws Exception { + // original user + GrantedAuthority[] auths = {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}; + UsernamePasswordAuthenticationToken source = new UsernamePasswordAuthenticationToken("dano", + "hawaii50", auths); + + // set current user (Admin) + GrantedAuthority[] adminAuths = {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO"), new SwitchUserGrantedAuthority("PREVIOUS_ADMINISTRATOR", + source)}; + UsernamePasswordAuthenticationToken admin = new UsernamePasswordAuthenticationToken("jacklord", + "hawaii50", adminAuths); + + SecurityContextHolder.getContext().setAuthentication(admin); + + // http request + MockHttpServletRequest request = createMockSwitchRequest(); + request.setRequestURI("/j_acegi_exit_user"); + + // http response + MockHttpServletResponse response = new MockHttpServletResponse(); + + // setup filter + SwitchUserProcessingFilter filter = new SwitchUserProcessingFilter(); + filter.setAuthenticationDao(new MockAuthenticationDaoUserJackLord()); + filter.setExitUserUrl("/j_acegi_exit_user"); + + MockFilterChain chain = new MockFilterChain(true); + + // run 'exit' + filter.doFilter(request, response, chain); + + // check current user, should be back to original user (dano) + Authentication targetAuth = SecurityContextHolder.getContext() + .getAuthentication(); + assertNotNull(targetAuth); + assertEquals("dano", targetAuth.getPrincipal()); + } + + public void testExitUserWithNoCurrentUser() throws Exception { + // no current user in secure context + SecurityContextHolder.getContext().setAuthentication(null); + + // http request + MockHttpServletRequest request = createMockSwitchRequest(); + request.setRequestURI("/j_acegi_exit_user"); + + // http response + MockHttpServletResponse response = new MockHttpServletResponse(); + + // setup filter + SwitchUserProcessingFilter filter = new SwitchUserProcessingFilter(); + filter.setAuthenticationDao(new MockAuthenticationDaoUserJackLord()); + filter.setExitUserUrl("/j_acegi_exit_user"); + + MockFilterChain chain = new MockFilterChain(true); + + // run 'exit', expect fail due to no current user + try { + filter.doFilter(request, response, chain); + + fail("Cannot exit from a user with no current user set!"); + } catch (AuthenticationException expected) {} + } + + public void testRedirectToTargetUrl() throws Exception { + // set current user + UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken("dano", + "hawaii50"); + SecurityContextHolder.getContext().setAuthentication(auth); + + MockHttpServletRequest request = createMockSwitchRequest(); + request.addParameter(SwitchUserProcessingFilter.ACEGI_SECURITY_SWITCH_USERNAME_KEY, + "jacklord"); + request.setRequestURI("/webapp/j_acegi_switch_user"); + + MockHttpServletResponse response = new MockHttpServletResponse(); + MockFilterChain chain = new MockFilterChain(true); + + SwitchUserProcessingFilter filter = new SwitchUserProcessingFilter(); + filter.setSwitchUserUrl("/j_acegi_switch_user"); + filter.setTargetUrl("/webapp/someOtherUrl"); + filter.setAuthenticationDao(new MockAuthenticationDaoUserJackLord()); + + filter.doFilter(request, response, chain); + + assertEquals("/webapp/someOtherUrl", response.getRedirectedUrl()); + } + + public void testRequiresExitUser() { + // filter + SwitchUserProcessingFilter filter = new SwitchUserProcessingFilter(); + filter.setExitUserUrl("/j_acegi_exit_user"); + + // request + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRequestURI("/j_acegi_exit_user"); + + assertTrue(filter.requiresExitUser(request)); + } + + public void testRequiresSwitch() { + // filter + SwitchUserProcessingFilter filter = new SwitchUserProcessingFilter(); + filter.setSwitchUserUrl("/j_acegi_switch_user"); + + // request + MockHttpServletRequest request = createMockSwitchRequest(); + + assertTrue(filter.requiresSwitchUser(request)); + } + + public void testSwitchRequestFromDanoToJackLord() throws Exception { + // set current user + UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken("dano", + "hawaii50"); + SecurityContextHolder.getContext().setAuthentication(auth); + + // http request + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRequestURI("/webapp/j_acegi_switch_user"); + request.addParameter(SwitchUserProcessingFilter.ACEGI_SECURITY_SWITCH_USERNAME_KEY, + "jacklord"); + + // http response + MockHttpServletResponse response = new MockHttpServletResponse(); + + // setup filter + SwitchUserProcessingFilter filter = new SwitchUserProcessingFilter(); + filter.setAuthenticationDao(new MockAuthenticationDaoUserJackLord()); + filter.setSwitchUserUrl("/j_acegi_switch_user"); + + MockFilterChain chain = new MockFilterChain(true); + + // test updates user token and context + filter.doFilter(request, response, chain); + + // check current user + Authentication targetAuth = SecurityContextHolder.getContext() + .getAuthentication(); + assertNotNull(targetAuth); + assertEquals("jacklord", targetAuth.getPrincipal()); + } + + private MockHttpServletRequest createMockSwitchRequest() { + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setScheme("http"); + request.setServerName("localhost"); + request.setRequestURI("/j_acegi_switch_user"); + + return request; + } + + //~ Inner Classes ========================================================== + + private class MockAuthenticationDaoUserJackLord implements AuthenticationDao { + private String password = "hawaii50"; + + public void setPassword(String password) { + this.password = password; + } + + public UserDetails loadUserByUsername(String username) + throws UsernameNotFoundException, DataAccessException { + // jacklord, dano (active) + // mcgarrett (disabled) + // wofat (account expired) + // steve (credentials expired) + if ("jacklord".equals(username) || "dano".equals(username)) { + return new User(username, password, true, true, true, true, + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}); + } else if ("mcgarrett".equals(username)) { + return new User(username, password, false, true, true, true, + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}); + } else if ("wofat".equals(username)) { + return new User(username, password, true, false, true, true, + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}); + } else if ("steve".equals(username)) { + return new User(username, password, true, true, false, true, + new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl( + "ROLE_TWO")}); + } else { + throw new UsernameNotFoundException("Could not find: " + + username); + } + } + } +} diff --git a/doc/xdocs/changes.xml b/doc/xdocs/changes.xml index 20b4c8da63..3d8f8dae61 100644 --- a/doc/xdocs/changes.xml +++ b/doc/xdocs/changes.xml @@ -26,6 +26,7 @@ + SwitchUserProcessingFilter to provide user security context switching JdbcDaoImpl modified to support synthetic primary keys Greatly improve BasicAclEntryAfterInvocationCollectionFilteringProvider performance with large collections (if the principal has access to relatively few collection elements) Reorder DaoAuthenticationProvider exception logic as per developer list discussion