Преглед изворни кода

OPEN - issue SEC-905: <protect-pointcut /> pointcuts do not respect method arguments
http://jira.springframework.org/browse/SEC-905. Added extra registration method to MapBasedMethodDefinitionSource which takes a Method instance rather than the method name.

Luke Taylor пре 17 година
родитељ
комит
55d357f42d

+ 4 - 2
core-tiger/src/test/java/org/springframework/security/annotation/BusinessService.java

@@ -39,8 +39,10 @@ public interface BusinessService {
     public void someUserMethod1();
 
     @Secured({"ROLE_USER"})
-    @RolesAllowed({"ROLE_USER"})    
+    @RolesAllowed({"ROLE_USER"})
     public void someUserMethod2();
-    
+
+    public int someOther(String s);
+
     public int someOther(int input);
 }

+ 7 - 3
core-tiger/src/test/java/org/springframework/security/annotation/BusinessServiceImpl.java

@@ -26,7 +26,11 @@ public class BusinessServiceImpl<E extends Entity> implements BusinessService {
         return entity;
     }
 
-	public int someOther(int input) {
-		return input;
-	}
+    public int someOther(String s) {
+        return 0;
+    }
+
+    public int someOther(int input) {
+        return input;
+    }
 }

+ 7 - 3
core-tiger/src/test/java/org/springframework/security/annotation/Jsr250BusinessServiceImpl.java

@@ -27,7 +27,11 @@ public class Jsr250BusinessServiceImpl implements BusinessService {
     public void someAdminMethod() {
     }
 
+    public int someOther(String input) {
+        return 0;
+    }
+
     public int someOther(int input) {
-		return input;
-	}
-}
+        return input;
+    }
+}

+ 26 - 11
core-tiger/src/test/java/org/springframework/security/config/GlobalMethodSecurityBeanDefinitionParserTests.java

