/* * JBoss, the OpenSource WebOS * * Distributable under LGPL license. * See terms of license at gnu.org. */ package org.jboss.web.tomcat.tc5.sso; import java.io.Serializable; import java.util.ArrayList; import java.util.HashSet; import java.util.Set; import javax.naming.InitialContext; import javax.naming.NamingException; import javax.transaction.UserTransaction; import org.apache.catalina.Host; import org.apache.catalina.LifecycleException; import org.apache.catalina.Session; import org.apache.catalina.authenticator.SSOClusterManagerBase; import org.jboss.cache.Fqn; import org.jboss.cache.TreeCache; import org.jboss.cache.TreeCacheListener; import org.jboss.cache.lock.IsolationLevel; import org.jboss.logging.Logger; import org.jgroups.View; /** * An implementation of Tomcat's SSOClusterManager that uses a TreeCache * to share SSO information between cluster nodes. */ public final class JBossSSOClusterManager extends SSOClusterManagerBase implements TreeCacheListener { // ------------------------------------------------------------- Constants /** * Final segment of any FQN that names a TreeCache node storing * SSO credential information. */ private static final String CREDENTIALS = "credentials"; /** * Final segment of any FQN that names a TreeCache node storing * the set of Sessions associated with an SSO. */ private static final String SESSIONS = "sessions"; /** * Key under which data is stored to the TreeCache. */ private static final String KEY = "key"; /** * Maximum time between cycles of the CredentialUpdater thread. */ private static final int UPDATE_THREAD_INTERVAL = 30000; /** * Maximum time between cycles of the LogoutHandler thread. */ private static final int LOGOUT_THREAD_INTERVAL = 30000; // ------------------------------------------------------- Instance Fields /** * List of SSO ids which this object is currently storing to the cache */ private ArrayList beingLocallyAdded = new ArrayList(); /** * List of SSO ids which this object is currently removing from the cache */ private ArrayList beingLocallyRemoved = new ArrayList(); /** * InitialContext used for JNDI lookups */ private InitialContext initialContext = null; /** * The Log-object for this class */ private Logger log; /** * Whether we have been started */ private boolean started = false; /** * The TreeCache in which sso data is stored. */ private TreeCache treeCache; /** * CredentialUpdater used to allow asynchronous updates of * SSO credentials */ private CredentialUpdater credentialUpdater = null; /** * LogoutHandle used to allow asynchronous handling of logouts */ private LogoutHandler logoutHandler = null; // ---------------------------------------------------------- Constructors /** * Creates a new JBossSSOClusterManager */ public JBossSSOClusterManager() { log = Logger.getLogger(getClass()); } // ----------------------------------------------------- SSOClusterManager /** * Notify the cluster of the addition of a Session to an SSO session. * * @param ssoId the id of the SSO session * @param session the Session that has been added */ public void addSession(String ssoId, Session session) { if (log.isTraceEnabled()) { log.trace("addSession(): adding Session " + session.getId() + " to cached session set for SSO " + ssoId); } Fqn fqn = new Fqn(new Object[] {ssoId, SESSIONS}); UserTransaction tx = null; try { tx = getNewTransaction(); tx.begin(); Set sessions = (Set) treeCache.get(fqn, KEY); if (sessions != null) { sessions.add(session.getId()); treeCache.put(fqn, KEY, sessions); } tx.commit(); } catch (Exception e) { if (tx != null) { try { tx.rollback(); } catch(Throwable t) {} } String sessId = (session == null ? "NULL" : session.getId()); log.error("caught exception adding session " + sessId + " to SSO id " + ssoId, e); } } /** * Notifies the cluster that a single sign on session has been terminated * due to a user logout. * * @param ssoId */ public void logout(String ssoId) { if (log.isTraceEnabled()) { log.trace("Registering logout of SSO " + ssoId + " in clustered cache"); } Fqn fqn = new Fqn(ssoId); // Add this SSO to our list of in-process local removals so // this.nodeRemoved() will ignore the removal synchronized (beingLocallyRemoved) { beingLocallyRemoved.add(ssoId); } UserTransaction tx = null; try { tx = getNewTransaction(); tx.begin(); treeCache.remove(fqn); tx.commit(); } catch (Exception e) { if (tx != null) { try { tx.rollback(); } catch(Throwable t) {} } log.error("Exception attempting to remove node " + fqn.toString() + " from TreeCache", e); } finally { synchronized (beingLocallyRemoved) { beingLocallyRemoved.remove(ssoId); } } } /** * Notifies the cluster of the creation of a new SSO entry. * * @param ssoId the id of the SSO session * @param authType the type of authenticator (BASIC, CLIENT-CERT, DIGEST * or FORM) used to authenticate the SSO. * @param username the username (if any) used for the authentication * @param password the password (if any) used for the authentication */ public void register(String ssoId, String authType, String username, String password) { if (log.isTraceEnabled()) { log.trace("Registering SSO " + ssoId + " in clustered cache"); } storeSSOData(ssoId, authType, username, password, true); } /** * Notify the cluster of the removal of a Session from an SSO session. * * @param ssoId the id of the SSO session * @param session the Session that has been removed */ public void removeSession(String ssoId, Session session) { if (log.isTraceEnabled()) { log.trace("removeSession(): removing Session " + session.getId() + " from cached session set for SSO " + ssoId); } Fqn fqn = new Fqn(new Object[] {ssoId, SESSIONS}); UserTransaction tx = null; boolean removing = false; try { tx = getNewTransaction(); tx.begin(); Set sessions = (Set) treeCache.get(fqn, KEY); if (sessions != null) { sessions.remove(session.getId()); if (sessions.size() == 0) { // Add this SSO to our list of in-process local removals so // this.nodeRemoved() will ignore the removal synchronized (beingLocallyRemoved) { beingLocallyRemoved.add(ssoId); } removing = true; // No sessions left; remove node treeCache.remove(new Fqn(ssoId)); } else { treeCache.put(fqn, KEY, sessions); } } tx.commit(); } catch (Exception e) { if (tx != null) { try { tx.rollback(); } catch(Throwable t) {} } String sessId = (session == null ? "NULL" : session.getId()); log.error("caught exception removing session " + sessId + " from SSO id " + ssoId, e); } finally { if (removing) { synchronized (beingLocallyRemoved) { beingLocallyRemoved.remove(ssoId); } } } } /** * Notifies the cluster of an update of the security credentials * associated with an SSO session. * * @param ssoId the id of the SSO session * @param authType the type of authenticator (BASIC, CLIENT-CERT, DIGEST * or FORM) used to authenticate the SSO. * @param username the username (if any) used for the authentication * @param password the password (if any) used for the authentication */ public void updateCredentials(String ssoId, String authType, String username, String password) { if (log.isTraceEnabled()) { log.trace("Updating credentials for SSO " + ssoId + " in clustered cache"); } storeSSOData(ssoId, authType, username, password, false); } // ------------------------------------------------------ TreeCacheListener /** * Does nothing */ public void nodeAdded(Fqn fqn) { ; // do nothing } /** * Does nothing */ public void nodeVisited(Fqn fqn) { ; // do nothing } /** * Does nothing */ public void cacheStarted(TreeCache cache) { ; // do nothing } /** * Does nothing */ public void cacheStopped(TreeCache cache) { ; // do nothing } /** * Extracts an SSO session id from the Fqn and uses it in an invocation of * {@link #receiveLogout receiveLogout}. *
* Ignores invocations resulting from TreeCache changes originated by * this object. * * @param fqn the fully-qualified name of the node that was removed */ public void nodeRemoved(Fqn fqn) { String ssoId = getIdFromFqn(fqn); // Ignore messages generated by our own activity if (beingLocallyRemoved.contains(ssoId)) { return; } // Ignore SSOs our local valve does not know about if (doLocalLookup(ssoId) == null) { return; } if (log.isTraceEnabled()) { log.trace("received a node removed message for SSO " + ssoId); } // Handle the logout in a separate thread so that if it starts // invalidating sessions, etc, we avoid deadlocks logoutHandler.enqueue(ssoId); } /** * Extracts an SSO session id from the Fqn and uses it in an invocation of * {@link #receiveUpdateCredentials receiveUpdateCredentials}. *
* Only responds to modifications of nodes whose FQN's final segment is * "credentials". *
* Ignores invocations resulting from TreeCache changes originated by * this object. *
* Ignores invocations for SSO session id's that are not registered
* with the local SingleSignOn valve.
*
* @param fqn the fully-qualified name of the node that was modified
*/
public void nodeModified(Fqn fqn)
{
// We are only interested in changes to the CREDENTIALS node
if (CREDENTIALS.equals(getTypeFromFqn(fqn)) == false)
{
return;
}
String ssoId = getIdFromFqn(fqn);
// Ignore invocations that come as a result of our additions
if (beingLocallyAdded.contains(ssoId))
{
return;
}
// Ignore invocations for SSOs the local valve knows nothing about
if (doLocalLookup(ssoId) == null)
{
return;
}
if (log.isTraceEnabled())
{
log.trace("received a credentials modified message for SSO " + ssoId);
}
// Put this SSO in the queue of those to be updated
credentialUpdater.enqueue(ssoId);
}
/**
* Does nothing
*/
public void viewChange(View new_view)
{
; // do nothing
}
// ------------------------------------------------------------- Lifecycle
/**
* Prepare for the beginning of active use of the public methods of this
* component. This method should be called before any of the public
* methods of this component are utilized. It should also send a
* LifecycleEvent of type START_EVENT to any registered listeners.
*
* @exception LifecycleException if this component detects a fatal error
* that prevents this component from being used
*/
public void start() throws LifecycleException
{
// Validate and update our current component state
if (started)
throw new LifecycleException
("JBossSSOClusterManager already Started");
try
{
createTreeCache();
}
catch (Exception e)
{
throw new LifecycleException("cannot start TreeCache", e);
}
started = true;
// Notify our interested LifecycleListeners
getLifecycleSupport().fireLifecycleEvent(START_EVENT, null);
}
/**
* Gracefully terminate the active use of the public methods of this
* component. This method should be the last one called on a given
* instance of this component. It should also send a LifecycleEvent
* of type STOP_EVENT to any registered listeners.
*
* @exception LifecycleException if this component detects a fatal error
* that needs to be reported
*/
public void stop() throws LifecycleException
{
// Validate and update our current component state
if (!started)
throw new LifecycleException
("JBossSSOClusterManager not Started");
treeCache.stop();
credentialUpdater.stop();
logoutHandler.stop();
started = false;
// Notify our interested LifecycleListeners
getLifecycleSupport().fireLifecycleEvent(STOP_EVENT, null);
}
// ----------------------------------------- Overridden Superclass Methods
protected final SSOClusterManagerBase.SSOCredentials
doCredentialLookup(String ssoId)
{
SSOClusterManagerBase.SSOCredentials result = null;
Fqn fqn = new Fqn(new Object[] {ssoId, CREDENTIALS});
UserTransaction tx = null;
try
{
tx = getNewTransaction();
tx.begin();
SSOData data = (SSOData) treeCache.get(fqn, KEY);
if (data != null)
{
result =
new SSOClusterManagerBase.SSOCredentials(data.getAuthType(),
data.getUsername(),
data.getPassword());
}
tx.commit();
}
catch (Exception e)
{
if (tx != null)
{
try { tx.rollback(); } catch(Throwable t) {}
}
log.error("caught exception looking up SSOData for SSO id " +
ssoId, e);
}
return result;
}
// ------------------------------------------------------- Private Methods
/**
* Configures and starts the TreeCache used to share SSOData objects
* between cluster members.
*
* @throws Exception if TreeCache does
*/
private void createTreeCache()
throws Exception
{
try
{
TreeCache cache = new TreeCache();
Host host = (Host) getSingleSignOnValve().getContainer();
String hostName = host.getName();
String clusterName = "sso-cluster/" + hostName;
cache.setClusterName(clusterName);
if (log.isDebugEnabled())
{
log.debug("clusterName set to " + clusterName);
}
cache.setClusterProperties("sso-channel.xml");
cache.setCacheMode(TreeCache.REPL_SYNC);
// TODO: 2004/02/09 we tried this with READ_COMMITTED, but kept
// getting inexplicable locking problems. May have been due to issues
// in Branch_3_2 TreeCache??? Should try again w/ HEAD
cache.setIsolationLevel(IsolationLevel.REPEATABLE_READ);
cache.setTransactionManagerLookupClass("org.jboss.cache.JBossTransactionManagerLookup");
cache.addTreeCacheListener(this);
cache.start();
treeCache = cache;
// Start the thread we use to clear nodeModified events
credentialUpdater = new CredentialUpdater();
// Start the thread we use to handle logout notifications
logoutHandler = new LogoutHandler();
}
catch (Exception e)
{
log.error("Exception starting TreeCache", e);
throw e;
}
}
/**
* Extracts an SSO session id from a fully qualified name object.
* @param fqn the Fully Qualified Name used by TreeCache
* @return the last element in the Fqn -- the SSO session id
*/
private String getIdFromFqn(Fqn fqn)
{
return (String) fqn.get(0);
}
private InitialContext getInitialContext() throws NamingException
{
if (initialContext == null)
{
initialContext = new InitialContext();
}
return initialContext;
}
/**
* Extracts an SSO session id from a fully qualified name object.
* @param fqn the Fully Qualified Name used by TreeCache
* @return the last element in the Fqn -- the SSO session id
*/
private String getTypeFromFqn(Fqn fqn)
{
return (String) fqn.get(fqn.size() - 1);
}
private UserTransaction getNewTransaction() throws NamingException
{
try
{
UserTransaction t =
(UserTransaction) getInitialContext().lookup("UserTransaction");
return t;
}
catch (NamingException n)
{
// Discard the cached initial context
// in case there is a problem with it
initialContext = null;
throw n;
}
}
/**
* Stores the given data to the clustered cache in a tree branch whose FQN
* is the given SSO id. Stores the given credential data in a child node
* named "credentials". If parameter storeSessions
is
* true
, also stores an empty HashSet in a sibling node
* named "sessions". This HashSet will later be used to hold session ids
* associated with the SSO.
*
* Any items stored are stored under the key "key".
*
* @param ssoId the id of the SSO session
* @param authType the type of authenticator (BASIC, CLIENT-CERT, DIGEST
* or FORM) used to authenticate the SSO.
* @param username the username (if any) used for the authentication
* @param password the password (if any) used for the authentication
* @param storeSessions true
if a new, empty HashSet should be
* also be stored
*/
private void storeSSOData(String ssoId, String authType, String username,
String password, boolean storeSessions)
{
SSOData data = new SSOData(authType, username, password);
// Add this SSO to our list of in-process local adds so
// this.nodeModified() will ignore the addition
synchronized (beingLocallyAdded)
{
beingLocallyAdded.add(ssoId);
}
UserTransaction tx = null;
try
{
tx = getNewTransaction();
tx.begin();
treeCache.put(new Fqn(new Object[] {ssoId, CREDENTIALS}),
KEY, data);
if (storeSessions)
{
treeCache.put(new Fqn(new Object[] {ssoId, SESSIONS}),
KEY, new HashSet());
}
tx.commit();
}
catch (Exception e)
{
if (tx != null)
{
try { tx.rollback(); } catch(Throwable t) {}
}
log.error("Exception attempting to add TreeCache nodes for SSO " +
ssoId, e);
}
finally
{
synchronized (beingLocallyAdded)
{
beingLocallyAdded.remove(ssoId);
}
}
}
// --------------------------------------------------------- Inner Classes
/**
* Spawns a thread to handle updates of credentials
*/
private class CredentialUpdater implements Runnable
{
private HashSet awaitingUpdate = new HashSet();
private Thread updateThread;
private boolean stopped = false;
CredentialUpdater()
{
updateThread = new Thread(this);
updateThread.start();
}
// --------------------------------------- Package Protected Methods
/**
* Adds an SSO id to the set of those awaiting credential updating, and
* interrupts the update handler thread to notify it of the addition.
*
* @param ssoId the id of the SSO session whose local credentials
* are to be updated
*/
void enqueue(String ssoId)
{
awaitingUpdate.add(ssoId);
updateThread.interrupt();
}
/**
* Stops the update handler thread.
*/
void stop()
{
stopped = true;
}
// ------------------------------------------------- Private Methods
private void processUpdate(String ssoId)
{
Fqn fqn = new Fqn(new Object[] {ssoId, CREDENTIALS});
UserTransaction tx = null;
try
{
tx = getNewTransaction();
tx.begin();
SSOData data = (SSOData) treeCache.get(fqn, KEY);
if (data != null)
{
// We want to release our read lock quickly, so get the needed
// data from the cache, commit the tx, and then use the data
String authType = data.getAuthType();
String username = data.getUsername();
String password = data.getPassword();
tx.commit();
if (log.isTraceEnabled())
{
log.trace("CredentialUpdater: Updating credentials for SSO " +
ssoId);
}
receiveUpdateCredentials(ssoId, authType, username, password);
}
else
{
tx.commit();
}
}
catch (Exception e)
{
if (tx != null)
{
try { tx.rollback(); } catch(Throwable t) {}
}
log.error("Exception attempting to get SSOData from TreeCache node " +
fqn.toString(), e);
}
}
// ------------------------------------------------------ Runnable
public void run()
{
while (!stopped)
{
// Get the current list of ids awaiting processing
String[] ids = null;
synchronized(awaitingUpdate)
{
ids = new String[awaitingUpdate.size()];
ids = (String[]) awaitingUpdate.toArray(ids);
awaitingUpdate.clear();
}
// Handle the credential update
for (int i = 0; i < ids.length; i++)
{
processUpdate(ids[i]);
}
// Wait for another invocation of enqueue(). But,
// first have to check in case it was invoked while we
// were processing the previous bunch
if (!Thread.interrupted())
{
try
{
updateThread.sleep(UPDATE_THREAD_INTERVAL);
}
catch (InterruptedException e)
{
if (log.isTraceEnabled())
{
log.trace("LogoutHandler: interrupted");
}
; // process the next bunch
}
}
else if (log.isTraceEnabled())
{
log.trace("CredentialUpdater: interrupted while handling " +
"updates");
}
}
}
} // end CredentialUpdater
/**
* Spawns a thread to handle logouts
*/
private class LogoutHandler implements Runnable
{
private HashSet awaitingLogout = new HashSet();
private Thread logoutThread;
private boolean stopped = false;
LogoutHandler()
{
logoutThread = new Thread(this);
logoutThread.start();
}
// --------------------------------------- Package Protected Methods
/**
* Adds an SSO id to the set of those awaiting logout handling, and
* interrupts the logout handler thread to notify it of the addition.
*
* @param ssoId the id of the SSO session to logout
*/
void enqueue(String ssoId)
{
awaitingLogout.add(ssoId);
logoutThread.interrupt();
}
/**
* Stops the logout handler thread
*/
void stop()
{
stopped = true;
}
// ------------------------------------------------------ Runnable
public void run()
{
while (!stopped)
{
// Get the current list of ids awaiting logout handling
String[] ids = null;
synchronized(awaitingLogout)
{
ids = new String[awaitingLogout.size()];
ids = (String[]) awaitingLogout.toArray(ids);
awaitingLogout.clear();
}
// Handle the logout
for (int i = 0; i < ids.length; i++)
{
if (log.isTraceEnabled())
{
log.trace("LogoutHandler: Handling logout for SSO " +
ids[i]);
}
receiveLogout(ids[i]);
}
// Wait for another invocation of enqueue(). But,
// first have to check in case it was invoked while we
// were processing the previous bunch
if (!Thread.interrupted())
{
try
{
logoutThread.sleep(LOGOUT_THREAD_INTERVAL);
}
catch (InterruptedException e)
{
if (log.isTraceEnabled())
{
log.trace("LogoutHandler: interrupted");
}
; // process the next bunch
}
}
else if (log.isTraceEnabled())
{
log.trace("LogoutHandler: interrupted while handling logouts");
}
}
}
}
// --------------------------------------------------------- Outer Classes
/**
* Private class used to store authentication credentials in the TreeCache.
*
* For security, all methods are private.
*/
private static class SSOData implements Serializable
{
// --------------------------------------------------- Instance Fields
private String authType = null;
private String password = null;
private String username = null;
/**
* Creates a new SSOData.
*
* @param authType The authorization method used to authorize the
* SSO (BASIC, CLIENT-CERT, DIGEST, FORM or NONE).
* @param username The username of the user associated with the SSO
* @param password The password of the user associated with the SSO
*/
SSOData(String authType, String username, String password) {
this.authType = authType;
this.username = username;
this.password = password;
}
/**
* Gets the authorization method used to authorize the SSO.
*
* @return "BASIC", "CLIENT-CERT", "DIGEST" or "FORM"
*/
private String getAuthType()
{
return authType;
}
/**
* Gets the password of the user associated with the SSO.
*
* @return the password, or null
if the authorization
* type was DIGEST or CLIENT-CERT.
*/
private String getPassword()
{
return password;
}
/**
* Gets the username of the user associated with the SSO.
*
* @return the username
*/
private String getUsername()
{
return username;
}
}
}