Friday, July 01, 2011

Java Generics and Covariance/Contravariance

Suppose you have Java classes Animal, Mammal, and Giraffe, with the obvious relationships,

class Animal {…}
class Mammal extends Animal {…}
class Giraffe extends Mammal {…}

A coworker asked me why, if he had a method that accepted a List<Mammal>, why couldn't he pass into it a List<Giraffe>? Surely if a method was okay with operating on Mammals, it should be okay to operate on Giraffes, right?

Method declaration:
void petMammals(final List<Mammal> mammalsToPet);
Usage:
final List<Giraffe> myGiraffes = Collections.singletonList(new Giraffe());
petMammals(myGiraffes); // Complier doesn't let work

The answer to this riddle is that Java is designed to work with all kinds of generic classes, not just those which are collection-like. For instance, Comparator is a generic class, but you can't take a Comparator<Giraffe> and pass it to a method requiring a Comparator<Mammal>, since it doesn't know how to compare all Mammals. But you probably could pass a Comparator<Animal> to a method requiring a Comparator<Mammal>, since if it can sort Animals, it can certainly sort Mammals too.

So, since List and Comparator and every other generic class have to be treated the same way by the Java compiler, and it doesn't know for which uses it makes sense to allow for a larger type or smaller type to be passed in, you have to tell it each time. This requires learning the "wildcard type" syntax. Really understanding this syntax is key to writing methods that actually work the way you intend with your class hierarchy. For instance, our prior example really ought to be (with change in bold):

Method declaration:
void petMammals(final List<? extends Mammal> mammalsToPet);
Usage:
final List<Giraffe> myGiraffes = Collections.singletonList(new Giraffe());
petMammals(myGiraffes); // Now works!

That is, if you tell Java that you're okay with getting a List of a subtype, then Java will allow a List of a subtype in there. Even more interestingly, suppose that sorting Mammals is a common operation in your application, then you might have a method like:

void sortMammals(final List<? extends Mammal> mammalsToSort, final Comparator<? super Mammal> howToSort);

Which is quite clear that you can sort a List<Giraffe> with a Comparator<Animal>. A good example of a use of these wildcard types that's built into Java is the Collections.binarySearch method, which is itself parameterized on a type T, and allows for Lists of T's subtypes and Comparators of T's supertypes.

So, when writing a method that's using generics, it helps to take a second to think about what type you're really trying to use, and making your method actually have that type can solve a lot of confusion later on. Usually, classes which are collection-like can accept subclasses, and classes which are comparator-like can accept superclasses, but there are more reasons that one might be writing or using a generic class.

For further reading:
  • For those who love reading dry specifications of the internals of programming languages, section 4.5.1 of the Java Language Specification describes the details of how wildcard parameters work.
  • I'm constantly referring to Angelika Langer's Java Generics FAQs when I have questions like these. It has a lot of practical information on using generics in Java, without assuming a Computer Science degree or a love for academic type theory.
  • Eric Lippert, a developer for Microsoft who works on the C# compiler, had a whole blog series on covariance and contravariance in C#, explaining some of their thoughts as they were thinking about add some of these features to C#. I don't even develop in C#, but I found his explanations about the tricky parts of type systems very useful and interesting.

Wednesday, May 16, 2007

Generic Daos without all the xml

I was real happy when http://www-128.ibm.com/developerworks/java/library/j-genericdao.html came out. It seemed ideal until a few days and 50 daos later I had spent far too much time repeating the same xml over and over again. Finally with a little help from some new classes in spring 2.1 we can get rid of all the repetitous xml. First we need to scan for all the applicable dao interfaces. ClassPathScanningCandidateComponentProvider would be perfect except it specfically disallows returning interfaces. So the the first step was to create InterfaceComponentProvider which is essentially a copy of the original class except it will match and return interfaces.

WIthin the spring core there are TypeFilters that match on all sorts of things. The best choice seemed to be AbstractTypeHierarchyTraversingFilter. DaoFilter extends this and looks for any class which extends/implements the GenericDao interface.


