Prechádzať zdrojové kódy

initial checking of JSR-168 Portlet support in to sandbox

John Lewis 18 rokov pred
rodič
commit
82b22563e5

+ 70 - 0
sandbox/portlet/pom.xml

@@ -0,0 +1,70 @@
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.acegisecurity</groupId>
+        <artifactId>acegi-security-sandbox</artifactId>
+        <version>1.0.4-SNAPSHOT</version>
+    </parent>
+    <artifactId>acegi-security-portlet</artifactId>
+    <name>Acegi Security System for Spring - Portlet support</name>
+    <description>Acegi Security System for Spring - Support for JSR-168 Portlets</description>
+    <version>0.1-SNAPSHOT</version>
+
+    <repositories>
+        <repository>
+            <id>AcegiMaven</id>
+            <name>Acegi 3rd party repository</name>
+            <url>http://acegisecurity.sourceforge.net/maven</url>
+            <releases>
+                <enabled>true</enabled>
+            </releases>
+        </repository>
+    </repositories>
+
+    <scm>
+        <connection>scm:svn:https://svn.sourceforge.net/svnroot/acegisecurity/trunk/acegisecurity/sandbox/portlet
+        </connection>
+        <developerConnection>
+            scm:svn:https://svn.sourceforge.net/svnroot/acegisecurity/trunk/acegisecurity/sandbox/portlet
+        </developerConnection>
+        <url>http://svn.sourceforge.net/viewcvs.cgi/acegisecurity/trunk/acegisecurity/sandbox/portlet/</url>
+    </scm>
+
+    <dependencies>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>servlet-api</artifactId>
+            <version>2.4</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+          <groupId>javax.portlet</groupId>
+          <artifactId>portlet-api</artifactId>
+          <version>1.0</version>
+          <scope>provided</scope>
+        </dependency>
+        <dependency>
+          <groupId>org.springframework</groupId>
+          <artifactId>spring-dao</artifactId>
+          <version>2.0.4</version>
+        </dependency>
+        <dependency>
+          <groupId>org.springframework</groupId>
+          <artifactId>spring-portlet</artifactId>
+          <version>2.0.4</version>
+        </dependency>
+        <dependency>
+          <groupId>org.springframework</groupId>
+          <artifactId>spring-mock</artifactId>
+          <version>2.0.4</version>
+          <optional>true</optional>
+        </dependency>
+        <dependency>
+          <groupId>net.sf.ehcache</groupId>
+          <artifactId>ehcache</artifactId>
+          <version>1.2.4</version>
+        </dependency>
+    </dependencies>
+
+</project>

+ 446 - 0
sandbox/portlet/src/main/java/org/acegisecurity/context/PortletSessionContextIntegrationInterceptor.java

