Browse Source

Anonymous principal support. As requested by the community at various times, including in http://forum.springframework.org/viewtopic.php?t=1925.

Ben Alex 20 years ago
parent
commit
693ac5a24a
21 changed files with 1467 additions and 135 deletions
  1. 7 3
      core/src/main/java/org/acegisecurity/AccessDecisionManager.java
  2. 7 8
      core/src/main/java/org/acegisecurity/AuthenticationException.java
  3. 66 0
      core/src/main/java/org/acegisecurity/AuthenticationTrustResolver.java
  4. 77 0
      core/src/main/java/org/acegisecurity/AuthenticationTrustResolverImpl.java
  5. 58 0
      core/src/main/java/org/acegisecurity/InsufficientAuthenticationException.java
  6. 85 63
      core/src/main/java/org/acegisecurity/intercept/web/SecurityEnforcementFilter.java
  7. 86 0
      core/src/main/java/org/acegisecurity/providers/anonymous/AnonymousAuthenticationProvider.java
  8. 133 0
      core/src/main/java/org/acegisecurity/providers/anonymous/AnonymousAuthenticationToken.java
  9. 169 0
      core/src/main/java/org/acegisecurity/providers/anonymous/AnonymousProcessingFilter.java
  10. 7 0
      core/src/main/java/org/acegisecurity/providers/anonymous/package.html
  11. 3 3
      core/src/main/java/org/acegisecurity/userdetails/memory/UserAttribute.java
  12. 4 4
      core/src/main/java/org/acegisecurity/userdetails/memory/UserAttributeEditor.java
  13. 2 3
      core/src/main/java/org/acegisecurity/userdetails/memory/UserMapEditor.java
  14. 67 0
      core/src/test/java/org/acegisecurity/AuthenticationTrustResolverImplTests.java
  15. 58 3
      core/src/test/java/org/acegisecurity/intercept/web/SecurityEnforcementFilterTests.java
  16. 122 0
      core/src/test/java/org/acegisecurity/providers/anonymous/AnonymousAuthenticationProviderTests.java
  17. 190 0
      core/src/test/java/org/acegisecurity/providers/anonymous/AnonymousAuthenticationTokenTests.java
  18. 200 0
      core/src/test/java/org/acegisecurity/providers/anonymous/AnonymousProcessingFilterTests.java
  19. 10 19
      core/src/test/java/org/acegisecurity/providers/dao/memory/UserAttributeEditorTests.java
  20. 99 3
      doc/docbook/acegi.xml
  21. 17 26
      samples/contacts/src/main/webapp/filter/WEB-INF/applicationContext-acegi-security.xml

+ 7 - 3
core/src/main/java/org/acegisecurity/AccessDecisionManager.java

@@ -1,4 +1,4 @@
-/* Copyright 2004 Acegi Technology Pty Limited
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -32,10 +32,14 @@ public interface AccessDecisionManager {
      * @param config the configuration attributes associated with the secured
      *        object being invoked
      *
-     * @throws AccessDeniedException if access is denied
+     * @throws AccessDeniedException if access is denied as the authentication
+     *         does not hold a required authority or ACL privilege
+     * @throws InsufficientAuthenticationException if access is denied as the
+     *         authentication does not provide a sufficient level of trust
      */
     public void decide(Authentication authentication, Object object,
-        ConfigAttributeDefinition config) throws AccessDeniedException;
+        ConfigAttributeDefinition config)
+        throws AccessDeniedException, InsufficientAuthenticationException;
 
     /**
      * Indicates whether this <code>AccessDecisionManager</code> is able to

+ 7 - 8
core/src/main/java/org/acegisecurity/AuthenticationException.java

@@ -1,4 +1,4 @@
-/* Copyright 2004 Acegi Technology Pty Limited
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -16,9 +16,8 @@
 package net.sf.acegisecurity;
 
 /**
- * Abstract superclass for all exceptions related to the {@link
- * AuthenticationManager} being unable to authenticate an {@link
- * Authentication} object.
+ * Abstract superclass for all exceptions related an {@link Authentication}
+ * object being invalid for whatever reason.
  *
  * @author Ben Alex
  * @version $Id$
@@ -57,11 +56,11 @@ public abstract class AuthenticationException extends AcegiSecurityException {
 
     //~ Methods ================================================================
 
-    void setAuthentication(Authentication authentication) {
-        this.authentication = authentication;
-    }
-
     public Authentication getAuthentication() {
         return authentication;
     }
+
+    void setAuthentication(Authentication authentication) {
+        this.authentication = authentication;
+    }
 }

+ 66 - 0
core/src/main/java/org/acegisecurity/AuthenticationTrustResolver.java

@@ -0,0 +1,66 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity;
+
+/**
+ * Evaluates <code>Authentication</code> tokens
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public interface AuthenticationTrustResolver {
+    //~ Methods ================================================================
+
+    /**
+     * Indicates whether the passed <code>Authentication</code> token
+     * represents an anonymous user. Typically the framework will call this
+     * method if it is trying to decide whether an
+     * <code>AccessDeniedException</code> should result in a final rejection
+     * (ie as would be the case if the principal was non-anonymous/fully
+     * authenticated) or direct the principal to attempt actual authentication
+     * (ie as would be the case if the <code>Authentication</code> was merely
+     * anonymous).
+     *
+     * @param authentication to test (may be <code>null</code> in which case
+     *        the method will always return <code>false</code>)
+     *
+     * @return <code>true</code> the passed authentication token represented an
+     *         anonymous principal, <code>false</code> otherwise
+     */
+    public boolean isAnonymous(Authentication authentication);
+
+    /**
+     * Indicates whether the passed <code>Authentication</code> token
+     * represents user that has been remembered (ie not a user that has been
+     * fully authenticated).
+     * 
+     * <p>
+     * <b>No part of the framework uses this method</b>, as it is a weak
+     * definition of trust levels. The method is provided simply to assist
+     * with custom <code>AccessDecisionVoter</code>s and the like that you
+     * might develop. Of course, you don't need to use this method either and
+     * can develop your own "trust level" hierarchy instead.
+     * </p>
+     *
+     * @param authentication to test (may be <code>null</code> in which case
+     *        the method will always return <code>false</code>)
+     *
+     * @return <code>true</code> the passed authentication token represented a
+     *         principal authenticated using a remember-me token,
+     *         <code>false</code> otherwise
+     */
+    public boolean isRememberMe(Authentication authentication);
+}

+ 77 - 0
core/src/main/java/org/acegisecurity/AuthenticationTrustResolverImpl.java

@@ -0,0 +1,77 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity;
+
+import net.sf.acegisecurity.providers.anonymous.AnonymousAuthenticationToken;
+
+
+/**
+ * Basic implementation of {@link AuthenticationTrustResolverImpl}.
+ * 
+ * <P>
+ * Makes trust decisions based on whether the passed
+ * <code>Authentication</code> is an instance of a defined class.
+ * </p>
+ * 
+ * <p>
+ * If {@link #anonymousClass} or {@link #rememberMeClass} is <code>null</code>,
+ * the corresponding method will always return <code>false</code>.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class AuthenticationTrustResolverImpl
+    implements AuthenticationTrustResolver {
+    //~ Instance fields ========================================================
+
+    private Class anonymousClass = AnonymousAuthenticationToken.class;
+    private Class rememberMeClass;
+
+    //~ Methods ================================================================
+
+    public boolean isAnonymous(Authentication authentication) {
+        if ((anonymousClass == null) || (authentication == null)) {
+            return false;
+        }
+
+        return anonymousClass.isAssignableFrom(authentication.getClass());
+    }
+
+    public void setAnonymousClass(Class anonymousClass) {
+        this.anonymousClass = anonymousClass;
+    }
+
+    public Class getAnonymousClass() {
+        return anonymousClass;
+    }
+
+    public boolean isRememberMe(Authentication authentication) {
+        if ((rememberMeClass == null) || (authentication == null)) {
+            return false;
+        }
+
+        return rememberMeClass.isAssignableFrom(authentication.getClass());
+    }
+
+    public void setRememberMeClass(Class rememberMeClass) {
+        this.rememberMeClass = rememberMeClass;
+    }
+
+    public Class getRememberMeClass() {
+        return rememberMeClass;
+    }
+}

+ 58 - 0
core/src/main/java/org/acegisecurity/InsufficientAuthenticationException.java

@@ -0,0 +1,58 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity;
+
+/**
+ * Thrown if an authentication request is rejected because the credentials are
+ * not sufficiently trusted.
+ * 
+ * <p>
+ * {{@link net.sf.acegisecurity.vote.AccessDecisionVoter}s will typically throw
+ * this exception if they are dissatisfied with the level of the
+ * authentication, such as if performed using a remember-me mechnanism or
+ * anonymously. The commonly used {@link
+ * net.sf.acegisecurity.intercept.web.SecurityEnforcementFilter} will thus
+ * cause the <code>AuthenticationEntryPoint</code> to be called, allowing the
+ * principal to authenticate with a stronger level of authentication. }
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class InsufficientAuthenticationException extends AuthenticationException {
+    //~ Constructors ===========================================================
+
+    /**
+     * Constructs an <code>InsufficientAuthenticationException</code> with the
+     * specified message.
+     *
+     * @param msg the detail message
+     */
+    public InsufficientAuthenticationException(String msg) {
+        super(msg);
+    }
+
+    /**
+     * Constructs an <code>InsufficientAuthenticationException</code> with the
+     * specified message and root cause.
+     *
+     * @param msg the detail message
+     * @param t root cause
+     */
+    public InsufficientAuthenticationException(String msg, Throwable t) {
+        super(msg, t);
+    }
+}

