From 693ac5a24ac6800323b4e8f1a9a3968d201c84a6 Mon Sep 17 00:00:00 2001 From: Ben Alex Date: Wed, 23 Feb 2005 06:09:56 +0000 Subject: [PATCH] Anonymous principal support. As requested by the community at various times, including in http://forum.springframework.org/viewtopic.php?t=1925. --- .../acegisecurity/AccessDecisionManager.java | 10 +- .../AuthenticationException.java | 15 +- .../AuthenticationTrustResolver.java | 66 ++++++ .../AuthenticationTrustResolverImpl.java | 77 +++++++ .../InsufficientAuthenticationException.java | 58 +++++ .../web/SecurityEnforcementFilter.java | 148 +++++++------ .../AnonymousAuthenticationProvider.java | 86 ++++++++ .../AnonymousAuthenticationToken.java | 133 ++++++++++++ .../anonymous/AnonymousProcessingFilter.java | 169 +++++++++++++++ .../providers/anonymous/package.html | 7 + ...buteDefinition.java => UserAttribute.java} | 6 +- .../memory/UserAttributeEditor.java | 8 +- .../userdetails/memory/UserMapEditor.java | 5 +- .../AuthenticationTrustResolverImplTests.java | 67 ++++++ .../web/SecurityEnforcementFilterTests.java | 61 +++++- .../AnonymousAuthenticationProviderTests.java | 122 +++++++++++ .../AnonymousAuthenticationTokenTests.java | 190 +++++++++++++++++ .../AnonymousProcessingFilterTests.java | 200 ++++++++++++++++++ .../dao/memory/UserAttributeEditorTests.java | 29 +-- doc/docbook/acegi.xml | 102 ++++++++- .../applicationContext-acegi-security.xml | 43 ++-- 21 files changed, 1467 insertions(+), 135 deletions(-) create mode 100644 core/src/main/java/org/acegisecurity/AuthenticationTrustResolver.java create mode 100644 core/src/main/java/org/acegisecurity/AuthenticationTrustResolverImpl.java create mode 100644 core/src/main/java/org/acegisecurity/InsufficientAuthenticationException.java create mode 100644 core/src/main/java/org/acegisecurity/providers/anonymous/AnonymousAuthenticationProvider.java create mode 100644 core/src/main/java/org/acegisecurity/providers/anonymous/AnonymousAuthenticationToken.java create mode 100644 core/src/main/java/org/acegisecurity/providers/anonymous/AnonymousProcessingFilter.java create mode 100644 core/src/main/java/org/acegisecurity/providers/anonymous/package.html rename core/src/main/java/org/acegisecurity/userdetails/memory/{UserAttributeDefinition.java => UserAttribute.java} (94%) create mode 100644 core/src/test/java/org/acegisecurity/AuthenticationTrustResolverImplTests.java create mode 100644 core/src/test/java/org/acegisecurity/providers/anonymous/AnonymousAuthenticationProviderTests.java create mode 100644 core/src/test/java/org/acegisecurity/providers/anonymous/AnonymousAuthenticationTokenTests.java create mode 100644 core/src/test/java/org/acegisecurity/providers/anonymous/AnonymousProcessingFilterTests.java diff --git a/core/src/main/java/org/acegisecurity/AccessDecisionManager.java b/core/src/main/java/org/acegisecurity/AccessDecisionManager.java index bb0abd873c..e53f60ade9 100644 --- a/core/src/main/java/org/acegisecurity/AccessDecisionManager.java +++ b/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 AccessDecisionManager is able to diff --git a/core/src/main/java/org/acegisecurity/AuthenticationException.java b/core/src/main/java/org/acegisecurity/AuthenticationException.java index 3476d5b94e..d82d053662 100644 --- a/core/src/main/java/org/acegisecurity/AuthenticationException.java +++ b/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; + } } diff --git a/core/src/main/java/org/acegisecurity/AuthenticationTrustResolver.java b/core/src/main/java/org/acegisecurity/AuthenticationTrustResolver.java new file mode 100644 index 0000000000..34f26efe23 --- /dev/null +++ b/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 Authentication tokens + * + * @author Ben Alex + * @version $Id$ + */ +public interface AuthenticationTrustResolver { + //~ Methods ================================================================ + + /** + * Indicates whether the passed Authentication token + * represents an anonymous user. Typically the framework will call this + * method if it is trying to decide whether an + * AccessDeniedException 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 Authentication was merely + * anonymous). + * + * @param authentication to test (may be null in which case + * the method will always return false) + * + * @return true the passed authentication token represented an + * anonymous principal, false otherwise + */ + public boolean isAnonymous(Authentication authentication); + + /** + * Indicates whether the passed Authentication token + * represents user that has been remembered (ie not a user that has been + * fully authenticated). + * + *