@@ -0,0 +1,446 @@
+/*
+ * Copyright 2005-2007 the original author or authors.
+ *
+ * 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 org.acegisecurity.context;
+
+import java.lang.reflect.Method;
+
+import javax.portlet.ActionRequest;
+import javax.portlet.ActionResponse;
+import javax.portlet.PortletException;
+import javax.portlet.PortletRequest;
+import javax.portlet.PortletResponse;
+import javax.portlet.PortletSession;
+import javax.portlet.RenderRequest;
+import javax.portlet.RenderResponse;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.util.Assert;
+import org.springframework.util.ReflectionUtils;
+import org.springframework.web.portlet.HandlerInterceptor;
+import org.springframework.web.portlet.ModelAndView;
+
+/**
+ * <p>This interceptor populates the {@link SecurityContextHolder} with information obtained from the
+ * <code>PortletSession</code>.  It is applied to both <code>ActionRequest</code>s and
+ * <code>RenderRequest</code>s</p>
+ *
+ * <p>The <code>PortletSession</code> will be queried to retrieve the <code>SecurityContext</code> that should
+ * be stored against the <code>SecurityContextHolder</code> for the duration of the portlet request. At the
+ * end of the request, any updates made to the <code>SecurityContextHolder</code> will be persisted back to the
+ * <code>PortletSession</code> by this interceptor.</p>
+ *
+ * <p> If a valid <code>SecurityContext</code> cannot be obtained from the <code>PortletSession</code> for
+ * whatever reason, a fresh <code>SecurityContext</code> will be created and used instead. The created object
+ * will be of the instance defined by the {@link #setContext(Class)} method (which defaults to
+ * {@link org.acegisecurity.context.SecurityContextImpl}. </p>
+ *
+ * <p>A <code>PortletSession</code> may be created by this interceptor if one does not already exist.  If at the
+ * end of the portlet request the <code>PortletSession</code> does not exist, one will <b>only</b> be created if
+ * the current contents of the <code>SecurityContextHolder</code> are not the {@link java.lang.Object#equals}
+ * to a <code>new</code> instance of {@link #context}.  This avoids needless <code>PortletSession</code> creation,
+ * and automates the storage of changes made to the <code>SecurityContextHolder</code>. There is one exception to
+ * this rule, that is if the {@link #forceEagerSessionCreation} property is <code>true</code>, in which case
+ * sessions will always be created irrespective of normal session-minimization logic (the default is
+ * <code>false</code>, as this is resource intensive and not recommended).</p>
+ *
+ * <p>If for whatever reason no <code>PortletSession</code> should <b>ever</b> be created, the
+ * {@link #allowSessionCreation} property should be set to <code>false</code>. Only do this if you really need
+ * to conserve server memory and ensure all classes using the <code>SecurityContextHolder</code> are designed to
+ * have no persistence of the <code>SecurityContext</code> between web requests. Please note that if
+ * {@link #forceEagerSessionCreation} is <code>true</code>, the <code>allowSessionCreation</code> must also be
+ * <code>true</code> (setting it to <code>false</code> will cause a startup-time error).</p>
+
+ * <p>This interceptor <b>must</b> be executed <b>before</p> any authentication processing mechanisms. These
+ * mechanisms (specifically {@link org.acegisecurity.ui.portlet.PortletProcessingInterceptor}) expect the
+ * <code>SecurityContextHolder</code> to contain a valid <code>SecurityContext</code> by the time they execute.</p>
+ *
+ * <p>An important nuance to this interceptor is that (by default) the <code>SecurityContext</code> is stored
+ * into the <code>APPLICATION_SCOPE</code> of the <code>PortletSession</code>.  This doesn't just mean you will be
+ * sharing it with all the other portlets in your webapp (which is generally a good idea).  It also means that (if
+ * you have done all the other appropriate magic), you will share this <code>SecurityContext</code> with servlets in
+ * your webapp.  This is very useful if you have servlets serving images or processing AJAX calls from your portlets
+ * since they can now use the {@link HttpSessionContextIntegrationFilter} to access the same <code>SecurityContext<code>
+ * object from the session.  This allows these calls to be secured as well as the portlet calls.</p>
+ *
+ * Much of the logic of this interceptor comes from the {@link HttpSessionContextIntegrationFilter} class which
+ * fills the same purpose on the servlet side.  Ben Alex and Patrick Burlson are listed as authors here because they
+ * are the authors of that class and there are blocks of code that essentially identical between the two. (Making this
+ * a good candidate for refactoring someday.)
+ *
+ * <p>Unlike <code>HttpSessionContextIntegrationFilter</code>, this interceptor does not check to see if it is
+ * getting applied multiple times.  This shouldn't be a problem since the application of interceptors is under the
+ * control of the Spring Portlet MVC framework and tends to be more explicit and more predictable than the application
+ * of filters.  However, you should still be careful to only apply this inteceptor to your request once.</p>
+ *
+ * @author John A. Lewis
+ * @author Ben Alex
+ * @author Patrick Burleson
+ * @since 2.0
+ * @version $Id$
+ */
+public class PortletSessionContextIntegrationInterceptor
+		implements InitializingBean, HandlerInterceptor {
+
+	//~ Static fields/initializers =====================================================================================
+
+	protected static final Log logger = LogFactory.getLog(PortletSessionContextIntegrationInterceptor.class);
+
+	public static final String ACEGI_SECURITY_CONTEXT_KEY = HttpSessionContextIntegrationFilter.ACEGI_SECURITY_CONTEXT_KEY;
+
+	private static final String SESSION_EXISTED = PortletSessionContextIntegrationInterceptor.class.getName() + ".SESSION_EXISTED";
+	private static final String CONTEXT_HASHCODE = PortletSessionContextIntegrationInterceptor.class.getName() + ".CONTEXT_HASHCODE";
+
+	//~ Instance fields ================================================================================================
+
+	private Class context = SecurityContextImpl.class;
+
+	private Object contextObject;
+
+	/**
+	 * Indicates if this interceptor can create a <code>PortletSession</code> if
+	 * needed (sessions are always created sparingly, but setting this value to
+	 * <code>false</code> will prohibit sessions from ever being created).
+	 * Defaults to <code>true</code>. Do not set to <code>false</code> if
+	 * you are have set {@link #forceEagerSessionCreation} to <code>true</code>,
+	 * as the properties would be in conflict.
+	 */
+	private boolean allowSessionCreation = true;
+
+	/**
+	 * Indicates if this interceptor is required to create a <code>PortletSession</code>
+	 * for every request before proceeding through the request process, even if the
+	 * <code>PortletSession</code> would not ordinarily have been created. By
+	 * default this is <code>false</code>, which is entirely appropriate for
+	 * most circumstances as you do not want a <code>PortletSession</code>
+	 * created unless the interceptor actually needs one. It is envisaged the main
+	 * situation in which this property would be set to <code>true</code> is
+	 * if using other interceptors that depend on a <code>PortletSession</code>
+	 * already existing. This is only required in specialized cases, so leave it set to
+	 * <code>false</code> unless you have an actual requirement and aware of the
+	 * session creation overhead.
+	 */
+	private boolean forceEagerSessionCreation = false;
+
+	/**
+	 * Indicates whether the <code>SecurityContext</code> will be cloned from
+	 * the <code>PortletSession</code>. The default is to simply reference
+	 * (the default is <code>false</code>). The default may cause issues if
+	 * concurrent threads need to have a different security identity from other
+	 * threads being concurrently processed that share the same
+	 * <code>PortletSession</code>. In most normal environments this does not
+	 * represent an issue, as changes to the security identity in one thread is
+	 * allowed to affect the security identity in other threads associated with
+	 * the same <code>PortletSession</code>. For unusual cases where this is not
+	 * permitted, change this value to <code>true</code> and ensure the
+	 * {@link #context} is set to a <code>SecurityContext</code> that
+	 * implements {@link Cloneable} and overrides the <code>clone()</code>
+	 * method.
+	 */
+	private boolean cloneFromPortletSession = false;
+
+	/**
+	 * Indicates wether the <code>APPLICATION_SCOPE</code> mode of the
+	 * <code>PortletSession</code> should be used for storing the
+	 * <code>SecurityContext</code>.  The default is </code>true</code>.
+	 * This allows it to be shared between the portlets in the webapp and
+	 * potentially with servlets in the webapp as well. If this is set to
+	 * <code>false</code>, then the <code>PORTLET_SCOPE</code> will be used
+	 * instead.
+	 */
+	private boolean useApplicationScopePortletSession = true;
+
+
+	//~ Constructors ===================================================================================================
+
+	public PortletSessionContextIntegrationInterceptor() throws PortletException {
+		this.contextObject = generateNewContext();
+	}
+
+	//~ Methods ========================================================================================================
+
+	public void afterPropertiesSet() throws Exception {
+
+		// check that the value of context is legal
+		if ((this.context == null) || (!SecurityContext.class.isAssignableFrom(this.context))) {
+			throw new IllegalArgumentException("context must be defined and implement SecurityContext "
+					+ "(typically use org.acegisecurity.context.SecurityContextImpl; existing class is "
+					+ this.context + ")");
+		}
+
+		// check that session creation options make sense
+		if ((forceEagerSessionCreation == true) && (allowSessionCreation == false)) {
+			throw new IllegalArgumentException(
+					"If using forceEagerSessionCreation, you must set allowSessionCreation to also be true");
+		}
+	}
+
+	public boolean preHandleAction(ActionRequest request, ActionResponse response,
+			Object handler) throws Exception {
+		// call to common preHandle method
+		return preHandle(request, response, handler);
+	}
+
+	public boolean preHandleRender(RenderRequest request, RenderResponse response,
+			Object handler) throws Exception {
+		// call to common preHandle method
+		return preHandle(request, response, handler);
+	}
+
+	public void postHandleRender(RenderRequest request, RenderResponse response,
+			Object handler, ModelAndView modelAndView) throws Exception {
+		// no-op
+	}
+
+	public void afterActionCompletion(ActionRequest request, ActionResponse response,
+			Object handler, Exception ex) throws Exception {
+		// call to common afterCompletion method
+		afterCompletion(request, response, handler, ex);
+	}
+
+	public void afterRenderCompletion(RenderRequest request, RenderResponse response,
+			Object handler, Exception ex) throws Exception {
+		// call to common afterCompletion method
+		afterCompletion(request, response, handler, ex);
+	}
+
+
+	private boolean preHandle(PortletRequest request, PortletResponse response,
+			Object handler) throws Exception {
+
+		// make sure the holder is clear
+		if (SecurityContextHolder.getContext() != null) {
+			if (logger.isWarnEnabled())
+				logger.warn("SecurityContextHolder should have been null but contained: '"
+					+ SecurityContextHolder.getContext() + "'; setting to null now");
+			SecurityContextHolder.clearContext();
+		}
+
+		PortletSession portletSession = null;
+		boolean portletSessionExistedAtStartOfRequest = false;
+
+		// see if the portlet session already exists (or should be eagerly created)
+		try {
+			portletSession = request.getPortletSession(forceEagerSessionCreation);
+		} catch (IllegalStateException ignored) {}
+
+		// if there is a session, then see if there is a context to bring in
+		if (portletSession != null) {
+
+			// remember that the session already existed
+			portletSessionExistedAtStartOfRequest = true;
+
+			// attempt to retrieve the context from the session
+			Object contextFromSessionObject = portletSession.getAttribute(ACEGI_SECURITY_CONTEXT_KEY, portletSessionScope());
+
+			// if we got a context then place it into the holder
+			if (contextFromSessionObject != null) {
+
+				// if we are supposed to clone it, then do so
+				if (cloneFromPortletSession) {
+					Assert.isInstanceOf(Cloneable.class, contextFromSessionObject,
+							"Context must implement Clonable and provide a Object.clone() method");
+					try {
+						Method m = contextFromSessionObject.getClass().getMethod("clone", new Class[] {});
+						if (!m.isAccessible()) {
+							m.setAccessible(true);
+						}
+						contextFromSessionObject = m.invoke(contextFromSessionObject, new Object[] {});
+					}
+					catch (Exception ex) {
+						ReflectionUtils.handleReflectionException(ex);
+					}
+				}
+
+				// if what we got is a valid context then place it into the holder, otherwise create a new one
+				if (contextFromSessionObject instanceof SecurityContext) {
+					if (logger.isDebugEnabled())
+						logger.debug("Obtained from ACEGI_SECURITY_CONTEXT a valid SecurityContext and "
+								+ "set to SecurityContextHolder: '" + contextFromSessionObject + "'");
+					SecurityContextHolder.setContext((SecurityContext) contextFromSessionObject);
+				} else {
+					if (logger.isWarnEnabled())
+						logger.warn("ACEGI_SECURITY_CONTEXT did not contain a SecurityContext but contained: '"
+										+ contextFromSessionObject
+										+ "'; are you improperly modifying the PortletSession directly "
+										+ "(you should always use SecurityContextHolder) or using the PortletSession attribute "
+										+ "reserved for this class? - new SecurityContext instance associated with "
+										+ "SecurityContextHolder");
+					SecurityContextHolder.setContext(generateNewContext());
+				}
+
+			} else {
+
+				// there was no context in the session, so create a new context and put it in the holder
+				if (logger.isDebugEnabled())
+					logger.debug("PortletSession returned null object for ACEGI_SECURITY_CONTEXT - new "
+							+ "SecurityContext instance associated with SecurityContextHolder");
+				SecurityContextHolder.setContext(generateNewContext());
+			}
+
+		} else {
+
+			// there was no session, so create a new context and place it in the holder
+			if (logger.isDebugEnabled())
+				logger.debug("No PortletSession currently exists - new SecurityContext instance "
+						+ "associated with SecurityContextHolder");
+			SecurityContextHolder.setContext(generateNewContext());
+
+		}
+
+		// place attributes onto the request to remember if the session existed and the hashcode of the context
+		request.setAttribute(SESSION_EXISTED, new Boolean(portletSessionExistedAtStartOfRequest));
+		request.setAttribute(CONTEXT_HASHCODE, new Integer(SecurityContextHolder.getContext().hashCode()));
+
+		return true;
+	}
+
+	private void afterCompletion(PortletRequest request, PortletResponse response,
+			Object handler, Exception ex) throws Exception {
+
+		PortletSession portletSession = null;
+
+		// retrieve the attributes that remember if the session existed and the hashcode of the context
+		boolean portletSessionExistedAtStartOfRequest = ((Boolean)request.getAttribute(SESSION_EXISTED)).booleanValue();
+		int oldContextHashCode = ((Integer)request.getAttribute(CONTEXT_HASHCODE)).intValue();
+
+		// try to retrieve an existing portlet session
+		try {
+			portletSession = request.getPortletSession(false);
+		} catch (IllegalStateException ignored) {}
+
+		// if there is now no session but there was one at the beginning then it must have been invalidated
+		if ((portletSession == null) && portletSessionExistedAtStartOfRequest) {
+			if (logger.isDebugEnabled())
+				logger.debug("PortletSession is now null, but was not null at start of request; "
+						+ "session was invalidated, so do not create a new session");
+		}
+
+		// create a new portlet session if we need to
+		if ((portletSession == null) && !portletSessionExistedAtStartOfRequest) {
+
+			// if we're not allowed to create a new session, then report that
+			if (!allowSessionCreation) {
+				if (logger.isDebugEnabled())
+					logger.debug("The PortletSession is currently null, and the "
+							+ "PortletSessionContextIntegrationInterceptor is prohibited from creating a PortletSession "
+							+ "(because the allowSessionCreation property is false) - SecurityContext thus not "
+							+ "stored for next request");
+			}
+			// if the context was changed during the request, then go ahead and create a session
+			else if (!contextObject.equals(SecurityContextHolder.getContext())) {
+				if (logger.isDebugEnabled())
+					logger.debug("PortletSession being created as SecurityContextHolder contents are non-default");
+				try {
+					portletSession = request.getPortletSession(true);
+				} catch (IllegalStateException ignored) {}
+			}
+			// if nothing in the context changed, then don't bother to create a session
+			else {
+				if (logger.isDebugEnabled())
+					logger.debug("PortletSession is null, but SecurityContextHolder has not changed from default: ' "
+							+ SecurityContextHolder.getContext()
+							+ "'; not creating PortletSession or storing SecurityContextHolder contents");
+			}
+		}
+
+		// if the session exists and the context has changes, then store the context back into the session
+		if ((portletSession != null)
+			&& (SecurityContextHolder.getContext().hashCode() != oldContextHashCode)) {
+			portletSession.setAttribute(ACEGI_SECURITY_CONTEXT_KEY,	SecurityContextHolder.getContext(), portletSessionScope());
+			if (logger.isDebugEnabled())
+				logger.debug("SecurityContext stored to PortletSession: '"
+					+ SecurityContextHolder.getContext() + "'");
+		}
+
+		// remove the contents of the holder
+		SecurityContextHolder.clearContext();
+		if (logger.isDebugEnabled())
+			logger.debug("SecurityContextHolder set to new context, as request processing completed");
+
+	}
+
+
+	/**
+	 * Creates a new <code>SecurityContext</code> object.  The specific class is
+	 * determined by the setting of the {@link #context} property.
+	 * @return the new <code>SecurityContext</code>
+	 * @throws PortletException if the creation throws an <code>InstantiationException</code> or
+	 *     an <code>IllegalAccessException</code>, then this method will wrap them in a
+	 *     <code>PortletException</code>
+	 */
+	public SecurityContext generateNewContext() throws PortletException {
+		try {
+			return (SecurityContext) this.context.newInstance();
+		} catch (InstantiationException ie) {
+			throw new PortletException(ie);
+		} catch (IllegalAccessException iae) {
+			throw new PortletException(iae);
+		}
+	}
+
+
+	private int portletSessionScope() {
+		// return the appropriate scope setting based on our property value
+		return (this.useApplicationScopePortletSession ?
+			PortletSession.APPLICATION_SCOPE :	PortletSession.PORTLET_SCOPE);
+	}
+
+
+	public Class getContext() {
+		return context;
+	}
+
+	public void setContext(Class secureContext) {
+		this.context = secureContext;
+	}
+
+	public boolean isAllowSessionCreation() {
+		return allowSessionCreation;
+	}
+
+	public void setAllowSessionCreation(boolean allowSessionCreation) {
+		this.allowSessionCreation = allowSessionCreation;
+	}
+
+	public boolean isForceEagerSessionCreation() {
+		return forceEagerSessionCreation;
+	}
+
+	public void setForceEagerSessionCreation(boolean forceEagerSessionCreation) {
+		this.forceEagerSessionCreation = forceEagerSessionCreation;
+	}
+
+	public boolean isCloneFromPortletSession() {
+		return cloneFromPortletSession;
+	}
+
+	public void setCloneFromPortletSession(boolean cloneFromPortletSession) {
+		this.cloneFromPortletSession = cloneFromPortletSession;
+	}
+
+	public boolean isUseApplicationScopePortletSession() {
+		return useApplicationScopePortletSession;
+	}
+
+	public void setUseApplicationScopePortletSession(
+			boolean useApplicationScopePortletSession) {
+		this.useApplicationScopePortletSession = useApplicationScopePortletSession;
+	}
+
+}

