So, should I take the silence to mean that no one is interested in this?

b

Ben Munat wrote:
Hello All,

First off, congratulations on the release of 0.90! I'll have to download it and migrate my stuff using 0.83.

Seeing the announcement reminded me that I've been meaning to write to offer the contribution of a few classes to Acegi Security. I wrote them a while back while working on integrating Acegi into my current project. Would have written this sooner, but I needed to clean up any references to my project code, amplify the comments, and write a new test (the tests I have use project-specific classes that need to be replaced with mocks). I started on this clean-up a while ago, but things got busy, I got distracted and kept meaning to get around to it. I finally decided today that I should probably see if you all are even interested in this stuff before I put more work into making it ready for public consumption.

Anyway, the contribution is basically just a new version of PasswordDaoAuthenticationProvider. While working on the authentication needs of my application, I was frustrated by two aspects of both DaoAuthenticationProvider and PasswordDaoAuthenticationProvider.

First, I wanted to be able to make slight changes to parts of the authenticate method, but I didn't want to subclass and override the entire method just to make these little changes. With this in mind, I created a TemplateAuthenticationProvider, which breaks the logic of the authenticate method out into a series of protected methods... some abstract, some with contcrete base implementations. Granted I have clearly gone ahead and rewritten the entire method to get the little change I needed but I have also done so in a way that any future providers I write (or others write if this is accepted) can benefit from the superclass algorithm, while tweaking the details of the various steps involved.

While working on breaking this logic up into steps, I was also frustrated that the existing tests of a user were hardcoded into the authenticate method. These test were always checked and it was awkward to add any additional test. Now, breaking up the authenticate method into overrideable template methods was probably good enough to address this, but, well, I was going a little IOC-crazy at that point. So, I created an AuthenticationCondition interface and a BaseAuthenticationCondition. The TemplateAuthenticationProvider has a List of these condition objects, which are iterated through and checked on authentication.

The developer can therefore create sophisticated authentication conditions and mix and match conditions for various parts of the application or categories of users. Also, the BaseAuthenticationCondition can be wired up directly in the applicationContext.xml with a configured exception, an event to publish, and the name of a boolean method to call on the developer's own UserDetails subclass. The BaseAuthenticationCondition invokes the configured method, and -- if it returns true -- publishes the configured event to the application context and throws the configured exception (with a message configured in the spring config!).

(Side note: yes, I did say "true" means failure above... looking back now, I'm not sure why I did that, though it's not that big a deal. The class also has an "invertTest" property, which takes the opposite of what the configured method returns. Still I should probably change the default behavior to "false" means failure. Hmmm, and maybe have two configurable events: one for failure and one for success.)

Anyway, this is getting long enough... I hope I've given you a clear idea of what this does. I'll also attach the classes here. I realize that the IOC-configurable conditions might be overkill, but the idea was driven by need... my app has some pretty sophisticated authentication condition needs. But, whatever one thinks of the condition objects, I think the authentication method was crying out for being broken up into template methods... any important method that contains a clear sequence of steps is a good candidate for template method in my book.

If you are interested in adding this code to Acegi, I'm happy to make any changes you'd like or do any additional clean-up... or you all can hammer on it. ;-) As I mentioned above, I'd need to write a new test. And there's probably features of the existing providers that I didn't incorporate that I would need to add (the salt-source stuff??). And, if you're not interested for any reason, that's cool too... just wanted to do my part and contribute back to the projects I use.

Thanks again for Acegi, by the way. It was pretty hard to get my head around at first, but it's a very cool system once you get into it.

Ben Munat
Olympia, WA



------------------------------------------------------------------------

/*
 * $Id: TemplateAuthenticationProvider.java,v 1.2 2005/10/01 00:31:01 ben Exp $
 */
package net.sf.acegisecurity.providers.dao;

import java.util.List;