import genericdao.GenericDao;

import org.springframework.core.typefilter.AbstractTypeHierarchyTraversingFilter;

/**
* TypeFilter used to match classes which implement the GenericDao interface
*
* @author Dan Thiffault
*
*/
public class DaoFilter extends AbstractTypeHierarchyTraversingFilter {
private Class daoInterfaceClass = GenericDao.class;

public DaoFilter() {
super(true, true);
}

@Override
protected boolean matchInterfaceName(String arg0) {
return daoInterfaceClass.getName().equals(arg0);
}

public Class getDaoInterfaceClass() {
return daoInterfaceClass;
}

public void setDaoInterfaceClass(Class daoInterfaceClass) {
this.daoInterfaceClass = daoInterfaceClass;
}

}


Now that we can discover all the appropriate beans we need a way to register them. First step is to extract the runtime type info from the discover beans.



public void scan() {
DaoFilter filter = new DaoFilter();
componentProvider.addIncludeFilter(filter);

Set candidateClasses = componentProvider.findCandidateComponents();

for(Class candidateDao : candidateClasses) {
for(Type interfaceType : candidateDao.getGenericInterfaces()) {
if(interfaceType instanceof ParameterizedType) {
ParameterizedType pInterfaceType = (ParameterizedType) interfaceType;
if(filter.getDaoInterfaceClass().equals(pInterfaceType.getRawType())) {
Type genericType = pInterfaceType.getActualTypeArguments()[0];
Assert.isInstanceOf(Class.class, genericType);
registerDao(candidateDao, (Class) genericType);
}
}
}
}

}


You may wonder what that registerDao method does. It creates the two required beans from the original Don't repeat the Dao article. Here's that method:


protected void registerDao(Class daoInterface, Class entityClass) {
//Create the target child bean
ConstructorArgumentValues cav = new ConstructorArgumentValues();
cav.addGenericArgumentValue(entityClass);
BeanDefinition targetBeanDefinition = new ChildBeanDefinition("abstractDaoTarget", cav, null);

//Create the proxy child bean
MutablePropertyValues propertyValues = new MutablePropertyValues();
propertyValues.addPropertyValue("proxyInterfaces", daoInterface);
propertyValues.addPropertyValue("target", targetBeanDefinition);
BeanDefinition proxyBeanDefinition = new ChildBeanDefinition("abstractDao", propertyValues);

registry.registerBeanDefinition(daoInterface.getSimpleName(), proxyBeanDefinition);
}


For the last step our new class implements BeanFactoryPostProcessor so it automatically registers beans at startup. Now we just need one line of xml per application instead of many per dao. Get the full code from here:

http://www.gspiral.com/autoDao.tgz

Thursday, December 14, 2006

Acegi/Spring Security with X509 and LDAP Authentication

We have chosen Acegi security for our security interface for our web applications. We have decided to use X509 certificates to obtain user’s credential and roles. If the certificate is not available or expired, we then show the login page having the user enter the user name and password. To accomplish this we used a special XML configuration and had to create 3 new classes.

EhCacheX509LdapUserCache.java (source)
This class is used to search the cache for the user. We are using two different authenticator providers and because each provider implements the cache the differently, this class is used to provide a bridge between the two concepts by delegating methods to the underlying store.

SecurityContextHolderAwareRequestWrapperWrapper.java (source)
This class wraps SecurityContextHolderAwareRequestWrapper because the SecurityContextHolderAwareRequestFilter api requires both request and port resolver, however, Acegi’s SecurityContextHolderAwareRequestWrapper only supports request. Hopefully Acegi will fix this problem in the future, removing the need for this class.

X509LdapAuthoritiesPopulator.java (source)
We are using 2 authorities populators so our class implements the X509 interface and delegates to the LDAP authority populator. We expect to obtain the userid from the certificate or login page while we obtain the roles from LDAP. One note of interest is that we use Perl to simplify the use of regular expressions because of the need to search the CN string for the user name.