@@ -31,8 +31,8 @@ public class GlobalMethodSecurityBeanDefinitionParserTests {
         setContext(
                 "<b:bean id='target' class='org.springframework.security.annotation.BusinessServiceImpl'/>" +
                 "<global-method-security>" +
-                "	<protect-pointcut expression='execution(* *.someUser*(..))' access='ROLE_USER'/>" +
-                "	<protect-pointcut expression='execution(* *.someAdmin*(..))' access='ROLE_ADMIN'/>" +
+                "    <protect-pointcut expression='execution(* *.someUser*(..))' access='ROLE_USER'/>" +
+                "    <protect-pointcut expression='execution(* *.someAdmin*(..))' access='ROLE_ADMIN'/>" +
                 "</global-method-security>" + ConfigTestUtils.AUTH_PROVIDER_XML
                     );
         target = (BusinessService) appContext.getBean("target");
@@ -106,6 +106,21 @@ public class GlobalMethodSecurityBeanDefinitionParserTests {
         service.loadUserByUsername("notused");
     }
 
+    @Test
+    public void supportsMethodArgumentsInPointcut() {
+        setContext(
+                "<b:bean id='target' class='org.springframework.security.annotation.BusinessServiceImpl'/>" +
+                "<global-method-security>" +
+                "   <protect-pointcut expression='execution(* *.someOther(String))' access='ROLE_ADMIN'/>" +
+                "   <protect-pointcut expression='execution(* *.BusinessService*(..))' access='ROLE_USER'/>" +
+                "</global-method-security>" + ConfigTestUtils.AUTH_PROVIDER_XML
+        );
+        SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken("user", "password"));
+        target = (BusinessService) appContext.getBean("target");
+        // someOther(int) should not be matched by someOther(String)
+        target.someOther(0);
+    }
+
     @Test(expected=BeanDefinitionParsingException.class)
     public void duplicateElementCausesError() {
         setContext(
@@ -116,21 +131,21 @@ public class GlobalMethodSecurityBeanDefinitionParserTests {
 
     @Test(expected=AccessDeniedException.class)
     public void worksWithoutTargetOrClass() {
-    	setContext(
-    			"<global-method-security secured-annotations='enabled'/>" +
-    		    "<b:bean id='businessService' class='org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean'>" +
-    		    "    <b:property name='serviceUrl' value='http://localhost:8080/SomeService'/>" +
-    		    "    <b:property name='serviceInterface' value='org.springframework.security.annotation.BusinessService'/>" +
-    		    "</b:bean>" + AUTH_PROVIDER_XML
-    			);
-    	
+        setContext(
+                "<global-method-security secured-annotations='enabled'/>" +
+                "<b:bean id='businessService' class='org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean'>" +
+                "    <b:property name='serviceUrl' value='http://localhost:8080/SomeService'/>" +
+                "    <b:property name='serviceInterface' value='org.springframework.security.annotation.BusinessService'/>" +
+                "</b:bean>" + AUTH_PROVIDER_XML
+                );
+
         UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("Test", "Password",
                 new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_SOMEOTHERROLE")});
         SecurityContextHolder.getContext().setAuthentication(token);
         target = (BusinessService) appContext.getBean("businessService");
         target.someUserMethod1();
     }
-    
+
     private void setContext(String context) {
         appContext = new InMemoryXmlApplicationContext(context);
     }

+ 112 - 95
core/src/main/java/org/springframework/security/intercept/method/MapBasedMethodDefinitionSource.java

@@ -34,13 +34,13 @@ import org.springframework.util.ClassUtils;
 
 /**
  * Stores a {@link ConfigAttributeDefinition} for a method or class signature.
- * 
+ *
  * <p>
  * This class is the preferred implementation of {@link MethodDefinitionSource} for XML-based
  * definition of method security metadata. To assist in XML-based definition, wildcard support
  * is provided.
  * </p>
- * 
+ *
  * @author Ben Alex
  * @version $Id$
  * @since 2.0
@@ -51,9 +51,9 @@ public class MapBasedMethodDefinitionSource extends AbstractFallbackMethodDefini
     private static final Log logger = LogFactory.getLog(MapBasedMethodDefinitionSource.class);
 
     //~ Instance fields ================================================================================================
-	private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader();
+    private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader();
 
-	/** Map from RegisteredMethod to ConfigAttributeDefinition */
+    /** Map from RegisteredMethod to ConfigAttributeDefinition */
     protected Map methodMap = new HashMap();
 
     /** Map from RegisteredMethod to name pattern used for registration */
@@ -74,48 +74,33 @@ public class MapBasedMethodDefinitionSource extends AbstractFallbackMethodDefini
         while (iterator.hasNext()) {
             Map.Entry entry = (Map.Entry) iterator.next();
             addSecureMethod((String)entry.getKey(), (ConfigAttributeDefinition)entry.getValue());
-        }        
+        }
     }
 
-	/**
-	 * Implementation does not support class-level attributes.
-	 */
-	protected ConfigAttributeDefinition findAttributes(Class clazz) {
-		return null;
-	}
-
-	/**
-	 * Will walk the method inheritance tree to find the most specific declaration applicable.
-	 */
-	protected ConfigAttributeDefinition findAttributes(Method method, Class targetClass) {
-		return findAttributesSpecifiedAgainst(method, targetClass);
-	}
-	
-	private ConfigAttributeDefinition findAttributesSpecifiedAgainst(Method method, Class clazz) {
-		RegisteredMethod registeredMethod = new RegisteredMethod(method, clazz);
-		if (methodMap.containsKey(registeredMethod)) {
-			return (ConfigAttributeDefinition) methodMap.get(registeredMethod);
-		}
-		// Search superclass
-		if (clazz.getSuperclass() != null) {
-			return findAttributesSpecifiedAgainst(method, clazz.getSuperclass());
-		}
-		return null;
-	}
+    /**
+     * Implementation does not support class-level attributes.
+     */
+    protected ConfigAttributeDefinition findAttributes(Class clazz) {
+        return null;
+    }
 
     /**
-     * Add configuration attributes for a secure method.
-     *
-     * @param method the method to be secured
-     * @param attr required authorities associated with the method
+     * Will walk the method inheritance tree to find the most specific declaration applicable.
      */
-    private void addSecureMethod(RegisteredMethod method, ConfigAttributeDefinition attr) {
-    	Assert.notNull(method, "RegisteredMethod required");
-    	Assert.notNull(attr, "Configuration attribute required");
-    	if (logger.isInfoEnabled()) {
-            logger.info("Adding secure method [" + method + "] with attributes [" + attr + "]");
-    	}
-        this.methodMap.put(method, attr);
+    protected ConfigAttributeDefinition findAttributes(Method method, Class targetClass) {
+        return findAttributesSpecifiedAgainst(method, targetClass);
+    }
+
+    private ConfigAttributeDefinition findAttributesSpecifiedAgainst(Method method, Class clazz) {
+        RegisteredMethod registeredMethod = new RegisteredMethod(method, clazz);
+        if (methodMap.containsKey(registeredMethod)) {
+            return (ConfigAttributeDefinition) methodMap.get(registeredMethod);
+        }
+        // Search superclass
+        if (clazz.getSuperclass() != null) {
+            return findAttributesSpecifiedAgainst(method, clazz.getSuperclass());
+        }
+        return null;
     }
 
     /**
@@ -126,7 +111,7 @@ public class MapBasedMethodDefinitionSource extends AbstractFallbackMethodDefini
      * @param attr required authorities associated with the method
      */
     public void addSecureMethod(String name, ConfigAttributeDefinition attr) {
-    	int lastDotIndex = name.lastIndexOf(".");
+        int lastDotIndex = name.lastIndexOf(".");
 
         if (lastDotIndex == -1) {
             throw new IllegalArgumentException("'" + name + "' is not a valid method name: format is FQN.methodName");
@@ -134,17 +119,17 @@ public class MapBasedMethodDefinitionSource extends AbstractFallbackMethodDefini
 
         String methodName = name.substring(lastDotIndex + 1);
         Assert.hasText(methodName, "Method not found for '" + name + "'");
-        
+
         String typeName = name.substring(0, lastDotIndex);
         Class type = ClassUtils.resolveClassName(typeName, this.beanClassLoader);
-        
+
         addSecureMethod(type, methodName, attr);
     }
 
     /**
      * Add configuration attributes for a secure method. Mapped method names can end or start with <code>&#42</code>
      * for matching multiple methods.
-     * 
+     *
      * @param javaType target interface or class the security configuration attribute applies to
      * @param mappedName mapped method name, which the javaType has declared or inherited
      * @param attr required authorities associated with the method
@@ -192,6 +177,38 @@ public class MapBasedMethodDefinitionSource extends AbstractFallbackMethodDefini
         }
     }
 
+    /**
+     * Adds configuration attributes for a specific method, for example where the method has been
+     * matched using a pointcut expression. If a match already exists in the map for the method, then 
+     * the existing match will be retained, so that if this method is called for a more general pointcut 
+     * it will not override a more specific one which has already been added. This  
+     */
+    public void addSecureMethod(Class javaType, Method method, ConfigAttributeDefinition attr) {
+        RegisteredMethod key = new RegisteredMethod(method, javaType);
+
+        if (methodMap.containsKey(key)) {
+            logger.debug("Method [" + method + "] is already registered with attributes [" + methodMap.get(key) + "]");
+            return;
+        }
+
+        methodMap.put(key, attr);
+    }
+
+    /**
+     * Add configuration attributes for a secure method.
+     *
+     * @param method the method to be secured
+     * @param attr required authorities associated with the method
+     */
+    private void addSecureMethod(RegisteredMethod method, ConfigAttributeDefinition attr) {
+        Assert.notNull(method, "RegisteredMethod required");
+        Assert.notNull(attr, "Configuration attribute required");
+        if (logger.isInfoEnabled()) {
+            logger.info("Adding secure method [" + method + "] with attributes [" + attr + "]");
+        }
+        this.methodMap.put(method, attr);
+    }
+
     /**
      * Obtains the configuration attributes explicitly defined against this bean.
      *
@@ -254,54 +271,54 @@ public class MapBasedMethodDefinitionSource extends AbstractFallbackMethodDefini
         attributes.addAll(toMerge.getConfigAttributes());
     }
 
-	public void setBeanClassLoader(ClassLoader beanClassLoader) {
-		Assert.notNull(beanClassLoader, "Bean class loader required");
-		this.beanClassLoader = beanClassLoader;
-	}
-
-	/**
-	 * @return map size (for unit tests and diagnostics)
-	 */
-	public int getMethodMapSize() {
-		return methodMap.size();
-	}
-	
-	/**
-	 * Stores both the Java Method as well as the Class we obtained the Method from. This is necessary because Method only
-	 * provides us access to the declaring class. It doesn't provide a way for us to introspect which Class the Method
-	 * was registered against. If a given Class inherits and redeclares a method (i.e. calls super();) the registered Class
-	 * and declaring Class are the same. If a given class merely inherits but does not redeclare a method, the registered
-	 * Class will be the Class we're invoking against and the Method will provide details of the declared class.
-	 */
-	private class RegisteredMethod {
-		private Method method;
-		private Class registeredJavaType;
-
-		public RegisteredMethod(Method method, Class registeredJavaType) {
-			Assert.notNull(method, "Method required");
-			Assert.notNull(registeredJavaType, "Registered Java Type required");
-			this.method = method;
-			this.registeredJavaType = registeredJavaType;
-		}
-
-		public boolean equals(Object obj) {
-			if (this == obj) {
-				return true;
-			}
-			if (obj != null && obj instanceof RegisteredMethod) {
-				RegisteredMethod rhs = (RegisteredMethod) obj;
-				return method.equals(rhs.method) && registeredJavaType.equals(rhs.registeredJavaType);
-			}
-			return false;
-		}
-
-		public int hashCode() {
-			return method.hashCode() * registeredJavaType.hashCode();
-		}
-
-		public String toString() {
-			return "RegisteredMethod[" + registeredJavaType.getName() + "; " + method + "]";
-		}
-	}
-    
+    public void setBeanClassLoader(ClassLoader beanClassLoader) {
+        Assert.notNull(beanClassLoader, "Bean class loader required");
+        this.beanClassLoader = beanClassLoader;
+    }
+
+    /**
+     * @return map size (for unit tests and diagnostics)
+     */
+    public int getMethodMapSize() {
+        return methodMap.size();
+    }
+
+    /**
+     * Stores both the Java Method as well as the Class we obtained the Method from. This is necessary because Method only
+     * provides us access to the declaring class. It doesn't provide a way for us to introspect which Class the Method
+     * was registered against. If a given Class inherits and redeclares a method (i.e. calls super();) the registered Class
+     * and declaring Class are the same. If a given class merely inherits but does not redeclare a method, the registered
+     * Class will be the Class we're invoking against and the Method will provide details of the declared class.
+     */
+    private class RegisteredMethod {
+        private Method method;
+        private Class registeredJavaType;
+
+        public RegisteredMethod(Method method, Class registeredJavaType) {
+            Assert.notNull(method, "Method required");
+            Assert.notNull(registeredJavaType, "Registered Java Type required");
+            this.method = method;
+            this.registeredJavaType = registeredJavaType;
+        }
+
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj != null && obj instanceof RegisteredMethod) {
+                RegisteredMethod rhs = (RegisteredMethod) obj;
+                return method.equals(rhs.method) && registeredJavaType.equals(rhs.registeredJavaType);
+            }
+            return false;
+        }
+
+        public int hashCode() {
+            return method.hashCode() * registeredJavaType.hashCode();
+        }
+
+        public String toString() {
+            return "RegisteredMethod[" + registeredJavaType.getName() + "; " + method + "]";
+        }
+    }
+
 }

+ 92 - 92
core/src/main/java/org/springframework/security/intercept/method/ProtectPointcutPostProcessor.java

@@ -21,25 +21,25 @@ import org.springframework.util.Assert;
 /**
  * Parses AspectJ pointcut expressions, registering methods that match the pointcut with a
  * traditional {@link MapBasedMethodDefinitionSource}.
- * 
+ *
  * <p>
  * This class provides a convenient way of declaring a list of pointcuts, and then
  * having every method of every bean defined in the Spring application context compared with
  * those pointcuts. Where a match is found, the matching method will be registered with the
  * {@link MapBasedMethodDefinitionSource}.
  * </p>
- * 
+ *
  * <p>
  * It is very important to understand that only the <b>first</b> pointcut that matches a given
  * method will be taken as authoritative for that method. This is why pointcuts should be provided
  * as a <tt>LinkedHashMap</tt>, because their order is very important.
  * </p>
- * 
+ *
  * <p>
  * Note also that only beans defined in the Spring application context will be examined by this
- * class. 
+ * class.
  * </p>
- * 
+ *
  * <p>
  * Because this class registers method security metadata with {@link MapBasedMethodDefinitionSource},
  * normal Spring Security capabilities such as {@link MethodDefinitionSourceAdvisor} can be used.
@@ -57,18 +57,18 @@ public final class ProtectPointcutPostProcessor implements BeanPostProcessor {
     private static final Log logger = LogFactory.getLog(ProtectPointcutPostProcessor.class);
 
     private Map pointcutMap = new LinkedHashMap(); /** Key: string-based pointcut, value: ConfigAttributeDefinition */
-	private MapBasedMethodDefinitionSource mapBasedMethodDefinitionSource;
-	private PointcutParser parser;
-	
-	public ProtectPointcutPostProcessor(MapBasedMethodDefinitionSource mapBasedMethodDefinitionSource) {
-		Assert.notNull(mapBasedMethodDefinitionSource, "MapBasedMethodDefinitionSource to populate is required");
-		this.mapBasedMethodDefinitionSource = mapBasedMethodDefinitionSource;
-		
-		// Setup AspectJ pointcut expression parser
-		Set supportedPrimitives = new HashSet();
-		supportedPrimitives.add(PointcutPrimitive.EXECUTION);
-		supportedPrimitives.add(PointcutPrimitive.ARGS);
-		supportedPrimitives.add(PointcutPrimitive.REFERENCE);
+    private MapBasedMethodDefinitionSource mapBasedMethodDefinitionSource;
+    private PointcutParser parser;
+
+    public ProtectPointcutPostProcessor(MapBasedMethodDefinitionSource mapBasedMethodDefinitionSource) {
+        Assert.notNull(mapBasedMethodDefinitionSource, "MapBasedMethodDefinitionSource to populate is required");
+        this.mapBasedMethodDefinitionSource = mapBasedMethodDefinitionSource;
+
+        // Setup AspectJ pointcut expression parser
+        Set supportedPrimitives = new HashSet();
+        supportedPrimitives.add(PointcutPrimitive.EXECUTION);
+        supportedPrimitives.add(PointcutPrimitive.ARGS);
+        supportedPrimitives.add(PointcutPrimitive.REFERENCE);
 //		supportedPrimitives.add(PointcutPrimitive.THIS);
 //		supportedPrimitives.add(PointcutPrimitive.TARGET);
 //		supportedPrimitives.add(PointcutPrimitive.WITHIN);
@@ -76,79 +76,79 @@ public final class ProtectPointcutPostProcessor implements BeanPostProcessor {
 //		supportedPrimitives.add(PointcutPrimitive.AT_WITHIN);
 //		supportedPrimitives.add(PointcutPrimitive.AT_ARGS);
 //		supportedPrimitives.add(PointcutPrimitive.AT_TARGET);
-		parser = PointcutParser.getPointcutParserSupportingSpecifiedPrimitivesAndUsingContextClassloaderForResolution(supportedPrimitives);
-	}
-
-	public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
-		return bean;
-	}
-
-	public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
-		// Obtain methods for the present bean
-		Method[] methods;
-		try {
-			methods = bean.getClass().getMethods();
-		} catch (Exception e) {
-			throw new IllegalStateException(e.getMessage());
-		}
-		
-		// Check to see if any of those methods are compatible with our pointcut expressions
-		for (int i = 0; i < methods.length; i++) {
-			Iterator iter = pointcutMap.keySet().iterator();
-			while (iter.hasNext()) {
-				String ex = iter.next().toString();
-				
-				// Parse the presented AspectJ pointcut expression
-				PointcutExpression expression = parser.parsePointcutExpression(ex);
-
-				// Try for the bean class directly
-				if (attemptMatch(bean.getClass(), methods[i], expression, beanName)) {
-					// We've found the first expression that matches this method, so move onto the next method now
-					break; // the "while" loop, not the "for" loop
-				}
-			}
-		}
-		
-		return bean;
-	}
-	
-	private boolean attemptMatch(Class targetClass, Method method, PointcutExpression expression, String beanName) {
-		// Determine if the presented AspectJ pointcut expression matches this method
-		boolean matches = expression.matchesMethodExecution(method).alwaysMatches();
-		
-		// Handle accordingly
-		if (matches) {
-			ConfigAttributeDefinition attr = (ConfigAttributeDefinition) pointcutMap.get(expression.getPointcutExpression());
-			
-			if (logger.isDebugEnabled()) {
-				logger.debug("AspectJ pointcut expression '" + expression.getPointcutExpression() + "' matches target class '" + targetClass.getName() + "' (bean ID '" + beanName + "') for method '" + method + "'; registering security configuration attribute '" + attr + "'");
-			}
-			
-			mapBasedMethodDefinitionSource.addSecureMethod(targetClass, method.getName(), attr);
-		}
-		
-		return matches;
-	}
-	
-	public void setPointcutMap(Map map) {
-		Assert.notEmpty(map);
-		Iterator i = map.keySet().iterator();
-		while (i.hasNext()) {
-			String expression = i.next().toString();
-			Object value = map.get(expression);
-			Assert.isInstanceOf(ConfigAttributeDefinition.class, value, "Map keys must be instances of ConfigAttributeDefinition");
-			addPointcut(expression, (ConfigAttributeDefinition) value);
-		}
-	}
-
-	public void addPointcut(String pointcutExpression, ConfigAttributeDefinition definition) {
-		Assert.hasText(pointcutExpression, "An AspectJ pointcut expression is required");
-		Assert.notNull(definition, "ConfigAttributeDefinition required");
-		pointcutMap.put(pointcutExpression, definition);
-		
-		if (logger.isDebugEnabled()) {
-			logger.debug("AspectJ pointcut expression '" + pointcutExpression + "' registered for security configuration attribute '" + definition + "'");
-		}
-	}
-	
+        parser = PointcutParser.getPointcutParserSupportingSpecifiedPrimitivesAndUsingContextClassloaderForResolution(supportedPrimitives);
+    }
+
+    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+        return bean;
+    }
+
+    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
+        // Obtain methods for the present bean
+        Method[] methods;
+        try {
+            methods = bean.getClass().getMethods();
+        } catch (Exception e) {
+            throw new IllegalStateException(e.getMessage());
+        }
+
+        // Check to see if any of those methods are compatible with our pointcut expressions
+        for (int i = 0; i < methods.length; i++) {
+            Iterator iter = pointcutMap.keySet().iterator();
+            while (iter.hasNext()) {
+                String ex = iter.next().toString();
+
+                // Parse the presented AspectJ pointcut expression
+                PointcutExpression expression = parser.parsePointcutExpression(ex);
+
+                // Try for the bean class directly
+                if (attemptMatch(bean.getClass(), methods[i], expression, beanName)) {
+                    // We've found the first expression that matches this method, so move onto the next method now
+                    break; // the "while" loop, not the "for" loop
+                }
+            }
+        }
+
+        return bean;
+    }
+
+    private boolean attemptMatch(Class targetClass, Method method, PointcutExpression expression, String beanName) {
+        // Determine if the presented AspectJ pointcut expression matches this method
+        boolean matches = expression.matchesMethodExecution(method).alwaysMatches();
+
+        // Handle accordingly
+        if (matches) {
+            ConfigAttributeDefinition attr = (ConfigAttributeDefinition) pointcutMap.get(expression.getPointcutExpression());
+
+            if (logger.isDebugEnabled()) {
+                logger.debug("AspectJ pointcut expression '" + expression.getPointcutExpression() + "' matches target class '" + targetClass.getName() + "' (bean ID '" + beanName + "') for method '" + method + "'; registering security configuration attribute '" + attr + "'");
+            }
+
+            mapBasedMethodDefinitionSource.addSecureMethod(targetClass, method, attr);
+        }
+
+        return matches;
+    }
+
+    public void setPointcutMap(Map map) {
+        Assert.notEmpty(map);
+        Iterator i = map.keySet().iterator();
+        while (i.hasNext()) {
+            String expression = i.next().toString();
+            Object value = map.get(expression);
+            Assert.isInstanceOf(ConfigAttributeDefinition.class, value, "Map keys must be instances of ConfigAttributeDefinition");
+            addPointcut(expression, (ConfigAttributeDefinition) value);
+        }
+    }
+
+    public void addPointcut(String pointcutExpression, ConfigAttributeDefinition definition) {
+        Assert.hasText(pointcutExpression, "An AspectJ pointcut expression is required");
+        Assert.notNull(definition, "ConfigAttributeDefinition required");
+        pointcutMap.put(pointcutExpression, definition);
+
+        if (logger.isDebugEnabled()) {
+            logger.debug("AspectJ pointcut expression '" + pointcutExpression + "' registered for security configuration attribute '" + definition + "'");
+        }
+    }
+
 }