+ 190 - 0
sandbox/portlet/src/main/java/org/acegisecurity/providers/portlet/PortletAuthenticationProvider.java

@@ -0,0 +1,190 @@
+/*
+ * Copyright 2005-2007 the original author or authors.
+ *
+ * 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 org.acegisecurity.providers.portlet;
+
+import java.security.Principal;
+
+import javax.portlet.PortletRequest;
+
+import org.acegisecurity.Authentication;
+import org.acegisecurity.AuthenticationException;
+import org.acegisecurity.AuthenticationServiceException;
+import org.acegisecurity.BadCredentialsException;
+import org.acegisecurity.providers.AuthenticationProvider;
+import org.acegisecurity.providers.portlet.cache.NullUserCache;
+import org.acegisecurity.userdetails.UserDetails;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.util.Assert;
+
+/**
+ * <p>Processes a JSR-168 Portlet authentication request.  The request will typically
+ * originate from {@link org.acegisecurity.ui.portlet.PortletProcessingInterceptor}.</p>
+ *
+ * <p>Be aware that this provider is trusting the portal and portlet container to handle
+ * actual authentication. If a valid {@link PortletAuthenticationToken} is presented with
+ * non-null principal and credentials, then the {@link #authenticate} method will succeed.</p>
+ *
+ * <p>If the <code>details</code> property of the requesting <code>Authentication</code>
+ * object happens to be the <code>PortletRequest</code>, then this provider will place
+ * the contents of the <code>USER_INFO</code> map from of the request attributes into
+ * the <code>details</code> property of the authentication result.</p>
+ *
+ * @author John A. Lewis
+ * @since 2.0
+ * @version $Id$
+ */
+public class PortletAuthenticationProvider
+		implements AuthenticationProvider, InitializingBean {
+
+	//~ Static fields/initializers =====================================================================================
+
+	private static final Log logger = LogFactory.getLog(PortletAuthenticationProvider.class);
+
+	//~ Instance fields ================================================================================================
+
+	private PortletAuthoritiesPopulator portletAuthoritiesPopulator;
+	private UserCache userCache = new NullUserCache();
+
+	//~ Methods ========================================================================================================
+
+	public void afterPropertiesSet() throws Exception {
+		Assert.notNull(this.portletAuthoritiesPopulator, "An authorities populator must be set");
+		Assert.notNull(this.userCache, "A user cache must be set");
+	}
+
+	public boolean supports(Class authentication) {
+		return PortletAuthenticationToken.class.isAssignableFrom(authentication);
+	}
+
+	public Authentication authenticate(Authentication authentication)
+		throws AuthenticationException {
+
+		// make sure we support the authentication
+		if (!supports(authentication.getClass())) {
+			return null;
+		}
+
+		if (logger.isDebugEnabled())
+			logger.debug("portlet authentication request: " + authentication);
+
+		// make sure there is a valid principal in the authentication attempt
+		Object principal = authentication.getPrincipal();
+		if (principal == null) {
+			throw new BadCredentialsException("No principal presented - user is not authenticated");
+		}
+
+		// make sure there are valid credentials in the authentication attempt
+		Object credentials = authentication.getCredentials();
+		if (credentials == null) {
+			throw new BadCredentialsException("No credentials presented - user is not authenticated");
+		}
+
+		// determine the username string from the principal
+		String username = getUsernameFromPrincipal(principal);
+		if (username == null) {
+			throw new BadCredentialsException("No username available - user is not authenticated");
+		}
+
+		// try to retrieve the user from the cache
+		UserDetails user = this.userCache.getUserFromCache(username);
+
+		// if the user is null then it wasn't in the cache so go get it
+		if (user == null) {
+
+			if (logger.isDebugEnabled())
+				logger.debug("user not found in the cache");
+
+			// get the user from the authorities populator
+			user = this.portletAuthoritiesPopulator.getUserDetails(authentication);
+
+			if (user == null) {
+				throw new AuthenticationServiceException(
+					"portletAuthoritiesPopulator returned null, which is an interface contract violation");
+			}
+
+			// store the result back in the cache
+			this.userCache.putUserInCache(user);
+
+		} else {
+
+			if (logger.isDebugEnabled())
+				logger.debug("got user from the cache");
+		}
+
+		// build the resulting successful authentication token
+		PortletAuthenticationToken result = new PortletAuthenticationToken(
+				user, authentication.getCredentials(), user.getAuthorities());
+
+		// see if the detail property on the request is the PortletRequest
+		if (authentication.getDetails() instanceof PortletRequest) {
+			// place the USER_INFO map into the details property of the result
+			PortletRequest request = (PortletRequest)authentication.getDetails();
+			result.setDetails(request.getAttribute(PortletRequest.USER_INFO));
+		} else {
+			// copy any other details information forward
+			result.setDetails(authentication.getDetails());
+		}
+
+		if (logger.isDebugEnabled())
+			logger.debug("portlet authentication succeeded: " + authentication);
+
+		return result;
+	}
+
+	/**
+	 * This method attempt to determine the username string from the principal object.
+	 * If the principal object is a {@link UserDetails} object then it will use the
+	 * {@link UserDetails#getUsername() method.  If the principal object is a
+	 * {@link Principal} object then it will use the {@link Principal#getName()}
+	 * method.  Otherwise it will simply call the <code>toString()<code> method
+	 * on the principal object and return that.
+	 * @param principal the principal object to inspect for a username
+	 * @return the determined username, or null if no principal is passed
+	 */
+	public static final String getUsernameFromPrincipal(Object principal) {
+		if (principal == null) {
+			return null;
+		}
+		if (principal instanceof UserDetails) {
+			return ((UserDetails)principal).getUsername();
+		}
+		if (principal instanceof Principal) {
+			return ((Principal)principal).getName();
+		}
+		return principal.toString();
+	}
+
+
+	public PortletAuthoritiesPopulator getPortletAuthoritiesPopulator() {
+		return this.portletAuthoritiesPopulator;
+	}
+
+	public void setPortletAuthoritiesPopulator(PortletAuthoritiesPopulator portletAuthoritiesPopulator) {
+		this.portletAuthoritiesPopulator = portletAuthoritiesPopulator;
+	}
+
+	public UserCache getUserCache() {
+		return userCache;
+	}
+
+	public void setUserCache(UserCache userCache) {
+		this.userCache = userCache;
+	}
+
+}