How they all tie together
We have enabled these all by creating a Spring configuration that utilizes them. In addition, we needed a login controller to redirect the user to their desired URI upon successful authentication (only necessary for form-based login). The last step is to make sure that your web.xml uses the correct servlet mappings, the following is how we organized ours:

<servlet-mapping>
<servlet-name>Phoenix</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>Phoenix</servlet-name>
<url-pattern>/login_error</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>Phoenix</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>


Source


EhCacheX509LdapUserCache.java (top)
package com.checkernet.security;

import java.security.cert.X509Certificate;
import net.sf.ehcache.Cache;
import net.sf.ehcache.CacheException;
import net.sf.ehcache.Element;
import org.acegisecurity.providers.dao.UserCache;
import org.acegisecurity.providers.x509.X509UserCache;
import org.acegisecurity.userdetails.UserDetails;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.oro.text.regex.MalformedPatternException;
import org.apache.oro.text.regex.MatchResult;
import org.apache.oro.text.regex.Pattern;
import org.apache.oro.text.regex.PatternMatcher;
import org.apache.oro.text.regex.Perl5Compiler;
import org.apache.oro.text.regex.Perl5Matcher;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.util.Assert;

/**
 * Notes:<br/>
 * Implements {@link org.acegisecurity.providers.x509.X509UserCache}
 * and {@link org.acegisecurity.providers.dao.UserCache} <br />
 * Used to provide a bridge between the two concepts by delegating methods to the underlying store
 *
 @author JoeS@checkernet.com
 @version 1.0.0
 */
@SuppressWarnings({"OverloadedMethodsWithSameNumberOfParameters"})
public class EhCacheX509LdapUserCache implements UserCache, X509UserCache, InitializingBean {

    private final Log logger = LogFactory.getLog(EhCacheX509LdapUserCache.class);

    private Cache cache;

    private Pattern subjectDNPattern;

    private String subjectDNRegex = "CN=(.*?),";

    public void setCache(final Cache cache) {
        this.cache = cache;
    }

