| TreeCacheSSOClusterManager.java |
/*
* 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.security.Principal;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;
import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.transaction.UserTransaction;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.LifecycleListener;
import org.apache.catalina.Session;
import org.apache.catalina.util.LifecycleSupport;
import org.jboss.cache.Fqn;
import org.jboss.cache.TreeCache;
import org.jboss.cache.TreeCacheListener;
import org.jboss.logging.Logger;
import org.jboss.mx.util.MBeanServerLocator;
import org.jboss.web.tomcat.tc5.Tomcat5;
import org.jgroups.View;
/**
* An implementation of SSOClusterManager that uses a TreeCache
* to share SSO information between cluster nodes.
*
* @author Brian E. Stansberry
* @version $Revision& $Date: 2005/04/03 07:23:28 $
*/
public final class TreeCacheSSOClusterManager
implements SSOClusterManager, TreeCacheListener
{
// ------------------------------------------------------------- Constants
/**
* Final segment of any FQN that names a TreeCache node storing
* SSO credential information.
*/
private static final String CREDENTIALS = "credentials";
/**
* First segment of any FQN that names a TreeCache node associated
* with an SSO
*/
private static final String SSO = "SSO";
/**
* 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";
/**
* Default global value for the cacheName property
*/
public static final String DEFAULT_GLOBAL_CACHE_NAME =
Tomcat5.DEFAULT_CACHE_NAME;
/**
* Parameter signature used for TreeCache.get calls over JMX
*/
private static final String[] GET_SIGNATURE =
{Fqn.class.getName(), Object.class.getName()};
/**
* Parameter signature used for TreeCache.put calls over JMX
*/
private static final String[] PUT_SIGNATURE =
{Fqn.class.getName(), Object.class.getName(), Object.class.getName()};
/**
* Parameter signature used for TreeCache.remove calls over JMX
*/
private static final String[] REMOVE_SIGNATURE = {Fqn.class.getName()};
// ------------------------------------------------------- Instance Fields
/**
* List of SSO ids which this object is currently storing to the cache
*/
private LinkedList beingLocallyAdded = new LinkedList();
/**
* List of SSO ids which this object is currently removing from the cache
*/
private LinkedList beingLocallyRemoved = new LinkedList();
/**
* List of SSO ids which are being deregistered due to removal on another
* node
*/
private LinkedList beingRemotelyRemoved = new LinkedList();
/**
* ObjectName of the TreeCache
*/
private ObjectName cacheObjectName = null;
/**
* String version of the object name to use to access the TreeCache
*/
private String cacheName = null;
/**
* CredentialUpdater used to allow asynchronous updates of
* SSO credentials
*/
private CredentialUpdater credentialUpdater = null;
/**
* InitialContext used for JNDI lookups
*/
private InitialContext initialContext = null;
/**
* The lifecycle event support for this component.
*/
private LifecycleSupport lifecycle = new LifecycleSupport(this);
/**
* The Log-object for this class
*/
private Logger log = Logger.getLogger(getClass().getName());;
/**
* Whether we are registered as a TreeCacheListener anywhere
*/
private boolean registeredAsListener = false;
/**
* The MBean server we use to access our TreeCache
*/
private MBeanServer server = null;
/**
* The SingleSignOn for which we are providing cluster support
*/
private ClusteredSingleSignOn ssoValve = null;
/**
* Whether we have been started
*/
private boolean started = false;
/**
* Whether a valid TreeCache is available for use
*/
private boolean treeCacheAvailable = false;
/**
* Whether we have logged an error due to not having a valid cache
*/
private boolean missingCacheErrorLogged = false;
// ---------------------------------------------------------- Constructors
/**
* Creates a new TreeCacheSSOClusterManager
*/
public TreeCacheSSOClusterManager()
{
// Find our MBeanServer
server = MBeanServerLocator.locate();
}
// ------------------------------------------------------------ Properties
public String getCacheName()
{
return cacheName;
}
public void setCacheName(String objectName)
throws Exception
{
if (objectName == null)
{
setCacheObjectName(null);
}
else if (objectName.equals(cacheName) == false)
{
setCacheObjectName(new ObjectName(objectName));
}
}
public ObjectName getCacheObjectName()
{
return cacheObjectName;
}
public void setCacheObjectName(ObjectName objectName)
throws Exception
{
// If no change, do nothing
if ((objectName != null && objectName.equals(cacheObjectName))
|| (cacheObjectName != null && cacheObjectName.equals(objectName))
|| (objectName == null && cacheObjectName == null))
{
return;
}
removeAsTreeCacheListener(cacheObjectName);
this.cacheObjectName = objectName;
this.cacheName = (objectName == null
? null
: objectName.getCanonicalName());
if (false == isTreeCacheAvailable(true))
{
if (started)
{
logMissingCacheError();
}
else
{
// Just put an advice in the log
log.info("Cannot find TreeCache using " + cacheName + " -- tree" +
"CacheName must be set to point to a running TreeCache " +
"before ClusteredSingleSignOn can handle requests");
}
}
}
// ----------------------------------------------------- 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 (ssoId == null || session == null)
{
return;
}
if (false == isTreeCacheAvailable(false))
{
logMissingCacheError();
return;
}
if (log.isTraceEnabled())
{
log.trace("addSession(): adding Session " + session.getId() +
" to cached session set for SSO " + ssoId);
}
Fqn fqn = getSessionsFqn(ssoId);
UserTransaction tx = null;
try
{
tx = getNewTransaction();
tx.begin();
Set sessions = getSessionSet(fqn, true);
sessions.add(session.getId());
putInTreeCache(fqn, sessions);
tx.commit();
}
catch (Exception e)
{
if (tx != null)
{
try
{
tx.rollback();
}
catch (Exception x)
{
}
}
String sessId = (session == null ? "NULL" : session.getId());
log.error("caught exception adding session " + sessId +
" to SSO id " + ssoId, e);
}
}
/**
* Gets the SingleSignOn valve for which this object is handling
* cluster communications.
*
* @return the <code>SingleSignOn</code> valve.
*/
public ClusteredSingleSignOn getSingleSignOnValve()
{
return ssoValve;
}
/**
* Sets the SingleSignOn valve for which this object is handling
* cluster communications.
* <p><b>NOTE:</b> This method must be called before calls can be
* made to the other methods of this interface.
*
* @param valve a <code>SingleSignOn</code> valve.
*/
public void setSingleSignOnValve(ClusteredSingleSignOn valve)
{
ssoValve = valve;
}
/**
* 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 (false == isTreeCacheAvailable(false))
{
logMissingCacheError();
return;
}
// Check whether we are already handling this removal
//synchronized (beingLocallyRemoved)
{
if (beingLocallyRemoved.contains(ssoId))
{
return;
}
// Add this SSO to our list of in-process local removals so
// this.nodeRemoved() will ignore the removal
beingLocallyRemoved.add(ssoId);
}
if (log.isTraceEnabled())
{
log.trace("Registering logout of SSO " + ssoId +
" in clustered cache");
}
Fqn fqn = getSingleSignOnFqn(ssoId);
//UserTransaction tx = null;
try
{
//tx = getNewTransaction();
//tx.begin();
removeFromTreeCache(fqn);
//tx.commit();
}
catch (Exception e)
{
/*
if (tx != null)
{
try
{
tx.rollback();
}
catch (Exception x) {}
}
*/
log.error("Exception attempting to remove node " +
fqn.toString() + " from TreeCache", e);
}
finally
{
//synchronized (beingLocallyRemoved)
{
beingLocallyRemoved.remove(ssoId);
}
}
}
/**
* Queries the cluster for the existence of an SSO session with the given
* id, returning a <code>SingleSignOnEntry</code> if one is found.
*
* @param ssoId the id of the SSO session
* @return a <code>SingleSignOnEntry</code> created using information
* found on another cluster node, or <code>null</code> if no
* entry could be found.
*/
public SingleSignOnEntry lookup(String ssoId)
{
if (false == isTreeCacheAvailable(false))
{
logMissingCacheError();
return null;
}
SingleSignOnEntry entry = null;
// Find the latest credential info from the cluster
Fqn fqn = getCredentialsFqn(ssoId);
//UserTransaction tx = null;
try
{
//tx = getNewTransaction();
//tx.begin();
SSOCredentials data = (SSOCredentials) getFromTreeCache(fqn);
if (data != null)
{
entry = new SingleSignOnEntry(null,
data.getAuthType(),
data.getUsername(),
data.getPassword());
}
//tx.commit();
}
catch (Exception e)
{
/*
if (tx != null)
{
try
{
tx.rollback();
}
catch (Exception x) {}
}
*/
log.error("caught exception looking up SSOCredentials for SSO id " +
ssoId, e);
}
return entry;
}
/**
* 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 (false == isTreeCacheAvailable(false))
{
logMissingCacheError();
return;
}
if (log.isTraceEnabled())
{
log.trace("Registering SSO " + ssoId + " in clustered cache");
}
storeSSOData(ssoId, authType, username, password);
}
/**
* 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 (false == isTreeCacheAvailable(false))
{
logMissingCacheError();
return;
}
// Check that this session removal is not due to our own deregistration
// of an SSO following receipt of a nodeRemoved() call
//synchronized(beingRemotelyRemoved)
{
if (beingRemotelyRemoved.contains(ssoId))
{
return;
}
}
if (log.isTraceEnabled())
{
log.trace("removeSession(): removing Session " + session.getId() +
" from cached session set for SSO " + ssoId);
}
Fqn fqn = getSessionsFqn(ssoId);
UserTransaction tx = null;
boolean removing = false;
try
{
tx = getNewTransaction();
tx.begin();
Set sessions = getSessionSet(fqn, false);
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
removeFromTreeCache(getSingleSignOnFqn(ssoId));
}
else
{
putInTreeCache(fqn, sessions);
}
}
tx.commit();
}
catch (Exception e)
{
if (tx != null)
{
try
{
tx.rollback();
}
catch (Exception x)
{
}
}
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 (false == isTreeCacheAvailable(false))
{
logMissingCacheError();
return;
}
if (log.isTraceEnabled())
{
log.trace("Updating credentials for SSO " + ssoId +
" in clustered cache");
}
storeSSOData(ssoId, authType, username, password);
}
// ------------------------------------------------------ TreeCacheListener
/**
* Does nothing
*/
public void nodeCreated(Fqn fqn)
{
; // do nothing
}
/**
* Does nothing
*/
public void nodeLoaded(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 ClusteredSingleSignOn#deregister(String) ClusteredSingleSignOn.deregister(String)}.
* <p/>
* 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
//synchronized(beingLocallyRemoved)
{
if (beingLocallyRemoved.contains(ssoId))
{
return;
}
}
//synchronized (beingRemotelyRemoved)
{
beingRemotelyRemoved.add(ssoId);
}
try
{
if (log.isTraceEnabled())
{
log.trace("received a node removed message for SSO " + ssoId);
}
ssoValve.deregister(ssoId);
}
finally
{
//synchronized(beingRemotelyRemoved)
{
beingRemotelyRemoved.remove(ssoId);
}
}
}
/**
* Extracts an SSO session id from the Fqn and uses it in an invocation of
* {@link ClusteredSingleSignOn#update ClusteredSingleSignOn.update()}.
* <p/>
* Only responds to modifications of nodes whose FQN's final segment is
* "credentials".
* <p/>
* Ignores invocations resulting from TreeCache changes originated by
* this object.
* <p/>
* 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
//synchronized(beingLocallyAdded)
{
if (beingLocallyAdded.contains(ssoId))
{
return;
}
}
SingleSignOnEntry sso = ssoValve.localLookup(ssoId);
if (sso == null || sso.getCanReauthenticate())
{
// No reason to update
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(sso, ssoId);
}
/**
* Does nothing
*/
public void viewChange(View new_view)
{
; // do nothing
}
/**
* Does nothing. Called when a node is evicted (not the same as remove()).
*
* @param fqn
*/
public void nodeEvicted(Fqn fqn)
{
// TODO do we need to handle this?
; // do nothing
}
// ------------------------------------------------------------- Lifecycle
/**
* Add a lifecycle event listener to this component.
*
* @param listener The listener to add
*/
public void addLifecycleListener(LifecycleListener listener)
{
lifecycle.addLifecycleListener(listener);
}
/**
* Get the lifecycle listeners associated with this lifecycle. If this
* Lifecycle has no listeners registered, a zero-length array is returned.
*/
public LifecycleListener[] findLifecycleListeners()
{
return lifecycle.findLifecycleListeners();
}
/**
* Remove a lifecycle event listener from this component.
*
* @param listener The listener to remove
*/
public void removeLifecycleListener(LifecycleListener listener)
{
lifecycle.removeLifecycleListener(listener);
}
/**
* 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.
*
* @throws 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
("TreeCacheSSOClusterManager already Started");
}
// Start the thread we use to clear nodeModified events
credentialUpdater = new CredentialUpdater();
started = true;
// Notify our interested LifecycleListeners
lifecycle.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.
*
* @throws 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
("TreeCacheSSOClusterManager not Started");
}
credentialUpdater.stop();
started = false;
// Notify our interested LifecycleListeners
lifecycle.fireLifecycleEvent(STOP_EVENT, null);
}
// ------------------------------------------------------- Private Methods
private Object getFromTreeCache(Fqn fqn) throws Exception
{
Object[] args = new Object[]{fqn, KEY};
return server.invoke(getCacheObjectName(), "get", args, GET_SIGNATURE);
}
private Fqn getCredentialsFqn(String ssoid)
{
Object[] objs = new Object[]{SSO, ssoid, CREDENTIALS};
return new Fqn(objs);
}
private Fqn getSessionsFqn(String ssoid)
{
Object[] objs = new Object[]{SSO, ssoid, SESSIONS};
return new Fqn(objs);
}
private Fqn getSingleSignOnFqn(String ssoid)
{
Object[] objs = new Object[]{SSO, ssoid};
return new Fqn(objs);
}
/**
* Extracts an SSO session id from a fully qualified name object.
*
* @param fqn the Fully Qualified Name used by TreeCache
* @return the second element in the Fqn -- the SSO session id
*/
private String getIdFromFqn(Fqn fqn)
{
return (String) fqn.get(1);
}
private InitialContext getInitialContext() throws NamingException
{
if (initialContext == null)
{
initialContext = new InitialContext();
}
return initialContext;
}
private Set getSessionSet(Fqn fqn, boolean create)
throws Exception
{
Set sessions = (Set) getFromTreeCache(fqn);
if (create && sessions == null)
{
sessions = new HashSet();
}
return sessions;
}
/**
* Extracts the SSO tree cache node type from a fully qualified name
* object.
*
* @param fqn the Fully Qualified Name used by TreeCache
* @return the last element in the Fqn -- either
* {@link #CREDENTIALS CREDENTIALS} or {@link #SESSIONS SESSIONS}.
*/
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;
}
}
/**
* Checks whether an MBean is registered under the value of property
* "cacheObjectName".
*
* @param forceCheck check for availability whether or not it has already
* been positively established
* @return <code>true</code> if property <code>cacheName</code> has been
* set and points to a registered MBean.
*/
private synchronized boolean isTreeCacheAvailable(boolean forceCheck)
{
if (forceCheck || treeCacheAvailable == false)
{
boolean available = (cacheObjectName != null);
if (available)
{
Set s = server.queryMBeans(cacheObjectName, null);
available = s.size() > 0;
if (available)
{
try
{
registerAsTreeCacheListener(cacheObjectName);
setMissingCacheErrorLogged(false);
}
catch (Exception e)
{
log.error("Caught exception registering as listener to " +
cacheObjectName, e);
available = false;
}
}
}
treeCacheAvailable = available;
}
return treeCacheAvailable;
}
private void putInTreeCache(Fqn fqn, Object data) throws Exception
{
Object[] args = new Object[]{fqn, KEY, data};
server.invoke(getCacheObjectName(), "put", args, PUT_SIGNATURE);
}
/**
* Invokes an operation on the JMX server to register ourself as a
* listener on the TreeCache service.
*
* @throws Exception
*/
private void registerAsTreeCacheListener(ObjectName listenTo)
throws Exception
{
server.invoke(listenTo, "addTreeCacheListener",
new Object[]{this},
new String[]{TreeCacheListener.class.getName()});
registeredAsListener = true;
}
/**
* Invokes an operation on the JMX server to register ourself as a
* listener on the TreeCache service.
*
* @throws Exception
*/
private void removeAsTreeCacheListener(ObjectName removeFrom)
throws Exception
{
if (registeredAsListener && removeFrom != null)
{
server.invoke(removeFrom, "removeTreeCacheListener",
new Object[]{this},
new String[]{TreeCacheListener.class.getName()});
}
}
private void removeFromTreeCache(Fqn fqn) throws Exception
{
server.invoke(getCacheObjectName(), "remove",
new Object[]{fqn},
REMOVE_SIGNATURE);
}
/**
* 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 <code>storeSessions</code> is
* <code>true</code>, 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.
* <p/>
* 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
*/
private void storeSSOData(String ssoId, String authType, String username,
String password)
{
SSOCredentials data = new SSOCredentials(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();
putInTreeCache(getCredentialsFqn(ssoId), data);
//tx.commit();
}
catch (Exception e)
{
/*
if (tx != null)
{
try
{
tx.rollback();
}
catch (Exception x) {}
}
*/
log.error("Exception attempting to add TreeCache nodes for SSO " +
ssoId, e);
}
finally
{
//synchronized (beingLocallyAdded)
{
beingLocallyAdded.remove(ssoId);
}
}
}
private boolean isMissingCacheErrorLogged()
{
return missingCacheErrorLogged;
}
private void setMissingCacheErrorLogged(boolean missingCacheErrorLogged)
{
this.missingCacheErrorLogged = missingCacheErrorLogged;
}
private void logMissingCacheError()
{
StringBuffer msg = new StringBuffer("Cannot find TreeCache using ");
msg.append(getCacheName());
msg.append(" -- TreeCache must be started before ClusteredSingleSignOn ");
msg.append("can handle requests");
if (isMissingCacheErrorLogged())
{
// Just log it as a warning
log.warn(msg);
}
else
{
log.error(msg);
// Set a flag so we don't relog this error over and over
setMissingCacheErrorLogged(true);
}
}
// --------------------------------------------------------- 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 updateThreadSleeping = false;
private boolean queueEmpty = true;
private boolean stopped = false;
private CredentialUpdater()
{
updateThread =
new Thread(this, "SSOClusterManager.CredentialUpdater");
updateThread.setDaemon(true);
updateThread.start();
}
// ------------------------------------------------------ Runnable
public void run()
{
while (!stopped)
{
// Ensure that no runtime exceptions kill this thread
try
{
updateThreadSleeping = false;
// Get the current list of ids awaiting processing
SSOWrapper[] ssos = null;
synchronized (awaitingUpdate)
{
ssos = new SSOWrapper[awaitingUpdate.size()];
ssos = (SSOWrapper[]) awaitingUpdate.toArray(ssos);
awaitingUpdate.clear();
queueEmpty = true;
}
// Handle the credential update
for (int i = 0; i < ssos.length; i++)
{
processUpdate(ssos[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 (queueEmpty)
{
try
{
// There is a slight possibility here of a race condition
// between the above check for queueEmpty and another
// thread accessing enqueue()'s check of
// updateThreadSleeping. If this happens, the update
// will not be processed by the local node until the
// updateThread wakes up (30 secs) or is interrupted by
// another update. This situation is quite unlikely,
// as updates only happen 1) in odd configurations where
// CLIENT-CERT authentication is used for some apps and
// FORM or BASIC are used for others and 2) the user has
// first logged in to a CLIENT-CERT app and later logs in
// to a FORM/BASIC app. If such a race condition were to
// occur, the only downside would be that if the user
// accessed a FORM/BASIC app on this node before the local
// update is processed, they would have to log in again.
updateThreadSleeping = true;
updateThread.sleep(30000);
}
catch (InterruptedException e)
{
if (log.isTraceEnabled())
{
log.trace("CredentialUpdater: interrupted");
}
// process the next bunch
}
}
else if (log.isTraceEnabled())
{
log.trace("CredentialUpdater: more updates added while " +
"handling existing updates");
}
}
catch (Exception e)
{
log.error("CredentialUpdater thread caught an exception", e);
}
}
}
// ------------------------------------------------- Private 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 sso the id of the SSO session whose local credentials
* are to be updated
*/
private void enqueue(SingleSignOnEntry sso, String ssoId)
{
synchronized (awaitingUpdate)
{
awaitingUpdate.add(new SSOWrapper(sso, ssoId));
queueEmpty = false;
}
// Interrupt the update thread so it wakes up to process
// the enqueued update. Only do this if its "sleeping" flag
// is set so we don't inadvertently interrupt it while its
// blocked waiting for a TreeCache lock to clear
if (updateThreadSleeping)
{
updateThread.interrupt();
}
}
private void processUpdate(SSOWrapper wrapper)
{
if (wrapper.sso.getCanReauthenticate())
{
// No need to update
return;
}
Fqn fqn = getCredentialsFqn(wrapper.id);
//UserTransaction tx = null;
try
{
//tx = getNewTransaction();
//tx.begin();
SSOCredentials data = (SSOCredentials) getFromTreeCache(fqn);
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 " +
wrapper.sso);
}
synchronized (wrapper.sso)
{
// Use the existing principal
Principal p = wrapper.sso.getPrincipal();
wrapper.sso.updateCredentials(p, authType, username, password);
}
}
/*
else
{
tx.commit();
}
*/
}
catch (Exception e)
{
/*
if (tx != null)
{
try
{
tx.rollback();
}
catch (Exception x) {}
}
*/
log.error("Exception attempting to get SSOCredentials from " +
"TreeCache node " + fqn.toString(), e);
}
}
/**
* Stops the update handler thread.
*/
private void stop()
{
stopped = true;
}
} // end CredentialUpdater
/**
* Wrapper class that holds a SingleSignOnEntry and its id
*/
private class SSOWrapper
{
private SingleSignOnEntry sso = null;
private String id = null;
private SSOWrapper(SingleSignOnEntry entry, String ssoId)
{
this.sso = entry;
this.id = ssoId;
}
}
// --------------------------------------------------------- Outer Classes
/**
* Private class used to store authentication credentials in the TreeCache.
* <p/>
* For security, password accessor is private.
*/
public static class SSOCredentials
implements Serializable
{
static final long serialVersionUID = 5704877226920571663L;
private String authType = null;
private String password = null;
private String username = null;
/**
* Creates a new SSOCredentials.
*
* @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
*/
private SSOCredentials(String authType, String username, String password)
{
this.authType = authType;
this.username = username;
this.password = password;
}
/**
* Gets the username of the user associated with the SSO.
*
* @return the username
*/
public String getUsername()
{
return username;
}
/**
* Gets the authorization method used to authorize the SSO.
*
* @return "BASIC", "CLIENT-CERT", "DIGEST" or "FORM"
*/
public String getAuthType()
{
return authType;
}
/**
* Gets the password of the user associated with the SSO.
*
* @return the password, or <code>null</code> if the authorization
* type was DIGEST or CLIENT-CERT.
*/
private String getPassword()
{
return password;
}
} // end SSOCredentials
} // end TreeCacheSSOClusterManager
| TreeCacheSSOClusterManager.java |