+ 57 - 0
sandbox/portlet/src/main/java/org/acegisecurity/providers/portlet/PortletAuthenticationToken.java

@@ -0,0 +1,57 @@
+/*
+ * Copyright 2005-2007 the original author or authors.
+ *
+ * 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 org.acegisecurity.providers.portlet;
+
+import org.acegisecurity.GrantedAuthority;
+import org.acegisecurity.providers.AbstractAuthenticationToken;
+
+/**
+ * <code>Authentication</code> implementation for JSR-168 Portlet authentication.  <p>The
+ * corresponding authentication provider is  {@link PortletAuthenticationProvider}.</p>
+ *
+ * @author John A. Lewis
+ * @since 2.0
+ * @version $Id$
+ */
+public class PortletAuthenticationToken extends AbstractAuthenticationToken {
+
+	//~ Instance fields ================================================================================================
+
+	private static final long serialVersionUID = 1L;
+
+	private Object principal;
+	private Object credentials;
+
+	//~ Constructors ===================================================================================================
+
+	public PortletAuthenticationToken(Object principal, Object credentials, GrantedAuthority[] authorities) {
+		super(authorities);
+		this.principal = principal;
+		this.credentials = credentials;
+	}
+
+	//~ Methods ========================================================================================================
+
+	public Object getPrincipal() {
+		return this.principal;
+	}
+
+	public Object getCredentials() {
+		return this.credentials;
+	}
+
+}

