/*
* JBoss, a division of Red Hat
* Copyright 2013, Red Hat Middleware, LLC, and individual
* contributors as indicated by the @authors tag. See the
* copyright.txt in the distribution for a full listing of
* individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.gatein.web.security.impersonation;
import org.exoplatform.container.web.AbstractHttpServlet;
import org.exoplatform.portal.config.UserACL;
import org.exoplatform.services.organization.OrganizationService;
import org.exoplatform.services.organization.User;
import org.exoplatform.services.organization.UserStatus;
import org.exoplatform.services.security.Authenticator;
import org.exoplatform.services.security.ConversationRegistry;
import org.exoplatform.services.security.ConversationState;
import org.exoplatform.services.security.Identity;
import org.exoplatform.services.security.IdentityRegistry;
import org.exoplatform.services.security.StateKey;
import org.exoplatform.services.security.web.HttpSessionStateKey;
import org.gatein.common.logging.Logger;
import org.gatein.common.logging.LoggerFactory;
import org.gatein.wci.ServletContainerFactory;
import org.gatein.wci.session.SessionTask;
import org.gatein.wci.session.SessionTaskVisitor;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* Servlet, which handles impersonation and impersonalization (de-impersonation) of users
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class ImpersonationServlet extends AbstractHttpServlet {
/** Request parameter to track if we want to start new impersonation session or stop existing impersonation session */
public static final String PARAM_ACTION = "_impersonationAction";
public static final String PARAM_ACTION_START_IMPERSONATION = "startImpersonation";
public static final String PARAM_ACTION_STOP_IMPERSONATION = "stopImpersonation";
/** Request parameter with name of user, who will be impersonated */
public static final String PARAM_USERNAME = "_impersonationUsername";
/**
* Request parameter where is stored URI, which will be used after impersonation session will be finished
* The point is that admin user will be redirected to same page (navigation node) from which original impersonation session was started
* */
public static final String PARAM_RETURN_IMPERSONATION_URI = "_returnImpersonationURI";
/** Session attribute where return impersonation URI will be saved */
public static final String ATTR_RETURN_IMPERSONATION_URI = "_returnImpersonationURI";
/** Impersonation suffix (Actually path of this servlet) */
public static final String IMPERSONATE_URL_SUFIX = "/impersonate";
/** Session attribute, which will be used to backup existing session of root user */
private static final String BACKUP_ATTR = "_impersonation.bck";
private static final Logger log = LoggerFactory.getLogger(ImpersonationServlet.class);
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
try {
// We set the character encoding now to UTF-8 before obtaining parameters
req.setCharacterEncoding("UTF-8");
} catch (UnsupportedEncodingException e) {
log.error("Encoding not supported", e);
}
String action = req.getParameter(PARAM_ACTION);
if (action == null) {
log.error("Parameter '" + PARAM_ACTION + "' not provided");
resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
} else if (PARAM_ACTION_START_IMPERSONATION.equals(action)) {
startImpersonation(req, resp);
} else if (PARAM_ACTION_STOP_IMPERSONATION.equals(action)) {
stopImpersonation(req, resp);
} else {
log.error("Unknown impersonation action: " + action);
resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
}
}
protected void startImpersonation(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// Obtain username
String usernameToImpersonate = req.getParameter(PARAM_USERNAME);
if (usernameToImpersonate == null) {
log.error("Parameter '" + PARAM_USERNAME + "' not provided");
resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
// Find user to impersonate
OrganizationService orgService = (OrganizationService)getContainer().getComponentInstanceOfType(OrganizationService.class);
User userToImpersonate;
try {
userToImpersonate = orgService.getUserHandler().findUserByName(usernameToImpersonate, UserStatus.ANY);
} catch (Exception e) {
throw new ServletException(e);
}
if (userToImpersonate == null) {
log.error("User '" + usernameToImpersonate + "' not found!");
resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
ConversationState currentConversationState = ConversationState.getCurrent();
Identity currentIdentity = currentConversationState.getIdentity();
if (currentIdentity instanceof ImpersonatedIdentity) {
log.error("Already impersonated as identity: " + currentIdentity);
resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
if (!checkPermission(userToImpersonate)) {
log.error("Current user represented by identity " + currentIdentity.getUserId() + " doesn't have permission to impersonate as "
+ userToImpersonate);
resp.sendError(HttpServletResponse.SC_FORBIDDEN);
return;
}
log.debug("Going to impersonate as user: " + usernameToImpersonate);
// Backup and clear current HTTP session
backupAndClearCurrentSession(req);
// Obtain URI where we need to redirect after finish impersonation session. Save it to current HTTP session
String returnImpersonationURI = req.getParameter(PARAM_RETURN_IMPERSONATION_URI);
if (returnImpersonationURI == null) {
returnImpersonationURI = req.getContextPath();
}
req.getSession().setAttribute(ATTR_RETURN_IMPERSONATION_URI, returnImpersonationURI);
if (log.isTraceEnabled()) {
log.trace("Saved URI " + returnImpersonationURI + " which will be used after finish of impersonation");
}
// Real impersonation done here
boolean success = impersonate(req, currentConversationState, usernameToImpersonate);
if (success) {
// Redirect to portal for now
resp.sendRedirect(req.getContextPath());
} else {
resp.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);
}
}
/**
* Check if current user has permission to impersonate as user 'userToImpersonate'
*
* @param userToImpersonate user to check
* @return true if current user has permission to impersonate as user 'userToImpersonate'
*/
protected boolean checkPermission(User userToImpersonate) {
UserACL userACL = (UserACL)getContainer().getComponentInstanceOfType(UserACL.class);
return userACL.hasImpersonateUserPermission(userToImpersonate);
}
/**
* Backup all session attributes of admin user as we will have new session for "impersonated" user
*
* @param req http servlet request
*/
protected void backupAndClearCurrentSession(HttpServletRequest req) {
HttpSession session = req.getSession(false);
if (session != null) {
String sessionId = session.getId();
// Backup attributes in sessions of portal and all portlet applications
ServletContainerFactory.getServletContainer().visit(new SessionTaskVisitor(sessionId, new SessionTask(){
@Override
public boolean executeTask(HttpSession session) {
if (log.isTraceEnabled()) {
log.trace("Starting with backup attributes for context: " + session.getServletContext().getContextPath());
}
// Create a copy just to make sure that attrNames is transient
List<String> attrNames = offlineCopy(session.getAttributeNames());
Map<String, Object> backup = new HashMap<String, Object>();
for (String attrName : attrNames) {
Object attrValue = session.getAttribute(attrName);
session.removeAttribute(attrName);
backup.put(attrName, attrValue);
if (log.isTraceEnabled()) {
log.trace("Finished backup of attribute: " + attrName);
}
}
session.setAttribute(BACKUP_ATTR, backup);
return true;
}
}));
}
}
/**
* Start impersonation session and update ConversationRegistry with new impersonated Identity
*
* @param req servlet request
* @param currentConvState current Conversation State. It will be wrapped inside impersonated identity, so we can later restore it
* @param usernameToImpersonate
* @return true if impersonation was successful
*/
protected boolean impersonate(HttpServletRequest req, ConversationState currentConvState, String usernameToImpersonate) {
// Create new identity for user, who will be impersonated
Identity newIdentity = createIdentity(usernameToImpersonate);
if (newIdentity == null) {
return false;
}
ImpersonatedIdentity impersonatedIdentity = new ImpersonatedIdentity(newIdentity, currentConvState);
// Create new entry to ConversationState
log.debug("Set ConversationState with current session. Admin user "
+ impersonatedIdentity.getParentConversationState().getIdentity().getUserId()
+ " will use identity of user " + impersonatedIdentity.getUserId());
ConversationState impersonatedConversationState = new ConversationState(impersonatedIdentity);
registerConversationState(req, impersonatedConversationState);
return true;
}
/**
* Stop impersonation session and restore previous Conversation State
*
* @param req servlet request
* @param resp servlet response
*/
protected void stopImpersonation(HttpServletRequest req, HttpServletResponse resp) throws IOException {
Identity currentIdentity = ConversationState.getCurrent().getIdentity();
if (!(currentIdentity instanceof ImpersonatedIdentity)) {
log.error("Can't stop impersonation session. Current identity is not instance of Impersonated Identity! Current identity: " + currentIdentity);
resp.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
ImpersonatedIdentity impersonatedIdentity = (ImpersonatedIdentity)currentIdentity;
log.debug("Cancel impersonation session. Impersonated user was: " + impersonatedIdentity.getUserId()
+ ", Admin user is: " + impersonatedIdentity.getParentConversationState().getIdentity().getUserId());
// Restore old conversation state
restoreConversationState(req, impersonatedIdentity);
// Restore return URI from session
String returnURI = getReturnURI(req);
// Restore session attributes of root user
restoreOldSessionAttributes(req);
if (log.isTraceEnabled()) {
log.trace("Impersonation finished. Redirecting to " + returnURI);
}
resp.sendRedirect(returnURI);
}
protected void restoreConversationState(HttpServletRequest req, ImpersonatedIdentity impersonatedIdentity) {
ConversationState adminConvState = impersonatedIdentity.getParentConversationState();
registerConversationState(req, adminConvState);
// Possibly restore identity if it's not available anymore in IdentityRegistry. This could happen during parallel logout of admin user from another session
IdentityRegistry identityRegistry = (IdentityRegistry)getContainer().getComponentInstanceOfType(IdentityRegistry.class);
String adminUsername = adminConvState.getIdentity().getUserId();
Identity adminIdentity = identityRegistry.getIdentity(adminUsername);
if (adminIdentity == null) {
log.debug("Restore of identity of user " + adminUsername + " in IdentityRegistry");
adminIdentity = createIdentity(adminUsername);
identityRegistry.register(adminIdentity);
}
}
protected void restoreOldSessionAttributes(HttpServletRequest req) {
HttpSession session = req.getSession(false);
if (session != null) {
String sessionId = session.getId();
// Restore attributes in sessions of portal and all portlet applications
ServletContainerFactory.getServletContainer().visit(new SessionTaskVisitor(sessionId, new SessionTask(){
@Override
public boolean executeTask(HttpSession session) {
if (log.isTraceEnabled()) {
log.trace("Starting with restoring attributes for context: " + session.getServletContext().getContextPath());
}
// Retrieve backup of previous attributes
Map<String, Object> backup = (Map<String, Object>)session.getAttribute(BACKUP_ATTR);
// Iteration 1 -- Remove all session attributes of current (impersonated) user.
List<String> attrNames = offlineCopy(session.getAttributeNames());
for (String attrName : attrNames) {
session.removeAttribute(attrName);
if (log.isTraceEnabled()) {
log.trace("Removed attribute: " + attrName);
}
}
// Iteration 2 -- Restore all session attributes of admin user
if (backup == null) {
if (log.isTraceEnabled()) {
log.trace("No session attributes found in previous impersonated session. Ignoring");
}
} else {
for (Map.Entry<String, Object> attr : backup.entrySet()) {
session.setAttribute(attr.getKey(), attr.getValue());
if (log.isTraceEnabled()) {
log.trace("Finished restore of attribute: " + attr.getKey());
}
}
}
return true;
}
}));
}
}
// Register given conversationState into ConversationRegistry. Key will be current Http session
private void registerConversationState(HttpServletRequest req, ConversationState conversationState) {
HttpSession httpSession = req.getSession();
StateKey stateKey = new HttpSessionStateKey(httpSession);
ConversationRegistry conversationRegistry = (ConversationRegistry)getContainer().getComponentInstanceOfType(ConversationRegistry.class);
conversationRegistry.register(stateKey, conversationState);
}
private Identity createIdentity(String username) {
Authenticator authenticator = (Authenticator) getContainer().getComponentInstanceOfType(Authenticator.class);
try {
return authenticator.createIdentity(username);
} catch (Exception e) {
log.error("New identity for user: " + username + " not created.", e);
return null;
}
}
private String getReturnURI(HttpServletRequest req) {
String returnURI = null;
HttpSession session = req.getSession(false);
if (session != null) {
returnURI = (String)session.getAttribute(ATTR_RETURN_IMPERSONATION_URI);
}
if (returnURI == null) {
returnURI = req.getContextPath();
}
return returnURI;
}
private List<String> offlineCopy(Enumeration<String> e) {
List<String> list = new LinkedList<String>();
while (e.hasMoreElements()) {
list.add(e.nextElement());
}
return list;
}
}