+ 2 - 3
core/src/test/java/org/springframework/security/config/ConfigTestUtils.java

@@ -6,9 +6,8 @@ public abstract class ConfigTestUtils {
         "        <user-service id='us'>" +
         "            <user name='bob' password='bobspassword' authorities='ROLE_A,ROLE_B' />" +
         "            <user name='bill' password='billspassword' authorities='ROLE_A,ROLE_B,AUTH_OTHER' />" +
+        "            <user name='admin' password='password' authorities='ROLE_ADMIN,ROLE_USER' />" +
+        "            <user name='user' password='password' authorities='ROLE_USER' />" +
         "        </user-service>" +
         "    </authentication-provider>";
-
-
-
 }

+ 54 - 0
core/src/test/java/org/springframework/security/intercept/method/MapBasedMethodDefinitionSourceTests.java

@@ -0,0 +1,54 @@
+package org.springframework.security.intercept.method;
+
+import static org.junit.Assert.*;
+
+import java.lang.reflect.Method;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.security.ConfigAttributeDefinition;
+
+/**
+ * Tests for {@link MapBasedMethodDefinitionSource}.
+ *
+ * @author Luke Taylor
+ * @since 2.0.4
+ */
+public class MapBasedMethodDefinitionSourceTests {
+    private final ConfigAttributeDefinition ROLE_A = new ConfigAttributeDefinition("ROLE_A");
+    private final ConfigAttributeDefinition ROLE_B = new ConfigAttributeDefinition("ROLE_B");
+    private MapBasedMethodDefinitionSource mds;
+    private Method someMethodString;
+    private Method someMethodInteger;
+
+    @Before
+    public void initialize() throws Exception {
+        mds = new MapBasedMethodDefinitionSource();
+        someMethodString = MockService.class.getMethod("someMethod", String.class);
+        someMethodInteger = MockService.class.getMethod("someMethod", Integer.class);
+    }
+
+    @Test
+    public void wildcardedMatchIsOverwrittenByMoreSpecificMatch() {
+        mds.addSecureMethod(MockService.class, "some*", ROLE_A);
+        mds.addSecureMethod(MockService.class, "someMethod*", ROLE_B);
+        assertEquals(ROLE_B, mds.getAttributes(someMethodInteger, MockService.class));
+    }
+
+    @Test
+    public void methodsWithDifferentArgumentsAreMatchedCorrectly() throws Exception {
+        mds.addSecureMethod(MockService.class, someMethodInteger, ROLE_A);
+        mds.addSecureMethod(MockService.class, someMethodString, ROLE_B);
+
+        assertEquals(ROLE_A, mds.getAttributes(someMethodInteger, MockService.class));
+        assertEquals(ROLE_B, mds.getAttributes(someMethodString, MockService.class));
+    }
+
+    private class MockService {
+        public void someMethod(String s) {
+        }
+
+        public void someMethod(Integer i) {
+        }
+    }
+}