+ 45 - 0
sandbox/portlet/src/main/java/org/acegisecurity/providers/portlet/PortletAuthoritiesPopulator.java

@@ -0,0 +1,45 @@
+/*
+ * Copyright 2005-2007 the original author or authors.
+ *
+ * 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 org.acegisecurity.providers.portlet;
+
+import org.acegisecurity.Authentication;
+import org.acegisecurity.AuthenticationException;
+import org.acegisecurity.userdetails.UserDetails;
+
+/**
+ * Populates the <code>UserDetails</code> associated with the
+ * portlet user presented by the portlet container.
+ *
+ * @author John A. Lewis
+ * @since 2.0
+ * @version $Id$
+ */
+public interface PortletAuthoritiesPopulator {
+
+	//~ Methods ========================================================================================================
+
+	/**
+	 * Obtains the granted authorities for the specified Authentication object.
+	 * <p>May throw any <code>AuthenticationException</code> or return <code>null</code>
+	 * if the authorities are unavailable.</p>
+	 * @param authentication the authentication object seeking authorities
+	 * @return the details of the indicated user (at minimum the granted authorities and the username)
+	 * @throws AuthenticationException if the user details are not available or the authentication is not valid for some reason
+	 */
+	public UserDetails getUserDetails(Authentication authentication) throws AuthenticationException;
+
+}

+ 39 - 0
sandbox/portlet/src/main/java/org/acegisecurity/providers/portlet/UserCache.java

@@ -0,0 +1,39 @@
+/*
+ * Copyright 2005-2007 the original author or authors.
+ *
+ * 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 org.acegisecurity.providers.portlet;
+
+import org.acegisecurity.userdetails.UserDetails;
+
+/**
+ * Provides a cache of {@link UserDetails} objects for the
+ * {@link PortletAuthenticationProvider}.
+ *
+ * @author John A. Lewis
+ * @since 2.0
+ * @version $Id$
+ */
+public interface UserCache {
+
+	//~ Methods ========================================================================================================
+
+	public UserDetails getUserFromCache(String username);
+
+	public void putUserInCache(UserDetails user);
+
+	public void removeUserFromCache(String username);
+
+}

+ 102 - 0
sandbox/portlet/src/main/java/org/acegisecurity/providers/portlet/cache/EhCacheBasedUserCache.java

@@ -0,0 +1,102 @@
+/*
+ * Copyright 2005-2007 the original author or authors.
+ *
+ * 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 org.acegisecurity.providers.portlet.cache;
+
+import net.sf.ehcache.Cache;
+import net.sf.ehcache.CacheException;
+import net.sf.ehcache.Element;
+
+import org.acegisecurity.providers.portlet.UserCache;
+import org.acegisecurity.userdetails.UserDetails;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.dao.DataRetrievalFailureException;
+import org.springframework.util.Assert;
+
+/**
+ * <code>UserCache</code> implementation for portlets that uses an injected
+ * <a href="http://ehcache.sourceforge.net">ehcache</a>.
+ *
+ * @author John A. Lewis
+ * @since 2.0
+ * @version $Id$
+ */
+public class EhCacheBasedUserCache
+		implements UserCache, InitializingBean {
+
+	//~ Static fields/initializers =====================================================================================
+
+	private static final Log logger = LogFactory.getLog(EhCacheBasedUserCache.class);
+
+	//~ Instance fields ================================================================================================
+
+	private Cache cache;
+
+	//~ Methods ========================================================================================================
+
+	public void afterPropertiesSet() throws Exception {
+		Assert.notNull(cache, "cache mandatory");
+	}
+
+	public UserDetails getUserFromCache(String username) {
+
+		Element element = null;
+
+		try {
+			element = cache.get(username);
+		} catch (CacheException cacheException) {
+			throw new DataRetrievalFailureException("Cache failure: "
+					+ cacheException.getMessage());
+		}
+
+		if (logger.isDebugEnabled())
+			logger.debug("Cache hit: " + (element != null) + "; username: " + username);
+
+		return (element != null ? (UserDetails) element.getValue() : null);
+	}
+
+	public void putUserInCache(UserDetails user) {
+
+		Element element = new Element(user.getUsername(), user);
+
+		if (logger.isDebugEnabled())
+			logger.debug("Cache put: " + element.getKey());
+
+		cache.put(element);
+	}
+
+	public void removeUserFromCache(UserDetails user) {
+		this.removeUserFromCache(user.getUsername());
+	}
+
+	public void removeUserFromCache(String username) {
+		if (logger.isDebugEnabled())
+			logger.debug("Cache remove: " + username);
+		cache.remove(username);
+	}
+
+
+	public Cache getCache() {
+		return cache;
+	}
+
+	public void setCache(Cache cache) {
+		this.cache = cache;
+	}
+
+}

