浏览代码

Initial checkin of user security context switching (see SEC-15). This is the first cut of the SwitchUserProcessingFilter that handles switching to a target uesr and exiting back to the original user. Note: This is going to be used for the common use-case of an Administrator 'switching' to another user (i.e. ROLE_ADMIN -> ROLE_USER). This is the initial cut of a Unix 'su' for Acegi managed web applications.

Mark St. Godard 20 年之前
父节点
当前提交
ec5e39c2e8

+ 59 - 0
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 <code>GrantedAuthority</code> used by {@link
+ * net.sf.acegisecurity.ui.switchuser.SwitchUserProcessingFilter}
+ * 
+ * <p>
+ * Stores the <code>Authentication</code> object of the original user to be
+ * used later when 'exiting' from a user switch.
+ * </p>
+ *
+ * @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 <code>Authentication</code> object of the switched
+     *         user.
+     */
+    public Authentication getSource() {
+        return source;
+    }
+}

+ 462 - 0
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.
+ * 
+ * <p>
+ * 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).
+ * </p>
+ * 
+ * <p>
+ * 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 <code>switchUserUrl</code>). <br>
+ * <b>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).</b>
+ * </p>
+ * 
+ * <p>
+ * On successful switch, the user's  <code>SecureContextHolder</code> 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.
+ * </p>
+ * 
+ * <p>
+ * To 'exit' from a user context, the user will then need to access a URL (see
+ * <code>exitUserUrl</code>)  that will switch back to the original user as
+ * identified by the <code>SWITCH_USER_GRANTED_AUTHORITY</code>.
+ * </p>
+ * 
+ * <p>
+ * To configure the Switch User Processing Filter, create a bean definition for
+ * the Switch User processing filter and add to the filterChainProxy. <br>
+ * Example:
+ * <pre>
+ * &lt;bean id="switchUserProcessingFilter" class="net.sf.acegisecurity.ui.switchuser.SwitchUserProcessingFilter">
+ *    &lt;property name="authenticationDao" ref="jdbcDaoImpl" />
+ *    &lt;property name="switchUserUrl">&lt;value>/j_acegi_switch_user&lt;/value>&lt;/property>
+ *    &lt;property name="exitUserUrl">&lt;value>/j_acegi_exit_user&lt;/value>&lt;/property>
+ *    &lt;property name="targetUrl">&lt;value>/index.jsp&lt;/value>&lt;/property>
+ * &lt;/bean>
+ * </pre>
+ * </p>
+ *
+ * @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 <code>/j_acegi_exit_user</code>.
+     *
+     * @return the default exit user url
+     */
+    public String getDefaultExitUserUrl() {
+        return "/j_acegi_exit_user";
+    }
+
+    // ~ Methods
+    // ================================================================
+
+    /**
+     * This filter by default responds to <code>/j_acegi_switch_user</code>.
+     *
+     * @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 <code>Authentication</code> object or
+     *         <code>null</code> otherwise.
+     *
+     * @throws AuthenticationCredentialsNotFoundException If no
+     *         <code>Authentication</code> 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 <code>Authentication</code> request if successfully
+     *         switched to another user, <code>null</code> 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 <tt>exitUserUrl</tt>.
+     *
+     * @param request The http servlet request
+     *
+     * @return <code>true</code> if the request requires a exit user,
+     *         <code>false</code> 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 <tt>switchUserUrl</tt>.
+     *
+     * @param request The http servlet request
+     *
+     * @return <code>true</code> if the request requires a switch,
+     *         <code>false</code> 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 <code>Authentication</code> object from the current
+     * user's granted authorities. A successfully switched user should have a
+     * <code>SwitchUserGrantedAuthority</code> that contains the original
+     * source user <code>Authentication</code> object.
+     *
+     * @param current The current <code>Authentication</code> object
+     *
+     * @return The source user <code>Authentication</code> object or
+     *         <code>null</code> 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
+     * <tt>GrantedAuthority</tt> that contains the original
+     * <code>Authentication</code> 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;
+    }
+}

+ 447 - 0
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);
+            }
+        }
+    }
+}

+ 1 - 0
doc/xdocs/changes.xml

@@ -26,6 +26,7 @@
   </properties>
   <body>
     <release version="0.9.0" date="In CVS">
+      <action dev="markstg" type="add">SwitchUserProcessingFilter to provide user security context switching</action>
       <action dev="benalex" type="update">JdbcDaoImpl modified to support synthetic primary keys</action>
       <action dev="benalex" type="update">Greatly improve BasicAclEntryAfterInvocationCollectionFilteringProvider performance with large collections (if the principal has access to relatively few collection elements)</action>
       <action dev="benalex" type="update">Reorder DaoAuthenticationProvider exception logic as per developer list discussion</action>