+ 85 - 63
core/src/main/java/org/acegisecurity/intercept/web/SecurityEnforcementFilter.java

@@ -1,4 +1,4 @@
-/* Copyright 2004 Acegi Technology Pty Limited
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -17,6 +17,9 @@ package net.sf.acegisecurity.intercept.web;
 
 import net.sf.acegisecurity.AccessDeniedException;
 import net.sf.acegisecurity.AuthenticationException;
+import net.sf.acegisecurity.AuthenticationTrustResolver;
+import net.sf.acegisecurity.AuthenticationTrustResolverImpl;
+import net.sf.acegisecurity.context.security.SecureContextUtils;
 import net.sf.acegisecurity.ui.AbstractProcessingFilter;
 import net.sf.acegisecurity.util.PortResolver;
 import net.sf.acegisecurity.util.PortResolverImpl;
@@ -26,6 +29,8 @@ import org.apache.commons.logging.LogFactory;
 
 import org.springframework.beans.factory.InitializingBean;
 
+import org.springframework.util.Assert;
+
 import java.io.IOException;
 
 import javax.servlet.Filter;
@@ -54,10 +59,13 @@ import javax.servlet.http.HttpServletResponse;
  * </p>
  * 
  * <p>
- * If an {@link AccessDeniedException} is detected, the filter will respond
- * with a <code>HttpServletResponse.SC_FORBIDDEN</code> (403 error).  In
- * addition, the <code>AccessDeniedException</code> itself will be placed in
- * the <code>HttpSession</code> attribute keyed against {@link
+ * If an {@link AccessDeniedException} is detected, the filter will determine
+ * whether or not the user is an anonymous user. If they are an anonymous
+ * user, the <code>authenticationEntryPoint</code> will be launched. If they
+ * are not an anonymous user, the filter will respond with a
+ * <code>HttpServletResponse.SC_FORBIDDEN</code> (403 error).  In addition,
+ * the <code>AccessDeniedException</code> itself will be placed in the
+ * <code>HttpSession</code> attribute keyed against {@link
  * #ACEGI_SECURITY_ACCESS_DENIED_EXCEPTION_KEY} (to allow access to the stack
  * trace etc). Again, this allows common access denied handling irrespective
  * of the originating security interceptor.
@@ -104,6 +112,7 @@ public class SecurityEnforcementFilter implements Filter, InitializingBean {
     //~ Instance fields ========================================================
 
     private AuthenticationEntryPoint authenticationEntryPoint;
+    private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();
     private FilterSecurityInterceptor filterSecurityInterceptor;
     private PortResolver portResolver = new PortResolverImpl();
 
@@ -118,6 +127,15 @@ public class SecurityEnforcementFilter implements Filter, InitializingBean {
         return authenticationEntryPoint;
     }
 
+    public void setAuthenticationTrustResolver(
+        AuthenticationTrustResolver authenticationTrustResolver) {
+        this.authenticationTrustResolver = authenticationTrustResolver;
+    }
+
+    public AuthenticationTrustResolver getAuthenticationTrustResolver() {
+        return authenticationTrustResolver;
+    }
+
     public void setFilterSecurityInterceptor(
         FilterSecurityInterceptor filterSecurityInterceptor) {
         this.filterSecurityInterceptor = filterSecurityInterceptor;
@@ -136,19 +154,13 @@ public class SecurityEnforcementFilter implements Filter, InitializingBean {
     }
 
     public void afterPropertiesSet() throws Exception {
-        if (authenticationEntryPoint == null) {
-            throw new IllegalArgumentException(
-                "authenticationEntryPoint must be specified");
-        }
-
-        if (filterSecurityInterceptor == null) {
-            throw new IllegalArgumentException(
-                "filterSecurityInterceptor must be specified");
-        }
-
-        if (portResolver == null) {
-            throw new IllegalArgumentException("portResolver must be specified");
-        }
+        Assert.notNull(authenticationEntryPoint,
+            "authenticationEntryPoint must be specified");
+        Assert.notNull(filterSecurityInterceptor,
+            "filterSecurityInterceptor must be specified");
+        Assert.notNull(portResolver, "portResolver must be specified");
+        Assert.notNull(authenticationTrustResolver,
+            "authenticationTrustResolver must be specified");
     }
 
     public void destroy() {}
@@ -172,43 +184,29 @@ public class SecurityEnforcementFilter implements Filter, InitializingBean {
                 logger.debug("Chain processed normally");
             }
         } catch (AuthenticationException authentication) {
-            HttpServletRequest httpRequest = (HttpServletRequest) request;
-
-            int port = portResolver.getServerPort(request);
-            boolean includePort = true;
-
-            if ("http".equals(request.getScheme().toLowerCase())
-                && (port == 80)) {
-                includePort = false;
-            }
-
-            if ("https".equals(request.getScheme().toLowerCase())
-                && (port == 443)) {
-                includePort = false;
-            }
-
-            String targetUrl = request.getScheme() + "://"
-                + request.getServerName() + ((includePort) ? (":" + port) : "")
-                + httpRequest.getContextPath() + fi.getRequestUrl();
-
             if (logger.isDebugEnabled()) {
-                logger.debug(
-                    "Authentication failed - adding target URL to Session: "
-                    + targetUrl, authentication);
+                logger.debug("Authentication exception occurred; redirecting to authentication entry point",
+                    authentication);
             }
 
-            ((HttpServletRequest) request).getSession().setAttribute(AbstractProcessingFilter.ACEGI_SECURITY_TARGET_URL_KEY,
-                targetUrl);
-            authenticationEntryPoint.commence(request, response, authentication);
+            sendStartAuthentication(fi, authentication);
         } catch (AccessDeniedException accessDenied) {
-            if (logger.isDebugEnabled()) {
-                logger.debug(
-                    "Access is denied - sending back forbidden response");
+            if (authenticationTrustResolver.isAnonymous(
+                    SecureContextUtils.getSecureContext().getAuthentication())) {
+                if (logger.isDebugEnabled()) {
+                    logger.debug("Access is denied (user is anonymous); redirecting to authentication entry point",
+                        accessDenied);
+                }
+
+                sendStartAuthentication(fi, null);
+            } else {
+                if (logger.isDebugEnabled()) {
+                    logger.debug("Access is denied (user is not anonymous); sending back forbidden response",
+                        accessDenied);
+                }
+
+                sendAccessDeniedError(fi, accessDenied);
             }
-
-            ((HttpServletRequest) request).getSession().setAttribute(ACEGI_SECURITY_ACCESS_DENIED_EXCEPTION_KEY,
-                accessDenied);
-            sendAccessDeniedError(request, response, accessDenied);
         } catch (Throwable otherException) {
             throw new ServletException(otherException);
         }
@@ -216,19 +214,43 @@ public class SecurityEnforcementFilter implements Filter, InitializingBean {
 
     public void init(FilterConfig filterConfig) throws ServletException {}
 
-    /**
-     * Allows subclasses to override if required
-     *
-     * @param request
-     * @param response
-     * @param accessDenied
-     *
-     * @throws IOException
-     */
-    protected void sendAccessDeniedError(ServletRequest request,
-        ServletResponse response, AccessDeniedException accessDenied)
-        throws IOException {
-        ((HttpServletResponse) response).sendError(HttpServletResponse.SC_FORBIDDEN,
+    protected void sendAccessDeniedError(FilterInvocation fi,
+        AccessDeniedException accessDenied)
+        throws ServletException, IOException {
+        ((HttpServletRequest) fi.getRequest()).getSession().setAttribute(ACEGI_SECURITY_ACCESS_DENIED_EXCEPTION_KEY,
+            accessDenied);
+        ((HttpServletResponse) fi.getResponse()).sendError(HttpServletResponse.SC_FORBIDDEN,
             accessDenied.getMessage()); // 403
     }
+
+    protected void sendStartAuthentication(FilterInvocation fi,
+        AuthenticationException reason) throws ServletException, IOException {
+        HttpServletRequest request = (HttpServletRequest) fi.getRequest();
+
+        int port = portResolver.getServerPort(request);
+        boolean includePort = true;
+
+        if ("http".equals(request.getScheme().toLowerCase()) && (port == 80)) {
+            includePort = false;
+        }
+
+        if ("https".equals(request.getScheme().toLowerCase()) && (port == 443)) {
+            includePort = false;
+        }
+
+        String targetUrl = request.getScheme() + "://"
+            + request.getServerName() + ((includePort) ? (":" + port) : "")
+            + request.getContextPath() + fi.getRequestUrl();
+
+        if (logger.isDebugEnabled()) {
+            logger.debug(
+                "Authentication entry point being called; target URL added to Session: "
+                + targetUrl);
+        }
+
+        ((HttpServletRequest) request).getSession().setAttribute(AbstractProcessingFilter.ACEGI_SECURITY_TARGET_URL_KEY,
+            targetUrl);
+        authenticationEntryPoint.commence(request,
+            (HttpServletResponse) fi.getResponse(), reason);
+    }
 }

+ 86 - 0
core/src/main/java/org/acegisecurity/providers/anonymous/AnonymousAuthenticationProvider.java

@@ -0,0 +1,86 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity.providers.anonymous;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.AuthenticationException;
+import net.sf.acegisecurity.BadCredentialsException;
+import net.sf.acegisecurity.providers.AuthenticationProvider;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.beans.factory.InitializingBean;
+
+import org.springframework.util.Assert;
+
+
+/**
+ * An {@link AuthenticationProvider} implementation that validates {@link
+ * net.sf.acegisecurity.providers.anonymous.AnonymousAuthenticationToken}s.
+ * 
+ * <p>
+ * To be successfully validated, the  {@link
+ * net.sf.acegisecurity.providers.anonymous.AnonymousAuthenticationToken#getKeyHash()}
+ * must match this class' {@link #getKey()}.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class AnonymousAuthenticationProvider implements AuthenticationProvider,
+    InitializingBean {
+    //~ Static fields/initializers =============================================
+
+    private static final Log logger = LogFactory.getLog(AnonymousAuthenticationProvider.class);
+
+    //~ Instance fields ========================================================
+
+    private String key;
+
+    //~ Methods ================================================================
+
+    public void setKey(String key) {
+        this.key = key;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public void afterPropertiesSet() throws Exception {
+        Assert.hasLength(key);
+    }
+
+    public Authentication authenticate(Authentication authentication)
+        throws AuthenticationException {
+        if (!supports(authentication.getClass())) {
+            return null;
+        }
+
+        if (this.key.hashCode() != ((AnonymousAuthenticationToken) authentication)
+            .getKeyHash()) {
+            throw new BadCredentialsException(
+                "The presented AnonymousAuthenticationToken does not contain the expected key");
+        }
+
+        return authentication;
+    }
+
+    public boolean supports(Class authentication) {
+        return (AnonymousAuthenticationToken.class.isAssignableFrom(authentication));
+    }
+}

+ 133 - 0
core/src/main/java/org/acegisecurity/providers/anonymous/AnonymousAuthenticationToken.java

@@ -0,0 +1,133 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity.providers.anonymous;
+
+import net.sf.acegisecurity.GrantedAuthority;
+import net.sf.acegisecurity.providers.AbstractAuthenticationToken;
+
+import java.io.Serializable;
+
+
+/**
+ * Represents an anonymous <code>Authentication</code>.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class AnonymousAuthenticationToken extends AbstractAuthenticationToken
+    implements Serializable {
+    //~ Instance fields ========================================================
+
+    private Object principal;
+    private GrantedAuthority[] authorities;
+    private int keyHash;
+
+    //~ Constructors ===========================================================
+
+    /**
+     * Constructor.
+     *
+     * @param key to identify if this object made by an authorised client
+     * @param principal the principal (typically a <code>UserDetails</code>)
+     * @param authorities the authorities granted to the principal
+     *
+     * @throws IllegalArgumentException if a <code>null</code> was passed
+     */
+    public AnonymousAuthenticationToken(String key, Object principal,
+        GrantedAuthority[] authorities) {
+        if ((key == null) || ("".equals(key)) || (principal == null)
+            || "".equals(principal) || (authorities == null)
+            || (authorities.length == 0)) {
+            throw new IllegalArgumentException(
+                "Cannot pass null or empty values to constructor");
+        }
+
+        for (int i = 0; i < authorities.length; i++) {
+            if (authorities[i] == null) {
+                throw new IllegalArgumentException("Granted authority element "
+                    + i
+                    + " is null - GrantedAuthority[] cannot contain any null elements");
+            }
+        }
+
+        this.keyHash = key.hashCode();
+        this.principal = principal;
+        this.authorities = authorities;
+    }
+
+    protected AnonymousAuthenticationToken() {
+        throw new IllegalArgumentException("Cannot use default constructor");
+    }
+
+    //~ Methods ================================================================
+
+    /**
+     * Ignored (always <code>true</code>).
+     *
+     * @param isAuthenticated ignored
+     */
+    public void setAuthenticated(boolean isAuthenticated) {
+        // ignored
+    }
+
+    /**
+     * Always returns <code>true</code>.
+     *
+     * @return true
+     */
+    public boolean isAuthenticated() {
+        return true;
+    }
+
+    public GrantedAuthority[] getAuthorities() {
+        return this.authorities;
+    }
+
+    /**
+     * Always returns an empty <code>String</code>
+     *
+     * @return an empty String
+     */
+    public Object getCredentials() {
+        return "";
+    }
+
+    public int getKeyHash() {
+        return this.keyHash;
+    }
+
+    public Object getPrincipal() {
+        return this.principal;
+    }
+
+    public boolean equals(Object obj) {
+        if (!super.equals(obj)) {
+            return false;
+        }
+
+        if (obj instanceof AnonymousAuthenticationToken) {
+            AnonymousAuthenticationToken test = (AnonymousAuthenticationToken) obj;
+
+            if (this.getKeyHash() != test.getKeyHash()) {
+                return false;
+            }
+
+            return true;
+        }
+
+        return false;
+    }
+}