+ 41 - 0
sandbox/portlet/src/main/java/org/acegisecurity/providers/portlet/cache/NullUserCache.java

@@ -0,0 +1,41 @@
+/*
+ * Copyright 2005-2007 the original author or authors.
+ *
+ * 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 org.acegisecurity.providers.portlet.cache;
+
+import org.acegisecurity.providers.portlet.UserCache;
+import org.acegisecurity.userdetails.UserDetails;
+
+/**
+ * <code>UserCache</code> implementation for portlets that does nothing.
+ *
+ * @author John A. Lewis
+ * @since 2.0
+ * @version $Id$
+ */
+public class NullUserCache implements UserCache {
+
+	//~ Methods ========================================================================================================
+
+	public UserDetails getUserFromCache(String username) {
+		return null;
+	}
+
+	public void putUserInCache(UserDetails user) {}
+
+	public void removeUserFromCache(String username) {}
+
+}

+ 136 - 0
sandbox/portlet/src/main/java/org/acegisecurity/providers/portlet/populator/ContainerPortletAuthoritiesPopulator.java

@@ -0,0 +1,136 @@
+/*
+ * Copyright 2005-2007 the original author or authors.
+ *
+ * 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 org.acegisecurity.providers.portlet.populator;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.portlet.PortletRequest;
+
+import org.acegisecurity.Authentication;
+import org.acegisecurity.AuthenticationException;
+import org.acegisecurity.AuthenticationServiceException;
+import org.acegisecurity.GrantedAuthority;
+import org.acegisecurity.GrantedAuthorityImpl;
+import org.acegisecurity.providers.portlet.PortletAuthenticationProvider;
+import org.acegisecurity.providers.portlet.PortletAuthoritiesPopulator;
+import org.acegisecurity.userdetails.User;
+import org.acegisecurity.userdetails.UserDetails;
+
+/**
+ * Populates the portlet authorities via role information from the portlet container.
+ * Primarily it uses the <code>PortletRequest.isUserInRole(role)</code> method to
+ * check if the user is in a list of configured roles.
+ * <p>This bean has the following configurable properties:</p>
+ * <ul>
+ *     <li><code>rolesToCheck</code> : A list of strings containing names of roles to check.
+ *         These roles must also be properly declared in a &lt;security-role-ref&gt; element
+ *         of the portlet descriptor in the portlet.xml file.</li>
+ *     <li><code>rolePrefix</code> : The prefix to be added onto each role name that as it is
+ *         added to the list of authorities.  The default value is 'ROLE_'.</li>
+ *     <li><code>userRole</code> : The role that all authenticated users will automatically be
+ *         granted.  The default value is 'ROLE_USER'.</li>
+ * </ul>
+ *
+ * @author John A. Lewis
+ * @since 2.0
+ * @version $Id$
+ */
+public class ContainerPortletAuthoritiesPopulator
+		implements PortletAuthoritiesPopulator {
+
+	//~ Static fields/initializers =====================================================================================
+
+	private static final String defaultRolePrefix = "ROLE_";
+	private static final String defaultUserRole = "ROLE_USER";
+
+	//~ Instance fields ================================================================================================
+
+    private List rolesToCheck;
+    private String rolePrefix = defaultRolePrefix;
+    private String userRole = defaultUserRole;
+
+	//~ Methods ========================================================================================================
+
+	public UserDetails getUserDetails(Authentication authentication)
+		throws AuthenticationException {
+
+		// get the username and password for the authentication
+		String username = PortletAuthenticationProvider.getUsernameFromPrincipal(authentication.getPrincipal());
+		String password = authentication.getCredentials().toString();
+
+		// see if we can load authorities from the portlet request
+		Object details = authentication.getDetails();
+		if (!(details instanceof PortletRequest)) {
+			throw new AuthenticationServiceException("expected getDetails() to return the PortletRequest object");
+		}
+		GrantedAuthority[] authorities = loadGrantedAuthorities((PortletRequest)details);
+
+		// construct and return the new user
+		return new User(username, password, true, true, true, true,	authorities);
+	}
+
+	private GrantedAuthority[] loadGrantedAuthorities(PortletRequest request) {
+
+		// start the list and add the standard user role
+		ArrayList authorities = new ArrayList();
+		authorities.add(new GrantedAuthorityImpl(getUserRole()));
+
+		// iterate through the configured list of roles to check (if there is one)
+		if (this.rolesToCheck != null) {
+			for(Iterator i = this.rolesToCheck.iterator(); i.hasNext(); ) {
+				String role = (String)i.next();
+
+				// if the request says the user has that role, then add it
+				if (request.isUserInRole(role)) {
+					authorities.add(new GrantedAuthorityImpl(getRolePrefix() + role));
+				}
+
+			}
+		}
+
+        // return the array of GrantedAuthority objects
+		return (GrantedAuthority[])authorities.toArray(new GrantedAuthority[authorities.size()]);
+	}
+
+
+	public List getRolesToCheck() {
+		return rolesToCheck;
+	}
+
+	public void setRolesToCheck(List rolesToCheck) {
+		this.rolesToCheck = rolesToCheck;
+	}
+
+	public String getRolePrefix() {
+		return rolePrefix;
+	}
+
+	public void setRolePrefix(String rolePrefix) {
+		this.rolePrefix = rolePrefix;
+	}
+
+	public String getUserRole() {
+		return userRole;
+	}
+
+	public void setUserRole(String userRole) {
+		this.userRole = userRole;
+	}
+
+}