import net.sf.acegisecurity.Authentication;
import net.sf.acegisecurity.AuthenticationException;
import net.sf.acegisecurity.AuthenticationServiceException;
import net.sf.acegisecurity.BadCredentialsException;
import net.sf.acegisecurity.GrantedAuthority;
import net.sf.acegisecurity.UserDetails;
import net.sf.acegisecurity.providers.AuthenticationProvider;
import net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken;
import net.sf.acegisecurity.providers.dao.PasswordAuthenticationDao;
import net.sf.acegisecurity.providers.dao.User;
import net.sf.acegisecurity.providers.dao.UserCache;
import net.sf.acegisecurity.providers.dao.UsernameNotFoundException;
import net.sf.acegisecurity.providers.dao.condition.AuthenticationCondition;
import 
net.sf.acegisecurity.providers.dao.event.AuthenticationFailurePasswordEvent;
import 
net.sf.acegisecurity.providers.dao.event.AuthenticationFailureUsernameNotFoundEvent;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.dao.DataAccessException;
import org.springframework.util.Assert;


/**
* An AuthenticationProvider implementation that offers two alternate approaches * to the DaoAuthenticationProvider or PasswordDaoAuthenticationProvider. * * First, the <code>authenticate</code> method follows the template method pattern: * it follows a set algorithm, but calls out to methods to fulfill steps in that * algorithm. Some of these are abstract and some are implemented with base * implementations. * * These methods are:
 *              getUsernameFromAuthentication(authentication)
 *              getPasswordFromAuthentication(authentication)
 *              getUserFromCache(username,password)
 *              getUserFromBackend(username,password)
 *              handleAuthenticationFailure(authentication,username,exception)
 *              beforeConditions(authentication,user)
 *              checkConditions(authentication,user)
 *              afterAuthentication(authentication,user)
* * The other novel approach offered by this class is that it checks the loaded UserDetails
 * object -- after it has at least matched username and password -- against a 
configurable
* list of conditions. These can be concrete implementations of the * [EMAIL PROTECTED] AuthenticationCondition} interface, or they can be created on the fly by the * Spring container. This is done by specifying [EMAIL PROTECTED] BaseAuthenticationCondition} as the * class and specifying an AuthenticationException to throw, an ApplicationEvent to publish * and a boolean test method to call in the implementation of UserDetails that will be * provided. * * Here is an example of such a configuration: * * <bean class="net.sf.acegisecurity.providers.dao.condition.BaseAuthenticationCondition">
 *              <property name="testMethod"><value>isEnabled</value></property>
 *              <property name="invertTest"><value>true</value></property>
 *              <property name="authenticationException">
 *                      <bean class="net.sf.acegisecurity.DisabledException">
 *                              <constructor-arg><value>We're sorry, your account appears 
to be disabled.</value></constructor-arg>
 *                      </bean>
 *              </property>
 *              <property name="authenticationEventClass">
 *                      
<value>net.sf.acegisecurity.providers.dao.event.AuthenticationFailureDisabledEvent</value>
 *              </property>
 *      </bean>
* * * * @author Ben Munat
 *
 */