+ 169 - 0
core/src/main/java/org/acegisecurity/providers/anonymous/AnonymousProcessingFilter.java

@@ -0,0 +1,169 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity.providers.anonymous;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.context.security.SecureContext;
+import net.sf.acegisecurity.context.security.SecureContextUtils;
+import net.sf.acegisecurity.intercept.web.AuthenticationEntryPoint;
+import net.sf.acegisecurity.providers.dao.memory.UserAttribute;
+import net.sf.acegisecurity.ui.basicauth.BasicProcessingFilterEntryPoint;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+import org.springframework.beans.factory.InitializingBean;
+
+import org.springframework.util.Assert;
+
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+
+/**
+ * Detects if there is no <code>Authentication</code> object in the
+ * <code>ContextHolder</code>,  and populates it with one if needed.
+ * 
+ * <P></p>
+ * 
+ * <p>
+ * In summary, this filter is responsible for processing any request that has a
+ * HTTP request header of <code>Authorization</code> with an authentication
+ * scheme of <code>Basic</code> and a Base64-encoded
+ * <code>username:password</code> token. For example, to authenticate user
+ * "Aladdin" with password "open sesame" the following header would be
+ * presented:
+ * </p>
+ * 
+ * <p>
+ * <code>Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==</code>.
+ * </p>
+ * 
+ * <p>
+ * This filter can be used to provide BASIC authentication services to both
+ * remoting protocol clients (such as Hessian and SOAP) as well as standard
+ * user agents (such as Internet Explorer and Netscape).
+ * </p>
+ * 
+ * <P>
+ * If authentication is successful, the resulting {@link Authentication} object
+ * will be placed into the <code>ContextHolder</code>.
+ * </p>
+ * 
+ * <p>
+ * If authentication fails, an {@link AuthenticationEntryPoint} implementation
+ * is called. Usually this should be {@link BasicProcessingFilterEntryPoint},
+ * which will prompt the user to authenticate again via BASIC authentication.
+ * </p>
+ * 
+ * <P>
+ * Basic authentication is an attractive protocol because it is simple and
+ * widely deployed. However, it still transmits a password in clear text and
+ * as such is undesirable in many situations. Digest authentication is also
+ * provided by Acegi Security and should be used instead of Basic
+ * authentication wherever possible. See {@link
+ * net.sf.acegisecurity.ui.digestauth.DigestProcessingFilter}.
+ * </p>
+ * 
+ * <P>
+ * <B>Do not use this class directly.</B> Instead configure
+ * <code>web.xml</code> to use the {@link
+ * net.sf.acegisecurity.util.FilterToBeanProxy}.
+ * </p>
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class AnonymousProcessingFilter implements Filter, InitializingBean {
+    //~ Static fields/initializers =============================================
+
+    private static final Log logger = LogFactory.getLog(AnonymousProcessingFilter.class);
+
+    //~ Instance fields ========================================================
+
+    private String key;
+    private UserAttribute userAttribute;
+
+    //~ Methods ================================================================
+
+    public void setKey(String key) {
+        this.key = key;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public void setUserAttribute(UserAttribute userAttributeDefinition) {
+        this.userAttribute = userAttributeDefinition;
+    }
+
+    public UserAttribute getUserAttribute() {
+        return userAttribute;
+    }
+
+    public void afterPropertiesSet() throws Exception {
+        Assert.notNull(userAttribute);
+        Assert.hasLength(key);
+    }
+
+    /**
+     * Does nothing - we reply on IoC lifecycle services instead.
+     */
+    public void destroy() {}
+
+    public void doFilter(ServletRequest request, ServletResponse response,
+        FilterChain chain) throws IOException, ServletException {
+        SecureContext sc = SecureContextUtils.getSecureContext();
+
+        if (sc.getAuthentication() == null) {
+            sc.setAuthentication(createAuthentication(request));
+
+            if (logger.isDebugEnabled()) {
+                logger.debug("Replaced ContextHolder with anonymous token: '"
+                    + sc.getAuthentication() + "'");
+            }
+        } else {
+            if (logger.isDebugEnabled()) {
+                logger.debug(
+                    "ContextHolder not replaced with anonymous token, as ContextHolder already contained: '"
+                    + sc.getAuthentication() + "'");
+            }
+        }
+
+        chain.doFilter(request, response);
+    }
+
+    /**
+     * Does nothing - we reply on IoC lifecycle services instead.
+     *
+     * @param arg0 DOCUMENT ME!
+     *
+     * @throws ServletException DOCUMENT ME!
+     */
+    public void init(FilterConfig arg0) throws ServletException {}
+
+    protected Authentication createAuthentication(ServletRequest request) {
+        return new AnonymousAuthenticationToken(key,
+            userAttribute.getPassword(), userAttribute.getAuthorities());
+    }
+}