+ 74 - 0
sandbox/portlet/src/main/java/org/acegisecurity/providers/portlet/populator/DaoPortletAuthoritiesPopulator.java

@@ -0,0 +1,74 @@
+/*
+ * Copyright 2005-2007 the original author or authors.
+ *
+ * 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 org.acegisecurity.providers.portlet.populator;
+
+import org.acegisecurity.Authentication;
+import org.acegisecurity.AuthenticationException;
+import org.acegisecurity.AuthenticationServiceException;
+import org.acegisecurity.providers.portlet.PortletAuthenticationProvider;
+import org.acegisecurity.providers.portlet.PortletAuthoritiesPopulator;
+import org.acegisecurity.userdetails.UserDetails;
+import org.acegisecurity.userdetails.UserDetailsService;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.util.Assert;
+
+/**
+ * Populates the portlet authorities via a {@link UserDetailsService}.
+ *
+ * @author John A. Lewis
+ * @since 2.0
+ * @version $Id$
+ */
+public class DaoPortletAuthoritiesPopulator
+		implements PortletAuthoritiesPopulator,	InitializingBean {
+
+	//~ Instance fields ================================================================================================
+
+    private UserDetailsService userDetailsService;
+
+	//~ Methods ========================================================================================================
+
+	public void afterPropertiesSet() throws Exception {
+		Assert.notNull(this.userDetailsService, "A userDetailsService must be set");
+	}
+
+	public UserDetails getUserDetails(Authentication authentication)
+			throws AuthenticationException {
+
+		// make sure the Authentication object is valid
+		if (authentication == null || authentication.getPrincipal() == null) {
+			throw new AuthenticationServiceException(
+					"must pass valid Authentication object with non-null principal");
+		}
+
+		// get the username from the principal
+		String username = PortletAuthenticationProvider.getUsernameFromPrincipal(authentication.getPrincipal());
+
+		// call the UserDetailsService with the username
+		return this.userDetailsService.loadUserByUsername(username);
+	}
+
+
+	public UserDetailsService getUserDetailsService() {
+		return userDetailsService;
+	}
+
+	public void setUserDetailsService(UserDetailsService userDetailsService) {
+		this.userDetailsService = userDetailsService;
+	}
+
+}

+ 293 - 0
sandbox/portlet/src/main/java/org/acegisecurity/ui/portlet/PortletProcessingInterceptor.java