+ * No part of the framework uses this method, as it is a weak + * definition of trust levels. The method is provided simply to assist + * with custom AccessDecisionVoters 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. + *

+ * + * @param authentication to test (may be null in which case + * the method will always return false) + * + * @return true the passed authentication token represented a + * principal authenticated using a remember-me token, + * false otherwise + */ + public boolean isRememberMe(Authentication authentication); +} diff --git a/core/src/main/java/org/acegisecurity/AuthenticationTrustResolverImpl.java b/core/src/main/java/org/acegisecurity/AuthenticationTrustResolverImpl.java new file mode 100644 index 0000000000..c043e268aa --- /dev/null +++ b/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}. + * + *

+ * Makes trust decisions based on whether the passed + * Authentication is an instance of a defined class. + *

+ * + *

+ * If {@link #anonymousClass} or {@link #rememberMeClass} is null, + * the corresponding method will always return false. + *

+ * + * @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; + } +} diff --git a/core/src/main/java/org/acegisecurity/InsufficientAuthenticationException.java b/core/src/main/java/org/acegisecurity/InsufficientAuthenticationException.java new file mode 100644 index 0000000000..aac2abb6c7 --- /dev/null +++ b/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. + * + *

+ * {{@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 AuthenticationEntryPoint to be called, allowing the + * principal to authenticate with a stronger level of authentication. } + *

+ * + * @author Ben Alex + * @version $Id$ + */ +public class InsufficientAuthenticationException extends AuthenticationException { + //~ Constructors =========================================================== + + /** + * Constructs an InsufficientAuthenticationException with the + * specified message. + * + * @param msg the detail message + */ + public InsufficientAuthenticationException(String msg) { + super(msg); + } + + /** + * Constructs an InsufficientAuthenticationException 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); + } +} diff --git a/core/src/main/java/org/acegisecurity/intercept/web/SecurityEnforcementFilter.java b/core/src/main/java/org/acegisecurity/intercept/web/SecurityEnforcementFilter.java index de19a2b6d6..23d610fe78 100644 --- a/core/src/main/java/org/acegisecurity/intercept/web/SecurityEnforcementFilter.java +++ b/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; *

* *

- * If an {@link AccessDeniedException} is detected, the filter will respond - * with a HttpServletResponse.SC_FORBIDDEN (403 error). In - * addition, the AccessDeniedException itself will be placed in - * the HttpSession 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 authenticationEntryPoint will be launched. If they + * are not an anonymous user, the filter will respond with a + * HttpServletResponse.SC_FORBIDDEN (403 error). In addition, + * the AccessDeniedException itself will be placed in the + * HttpSession 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); + } - ((HttpServletRequest) request).getSession().setAttribute(ACEGI_SECURITY_ACCESS_DENIED_EXCEPTION_KEY, - accessDenied); - sendAccessDeniedError(request, response, 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); + } } 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); + } } diff --git a/core/src/main/java/org/acegisecurity/providers/anonymous/AnonymousAuthenticationProvider.java b/core/src/main/java/org/acegisecurity/providers/anonymous/AnonymousAuthenticationProvider.java new file mode 100644 index 0000000000..137f4d8507 --- /dev/null +++ b/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. + * + *

+ * To be successfully validated, the {@link + * net.sf.acegisecurity.providers.anonymous.AnonymousAuthenticationToken#getKeyHash()} + * must match this class' {@link #getKey()}. + *

+ * + * @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)); + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/anonymous/AnonymousAuthenticationToken.java b/core/src/main/java/org/acegisecurity/providers/anonymous/AnonymousAuthenticationToken.java new file mode 100644 index 0000000000..2f52920f58 --- /dev/null +++ b/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 Authentication. + * + * @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 UserDetails) + * @param authorities the authorities granted to the principal + * + * @throws IllegalArgumentException if a null 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 true). + * + * @param isAuthenticated ignored + */ + public void setAuthenticated(boolean isAuthenticated) { + // ignored + } + + /** + * Always returns true. + * + * @return true + */ + public boolean isAuthenticated() { + return true; + } + + public GrantedAuthority[] getAuthorities() { + return this.authorities; + } + + /** + * Always returns an empty String + * + * @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; + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/anonymous/AnonymousProcessingFilter.java b/core/src/main/java/org/acegisecurity/providers/anonymous/AnonymousProcessingFilter.java new file mode 100644 index 0000000000..6542dcebdb --- /dev/null +++ b/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 Authentication object in the + * ContextHolder, and populates it with one if needed. + * + *

+ * + *

+ * In summary, this filter is responsible for processing any request that has a + * HTTP request header of Authorization with an authentication + * scheme of Basic and a Base64-encoded + * username:password token. For example, to authenticate user + * "Aladdin" with password "open sesame" the following header would be + * presented: + *

+ * + *

+ * Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==. + *

+ * + *

+ * 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). + *

