blob: c640a06020844bd1aec01d11e5e54c58ae64c97c [file] [log] [blame]
package com.ecyrd.jspwiki;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import javax.security.auth.Subject;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import org.apache.log4j.Logger;
import com.ecyrd.jspwiki.auth.WikiPrincipal;
import com.ecyrd.jspwiki.auth.authorize.Role;
import com.ecyrd.jspwiki.auth.login.CookieAssertionLoginModule;
import com.ecyrd.jspwiki.auth.login.PrincipalWrapper;
/**
* Represents a long-running wiki session, with an associated user Principal,
* user Subject, and authentication status. This class is initialized with
* minimal, default-deny values: authentication is set to <code>false</code>,
* and the user principal is set to <code>null</code>.
* @author Andrew R. Jaquith
* @version $Revision: 2.11 $ $Date: 2005-11-08 18:17:35 $
*/
public class WikiSession
{
public static final String ANONYMOUS = "anonymous";
public static final String ASSERTED = "asserted";
public static final String AUTHENTICATED = "authenticated";
/** Weak hashmap that maps wiki sessions to HttpSessions. */
private static final Map c_sessions = new WeakHashMap();
private Subject m_subject = new Subject();
protected String m_cachedCookieIdentity= null;
protected String m_cachedRemoteUser = null;
protected Principal m_cachedUserPrincipal = null;
private WikiContext m_lastContext = null;
protected static int ONE = 48;
protected static int NINE = 57;
protected static int DOT = 46;
protected static Logger log = Logger.getLogger( WikiSession.class );
/**
* Returns <code>true</code> if this WikiSession's Subject contains a
* given Principal in its Principal set.
* @param principal the Principal to look for
* @return <code>true</code> if the Set of Principals returned by
* {@link javax.security.auth.Subject#getPrincipals()} contains the
* specified Principal; <code>false</code> otherwise.
*/
protected boolean hasPrincipal( Principal principal )
{
for( Iterator it = m_subject.getPrincipals().iterator(); it.hasNext(); )
{
Principal current = (Principal) it.next();
if ( principal.equals( current ) )
{
return true;
}
}
return false;
}
/**
* Protected method that caches the most recent wiki context. This method is
* called only from
* {@link WikiContext#WikiContext(WikiEngine, HttpServletRequest, WikiPage)}
* and {@link WikiContext#WikiContext(WikiEngine, WikiPage)}, and nowhere
* else. Its primary function is to allow downstream classes such as
* {@link com.ecyrd.jspwiki.auth.authorize.WebContainerAuthorizer}to access
* the most recent WikiContext, and thus, the HttpServletRequest.
* @param context the most recent wiki context, which may be
* <code>null</code>
*/
protected void setLastContext( WikiContext context )
{
// TODO: callers should supply the WikiSessionPermission "setContext"
if ( context != null )
{
m_lastContext = context;
}
}
/**
* Private constructor to prevent WikiSession from being instantiated
* directly.
*/
private WikiSession()
{
}
/**
* Returns the authentication status of the user's session. The user is
* considered authenticated if the Subject contains the Principal
* Role.AUTHENTICATED;
* @return Returns <code>true</code> if the user is authenticated
*/
public boolean isAuthenticated()
{
return ( hasPrincipal( Role.AUTHENTICATED ) );
}
/**
* <p>Determines whether the current session is anonymous. This will be
* true if any of these conditions are true:</p>
* <ul>
* <li>The session's Principal set contains
* {@link com.ecyrd.jspwiki.auth.authorize.Role#ANONYMOUS</li>
* <li>The session's Principal set contains
* {@link com.ecyrd.jspwiki.auth.WikiPrincipal#GUEST</li>
* <li>The Principal returned by {@link #getUserPrincipal()} evaluates
* to an IP address.</li>
* </ul>
* <p>The criteria above are listed in the order in which they are
* evaluated.</p>
* @return whether the current user's identity is equivalent to an IP
* address
*/
public boolean isAnonymous()
{
Set principals = m_subject.getPrincipals();
return ( principals.contains( Role.ANONYMOUS ) ||
principals.contains( WikiPrincipal.GUEST ) ||
isIPV4Address( getUserPrincipal().getName() ) );
}
/**
* Verifies whether a String represents an IP address. The algorithm is
* extremely efficient and does not allocate any objects.
* @param name the address to test
* @return the result
*/
protected static boolean isIPV4Address( String name )
{
if ( name.charAt( 0 ) == DOT || name.charAt( name.length() - 1 ) == DOT )
{
return false;
}
int[] addr = new int[]
{ 0, 0, 0, 0 };
int currentOctet = 0;
for( int i = 0; i < name.length(); i++ )
{
int ch = name.charAt( i );
boolean isDigit = ( ch >= ONE && ch <= NINE );
boolean isDot = ( ch == DOT );
if ( !isDigit && !isDot )
{
return false;
}
if ( isDigit )
{
addr[currentOctet] = 10 * addr[currentOctet] + ( ch - ONE );
if ( addr[currentOctet] > 255 )
{
return false;
}
}
else if ( name.charAt( i - 1 ) == DOT )
{
return false;
}
else
{
currentOctet++;
}
}
return ( currentOctet == 3 );
}
/**
* Returns the most recently-accessed WikiContext for this session. This
* method may return <code>null</code>.
* @return the most recent wiki context
*/
public WikiContext getLastContext()
{
return m_lastContext;
}
/**
* <p> Returns the Principal used to log in to an authenticated session. The
* login principal is determined by examining the Subject's Principal set
* for PrincipalWrappers or WikiPrincipals with type designator
* <code>LOGIN_NAME</code>; the first one found is the login principal.
* If one is not found, this method returns the first principal that isn't
* of type Role. If neither of these conditions hold, this method returns
* {@link com.ecyrd.jspwiki.auth.WikiPrincipal#GUEST}.
* @return the login Principal. If it is a PrincipalWrapper containing an
* externally-provided Principal, the object returned is the Principal, not
* the wrapper around it.
*/
public Principal getLoginPrincipal()
{
Set principals = m_subject.getPrincipals();
Principal secondChoice = null;
// Take the first PrincipalWrapper or WikiPrincipal of type LOGIN_NAME
for( Iterator it = principals.iterator(); it.hasNext(); )
{
Principal currentPrincipal = (Principal) it.next();
if ( !( currentPrincipal instanceof Role ) )
{
if ( currentPrincipal instanceof WikiPrincipal )
{
WikiPrincipal wp = (WikiPrincipal) currentPrincipal;
if ( wp.getType().equals( WikiPrincipal.LOGIN_NAME ) )
{
return currentPrincipal;
}
}
else if ( currentPrincipal instanceof PrincipalWrapper )
{
return ( (PrincipalWrapper) currentPrincipal ).getPrincipal();
}
if ( secondChoice == null )
{
secondChoice = currentPrincipal;
}
}
}
return ( secondChoice == null ? WikiPrincipal.GUEST : secondChoice );
}
/**
* <p>Returns the primary user Principal associated with this session. The
* primary user principal is determined as follows:</p> <ol> <li>If the
* Subject's Principal set contains WikiPrincipals, the first WikiPrincipal
* with type designator <code>FULL_NAME</code> or (alternatively)
* <code>WIKI_NAME</true> is the primary Principal.</li>
* <li>For all other cases, the first Principal in the Subject's principal
* collection that that isn't of type Role is the primary.</li>
* </ol>
* If no primary user Principal is found, this method returns
* {@link com.ecyrd.jspwiki.auth.WikiPrincipal#GUEST}.
* @return the primary user Principal
*/
public Principal getUserPrincipal()
{
Set principals = m_subject.getPrincipals();
Principal secondChoice = null;
// Take the first WikiPrincipal of type FULL_NAME as primary
// Take the first non-Role as the alternate
for( Iterator it = principals.iterator(); it.hasNext(); )
{
Principal currentPrincipal = (Principal) it.next();
if ( !( currentPrincipal instanceof Role ) )
{
if ( currentPrincipal instanceof WikiPrincipal )
{
WikiPrincipal wp = (WikiPrincipal) currentPrincipal;
if ( wp.getType().equals( WikiPrincipal.FULL_NAME ) )
{
return currentPrincipal;
}
else if ( wp.getType().equals( WikiPrincipal.WIKI_NAME ) )
{
return currentPrincipal;
}
}
if ( currentPrincipal instanceof PrincipalWrapper )
{
currentPrincipal = ( (PrincipalWrapper) currentPrincipal ).getPrincipal();
}
if ( secondChoice == null )
{
secondChoice = currentPrincipal;
}
}
}
return ( secondChoice == null ? WikiPrincipal.GUEST : secondChoice );
}
/**
* Returns all user Principals associated with this session. User principals
* are those in the Subject's principal collection that aren't of type Role.
* This is a defensive copy.
* @return Returns the user principal
*/
public Principal[] getPrincipals()
{
ArrayList principals = new ArrayList();
{
// Take the first non Role as the main Principal
for( Iterator it = m_subject.getPrincipals().iterator(); it.hasNext(); )
{
Principal currentPrincipal = (Principal) it.next();
if ( !( currentPrincipal instanceof Role ) )
{
principals.add( currentPrincipal );
}
}
}
return (Principal[]) principals.toArray( new Principal[principals.size()] );
}
/**
* Sets the Subject representing the user.
* @param subject
*/
public void setSubject( Subject subject )
{
// TODO: this should be a privileged action
m_subject = subject;
}
/**
* Returns the Subject representing the user.
* @return the subject
*/
public Subject getSubject()
{
// TODO: this should be a privileged action
return m_subject;
}
/**
* Factory method that creates a new "guest" session containing a single
* user Principal,
* @link com.ecyrd.jspwiki.auth.WikiPrincipal#GUEST}, plus the role
* principals
* @link Role#ALL and
* @link Role#ANONYMOUS.
* @return the guest wiki session
*/
public static WikiSession guestSession()
{
WikiSession session = new WikiSession();
session.invalidate();
return session;
}
/**
* Static factory method that returns the WikiSession object associated with
* the current HTTP request. This method looks up the associated HttpSession
* in an internal WeakHashMap and attempts to retrieve the WikiSession. If
* not found, one is created. This method is guaranteed to always return a
* WikiSession, although the authentication status is unpredictable until
* the user attempts to log in. If the servlet request parameter is
* <code>null</code>, a synthetic {@link #guestSession()}is returned.
* @param request the current servlet request object
* @return the existing (or newly created) wiki session
*/
public static WikiSession getWikiSession( HttpServletRequest request )
{
// If request is null, return guest session
if ( request == null )
{
if ( log.isDebugEnabled() )
{
log.debug( "Looking up WikiSession for NULL HttpRequest: returning guestSession()" );
}
return guestSession();
}
// Look for a WikiSession associated with the user's Http Session
// and create one if it isn't there yet.
WikiSession wikiSession;
HttpSession session = request.getSession();
String sid = ( session == null ) ? "(null)" : session.getId();
Object storedSession = c_sessions.get( session );
if ( storedSession != null && storedSession instanceof WikiSession )
{
if ( log.isDebugEnabled() )
{
log.debug( "Looking up WikiSession for session ID=" + sid + "... found it" );
}
wikiSession = (WikiSession) storedSession;
}
else
{
if ( log.isDebugEnabled() )
{
log.debug( "Looking up WikiSession for session ID=" + sid + "... not found. Creating guestSession()" );
}
wikiSession = guestSession();
c_sessions.put( session, wikiSession );
}
return wikiSession;
}
/**
* Invalidates the WikiSession and resets its Subject's
* Principal set to the equivalent of a "guest session"
* @param session the wiki session to invalidate
*/
public void invalidate()
{
m_subject.getPrincipals().clear();
m_subject.getPrincipals().add( WikiPrincipal.GUEST );
m_subject.getPrincipals().add( Role.ANONYMOUS );
m_subject.getPrincipals().add( Role.ALL );
m_cachedCookieIdentity = null;
m_cachedRemoteUser = null;
m_cachedUserPrincipal = null;
}
/**
* Returns whether the Http servlet container's authentication status has
* changed. Used to detect whether the container has logged in a user since
* the last call to this function. This method is stateful. After calling
* this function, the cached values are set to those in the current request.
* If the servlet request is null, this method always returns false.
* @param request the current servlet request
* @return <code>true</code> if the status has changed, <code>false</code>
* otherwise
*/
protected boolean isContainerStatusChanged( HttpServletRequest request )
{
if ( request == null )
{
return false;
}
String remoteUser = request.getRemoteUser();
Principal userPrincipal = request.getUserPrincipal();
String cookieIdentity = CookieAssertionLoginModule.getUserCookie( request );
boolean changed= false;
// If request contains non-null remote user, update cached value if changed
if ( remoteUser != null && !remoteUser.equals( m_cachedRemoteUser) )
{
m_cachedRemoteUser = remoteUser;
log.info( "Remote user changed to " + remoteUser );
changed = true;
}
// If request contains non-null user principal, updated cached value if changed
if ( userPrincipal != null && !userPrincipal.equals( m_cachedUserPrincipal ) )
{
m_cachedUserPrincipal = userPrincipal;
log.info( "User principal changed to " + userPrincipal );
changed = true;
}
// If cookie identity changed (to a different value or back to null), update cache
if ( ( cookieIdentity != null && !cookieIdentity.equals( m_cachedCookieIdentity ) )
|| ( cookieIdentity == null && m_cachedCookieIdentity != null ) )
{
m_cachedCookieIdentity = cookieIdentity;
log.info( "Cookie changed to " + cookieIdentity );
changed = true;
}
return changed;
}
/**
* <p>Returns the status of the session as a text string. Valid values are:</p>
* <ul> <li>{@link #AUTHENTICATED}</li> <li>{@link #ASSERTED}</li> <li>{@link #ANONYMOUS}</li>
* </ul>
* @return the session status
*/
public String getStatus()
{
if ( isAuthenticated() )
{
return AUTHENTICATED;
}
if ( isAnonymous() )
{
return ANONYMOUS;
}
else if ( hasPrincipal( Role.ASSERTED ) )
{
return ASSERTED;
}
return "ILLEGAL STATUS!";
}
}