@@ -0,0 +1,293 @@
+/*
+ * Copyright 2005-2007 the original author or authors.
+ *
+ * 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 org.acegisecurity.ui.portlet;
+
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+import javax.portlet.ActionRequest;
+import javax.portlet.ActionResponse;
+import javax.portlet.PortletRequest;
+import javax.portlet.PortletResponse;
+import javax.portlet.RenderRequest;
+import javax.portlet.RenderResponse;
+
+import org.acegisecurity.Authentication;
+import org.acegisecurity.AuthenticationException;
+import org.acegisecurity.AuthenticationManager;
+import org.acegisecurity.context.SecurityContext;
+import org.acegisecurity.context.SecurityContextHolder;
+import org.acegisecurity.providers.portlet.PortletAuthenticationProvider;
+import org.acegisecurity.providers.portlet.PortletAuthenticationToken;
+import org.acegisecurity.providers.portlet.populator.ContainerPortletAuthoritiesPopulator;
+import org.acegisecurity.ui.AbstractProcessingFilter;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.util.Assert;
+import org.springframework.web.portlet.HandlerInterceptor;
+import org.springframework.web.portlet.ModelAndView;
+
+/**
+ * <p>This interceptor is responsible for processing portlet authentication requests.  This
+ * is the portlet equivalent of the <code>AuthenticationProcessingFilter</code> used for
+ * traditional servlet-based web applications. It is applied to both <code>ActionRequest</code>s
+ * and <code>RenderRequest</code>s alike.  If authentication is successful, the resulting
+ * {@link Authentication} object will be placed into the <code>SecurityContext</code>, which
+ * is guaranteed to have already been created by an earlier interceptor.  If authentication
+ * fails, the <code>AuthenticationException</code> will be placed into the
+ * <code>PortletSession</code> with the attribute defined by
+ * {@link AbstractProcessingFilter#ACEGI_SECURITY_LAST_EXCEPTION_KEY}.</p>
+ *
+ *  <p>Some portals do not properly provide the identity of the current user via the
+ * <code>getRemoteUser()</code> or <code>getUserPrincipal()</code> methods of the
+ * <code>PortletRequest</code>.  In these cases they sometimes make it available in the
+ * <code>USER_INFO</code> map provided as one of the attributes of the request.  If this is
+ * the case in your portal, you can specify a list of <code>USER_INFO</code> attributes
+ * to check for the username via the <code>userNameAttributes</code> property of this bean.
+ * You can also completely override the {@link #getPrincipalFromRequest(PortletRequest)}
+ * and {@link #getCredentialsFromRequest(PortletRequest)} methods to suit the particular
+ * behavior of your portal.</p>
+ *
+ * <p>This interceptor will put the <code>PortletRequest</code> object into the
+ * <code>details<code> property of the <code>Authentication</code> object that is sent
+ * as a request to the <code>AuthenticationManager</code>.  This is done so that the request
+ * is available to classes like {@link ContainerPortletAuthoritiesPopulator} that need
+ * access to information from the portlet container.  The {@link PortletAuthenticationProvider}
+ * will replace this with the <code>USER_INFO</code> map in the resulting <code>Authentication</code>
+ * object.</p>
+ *
+ * @see org.acegisecurity.ui.AbstractProcessingFilter
+ * @see org.acegisecurity.ui.webapp.AuthenticationProcessingFilter
+ * @author John A. Lewis
+ * @since 2.0
+ * @version $Id$
+ */
+public class PortletProcessingInterceptor implements
+		HandlerInterceptor, InitializingBean {
+
+	//~ Static fields/initializers =====================================================================================
+
+	private static final Log logger = LogFactory.getLog(PortletProcessingInterceptor.class);
+
+	//~ Instance fields ================================================================================================
+
+	private AuthenticationManager authenticationManager;
+
+	private List userNameAttributes;
+
+	//~ Methods ========================================================================================================
+
+	public void afterPropertiesSet() throws Exception {
+		Assert.notNull(authenticationManager, "An AuthenticationManager must be set");
+	}
+
+	public boolean preHandleAction(ActionRequest request, ActionResponse response,
+			Object handler) throws Exception {
+		return preHandle(request, response, handler);
+	}
+
+	public boolean preHandleRender(RenderRequest request,
+			RenderResponse response, Object handler) throws Exception {
+		return preHandle(request, response, handler);
+	}
+
+	public void postHandleRender(RenderRequest request, RenderResponse response,
+			Object handler, ModelAndView modelAndView) throws Exception {
+	}
+
+	public void afterActionCompletion(ActionRequest request, ActionResponse response,
+			Object handler, Exception ex) throws Exception {
+	}
+
+	public void afterRenderCompletion(RenderRequest request, RenderResponse response,
+			Object handler, Exception ex) throws Exception {
+	}
+
+	/**
+	 * Common preHandle method for both the action and render phases of the interceptor.
+	 */
+	private boolean preHandle(PortletRequest request, PortletResponse response,
+			Object handler) throws Exception {
+
+		// get the SecurityContext
+		SecurityContext ctx = SecurityContextHolder.getContext();
+
+		if (logger.isDebugEnabled())
+			logger.debug("Checking secure context token: " + ctx.getAuthentication());
+
+		// if there is no existing Authentication object, then lets create one
+		if (ctx.getAuthentication() == null) {
+
+			try {
+
+				// build the authentication request from the PortletRequest
+				PortletAuthenticationToken authRequest = new PortletAuthenticationToken(
+						getPrincipalFromRequest(request),
+						getCredentialsFromRequest(request),
+						null);
+
+				// put the PortletRequest into the authentication request as the "details"
+				authRequest.setDetails(request);
+
+				if (logger.isDebugEnabled())
+					logger.debug("Beginning authentication request for user '" + authRequest.getName() + "'");
+
+				onPreAuthentication(request, response);
+
+				// ask the authentication manager to authenticate the request
+				// it will throw an AuthenticationException if it fails, otherwise it succeeded
+				Authentication authResult = authenticationManager.authenticate(authRequest);
+
+				// process a successful authentication
+				if (logger.isDebugEnabled())
+					logger.debug("Authentication success: " + authResult);
+				ctx.setAuthentication(authResult);
+				onSuccessfulAuthentication(request, response, authResult);
+
+			} catch (AuthenticationException failed) {
+
+				// process an unsuccessful authentication
+				if (logger.isDebugEnabled())
+					logger.debug("Authentication failed - updating ContextHolder to contain null Authentication");
+				ctx.setAuthentication(null);
+				request.getPortletSession().setAttribute(AbstractProcessingFilter.ACEGI_SECURITY_LAST_EXCEPTION_KEY, failed);
+				onUnsuccessfulAuthentication(request, response, failed);
+			}
+		}
+
+		return true;
+	}
+
+	/**
+	 * This method attempts to extract a principal from the portlet request.
+	 * According to the JSR-168 spec, the <code>PortletRequest<code> should return the name
+	 * of the user in the <code>getRemoteUser()</code> method.  It should also provide a
+	 * <code>java.security.Principal</code> object from the <code>getUserPrincipal()</code>
+	 * method.  We will first try these to come up with a valid username.
+	 * <p>Unfortunately, some portals do not properly return these values for authenticated
+	 * users.  So, if neither of those succeeds and if the <code>userNameAttributes</code>
+	 * property has been populated, then we will search through the <code>USER_INFO<code>
+	 * map from the request to see if we can find a valid username.
+	 * <p>This method can be overridden by subclasses to provide special handling
+	 * for portals with weak support for the JSR-168 spec.</p>
+	 * @param request the portlet request object
+	 * @return the determined principal object, or null if none found
+	 */
+	protected Object getPrincipalFromRequest(PortletRequest request) {
+
+		// first try getRemoteUser()
+		Object principal = request.getRemoteUser();
+		if (principal != null) {
+			return principal;
+		}
+
+		// next try getUserPrincipal()
+		principal = request.getUserPrincipal();
+		if (principal != null) {
+			return principal;
+		}
+
+		// last try entries in USER_INFO if any attributes were defined
+		if (this.userNameAttributes != null) {
+			Map userInfo = (Map)request.getAttribute(PortletRequest.USER_INFO);
+			if (userInfo != null) {
+			    Iterator i = this.userNameAttributes.iterator();
+			    while(i.hasNext()) {
+					principal = (String)userInfo.get(i.next());
+					if (principal != null) {
+						return principal;
+					}
+				}
+			}
+		}
+
+		// none found so return null
+		return null;
+	}
+
+	/**
+	 * This method attempts to extract a credentials from the portlet request.
+	 * We are trusting the portal framework to authenticate the user, so all
+	 * we are really doing is trying to put something intelligent in here to
+	 * indicate the user is authenticated.  According to the JSR-168 spec,
+	 * PortletRequest.getAuthType() should return a non-null value if the
+	 * user is authenticated and should be null if not authenticated. So we
+	 * will use this as the credentials and the token will be trusted as
+	 * authenticated if the credentials are not null.
+	 * <p>This method can be overridden by subclasses to provide special handling
+	 * for portals with weak support for the JSR-168 spec.  If that is done,
+	 * be sure the value is non-null for authenticated users and null for
+	 * non-authenticated users.</p>
+	 * @param request the portlet request object
+	 * @return the determined credentials object, or null if none found
+	 */
+	protected Object getCredentialsFromRequest(PortletRequest request) {
+		return request.getAuthType();
+	}
+
+
+	/**
+	 * Callback for custom processing prior to the authentication attempt.
+	 * @param request the portlet request to be authenticated
+	 * @param response the portlet response to be authenticated
+	 * @throws AuthenticationException to indicate that authentication attempt is not valid and should be terminated
+	 * @throws IOException
+	 */
+    protected void onPreAuthentication(PortletRequest request, PortletResponse response)
+    	throws AuthenticationException, IOException {}
+
+    /**
+     * Callback for custom processing after a successful authentication attempt.
+     * @param request the portlet request that was authenticated
+     * @param response the portlet response that was authenticated
+     * @param authResult the resulting Authentication object
+     * @throws IOException
+     */
+    protected void onSuccessfulAuthentication(PortletRequest request, PortletResponse response, Authentication authResult)
+    	throws IOException {}
+
+    /**
+     * Callback for custom processing after an unsuccessful authentication attempt.
+     * @param request the portlet request that failed authentication
+     * @param response the portlet response that failed authentication
+     * @param failed the AuthenticationException that occurred
+     * @throws IOException
+     */
+    protected void onUnsuccessfulAuthentication(PortletRequest request, PortletResponse response, AuthenticationException failed)
+    	throws IOException {}
+
+
+	public AuthenticationManager getAuthenticationManager() {
+		return authenticationManager;
+	}
+
+	public void setAuthenticationManager(AuthenticationManager authenticationManager) {
+		this.authenticationManager = authenticationManager;
+	}
+
+	public List getUserNameAttributes() {
+		return userNameAttributes;
+	}
+
+	public void setUserNameAttributes(List userNameAttributes) {
+		this.userNameAttributes = userNameAttributes;
+	}
+
+}