public abstract class TemplateAuthenticationProvider implements 
AuthenticationProvider,
                        InitializingBean, ApplicationContextAware
{
    private static Log log = 
LogFactory.getLog(TemplateAuthenticationProvider.class);
/**
     * The list of authentication conditions to check
     */
    protected List<AuthenticationCondition> authenticationConditions;
/**
     * The authentication DAO used to retrieve user data
     */
    protected PasswordAuthenticationDao authenticationDao;
/**
     * The Spring ApplicationContext
     */
    protected ApplicationContext context;
/**
     * The UserCache in which to store UserDetails objects
     */
    protected UserCache userCache;

public void afterPropertiesSet() throws Exception
    {
        Assert.notNull(this.authenticationDao, "A password authentication DAO must 
be set");
        Assert.notNull(this.userCache, "A user cache must be set");
    }

    /**
* Implementation of the AuthenticationProvider-required method providing template method hooks, * including treating authentication approval conditions as a collection of AuthenticationCondition * objects. These can be wired up in the Spring config file.
     */
    public Authentication authenticate(Authentication authentication) throws 
AuthenticationException
    {
String username = getUsernameFromAuthentication(authentication); String password = getPasswordFromAuthentication(authentication); if(log.isDebugEnabled()) log.debug("Starting authentication process with username " + username + " and password " + password); boolean cacheWasUsed = true;
        UserDetails user = getUserFromCache(username,password);
if(user == null){
            cacheWasUsed = false;
            log.debug("User not found in cache; loading from db.");
            try {
                user = getUserFromBackend(username,password);
                if(log.isDebugEnabled()) {
                    log.debug("Retrieved user " + username + " from database");
                }
            } catch (BadCredentialsException ex) {
                handleAuthenticationFailure(authentication,username,ex);
                throw ex;
            }
        } else {
            if(log.isDebugEnabled()) {
                log.debug("Retrieved user " + username + " from cache");
            }
        }
authentication = beforeConditions(authentication,user); authentication = checkConditions(authentication,user); if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }
authentication = afterAuthentication(authentication,user); return authentication;
    }

    /**
     * Gets the UserDetails object from the configured cache.
* Returns null if the given password doesn't match that of * the user retrieved from the cache. * Default implemetation gets user from cache but throws it out * if the passed in password does not match that of the user object. * * @param username
     * @param password
* @return the appropriate UserDetails object or null if * the user wasn't found or the null cache is being used.
     */
    protected UserDetails getUserFromCache(String username, String password)
    {
        UserDetails user = userCache.getUserFromCache(username);
// Check if the provided password is the same as the password in cache
        if ((user != null) && !password.equals(user.getPassword())) {
            user = null;
            this.userCache.removeUserFromCache(username);
        }
return user;
    }

    /**
     * Template method hook to get the username from the given authentication.
* Default implementation gets Principal as String or gets the username from * the Authentication if it's an instance of UserDetails. * * @return String username or "NONE_PROVIDED"
     */
    protected String getUsernameFromAuthentication(Authentication 
authentication)
    {
        String username = "NONE_PROVIDED";
if (authentication.getPrincipal() != null) {
            username = authentication.getPrincipal().toString();
        }
        if (authentication.getPrincipal() instanceof UserDetails) {
            username = ((UserDetails) 
authentication.getPrincipal()).getUsername();
        }
        return username;
    }

    /**
     * Template method hook to get the password from the authentication.
     * Default implementation gets the authentication's credentials as a String.
* * @param authentication the Authentication from which to get password
     * @return the password
     */
    protected String getPasswordFromAuthentication(Authentication 
authentication)
    {
        return authentication.getCredentials().toString();
    }

    /**
     * Template method hook to override the call to the AuthenticationDao.
* * @param username the username provided for authentication
     * @param password the password provided for authentication
     * @return the UserDetails object to be authenticated
     */
    protected UserDetails getUserFromBackend(String username, String password)
    {
        try {
            return 
this.authenticationDao.loadUserByUsernameAndPassword(username, password);
        } catch (DataAccessException repositoryProblem) {
            throw new 
AuthenticationServiceException(repositoryProblem.getMessage(), 
repositoryProblem);
        }
    }

    /**
     * Called by authenticate method when a call to load user fails due to bad 
credentials.
     * Publishes a AuthenticationFailureUsernameNotFoundEvent to the context.
     * Convenient hook for subclasses to change event published.
* * @param authentication the Authentication as passed to #authenticate
     * @param username the username as derived by #getUsernameFromAuthentication
     */
    protected void handleAuthenticationFailure(Authentication authentication, 
String username, BadCredentialsException ex)
    {
        ApplicationContext context = getContext();
        if (context != null) {
            User dummy = new User("".equals(username) ? "EMPTY_STRING_PROVIDED" : 
username, "*****",
                            false, false, false, false, new 
GrantedAuthority[0]);
            if(ex instanceof UsernameNotFoundException) {
                context.publishEvent(new 
AuthenticationFailureUsernameNotFoundEvent(authentication, dummy));
            } else {
                context.publishEvent(new 
AuthenticationFailurePasswordEvent(authentication,dummy));
            }
        }
    }

    /**
     * Template method hook for subclasses to do things with the Authentication
     * object or the retrieved UserDetails before the AuthenticationConditions
     * are checked.
* * @param authentication the authentication being considered
     * @param user the user being considered
* @return */
    protected abstract Authentication beforeConditions(Authentication 
authentication, UserDetails user);

    /**
     * Called by #authenticate to run through list of 
@link{AuthenticationCondition}s,
     * which can be configured in the application context.
     * Subclasses can override this to use a hardcoded/different approach.
* * @param authentication the authentication being considered
     * @param user the user being considered
     * @return the authentication; possibly altered by conditions
     */
    protected Authentication checkConditions(Authentication authentication, 
UserDetails user)
    {
        if(log.isDebugEnabled()) {
            log.debug("Checking authentication conditions for user 
"+user.getUsername());
        }
for (AuthenticationCondition condition : authenticationConditions) {
            condition.checkCondition(authentication,user);
        }
        return authentication;
    }

    /**
     * Provides hook for subclasses to perform work after user is authenticated.
     * Called right after #createSuccessAuthentication in #authenticate.
* * @param authentication the authentication being considered
     * @param user the user being considered
     * @param cacheWasUsed indicates whether the user was pulled from the cache 
or the backend
     * @return the authentication object again
     */
    protected abstract Authentication afterAuthentication(Authentication 
authentication, UserDetails user);

    /**
     * Set the <code>List</code> of <code>AuthenticationCondition</code>s 
required for a
     * user to authenticate.
* * @param authenticationConditions The authenticationConditions to set.
     */
    public void setAuthenticationConditions(List<AuthenticationCondition> 
authenticationConditions)
    {
        this.authenticationConditions = authenticationConditions;
    }

    /**
* Set the [EMAIL PROTECTED] AuthenticationDao} * @param authenticationDao The authenticationDao to set.
     */
    public void setAuthenticationDao(PasswordAuthenticationDao 
authenticationDao)
    {
        this.authenticationDao = authenticationDao;
    }

    /**
     * Set the ApplicationContext.
     * [EMAIL PROTECTED] 
org.springframework.beans.factory.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext)}
     */
    public void setApplicationContext(ApplicationContext applicationContext) 
throws BeansException
    {
        this.context = applicationContext;
    }

    /**
     * Get the ApplicationContext set by the container.
     * @return a Spring ApplicationContext
     */
    public ApplicationContext getContext()
    {
        return context;
    }

    /**
     * @param userCache The userCache to set.
     */
    public void setUserCache(UserCache userCache)
    {
        this.userCache = userCache;
    }

    public boolean supports(Class authentication)
    {
        if 
(UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication)) {
            return true;
        }
        return false;
    }

}