    /**
     * Obtains a {@link UserDetails} from the cache.
     *
     @param username the {@link org.acegisecurity.userdetails.User#getUsername()} used to place the user in
     *                 the cache
     @return the populated <code>UserDetails</code> or <code>null</code> if
     *         the user could not be found or if the cache entry has expired
     */
    public UserDetails getUserFromCache(final String username) {
        final Element element;

        try {
            element = cache.get(username);
        catch (CacheException cacheException) {
            //noinspection ThrowInsideCatchBlockWhichIgnoresCaughtException
            throw new DataRetrievalFailureException("Cache failure: " + cacheException.getMessage());
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Cache hit for userName: " + username);
        }

        if (element == null) {
            //noinspection ReturnOfNull
            return null;
        else {
            return (UserDetailselement.getValue();
        }
    }

    /**
     * Obtains a {@link UserDetails} from the cache.
     *
     @param userCert the {@link X509Certificate} used to place the user in
     *                 the cache
     @return the populated <code>UserDetails</code> or <code>null</code> if
     *         the user could not be found or if the cache entry has expired
     */
    public UserDetails getUserFromCache(final X509Certificate userCert) {
        if (userCert != null && userCert.getSubjectDN() != null) {
            return getUserFromCache(extractUsernameFromCert(userCert));
        else if (logger.isDebugEnabled()) {
            logger.debug("Certificate or user is null");
        }

        //noinspection ReturnOfNull
        return null;
    }

    /**
     * Places a {@link UserDetails} in the cache. The <code>username</code> is
     * the key used to subsequently retrieve the <code>UserDetails</code>.
     *
     @param user the fully populated <code>UserDetails</code> to place in the
     *             cache
     */
    public void putUserInCache(final UserDetails user) {
        Element element = new Element(user.getUsername(), user);

        if (logger.isDebugEnabled()) {
            logger.debug("Cache put: " + user.getUsername());
        }

        cache.put(element);
    }

    /**
     * delegates to #putUserInCache(final UserDetails user)
     *
     @param userCert user's X509Certificate
     @param user     the fully populated <code>UserDetails</code> to place in the
     *                 cache
     */
    public void putUserInCache(final X509Certificate userCert, final UserDetails user) {
        putUserInCache(user);
    }

    /**
     * Removes the specified user from the cache. The <code>username</code> is
     * the key used to remove the user. If the user is not found, the method
     * should simply return (not thrown an exception).
     <p></p>
     <p>Some cache implementations may not support eviction from the cache,  in
     * which case they should provide appropriate behaviour to alter the user
     * in either its documentation, via an exception, or through a log
     * message.</p>
     *
     @param username to be evicted from the cache
     */
    public void removeUserFromCache(final String username) {
        if (logger.isDebugEnabled()) {
            logger.debug("Cache remove: " + username);
        }

        cache.remove(username);
    }

    /**
     * delegates to #removeUserFromCache(final String username)
     *
     @param userCert user's X509Certificate
     */
    public void removeUserFromCache(final X509Certificate userCert) {
        removeUserFromCache(extractUsernameFromCert(userCert));
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings({"ProhibitedExceptionDeclared"})
    public void afterPropertiesSet() throws Exception {
        Assert.notNull(cache, "cache is mandatory");

        final Perl5Compiler compiler = new Perl5Compiler();

        //noinspection UnusedCatchParameter
        try {
            subjectDNPattern = compiler.compile(subjectDNRegex,
                                                Perl5Compiler.READ_ONLY_MASK
                                                | Perl5Compiler.CASE_INSENSITIVE_MASK);
        catch (MalformedPatternException mpe) {
            //noinspection ThrowInsideCatchBlockWhichIgnoresCaughtException
            throw new IllegalArgumentException("Malformed regular expression: "
                                               + subjectDNRegex);
        }
    }

    /**
     * extracts a username from a certificate's subjectDN
     *
     @param userCert X509Certificate
     @return String username
     */
    private String extractUsernameFromCert(final X509Certificate userCert) {
        final PatternMatcher matcher = new Perl5Matcher();

        if (userCert == null || userCert.getSubjectDN() == null
            || !matcher.contains(userCert.getSubjectDN().toString(), subjectDNPattern)) {
            //noinspection ReturnOfNull
            return null;
        }

        final MatchResult match = matcher.getMatch();

        if (match.groups() != 2) { // 2 = 1 + the entire match
            throw new IllegalArgumentException(
                    "Regular expression must contain a single group ");
        }

        return match.group(1);
    }
}


SecurityContextHolderAwareRequestWrapperWrapper.java (top)
package com.checkernet.security;

import javax.servlet.http.HttpServletRequest;
import org.acegisecurity.util.PortResolver;
import org.acegisecurity.wrapper.SecurityContextHolderAwareRequestWrapper;

/**
 * Notes:<br />
 * This class acts as a wrapper for org.acegisecurity.wrapper.SecurityContextHolderAwareRequestWrapper because it does
 * not conform to the SecurityContextHolderAwareRequestFilter api. This class may be removed when Acegi fixes this bug.
 *
 @author JoeS@checkernet.com
 @version 1.0.0
 */
@SuppressWarnings({"ClassNamePrefixedWithPackageName""ClassNameSameAsAncestorName"})
public class SecurityContextHolderAwareRequestWrapperWrapper
        extends SecurityContextHolderAwareRequestWrapper {

    /**
     * conforming to SecurityContextHolderAwareRequestFilter api
     *
     @param request      HttpServletRequest
     @param portResolver PortResolver (not currently used)
     */
    @SuppressWarnings({"UNUSED_SYMBOL"})
    public SecurityContextHolderAwareRequestWrapperWrapper(final HttpServletRequest request,
                                                           final PortResolver portResolver) {
        super(request);
    }
}


X509LdapAuthoritiesPopulator.java (top)
package com.checkernet.security;

import java.security.cert.X509Certificate;
import org.acegisecurity.BadCredentialsException;
import org.acegisecurity.ldap.LdapUserSearch;
import org.acegisecurity.providers.ldap.LdapAuthoritiesPopulator;
import org.acegisecurity.providers.x509.X509AuthoritiesPopulator;
import org.acegisecurity.userdetails.User;
import org.acegisecurity.userdetails.UserDetails;
import org.acegisecurity.userdetails.ldap.LdapUserDetails;
import org.apache.oro.text.regex.MalformedPatternException;
import org.apache.oro.text.regex.MatchResult;
import org.apache.oro.text.regex.Pattern;
import org.apache.oro.text.regex.PatternMatcher;
import org.apache.oro.text.regex.Perl5Compiler;
import org.apache.oro.text.regex.Perl5Matcher;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;

/**
 * Notes:<br/>
 * Implements the {@link X509AuthoritiesPopulator} while providing roles lookup from LDAP
 *
 @author JoeS@checkernet.com
 @version 1.0.0
 */
@SuppressWarnings({"ClassNamingConvention"})
public class X509LdapAuthoritiesPopulator implements X509AuthoritiesPopulator, InitializingBean {

    private LdapUserSearch userSearch;

    private LdapAuthoritiesPopulator authoritiesPopulator;

    private Pattern subjectDNPattern;

    private String subjectDNRegex = "CN=(.*?),";

    public void setAuthoritiesPopulator(final LdapAuthoritiesPopulator authoritiesPopulator) {
        this.authoritiesPopulator = authoritiesPopulator;
    }

    public void setUserSearch(final LdapUserSearch userSearch) {
        this.userSearch = userSearch;
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings({"ProhibitedExceptionDeclared"})
    public void afterPropertiesSet() throws Exception {
        Assert.notNull(userSearch, "A userSearch must be set");
        Assert.notNull(authoritiesPopulator, "An authoritiesPopulator must be set");

        final Perl5Compiler compiler = new Perl5Compiler();

        //noinspection UnusedCatchParameter
        try {
            subjectDNPattern = compiler.compile(subjectDNRegex,
                                                Perl5Compiler.READ_ONLY_MASK
                                                | Perl5Compiler.CASE_INSENSITIVE_MASK);
        catch (MalformedPatternException mpe) {
            //noinspection ThrowInsideCatchBlockWhichIgnoresCaughtException
            throw new IllegalArgumentException("Malformed regular expression: "
                                               + subjectDNRegex);
        }
    }

    /**
     * throws AuthenticationException
     * {@inheritDoc}
     */
    public UserDetails getUserDetails(final X509Certificate userCertificate) {
        final String subjectDN = userCertificate.getSubjectDN().getName();
        final PatternMatcher matcher = new Perl5Matcher();

        if (!matcher.contains(subjectDN, subjectDNPattern)) {
            throw new BadCredentialsException("No matching pattern was found in subjectDN: {0}");
            /*throw new BadCredentialsException(messages.getMessage(
                    "DaoX509AuthoritiesPopulator.noMatching",
                    new Object[] {subjectDN},
                    "No matching pattern was found in subjectDN: {0}"));*/
        }

        final MatchResult match = matcher.getMatch();

        if (match.groups() != 2) { // 2 = 1 + the entire match
            throw new IllegalArgumentException(
                    "Regular expression must contain a single group ");
        }

        final String userName = match.group(1);
        LdapUserDetails userDetails = userSearch.searchForUser(userName);
        return new User(userName, "[PROTECTED]", true, true, true, true,
                        authoritiesPopulator.getGrantedAuthorities(userDetails));
    }

}


LoginController.java (top)
package com.checkernet.controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.acegisecurity.Authentication;
import org.acegisecurity.context.SecurityContextHolder;
import org.acegisecurity.providers.anonymous.AnonymousAuthenticationToken;
import org.acegisecurity.ui.AbstractProcessingFilter;
import org.acegisecurity.ui.savedrequest.SavedRequest;
import org.acegisecurity.ui.webapp.AuthenticationProcessingFilter;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.util.Assert;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.view.RedirectView;

/**
 * Notes:<br />
 *
 @author JoeS@checkernet.com
 @version 1.0.0
 */
public class LoginController extends BaseController implements InitializingBean {

    /**
     * user-defined loginPage view name
     */
    protected String loginPage;

    public void setLoginPage(final String loginPage) {
        this.loginPage = loginPage;
    }

    /**
     * Login Page, redirects to saved request on authentication, otherwise goes to loginPage
     * {@inheritDoc}
     */
    @SuppressWarnings({"ProhibitedExceptionDeclared"})
    protected ModelAndView handleRequestInternal(final HttpServletRequest request, final HttpServletResponse response)
            throws Exception {
        if (logger.isDebugEnabled()) {
            logger.debug("login attempt: "
                         + request.getParameter(AuthenticationProcessingFilter.ACEGI_SECURITY_FORM_USERNAME_KEY));
        }

        final Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (auth != null && auth.isAuthenticated() && !(auth instanceof AnonymousAuthenticationToken)) {
            logger.debug("redirecting to first request");
            final Object savedRequest = request.getSession()
                    .getAttribute(AbstractProcessingFilter.ACEGI_SAVED_REQUEST_KEY);
            if (savedRequest instanceof SavedRequest) {
                return new ModelAndView(new RedirectView(((SavedRequestsavedRequest).getFullRequestUrl()));
            else {
                logger.warn(AbstractProcessingFilter.ACEGI_SAVED_REQUEST_KEY
                            " found in session not of correct type returning to login page");
            }
        }
        return new ModelAndView(loginPage);
    }

    /**
     * {@inheritDoc}
     */
    @SuppressWarnings({"ProhibitedExceptionDeclared"})
    public void afterPropertiesSet() throws Exception {
        Assert.notNull(loginPage, "loginPage is required");
    }
}


x509Ldap-servlet.xml (top)
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">

<!-- This file describes standard concrete beans for security use,
as well as provides abstract beans for bean defaults -->

<!-- Automatically receives AuthenticationEvent messages -->
<bean id="loggerListener" class="org.acegisecurity.event.authentication.LoggerListener" />

<!--ldap configuration-->
<bean id="initialDirContextFactory" class="org.acegisecurity.ldap.DefaultInitialDirContextFactory">
<constructor-arg value="ldap://checkernet.com:389" />
<property name="managerDn">
<value>cn=ReadOnlyUser,ou=webappgroup,dc=checkernet,dc=com</value>
</property>
<property name="managerPassword">
<value>bogusPassword</value>
</property>
<property name="extraEnvVars">
<map>
<entry key="java.naming.referral" value="follow" />
</map>
</property>
</bean>

<bean id="checkerboardLdapProvider" class="org.acegisecurity.providers.ldap.LdapAuthenticationProvider">
<constructor-arg>
<bean class="org.acegisecurity.providers.ldap.authenticator.BindAuthenticator">
<constructor-arg>
<ref local="initialDirContextFactory" />
</constructor-arg>
<property name="userSearch">
<bean class="org.acegisecurity.ldap.search.FilterBasedLdapUserSearch">
<constructor-arg>
<value>DC=checkernet,DC=com</value>
</constructor-arg>
<constructor-arg>
<value>(sAMAccountName={0})</value>
</constructor-arg>
<constructor-arg>
<ref local="initialDirContextFactory" />
</constructor-arg>
<property name="searchSubtree" value="true" />
</bean>
</property>
</bean>
</constructor-arg>
<constructor-arg ref="ldapAuthoritiesPopulator" />
<!--you supply this part:-->
<!--<property name="userCache" ref="userCache" />-->
</bean>

<bean id="ldapAuthoritiesPopulator"
class="org.acegisecurity.providers.ldap.populator.DefaultLdapAuthoritiesPopulator">
<constructor-arg>
<ref local="initialDirContextFactory" />
</constructor-arg>
<constructor-arg>
<value>dc=checkernet,dc=com</value>
</constructor-arg>
<property name="groupSearchFilter">
<value>(member={0})</value>
</property>
<property name="groupRoleAttribute">
<value>CN</value>
</property>
<property name="searchSubtree" value="true" />
</bean>

<!--this bean specifies the special case of binding an x509 cert to an Ldap entry
it requires a userCache to exist-->
<bean id="x509LdapAuthoritiesPopulator" class="com.checkernet.security.X509LdapAuthoritiesPopulator">
<property name="userSearch">
<bean class="org.acegisecurity.ldap.search.FilterBasedLdapUserSearch">
<constructor-arg>
<value>DC=checkernet,DC=com</value>
</constructor-arg>
<constructor-arg>
<value>(cn={0})</value>
</constructor-arg>
<constructor-arg>
<ref local="initialDirContextFactory" />
</constructor-arg>
<property name="searchSubtree" value="true" />
</bean>
</property>
<property name="authoritiesPopulator">
<ref local="ldapAuthoritiesPopulator" />
</property>
</bean>

<!-- An access decision voter that reads ROLE_* configuration settings -->
<bean id="roleVoter" class="org.acegisecurity.vote.RoleVoter" />

<!-- An access decision manager used by the business objects -->
<bean id="accessDecisionManager" class="org.acegisecurity.vote.AffirmativeBased">
<property name="allowIfAllAbstainDecisions" value="false" />
<property name="decisionVoters">
<list>
<ref local="roleVoter" />
</list>
</property>
</bean>

<!-- ================= METHOD INVOCATION AUTHORIZATION ==================== -->

<!--
NOTE:
users need to already be authenticated before invoking this security check
-->
<bean id="securityAttributes" class="org.acegisecurity.annotation.SecurityAnnotationAttributes" />

<!--AOP security config-->
<bean id="securityInterceptor" class="org.acegisecurity.intercept.method.aopalliance.MethodSecurityInterceptor">
<property name="validateConfigAttributes" value="true" />
<property name="authenticationManager" ref="authenticationManager" />
<property name="accessDecisionManager" ref="accessDecisionManager" />
<property name="objectDefinitionSource">
<bean class="org.acegisecurity.intercept.method.MethodDefinitionAttributes">
<property name="attributes" ref="securityAttributes" />
</bean>
</property>
</bean>

<!--AspectJ security config-->
<bean id="securityInterceptorAspectJ" class="org.acegisecurity.intercept.method.aspectj.AspectJSecurityInterceptor">
<property name="validateConfigAttributes" value="true" />
<property name="authenticationManager" ref="authenticationManager" />
<property name="accessDecisionManager" ref="accessDecisionManager" />
<property name="objectDefinitionSource">
<bean class="org.acegisecurity.intercept.method.MethodDefinitionAttributes">
<property name="attributes" ref="securityAttributes" />
</bean>
</property>
</bean>

<!-- ======================== FILTER CHAIN ======================= -->

<!-- If you wish to use channel security, add "channelProcessingFilter," in front
of "httpSessionContextIntegrationFilter" in the list below -->
<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
<property name="filterInvocationDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/**=httpSessionContextIntegrationFilter,logoutFilter,securityContextHolderAwareRequestFilter,x509ProcessingFilter,authenticationProcessingFilter,anonymousProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor
</value>
</property>
</bean>

<!-- ======================== AUTHENTICATION ======================= -->

<bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
<property name="providers">
<list>
<ref bean="x509AuthenticationProvider" />
<ref bean="ldapAuthenticationProvider" />
<ref bean="anonymousAuthenticationProvider" />
</list>
</property>
</bean>

<bean id="anonymousAuthenticationProvider"
class="org.acegisecurity.providers.anonymous.AnonymousAuthenticationProvider">
<property name="key" value="fakeAnonymousKey" />
<!--must match anonymousProcessingFilter key-->
</bean>

<bean id="x509AuthenticationProvider" class="org.acegisecurity.providers.x509.X509AuthenticationProvider">
<property name="x509AuthoritiesPopulator" ref="x509LdapAuthoritiesPopulator" />
<property name="x509UserCache" ref="userCache" />
</bean>

<bean id="ldapAuthenticationProvider" class="org.acegisecurity.providers.ldap.LdapAuthenticationProvider"
parent="checkerboardLdapProvider">
<property name="userCache" ref="userCache" />
</bean>

<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" />

<bean id="userCache" class="com.checkernet.security.EhCacheX509LdapUserCache">
<property name="cache">
<bean class="org.springframework.cache.ehcache.EhCacheFactoryBean">
<property name="cacheManager" ref="cacheManager" />
<property name="cacheName" value="x509Cache" />
</bean>
</property>
</bean>

<bean id="httpSessionContextIntegrationFilter"
class="org.acegisecurity.context.HttpSessionContextIntegrationFilter">
</bean>
<bean id="securityContextHolderAwareRequestFilter"
class="org.acegisecurity.wrapper.SecurityContextHolderAwareRequestFilter">
<property name="wrapperClass" value="com.checkernet.security.SecurityContextHolderAwareRequestWrapperWrapper" />
</bean>

<bean id="logoutFilter" class="org.acegisecurity.ui.logout.LogoutFilter">
<constructor-arg value="/" />
<constructor-arg>
<list>
<ref bean="securityContextLogoutHandler" />
<bean class="org.acegisecurity.ui.logout.SecurityContextLogoutHandler" />
</list>
</constructor-arg>
<property name="filterProcessesUrl" value="/app/logout" />
</bean>
<bean id="securityContextLogoutHandler"
class="org.acegisecurity.ui.logout.SecurityContextLogoutHandler">
</bean>


<!-- ===================== HTTP REQUEST SECURITY ==================== -->

<bean id="anonymousProcessingFilter" class="org.acegisecurity.providers.anonymous.AnonymousProcessingFilter">
<property name="key" value="fakeAnonymousKey" />
<!--must match anonymousAuthenticationProvider key-->
<property name="userAttribute" value="anonymousUser,ROLE_ANONYMOUS" />
</bean>

<bean id="exceptionTranslationFilter" class="org.acegisecurity.ui.ExceptionTranslationFilter">
<property name="authenticationEntryPoint">
<bean class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint">
<property name="loginFormUrl">
<value>/login</value>
</property>
<property name="forceHttps" value="true" />
</bean>
</property>
</bean>

<bean id="x509ProcessingFilter" class="org.acegisecurity.ui.x509.X509ProcessingFilter">
<property name="authenticationManager" ref="authenticationManager" />
</bean>

<bean id="authenticationProcessingFilter" class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter">
<property name="authenticationManager" ref="authenticationManager" />
<property name="authenticationFailureUrl">
<value>/login_error</value>
</property>
<property name="defaultTargetUrl">
<value>/</value>
</property>
<property name="filterProcessesUrl">
<value>/login/check</value>
</property>
</bean>

<bean id="httpRequestAccessDecisionManager" class="org.acegisecurity.vote.AffirmativeBased">
<property name="allowIfAllAbstainDecisions" value="false" />
<property name="decisionVoters">
<list>
<ref local="roleVoter" />
</list>
</property>
</bean>

<!-- Note the order that entries are placed against the objectDefinitionSource is critical.
The FilterSecurityInterceptor will work from the top of the list down to the FIRST pattern that matches the request URL.
Accordingly, you should place MOST SPECIFIC (ie a/b/c/d.*) expressions first, with LEAST SPECIFIC (ie a/.*) expressions last -->
<bean id="filterInvocationInterceptor" class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
<property name="authenticationManager" ref="authenticationManager" />
<property name="accessDecisionManager" ref="httpRequestAccessDecisionManager" />
<property name="objectDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
\A/login.*\Z=ROLE_ANONYMOUS,ROLE_PHOENIX
\A/app/SecuredPage.*\Z=ROLE_SUPERUSER
\A/app/AnotherSecuredPage.*\Z=ROLE_SUPERUSER
\A/.*\Z=ROLE_PHOENIX
</value>
</property>
</bean>
</beans>