+ * + *

+ * If authentication is successful, the resulting {@link Authentication} object + * will be placed into the ContextHolder. + *

+ * + *

+ * 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. + *

+ * + *

+ * 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}. + *

+ * + *

+ * Do not use this class directly. Instead configure + * web.xml to use the {@link + * net.sf.acegisecurity.util.FilterToBeanProxy}. + *

+ * + * @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()); + } +} diff --git a/core/src/main/java/org/acegisecurity/providers/anonymous/package.html b/core/src/main/java/org/acegisecurity/providers/anonymous/package.html new file mode 100644 index 0000000000..6a9d20f197 --- /dev/null +++ b/core/src/main/java/org/acegisecurity/providers/anonymous/package.html @@ -0,0 +1,7 @@ + + +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. + + diff --git a/core/src/main/java/org/acegisecurity/userdetails/memory/UserAttributeDefinition.java b/core/src/main/java/org/acegisecurity/userdetails/memory/UserAttribute.java similarity index 94% rename from core/src/main/java/org/acegisecurity/userdetails/memory/UserAttributeDefinition.java rename to core/src/main/java/org/acegisecurity/userdetails/memory/UserAttribute.java index ec102aeb56..e42ec4be8f 100644 --- a/core/src/main/java/org/acegisecurity/userdetails/memory/UserAttributeDefinition.java +++ b/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(); } diff --git a/core/src/main/java/org/acegisecurity/userdetails/memory/UserAttributeEditor.java b/core/src/main/java/org/acegisecurity/userdetails/memory/UserAttributeEditor.java index 19fa96d0b0..b6db0615d8 100644 --- a/core/src/main/java/org/acegisecurity/userdetails/memory/UserAttributeEditor.java +++ b/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]; diff --git a/core/src/main/java/org/acegisecurity/userdetails/memory/UserMapEditor.java b/core/src/main/java/org/acegisecurity/userdetails/memory/UserMapEditor.java index cc402f9924..b7a97f7149 100644 --- a/core/src/main/java/org/acegisecurity/userdetails/memory/UserMapEditor.java +++ b/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) { diff --git a/core/src/test/java/org/acegisecurity/AuthenticationTrustResolverImplTests.java b/core/src/test/java/org/acegisecurity/AuthenticationTrustResolverImplTests.java new file mode 100644 index 0000000000..c1fc48c938 --- /dev/null +++ b/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()); + } +} diff --git a/core/src/test/java/org/acegisecurity/intercept/web/SecurityEnforcementFilterTests.java b/core/src/test/java/org/acegisecurity/intercept/web/SecurityEnforcementFilterTests.java index dd2a2e6539..61ac89da34 100644 --- a/core/src/test/java/org/acegisecurity/intercept/web/SecurityEnforcementFilterTests.java +++ b/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,8 +68,47 @@ 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); + + // 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(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, @@ -77,6 +122,11 @@ 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(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 { diff --git a/core/src/test/java/org/acegisecurity/providers/anonymous/AnonymousAuthenticationProviderTests.java b/core/src/test/java/org/acegisecurity/providers/anonymous/AnonymousAuthenticationProviderTests.java new file mode 100644 index 0000000000..4dd3efab71 --- /dev/null +++ b/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)); + } +} diff --git a/core/src/test/java/org/acegisecurity/providers/anonymous/AnonymousAuthenticationTokenTests.java b/core/src/test/java/org/acegisecurity/providers/anonymous/AnonymousAuthenticationTokenTests.java new file mode 100644 index 0000000000..a631756173 --- /dev/null +++ b/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()); + } +} diff --git a/core/src/test/java/org/acegisecurity/providers/anonymous/AnonymousProcessingFilterTests.java b/core/src/test/java/org/acegisecurity/providers/anonymous/AnonymousProcessingFilterTests.java new file mode 100644 index 0000000000..25234b89db --- /dev/null +++ b/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"); + } + } + } +} diff --git a/core/src/test/java/org/acegisecurity/providers/dao/memory/UserAttributeEditorTests.java b/core/src/test/java/org/acegisecurity/providers/dao/memory/UserAttributeEditorTests.java index ca01016c4a..aa5b1b5f11 100644 --- a/core/src/test/java/org/acegisecurity/providers/dao/memory/UserAttributeEditorTests.java +++ b/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); } } diff --git a/doc/docbook/acegi.xml b/doc/docbook/acegi.xml index 6b4d9522e8..9d214c11d3 100644 --- a/doc/docbook/acegi.xml +++ b/doc/docbook/acegi.xml @@ -2485,7 +2485,7 @@ public boolean supports(Class clazz); 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 The DigestProcessingFilterEntryPoint has a @@ -2548,7 +2548,7 @@ key: A private key to prevent modification of the nounce token The configured AuthenticationDao is needed because DigestProcessingFilter 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 UserCache, are typically shared directly with a DaoAuthenticationProvider. The @@ -2556,7 +2556,7 @@ key: A private key to prevent modification of the nounce token DigestProcessingFilterEntryPoint, so that DigestProcessingFilter can obtain the correct realmName and key for digest - calculations. + calculations. Like BasicAuthenticationFilter, if authentication is successful an Authentication @@ -2580,6 +2580,95 @@ key: A private key to prevent modification of the nounce token does comply with the minimum standards of this RFC. + + Anonymous Authentication + + 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 ROLE_SOMETHING 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 + ContextHolder to identify which principal was + responsible for a given operation. Such classes can be authored with + more robustness if they know the ContextHolder + always contains an Authentication object, and never + null. + + Acegi Security provides three classes that together provide an + anoymous authentication feature. + AnonymousAuthenticationToken is an implementation + of Authentication, and stores the + GrantedAuthority[]s which apply to the anonymous + principal. There is a corresponding + AnonymousAuthenticationProvider, which is chained + into the ProviderManager so that + AnonymousAuthenticationTokens are accepted. + Finally, there is an AnonymousProcessingFilter, which is chained after + the normal authentication mechanisms and automatically add an + AnonymousAuthenticationToken to the + ContextHolder if there is no existing + Authentication held there. The definition of the + filter and authentication provider appears as follows: + + <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> + + The key is shared between the filter and + authentication provider, so that tokens created by the former are + accepted by the latter. The userAttribute is + expressed in the form of + usernameInTheAuthenticationToken,grantedAuthority[,grantedAuthority]. + This is the same syntax as used after the equals sign for + InMemoryDaoImpl's userMap + property. + + As explained earlier, the benefit of anonymous authentication is + that all URI patterns can have security applied to them. For + example: + + <bean id="filterInvocationInterceptor" class="net.sf.acegisecurity.intercept.web.FilterSecurityInterceptor"> + <property name="authenticationManager"><ref bean="authenticationManager"/></property> + <property name="accessDecisionManager"><ref local="httpRequestAccessDecisionManager"/></property> + <property name="objectDefinitionSource"> + <value> + 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 + </value> + </property> +</bean>Rounding out the anonymous authentication + discussion is the AuthenticationTrustResolver + interface, with its corresponding + AuthenticationTrustResolverImpl implementation. + This interface provides an + isAnonymous(Authentication) method, which allows + interested classes to take into account this special type of + authentication status. The + SecurityEnforcementFilter uses this interface in + processing AccessDeniedExceptions. If an + AccessDeniedException is thrown, and the + authentication is of an anonymous type, instead of throwing a 403 + (forbidden) response, the filter will instead commence the + AuthenticationEntryPoint 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. + + Well-Known Locations @@ -4393,6 +4482,13 @@ INSERT INTO acl_permission VALUES (null, 6, 'scott', 1); container + + AnonymousProcessingFilter, so that if no + earlier authentication processing mechanism updated the + ContextHolder, an anonymous + Authentication object will be put there + + SecurityEnforcementFilter, to protect web URIs and catch any Acegi Security exceptions so that an diff --git a/samples/contacts/src/main/webapp/filter/WEB-INF/applicationContext-acegi-security.xml b/samples/contacts/src/main/webapp/filter/WEB-INF/applicationContext-acegi-security.xml index 95ca7effc9..3c523a3bcd 100644 --- a/samples/contacts/src/main/webapp/filter/WEB-INF/applicationContext-acegi-security.xml +++ b/samples/contacts/src/main/webapp/filter/WEB-INF/applicationContext-acegi-security.xml @@ -21,7 +21,7 @@ CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON PATTERN_TYPE_APACHE_ANT - /**=httpSessionContextIntegrationFilter,authenticationProcessingFilter,basicProcessingFilter,securityEnforcementFilter + /**=httpSessionContextIntegrationFilter,authenticationProcessingFilter,basicProcessingFilter,anonymousProcessingFilter,securityEnforcementFilter @@ -32,6 +32,7 @@ + @@ -75,6 +76,15 @@ Contacts Realm + + foobar + anonymousUser,ROLE_ANONYMOUS + + + + foobar + + net.sf.acegisecurity.context.security.SecureContextImpl @@ -146,33 +156,14 @@ 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 - -