------------------------------------------------------------------------

/*
 * $Id: AuthenticationCondition.java,v 1.1 2005/09/29 00:10:28 ben Exp $
 */
package net.sf.acegisecurity.providers.dao.condition;

import net.sf.acegisecurity.Authentication;
import net.sf.acegisecurity.AuthenticationException;
import net.sf.acegisecurity.UserDetails;

/**
 * Represents a condition that must be met for authentication to succeed.
 *
 * @author Ben Munat
 *
 */
public interface AuthenticationCondition
{
    /**
     * Implements the decision logic that determines if this condition is met.
     * If the condition passes, the method should simply return. If not, the
     * method should throw an appropriate subclass of AuthenticationException.
* * @param authentication the Authentication to consider
     * @param user the UserDetails to consider
     * @throws AuthenticationException if condition is not met.
     */
    public void checkCondition(Authentication authentication, UserDetails user) 
throws AuthenticationException;
}


------------------------------------------------------------------------

/*
 * $Id: BaseAuthenticationCondition.java,v 1.3 2005/10/01 00:30:31 ben Exp $
 */
package net.sf.acegisecurity.providers.dao.condition;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

import net.sf.acegisecurity.Authentication;
import net.sf.acegisecurity.AuthenticationException;
import net.sf.acegisecurity.UserDetails;
import net.sf.acegisecurity.providers.dao.event.AuthenticationEvent;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.Assert;