+ 7 - 0
core/src/main/java/org/acegisecurity/providers/anonymous/package.html

@@ -0,0 +1,7 @@
+<html>
+<body>
+Allows you to secure every invocation (especially useful for web request
+URI security) by always having either an actual principal or an anonymous
+principal authenticated.
+</body>
+</html>

+ 3 - 3
core/src/main/java/org/acegisecurity/userdetails/memory/UserAttributeDefinition.java → core/src/main/java/org/acegisecurity/userdetails/memory/UserAttribute.java

@@ -1,4 +1,4 @@
-/* Copyright 2004 Acegi Technology Pty Limited
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -29,7 +29,7 @@ import java.util.Vector;
  * @author Ben Alex
  * @version $Id$
  */
-public class UserAttributeDefinition {
+public class UserAttribute {
     //~ Instance fields ========================================================
 
     private List authorities = new Vector();
@@ -38,7 +38,7 @@ public class UserAttributeDefinition {
 
     //~ Constructors ===========================================================
 
-    public UserAttributeDefinition() {
+    public UserAttribute() {
         super();
     }
 

+ 4 - 4
core/src/main/java/org/acegisecurity/userdetails/memory/UserAttributeEditor.java

@@ -1,4 +1,4 @@
-/* Copyright 2004 Acegi Technology Pty Limited
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -23,8 +23,8 @@ import java.beans.PropertyEditorSupport;
 
 
 /**
- * Property editor that creates a {@link UserAttributeDefinition} from a  comma
- * separated list of values.
+ * Property editor that creates a {@link UserAttribute} from a  comma separated
+ * list of values.
  *
  * @author Ben Alex
  * @version $Id$
@@ -37,7 +37,7 @@ public class UserAttributeEditor extends PropertyEditorSupport {
             setValue(null);
         } else {
             String[] tokens = StringUtils.commaDelimitedListToStringArray(s);
-            UserAttributeDefinition userAttrib = new UserAttributeDefinition();
+            UserAttribute userAttrib = new UserAttribute();
 
             for (int i = 0; i < tokens.length; i++) {
                 String currentToken = tokens[i];

+ 2 - 3
core/src/main/java/org/acegisecurity/userdetails/memory/UserMapEditor.java

@@ -1,4 +1,4 @@
-/* Copyright 2004 Acegi Technology Pty Limited
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -91,8 +91,7 @@ public class UserMapEditor extends PropertyEditorSupport {
                 // Convert value to a password, enabled setting, and list of granted authorities
                 configAttribEd.setAsText(value);
 
-                UserAttributeDefinition attr = (UserAttributeDefinition) configAttribEd
-                    .getValue();
+                UserAttribute attr = (UserAttribute) configAttribEd.getValue();
 
                 // Make a user object, assuming the properties were properly provided
                 if (attr != null) {

+ 67 - 0
core/src/test/java/org/acegisecurity/AuthenticationTrustResolverImplTests.java

@@ -0,0 +1,67 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.providers.TestingAuthenticationToken;
+import net.sf.acegisecurity.providers.anonymous.AnonymousAuthenticationToken;
+
+
+/**
+ * Tests {@link net.sf.acegisecurity.AuthenticationTrustResolverImpl}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class AuthenticationTrustResolverImplTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public AuthenticationTrustResolverImplTests() {
+        super();
+    }
+
+    public AuthenticationTrustResolverImplTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(AuthenticationTrustResolverImplTests.class);
+    }
+
+    public void testCorrectOperationIsAnonymous() {
+        AuthenticationTrustResolverImpl trustResolver = new AuthenticationTrustResolverImpl();
+        assertTrue(trustResolver.isAnonymous(
+                new AnonymousAuthenticationToken("ignored", "ignored",
+                    new GrantedAuthority[] {new GrantedAuthorityImpl("ignored")})));
+        assertFalse(trustResolver.isAnonymous(
+                new TestingAuthenticationToken("ignored", "ignored",
+                    new GrantedAuthority[] {new GrantedAuthorityImpl("ignored")})));
+    }
+
+    public void testGettersSetters() {
+        AuthenticationTrustResolverImpl trustResolver = new AuthenticationTrustResolverImpl();
+
+        assertEquals(AnonymousAuthenticationToken.class,
+            trustResolver.getAnonymousClass());
+        trustResolver.setAnonymousClass(String.class);
+        assertEquals(String.class, trustResolver.getAnonymousClass());
+
+        assertNull(trustResolver.getRememberMeClass());
+    }
+}

+ 58 - 3
core/src/test/java/org/acegisecurity/intercept/web/SecurityEnforcementFilterTests.java

@@ -1,4 +1,4 @@
-/* Copyright 2004 Acegi Technology Pty Limited
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -19,11 +19,17 @@ import junit.framework.TestCase;
 
 import net.sf.acegisecurity.AccessDeniedException;
 import net.sf.acegisecurity.BadCredentialsException;
+import net.sf.acegisecurity.GrantedAuthority;
+import net.sf.acegisecurity.GrantedAuthorityImpl;
 import net.sf.acegisecurity.MockAuthenticationEntryPoint;
 import net.sf.acegisecurity.MockHttpServletRequest;
 import net.sf.acegisecurity.MockHttpServletResponse;
 import net.sf.acegisecurity.MockHttpSession;
 import net.sf.acegisecurity.MockPortResolver;
+import net.sf.acegisecurity.context.ContextHolder;
+import net.sf.acegisecurity.context.security.SecureContext;
+import net.sf.acegisecurity.context.security.SecureContextImpl;
+import net.sf.acegisecurity.providers.anonymous.AnonymousAuthenticationToken;
 import net.sf.acegisecurity.ui.webapp.AuthenticationProcessingFilter;
 
 import java.io.IOException;
@@ -62,13 +68,18 @@ public class SecurityEnforcementFilterTests extends TestCase {
         junit.textui.TestRunner.run(SecurityEnforcementFilterTests.class);
     }
 
-    public void testAccessDeniedWhenAccessDeniedException()
-        throws Exception {
+    public void testAccessDeniedWhenAnonymous() throws Exception {
         // Setup our HTTP request
         HttpSession session = new MockHttpSession();
         MockHttpServletRequest request = new MockHttpServletRequest(null,
                 session);
         request.setServletPath("/secure/page.html");
+        request.setServerPort(80);
+        request.setScheme("http");
+        request.setServerName("www.example.com");
+        request.setContextPath("/mycontext");
+        request.setRequestURL(
+            "http://www.example.com/mycontext/secure/page.html");
 
         // Setup our expectation that the filter chain will not be invoked, as access is denied
         MockFilterChain chain = new MockFilterChain(false);
@@ -77,6 +88,45 @@ public class SecurityEnforcementFilterTests extends TestCase {
         MockFilterSecurityInterceptor interceptor = new MockFilterSecurityInterceptor(true,
                 false);
 
+        // Setup ContextHolder, as filter needs to check if user is anonymous
+        SecureContext sc = new SecureContextImpl();
+        sc.setAuthentication(new AnonymousAuthenticationToken("ignored",
+                "ignored",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("IGNORED")}));
+        ContextHolder.setContext(sc);
+
+        // Test
+        SecurityEnforcementFilter filter = new SecurityEnforcementFilter();
+        filter.setFilterSecurityInterceptor(interceptor);
+        filter.setAuthenticationEntryPoint(new MockAuthenticationEntryPoint(
+                "/login.jsp"));
+
+        MockHttpServletResponse response = new MockHttpServletResponse();
+        filter.doFilter(request, response, chain);
+        assertEquals("/mycontext/login.jsp", response.getRedirect());
+        assertEquals("http://www.example.com/mycontext/secure/page.html",
+            request.getSession().getAttribute(AuthenticationProcessingFilter.ACEGI_SECURITY_TARGET_URL_KEY));
+    }
+
+    public void testAccessDeniedWhenNonAnonymous() throws Exception {
+        // Setup our HTTP request
+        HttpSession session = new MockHttpSession();
+        MockHttpServletRequest request = new MockHttpServletRequest(null,
+                session);
+        request.setServletPath("/secure/page.html");
+
+        // Setup our expectation that the filter chain will not be invoked, as access is denied
+        MockFilterChain chain = new MockFilterChain(false);
+
+        // Setup the FilterSecurityInterceptor thrown an access denied exception
+        MockFilterSecurityInterceptor interceptor = new MockFilterSecurityInterceptor(true,
+                false);
+
+        // Setup ContextHolder, as filter needs to check if user is anonymous
+        SecureContext sc = new SecureContextImpl();
+        sc.setAuthentication(null);
+        ContextHolder.setContext(sc);
+
         // Test
         SecurityEnforcementFilter filter = new SecurityEnforcementFilter();
         filter.setFilterSecurityInterceptor(interceptor);
@@ -281,6 +331,11 @@ public class SecurityEnforcementFilterTests extends TestCase {
         assertTrue(true);
     }
 
+    protected void tearDown() throws Exception {
+        super.tearDown();
+        ContextHolder.setContext(null);
+    }
+
     //~ Inner Classes ==========================================================
 
     private class MockFilterChain implements FilterChain {

+ 122 - 0
core/src/test/java/org/acegisecurity/providers/anonymous/AnonymousAuthenticationProviderTests.java

@@ -0,0 +1,122 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity.providers.anonymous;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.BadCredentialsException;
+import net.sf.acegisecurity.GrantedAuthority;
+import net.sf.acegisecurity.GrantedAuthorityImpl;
+import net.sf.acegisecurity.providers.TestingAuthenticationToken;
+
+
+/**
+ * Tests {@link AnonymousAuthenticationProvider}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class AnonymousAuthenticationProviderTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public AnonymousAuthenticationProviderTests() {
+        super();
+    }
+
+    public AnonymousAuthenticationProviderTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(AnonymousAuthenticationProviderTests.class);
+    }
+
+    public void testDetectsAnInvalidKey() throws Exception {
+        AnonymousAuthenticationProvider aap = new AnonymousAuthenticationProvider();
+        aap.setKey("qwerty");
+
+        AnonymousAuthenticationToken token = new AnonymousAuthenticationToken("WRONG_KEY",
+                "Test",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+
+        try {
+            Authentication result = aap.authenticate(token);
+            fail("Should have thrown BadCredentialsException");
+        } catch (BadCredentialsException expected) {
+            assertEquals("The presented AnonymousAuthenticationToken does not contain the expected key",
+                expected.getMessage());
+        }
+    }
+
+    public void testDetectsMissingKey() throws Exception {
+        AnonymousAuthenticationProvider aap = new AnonymousAuthenticationProvider();
+
+        try {
+            aap.afterPropertiesSet();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testGettersSetters() throws Exception {
+        AnonymousAuthenticationProvider aap = new AnonymousAuthenticationProvider();
+        aap.setKey("qwerty");
+        aap.afterPropertiesSet();
+        assertEquals("qwerty", aap.getKey());
+    }
+
+    public void testIgnoresClassesItDoesNotSupport() throws Exception {
+        AnonymousAuthenticationProvider aap = new AnonymousAuthenticationProvider();
+        aap.setKey("qwerty");
+
+        TestingAuthenticationToken token = new TestingAuthenticationToken("user",
+                "password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_A")});
+        assertFalse(aap.supports(TestingAuthenticationToken.class));
+
+        // Try it anyway
+        assertNull(aap.authenticate(token));
+    }
+
+    public void testNormalOperation() throws Exception {
+        AnonymousAuthenticationProvider aap = new AnonymousAuthenticationProvider();
+        aap.setKey("qwerty");
+
+        AnonymousAuthenticationToken token = new AnonymousAuthenticationToken("qwerty",
+                "Test",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+
+        Authentication result = aap.authenticate(token);
+
+        assertEquals(result, token);
+    }
+
+    public void testSupports() {
+        AnonymousAuthenticationProvider aap = new AnonymousAuthenticationProvider();
+        assertTrue(aap.supports(AnonymousAuthenticationToken.class));
+        assertFalse(aap.supports(TestingAuthenticationToken.class));
+    }
+}

+ 190 - 0
core/src/test/java/org/acegisecurity/providers/anonymous/AnonymousAuthenticationTokenTests.java

@@ -0,0 +1,190 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity.providers.anonymous;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.GrantedAuthority;
+import net.sf.acegisecurity.GrantedAuthorityImpl;
+import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken;
+
+import java.util.List;
+import java.util.Vector;
+
+
+/**
+ * Tests {@link AnonymousAuthenticationToken}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class AnonymousAuthenticationTokenTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public AnonymousAuthenticationTokenTests() {
+        super();
+    }
+
+    public AnonymousAuthenticationTokenTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public final void setUp() throws Exception {
+        super.setUp();
+    }
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(AnonymousAuthenticationTokenTests.class);
+    }
+
+    public void testConstructorRejectsNulls() {
+        try {
+            new AnonymousAuthenticationToken(null, "Test",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+
+        try {
+            new AnonymousAuthenticationToken("key", null,
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+
+        try {
+            new AnonymousAuthenticationToken("key", "Test", null);
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+
+        try {
+            new AnonymousAuthenticationToken("key", "Test",
+                new GrantedAuthority[] {null});
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+
+        try {
+            new AnonymousAuthenticationToken("key", "Test",
+                new GrantedAuthority[] {});
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testEqualsWhenEqual() {
+        List proxyList1 = new Vector();
+        proxyList1.add("https://localhost/newPortal/j_acegi_cas_security_check");
+
+        AnonymousAuthenticationToken token1 = new AnonymousAuthenticationToken("key",
+                "Test",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+
+        AnonymousAuthenticationToken token2 = new AnonymousAuthenticationToken("key",
+                "Test",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+
+        assertEquals(token1, token2);
+    }
+
+    public void testGetters() {
+        AnonymousAuthenticationToken token = new AnonymousAuthenticationToken("key",
+                "Test",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+
+        assertEquals("key".hashCode(), token.getKeyHash());
+        assertEquals("Test", token.getPrincipal());
+        assertEquals("", token.getCredentials());
+        assertEquals("ROLE_ONE", token.getAuthorities()[0].getAuthority());
+        assertEquals("ROLE_TWO", token.getAuthorities()[1].getAuthority());
+        assertTrue(token.isAuthenticated());
+    }
+
+    public void testNoArgConstructor() {
+        try {
+            new AnonymousAuthenticationToken();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testNotEqualsDueToAbstractParentEqualsCheck() {
+        AnonymousAuthenticationToken token1 = new AnonymousAuthenticationToken("key",
+                "Test",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+
+        AnonymousAuthenticationToken token2 = new AnonymousAuthenticationToken("key",
+                "DIFFERENT_PRINCIPAL",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+
+        assertFalse(token1.equals(token2));
+    }
+
+    public void testNotEqualsDueToDifferentAuthenticationClass() {
+        AnonymousAuthenticationToken token1 = new AnonymousAuthenticationToken("key",
+                "Test",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+
+        UsernamePasswordAuthenticationToken token2 = new UsernamePasswordAuthenticationToken("Test",
+                "Password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+        token2.setAuthenticated(true);
+
+        assertFalse(token1.equals(token2));
+    }
+
+    public void testNotEqualsDueToKey() {
+        AnonymousAuthenticationToken token1 = new AnonymousAuthenticationToken("key",
+                "Test",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+
+        AnonymousAuthenticationToken token2 = new AnonymousAuthenticationToken("DIFFERENT_KEY",
+                "Test",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+
+        assertFalse(token1.equals(token2));
+    }
+
+    public void testSetAuthenticatedIgnored() {
+        AnonymousAuthenticationToken token = new AnonymousAuthenticationToken("key",
+                "Test",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_ONE"), new GrantedAuthorityImpl(
+                        "ROLE_TWO")});
+        assertTrue(token.isAuthenticated());
+        token.setAuthenticated(false); // ignored
+        assertTrue(token.isAuthenticated());
+    }
+}

+ 200 - 0
core/src/test/java/org/acegisecurity/providers/anonymous/AnonymousProcessingFilterTests.java

@@ -0,0 +1,200 @@
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
+ *
+ * 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 net.sf.acegisecurity.providers.anonymous;
+
+import junit.framework.TestCase;
+
+import net.sf.acegisecurity.Authentication;
+import net.sf.acegisecurity.GrantedAuthority;
+import net.sf.acegisecurity.GrantedAuthorityImpl;
+import net.sf.acegisecurity.MockFilterConfig;
+import net.sf.acegisecurity.MockHttpServletRequest;
+import net.sf.acegisecurity.MockHttpServletResponse;
+import net.sf.acegisecurity.context.ContextHolder;
+import net.sf.acegisecurity.context.security.SecureContext;
+import net.sf.acegisecurity.context.security.SecureContextImpl;
+import net.sf.acegisecurity.context.security.SecureContextUtils;
+import net.sf.acegisecurity.providers.TestingAuthenticationToken;
+import net.sf.acegisecurity.providers.dao.memory.UserAttribute;
+
+import java.io.IOException;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+
+/**
+ * Tests {@link AnonymousProcessingFilter}.
+ *
+ * @author Ben Alex
+ * @version $Id$
+ */
+public class AnonymousProcessingFilterTests extends TestCase {
+    //~ Constructors ===========================================================
+
+    public AnonymousProcessingFilterTests() {
+        super();
+    }
+
+    public AnonymousProcessingFilterTests(String arg0) {
+        super(arg0);
+    }
+
+    //~ Methods ================================================================
+
+    public static void main(String[] args) {
+        junit.textui.TestRunner.run(AnonymousProcessingFilterTests.class);
+    }
+
+    public void testDetectsMissingKey() throws Exception {
+        UserAttribute user = new UserAttribute();
+        user.setPassword("anonymousUsername");
+        user.addAuthority(new GrantedAuthorityImpl("ROLE_ANONYMOUS"));
+
+        AnonymousProcessingFilter filter = new AnonymousProcessingFilter();
+        filter.setUserAttribute(user);
+
+        try {
+            filter.afterPropertiesSet();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testDetectsUserAttribute() throws Exception {
+        AnonymousProcessingFilter filter = new AnonymousProcessingFilter();
+        filter.setKey("qwerty");
+
+        try {
+            filter.afterPropertiesSet();
+            fail("Should have thrown IllegalArgumentException");
+        } catch (IllegalArgumentException expected) {
+            assertTrue(true);
+        }
+    }
+
+    public void testGettersSetters() throws Exception {
+        UserAttribute user = new UserAttribute();
+        user.setPassword("anonymousUsername");
+        user.addAuthority(new GrantedAuthorityImpl("ROLE_ANONYMOUS"));
+
+        AnonymousProcessingFilter filter = new AnonymousProcessingFilter();
+        filter.setKey("qwerty");
+        filter.setUserAttribute(user);
+        filter.afterPropertiesSet();
+
+        assertEquals("qwerty", filter.getKey());
+        assertEquals(user, filter.getUserAttribute());
+    }
+
+    public void testOperationWhenAuthenticationExistsInContextHolder()
+        throws Exception {
+        // Put an Authentication object into the ContextHolder
+        SecureContext sc = SecureContextUtils.getSecureContext();
+        Authentication originalAuth = new TestingAuthenticationToken("user",
+                "password",
+                new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_A")});
+        sc.setAuthentication(originalAuth);
+        ContextHolder.setContext(sc);
+
+        // Setup our filter correctly
+        UserAttribute user = new UserAttribute();
+        user.setPassword("anonymousUsername");
+        user.addAuthority(new GrantedAuthorityImpl("ROLE_ANONYMOUS"));
+
+        AnonymousProcessingFilter filter = new AnonymousProcessingFilter();
+        filter.setKey("qwerty");
+        filter.setUserAttribute(user);
+        filter.afterPropertiesSet();
+
+        // Test
+        executeFilterInContainerSimulator(new MockFilterConfig(), filter,
+            new MockHttpServletRequest("x"), new MockHttpServletResponse(),
+            new MockFilterChain(true));
+
+        // Ensure filter didn't change our original object
+        assertEquals(originalAuth,
+            SecureContextUtils.getSecureContext().getAuthentication());
+    }
+
+    public void testOperationWhenNoAuthenticationInContextHolder()
+        throws Exception {
+        UserAttribute user = new UserAttribute();
+        user.setPassword("anonymousUsername");
+        user.addAuthority(new GrantedAuthorityImpl("ROLE_ANONYMOUS"));
+
+        AnonymousProcessingFilter filter = new AnonymousProcessingFilter();
+        filter.setKey("qwerty");
+        filter.setUserAttribute(user);
+        filter.afterPropertiesSet();
+
+        executeFilterInContainerSimulator(new MockFilterConfig(), filter,
+            new MockHttpServletRequest("x"), new MockHttpServletResponse(),
+            new MockFilterChain(true));
+
+        Authentication auth = SecureContextUtils.getSecureContext()
+                                                .getAuthentication();
+        assertEquals("anonymousUsername", auth.getPrincipal());
+        assertEquals(new GrantedAuthorityImpl("ROLE_ANONYMOUS"),
+            auth.getAuthorities()[0]);
+    }
+
+    protected void setUp() throws Exception {
+        super.setUp();
+        ContextHolder.setContext(new SecureContextImpl());
+    }
+
+    protected void tearDown() throws Exception {
+        super.tearDown();
+        ContextHolder.setContext(null);
+    }
+
+    private void executeFilterInContainerSimulator(FilterConfig filterConfig,
+        Filter filter, ServletRequest request, ServletResponse response,
+        FilterChain filterChain) throws ServletException, IOException {
+        filter.init(filterConfig);
+        filter.doFilter(request, response, filterChain);
+        filter.destroy();
+    }
+
+    //~ Inner Classes ==========================================================
+
+    private class MockFilterChain implements FilterChain {
+        private boolean expectToProceed;
+
+        public MockFilterChain(boolean expectToProceed) {
+            this.expectToProceed = expectToProceed;
+        }
+
+        private MockFilterChain() {
+            super();
+        }
+
+        public void doFilter(ServletRequest request, ServletResponse response)
+            throws IOException, ServletException {
+            if (expectToProceed) {
+                assertTrue(true);
+            } else {
+                fail("Did not expect filter chain to proceed");
+            }
+        }
+    }
+}

+ 10 - 19
core/src/test/java/org/acegisecurity/providers/dao/memory/UserAttributeEditorTests.java

@@ -1,4 +1,4 @@
-/* Copyright 2004 Acegi Technology Pty Limited
+/* Copyright 2004, 2005 Acegi Technology Pty Limited
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -19,8 +19,7 @@ import junit.framework.TestCase;
 
 
 /**
- * Tests {@link UserAttributeEditor} and associated {@link
- * UserAttributeDefinition}.
+ * Tests {@link UserAttributeEditor} and associated {@link UserAttribute}.
  *
  * @author Ben Alex
  * @version $Id$
@@ -50,8 +49,7 @@ public class UserAttributeEditorTests extends TestCase {
         UserAttributeEditor editor = new UserAttributeEditor();
         editor.setAsText("password,ROLE_ONE,ROLE_TWO");
 
-        UserAttributeDefinition user = (UserAttributeDefinition) editor
-            .getValue();
+        UserAttribute user = (UserAttribute) editor.getValue();
         assertTrue(user.isValid());
         assertTrue(user.isEnabled()); // default
         assertEquals("password", user.getPassword());
@@ -64,8 +62,7 @@ public class UserAttributeEditorTests extends TestCase {
         UserAttributeEditor editor = new UserAttributeEditor();
         editor.setAsText("password,disabled,ROLE_ONE,ROLE_TWO");
 
-        UserAttributeDefinition user = (UserAttributeDefinition) editor
-            .getValue();
+        UserAttribute user = (UserAttribute) editor.getValue();
         assertTrue(user.isValid());
         assertTrue(!user.isEnabled());
         assertEquals("password", user.getPassword());
@@ -78,8 +75,7 @@ public class UserAttributeEditorTests extends TestCase {
         UserAttributeEditor editor = new UserAttributeEditor();
         editor.setAsText("");
 
-        UserAttributeDefinition user = (UserAttributeDefinition) editor
-            .getValue();
+        UserAttribute user = (UserAttribute) editor.getValue();
         assertTrue(user == null);
     }
 
@@ -87,8 +83,7 @@ public class UserAttributeEditorTests extends TestCase {
         UserAttributeEditor editor = new UserAttributeEditor();
         editor.setAsText("password,ROLE_ONE,enabled,ROLE_TWO");
 
-        UserAttributeDefinition user = (UserAttributeDefinition) editor
-            .getValue();
+        UserAttribute user = (UserAttribute) editor.getValue();
         assertTrue(user.isValid());
         assertTrue(user.isEnabled());
         assertEquals("password", user.getPassword());
@@ -101,8 +96,7 @@ public class UserAttributeEditorTests extends TestCase {
         UserAttributeEditor editor = new UserAttributeEditor();
         editor.setAsText("MALFORMED_STRING");
 
-        UserAttributeDefinition user = (UserAttributeDefinition) editor
-            .getValue();
+        UserAttribute user = (UserAttribute) editor.getValue();
         assertTrue(user == null);
     }
 
@@ -110,8 +104,7 @@ public class UserAttributeEditorTests extends TestCase {
         UserAttributeEditor editor = new UserAttributeEditor();
         editor.setAsText("disabled");
 
-        UserAttributeDefinition user = (UserAttributeDefinition) editor
-            .getValue();
+        UserAttribute user = (UserAttribute) editor.getValue();
         assertTrue(user == null);
     }
 
@@ -119,8 +112,7 @@ public class UserAttributeEditorTests extends TestCase {
         UserAttributeEditor editor = new UserAttributeEditor();
         editor.setAsText("password,enabled");
 
-        UserAttributeDefinition user = (UserAttributeDefinition) editor
-            .getValue();
+        UserAttribute user = (UserAttribute) editor.getValue();
         assertTrue(user == null);
     }
 
@@ -128,8 +120,7 @@ public class UserAttributeEditorTests extends TestCase {
         UserAttributeEditor editor = new UserAttributeEditor();
         editor.setAsText(null);
 
-        UserAttributeDefinition user = (UserAttributeDefinition) editor
-            .getValue();
+        UserAttribute user = (UserAttribute) editor.getValue();
         assertTrue(user == null);
     }
 }

+ 99 - 3
doc/docbook/acegi.xml

@@ -2485,7 +2485,7 @@ public boolean supports(Class clazz);</programlisting></para>
         <para><programlisting>base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))
 
 expirationTime:   The date and time when the nonce expires, expressed in milliseconds
-key:              A private key to prevent modification of the nounce token
+key:              A private key to prevent modification of the nonce token
 </programlisting></para>
 
         <para>The <literal>DigestProcessingFilterEntryPoint</literal> has a
@@ -2548,7 +2548,7 @@ key:              A private key to prevent modification of the nounce token
         <para>The configured <literal>AuthenticationDao</literal> is needed
         because <literal>DigestProcessingFilter</literal> must have direct
         access to the clear text password of a user. Digest Authentication
-        will NOT work if you are using encoded passwords ni your DAO. The DAO
+        will NOT work if you are using encoded passwords in your DAO. The DAO
         collaborator, along with the <literal>UserCache</literal>, are
         typically shared directly with a
         <literal>DaoAuthenticationProvider</literal>. The
@@ -2556,7 +2556,7 @@ key:              A private key to prevent modification of the nounce token
         <literal>DigestProcessingFilterEntryPoint</literal>, so that
         <literal>DigestProcessingFilter</literal> can obtain the correct
         <literal>realmName</literal> and <literal>key</literal> for digest
-        calculations. </para>
+        calculations.</para>
 
         <para>Like <literal>BasicAuthenticationFilter</literal>, if
         authentication is successful an <literal>Authentication</literal>
@@ -2580,6 +2580,95 @@ key:              A private key to prevent modification of the nounce token
         does comply with the minimum standards of this RFC.</para>
       </sect2>
 
+      <sect2 id="security-ui-anonymous">
+        <title>Anonymous Authentication</title>
+
+        <para>Particularly in the case of web request URI security, sometimes
+        it is more convenient to assign configuration attributes against every
+        possible secure object invocation. Put differently, sometimes it is
+        nice to say <literal>ROLE_SOMETHING</literal> is required by default
+        and only allow certain exceptions to this rule, such as for login,
+        logout and home pages of an application. There are also other
+        situations where anonymous authentication would be desired, such as
+        when an auditing interceptor queries the
+        <literal>ContextHolder</literal> to identify which principal was
+        responsible for a given operation. Such classes can be authored with
+        more robustness if they know the <literal>ContextHolder</literal>
+        always contains an <literal>Authentication</literal> object, and never
+        <literal>null</literal>.</para>
+
+        <para>Acegi Security provides three classes that together provide an
+        anoymous authentication feature.
+        <literal>AnonymousAuthenticationToken</literal> is an implementation
+        of <literal>Authentication</literal>, and stores the
+        <literal>GrantedAuthority</literal>[]s which apply to the anonymous
+        principal. There is a corresponding
+        <literal>AnonymousAuthenticationProvider</literal>, which is chained
+        into the <literal>ProviderManager</literal> so that
+        <literal>AnonymousAuthenticationTokens</literal> are accepted.
+        Finally, there is an AnonymousProcessingFilter, which is chained after
+        the normal authentication mechanisms and automatically add an
+        <literal>AnonymousAuthenticationToken</literal> to the
+        <literal>ContextHolder</literal> if there is no existing
+        <literal>Authentication</literal> held there. The definition of the
+        filter and authentication provider appears as follows:</para>
+
+        <para><programlisting>&lt;bean id="anonymousProcessingFilter" class="net.sf.acegisecurity.providers.anonymous.AnonymousProcessingFilter"&gt;
+  &lt;property name="key"&gt;&lt;value&gt;foobar&lt;/value&gt;&lt;/property&gt;
+  &lt;property name="userAttribute"&gt;&lt;value&gt;anonymousUser,ROLE_ANONYMOUS&lt;/value&gt;&lt;/property&gt;
+&lt;/bean&gt;
+
+&lt;bean id="anonymousAuthenticationProvider" class="net.sf.acegisecurity.providers.anonymous.AnonymousAuthenticationProvider"&gt;
+  &lt;property name="key"&gt;&lt;value&gt;foobar&lt;/value&gt;&lt;/property&gt;
+&lt;/bean&gt;</programlisting></para>
+
+        <para>The <literal>key</literal> is shared between the filter and
+        authentication provider, so that tokens created by the former are
+        accepted by the latter. The <literal>userAttribute</literal> is
+        expressed in the form of
+        <literal>usernameInTheAuthenticationToken,grantedAuthority[,grantedAuthority]</literal>.
+        This is the same syntax as used after the equals sign for
+        <literal>InMemoryDaoImpl</literal>'s <literal>userMap</literal>
+        property.</para>
+
+        <para>As explained earlier, the benefit of anonymous authentication is
+        that all URI patterns can have security applied to them. For
+        example:</para>
+
+        <para><programlisting>&lt;bean id="filterInvocationInterceptor" class="net.sf.acegisecurity.intercept.web.FilterSecurityInterceptor"&gt;
+  &lt;property name="authenticationManager"&gt;&lt;ref bean="authenticationManager"/&gt;&lt;/property&gt;
+  &lt;property name="accessDecisionManager"&gt;&lt;ref local="httpRequestAccessDecisionManager"/&gt;&lt;/property&gt;
+  &lt;property name="objectDefinitionSource"&gt;
+    &lt;value&gt;
+      CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
+      PATTERN_TYPE_APACHE_ANT
+      /index.jsp=ROLE_ANONYMOUS,ROLE_USER
+      /hello.htm=ROLE_ANONYMOUS,ROLE_USER
+      /logoff.jsp=ROLE_ANONYMOUS,ROLE_USER
+      /acegilogin.jsp=ROLE_ANONYMOUS,ROLE_USER
+      /**=ROLE_USER
+    &lt;/value&gt;
+  &lt;/property&gt;
+&lt;/bean&gt;</programlisting>Rounding out the anonymous authentication
+        discussion is the <literal>AuthenticationTrustResolver</literal>
+        interface, with its corresponding
+        <literal>AuthenticationTrustResolverImpl</literal> implementation.
+        This interface provides an
+        <literal>isAnonymous(Authentication)</literal> method, which allows
+        interested classes to take into account this special type of
+        authentication status. The
+        <literal>SecurityEnforcementFilter</literal> uses this interface in
+        processing <literal>AccessDeniedException</literal>s. If an
+        <literal>AccessDeniedException</literal> is thrown, and the
+        authentication is of an anonymous type, instead of throwing a 403
+        (forbidden) response, the filter will instead commence the
+        <literal>AuthenticationEntryPoint</literal> so the principal can
+        authenticate properly. This is a necessary distinction, otherwise
+        principals would always be deemed "authenticated" and never be given
+        an opportunity to login via form, basic, digest or some other normal
+        authentication mechanism.</para>
+      </sect2>
+
       <sect2 id="security-ui-well-known">
         <title>Well-Known Locations</title>
 
@@ -4393,6 +4482,13 @@ INSERT INTO acl_permission VALUES (null, 6, 'scott', 1);</programlisting></para>
             container</para>
           </listitem>
 
+          <listitem>
+            <para><literal>AnonymousProcessingFilter</literal>, so that if no
+            earlier authentication processing mechanism updated the
+            <literal>ContextHolder</literal>, an anonymous
+            <literal>Authentication</literal> object will be put there</para>
+          </listitem>
+
           <listitem>
             <para><literal>SecurityEnforcementFilter</literal>, to protect web
             URIs and catch any Acegi Security exceptions so that an

+ 17 - 26
samples/contacts/src/main/webapp/filter/WEB-INF/applicationContext-acegi-security.xml

@@ -21,7 +21,7 @@
          <value>
 		    CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
 		    PATTERN_TYPE_APACHE_ANT
-            /**=httpSessionContextIntegrationFilter,authenticationProcessingFilter,basicProcessingFilter,securityEnforcementFilter
+            /**=httpSessionContextIntegrationFilter,authenticationProcessingFilter,basicProcessingFilter,anonymousProcessingFilter,securityEnforcementFilter
          </value>
       </property>
     </bean>
@@ -32,6 +32,7 @@
       <property name="providers">
          <list>
             <ref local="daoAuthenticationProvider"/>
+            <ref local="anonymousAuthenticationProvider"/>
          </list>
       </property>
    </bean>
@@ -75,6 +76,15 @@
       <property name="realmName"><value>Contacts Realm</value></property>
    </bean>
 
+   <bean id="anonymousProcessingFilter" class="net.sf.acegisecurity.providers.anonymous.AnonymousProcessingFilter">
+      <property name="key"><value>foobar</value></property>
+      <property name="userAttribute"><value>anonymousUser,ROLE_ANONYMOUS</value></property>
+   </bean>
+
+   <bean id="anonymousAuthenticationProvider" class="net.sf.acegisecurity.providers.anonymous.AnonymousAuthenticationProvider">
+      <property name="key"><value>foobar</value></property>
+   </bean>
+
    <bean id="httpSessionContextIntegrationFilter" class="net.sf.acegisecurity.context.HttpSessionContextIntegrationFilter">
       <property name="context"><value>net.sf.acegisecurity.context.security.SecureContextImpl</value></property>
    </bean>
@@ -146,33 +156,14 @@
       <property name="objectDefinitionSource">
          <value>
 			    CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
-				\A/secure/super.*\Z=ROLE_WE_DONT_HAVE
-				\A/secure/.*\Z=ROLE_SUPERVISOR,ROLE_USER
+			    PATTERN_TYPE_APACHE_ANT
+			    /index.jsp=ROLE_ANONYMOUS,ROLE_USER
+			    /hello.htm=ROLE_ANONYMOUS,ROLE_USER
+			    /logoff.jsp=ROLE_ANONYMOUS,ROLE_USER
+			    /acegilogin.jsp=ROLE_ANONYMOUS,ROLE_USER
+				/**=ROLE_USER
          </value>
       </property>
    </bean>
 
-   <!-- BASIC Regular Expression Syntax (for beginners):
-
-        \A means the start of the string (ie the beginning of the URL)
-        \Z means the end of the string (ie the end of the URL)
-        .  means any single character
-        *  means null or any number of repetitions of the last expression (so .* means zero or more characters)
-
-        Some examples:
-
-        Expression:   \A/my/directory/.*\Z
-        Would match:    /my/directory/
-                        /my/directory/hello.html
-
-        Expression:   \A/.*\Z
-        Would match:    /hello.html
-                        /
-
-        Expression:   \A/.*/secret.html\Z
-        Would match:    /some/directory/secret.html
-                        /another/secret.html
-        Not match:      /anothersecret.html (missing required /)
-   -->
-
 </beans>