/**
 * Abstract base class for <code>AuthenticationCondition</code>s.
* * Provides AuthenticationException and AuthenticationEvent classname fields,
 * which can be set in the Spring applicationContext.xml configuration file.
* * Here is an example of such a configuration: * * <bean class="net.sf.acegisecurity.providers.dao.condition.BaseAuthenticationCondition">
 *              <property name="testMethod"><value>isEnabled</value></property>
 *              <property name="invertTest"><value>true</value></property>
 *              <property name="authenticationException">
 *                      <bean class="net.sf.acegisecurity.DisabledException">
 *                              <constructor-arg><value>We're sorry, your account appears 
to be disabled.</value></constructor-arg>
 *                      </bean>
 *              </property>
 *              <property name="authenticationEventClass">
 *                      
<value>net.sf.acegisecurity.providers.dao.event.AuthenticationFailureDisabledEvent</value>
 *              </property>
 *      </bean>
* * However, IOC config is not required: subclasses can throw a hardcoded
 * Exception and publish a hardcoded event if desired.
* * @author Ben Munat * */ public class BaseAuthenticationCondition implements AuthenticationCondition, InitializingBean, ApplicationContextAware
{
    protected static Log log = 
LogFactory.getLog(BaseAuthenticationCondition.class);

    /**
     * The exception to be throw if checkCondition fails.
     */
    protected AuthenticationException authenticationException;
/**
     * The AuthenticationEvent class name to publish if checkCondition succeeds.
     * Must be a subclass of 
net.sf.acegisecurity.providers.dao.event.AuthenticationEvent
     */
    protected String authenticationEventClass;
/**
     * The ApplicationContext: set by container on startup.
     */
    protected ApplicationContext context;
/**
     * The method on UserDetails to call as the test condition.
     * Must be a boolean method!!
     */
    protected String testMethod;
/**
     * Whether to invert the result of the test method. Defaults to false.
     */
    protected boolean invertTest = false;
/**
     * Base implementation of the method to check an authentication condition.
     * Expects a testMethod name to be configured in the config file.
     * Will invert the result of that test method if invertTest is set to true.
     * If the test method (after any inversion) is true, publishes the
     * configured AuthenticationEvent to the context and throws
     * the configured AuthenticationException.
* * Subclasses can override this to perform more sophisticated logic than a * simple call to a test method. * * @param authentication the Authentication request object. * * @param user the UserDetails for which to check this condition * * @throws the AuthenticatonException subclass as configured
     */
    public void checkCondition(Authentication authentication, UserDetails user) 
throws AuthenticationException
    {
        if(testMethod == null || "".equals(testMethod))
            throw new IllegalStateException("Base checkCondition method called 
without testMethod being configured.");
if(log.isDebugEnabled())
                log.debug("Base checkCondition called: invoking method 
"+testMethod+" on user object with invertTest = "+ invertTest);
boolean result = false; try {
            Method m = user.getClass().getMethod(testMethod,new Class[]{});
            result = ((Boolean) m.invoke(user,new Object[]{})).booleanValue();
            if(log.isDebugEnabled()){
                log.debug("Result of running test method "+testMethod+" was 
"+result);
            }
            // if we configured condition to take the opposite, flip the result
            if(invertTest){
                result = !result;
            }
        } catch (Exception e) {
            throw new IllegalStateException("Attempt to invoke configured method 
threw exception: ",e);
        }
if(result){
            publishEvent(authentication,user);
            throw authenticationException;
        }
    }

    /**
* Creates an instance of the configured [EMAIL PROTECTED] AuthenticationEvent}. Subclasses should * call this from their implementation of checkCondition(Authentication,UserDetails). * * @param authentication The Authentication being checked. * * @param user the UserDetails being checked. * * @return an instance of the configured AuthenticationEvent * */
    protected AuthenticationEvent createEvent(Authentication authentication, 
UserDetails user)
    {
        if(authenticationEventClass == null || 
authenticationEventClass.length() == 0) return null;
        try {
            Class c = Class.forName(authenticationEventClass);
            Constructor constructor = c.getConstructor(new 
Class[]{Authentication.class,UserDetails.class});
            AuthenticationEvent ae = (AuthenticationEvent) 
constructor.newInstance(new Object[]{authentication,user});
            if(log.isDebugEnabled()) log.debug("Successfully created 
AuthenticationEvent: "+ae);
            return ae;
        } catch (Exception e) {
            throw new IllegalStateException("AuthenticationCondition not configured 
correctly.",e);
} }

    /**
     * Subclasses can call this to handle publishing their IOC-configured 
AuthenticationEvent.
     * Note that this method does nothing if the ApplicationContext and the 
AuthenticationEvent aren't set.
* * @param authentication the Authentication to include in the published event * * @param user the user to include in the published event * */
    protected void publishEvent(Authentication authentication, UserDetails user)
    {
        if (this.context != null && this.authenticationEventClass != null && 
!"".equals(this.authenticationEventClass)) {
            AuthenticationEvent event = createEvent(authentication,user);
            if(log.isDebugEnabled()) log.debug("Publishing event: " + event);
            context.publishEvent(event);
        }
    }
/**
     * Set the class name of the [EMAIL PROTECTED] AuthenticationEvent} to fire 
if this condition if met.
* * @param applicationEvent The applicationEvent to set. * */
    public void setAuthenticationEventClass(String applicationEventClass)
    {
        this.authenticationEventClass = applicationEventClass;
    }

    /**
     * Set the [EMAIL PROTECTED] AuthenticationException} to throw if this 
condition is not met.
* * @param authenticationException The authenticationException to set. * */
    public void setAuthenticationException(AuthenticationException 
authenticationException)
    {
        this.authenticationException = authenticationException;
    }

    /**
     * Checks Spring configuration to this condition.
     * [EMAIL PROTECTED] 
org.springframework.beans.factory.InitializingBean#afterPropertiesSet()}
     */
    public void afterPropertiesSet() throws Exception
    {
        testExceptionConfig();
        testApplicationEventCreate();
    }

    /**
     * Set the ApplicationContext.
     * [EMAIL PROTECTED] 
org.springframework.beans.factory.ApplicationContextAware#setApplicationContext(org.springframework.context.ApplicationContext)}
     */
    public void setApplicationContext(ApplicationContext applicationContext) 
throws BeansException
    {
        this.context = applicationContext;
    }
/**
     * Get the ApplicationContext
     * @return The Spring ApplicationContext
     */
    public ApplicationContext getApplicationContext()
    {
        return context;
    }
/**
     * Setting to true causes the result of the condition to be reversed.
     * @param invertTest The invertTest to set.
     */
    public void setInvertTest(boolean invertTest)
    {
        this.invertTest = invertTest;
    }


    /**
* Set the name of a no-arg boolean method in the [EMAIL PROTECTED] net.sf.acegisecurity.UserDetails} * object that this condition should run as its test.
     * @param testMethod The testMethod to set.
     */
    public void setTestMethod(String testMethod)
    {
        this.testMethod = testMethod;
    }


    // test if the configured AuthenticationException is valid
    private void testExceptionConfig()
    {
        if(this.authenticationException != null){
Assert.state(AuthenticationException.class.isAssignableFrom(this.authenticationException.getClass()), "authenticationException must be configured with a valid AuthenticationException class name.");
        }
    }

    // test if the configured ApplicationEvent can be created
    private void testApplicationEventCreate()
    {
        if(authenticationEventClass != null && authenticationEventClass.length() 
> 0) {
            try {
                Class c = Class.forName(authenticationEventClass);
Assert.state(AuthenticationEvent.class.isAssignableFrom(c), "authenticationCondition must be configured with a valid AuthenticationEvent class name.");
            } catch (Exception e){
                throw new IllegalArgumentException("AuthenticationCondition not 
configured correctly",e);
            }
        }
    }

}



-------------------------------------------------------
This SF.Net email is sponsored by the JBoss Inc.  Get Certified Today
Register for a JBoss Training Course.  Free Certification Exam
for All Training Attendees Through End of 2005. For more info visit:
http://ads.osdn.com/?ad_id=7628&alloc_id=16845&op=click
_______________________________________________
Home: http://acegisecurity.org
Acegisecurity-developer mailing list
Acegisecurity-developer@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/acegisecurity-developer

Reply via email to