View Javadoc

1   /* Copyright 2004, 2005, 2006 Acegi Technology Pty Limited
2    *
3    * Licensed under the Apache License, Version 2.0 (the "License");
4    * you may not use this file except in compliance with the License.
5    * You may obtain a copy of the License at
6    *
7    *     http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  
16  package org.springframework.security.intercept.method;
17  
18  import java.lang.reflect.Method;
19  import java.util.ArrayList;
20  import java.util.Collection;
21  import java.util.Collections;
22  import java.util.HashMap;
23  import java.util.Iterator;
24  import java.util.List;
25  import java.util.Map;
26  
27  import org.apache.commons.logging.Log;
28  import org.apache.commons.logging.LogFactory;
29  import org.springframework.beans.factory.BeanClassLoaderAware;
30  import org.springframework.security.ConfigAttributeDefinition;
31  import org.springframework.util.Assert;
32  import org.springframework.util.ClassUtils;
33  
34  
35  /**
36   * Stores a {@link ConfigAttributeDefinition} for a method or class signature.
37   *
38   * <p>
39   * This class is the preferred implementation of {@link MethodDefinitionSource} for XML-based
40   * definition of method security metadata. To assist in XML-based definition, wildcard support
41   * is provided.
42   * </p>
43   *
44   * @author Ben Alex
45   * @version $Id: MapBasedMethodDefinitionSource.java 3254 2008-08-18 13:13:18Z luke_t $
46   * @since 2.0
47   */
48  public class MapBasedMethodDefinitionSource extends AbstractFallbackMethodDefinitionSource implements BeanClassLoaderAware {
49      //~ Static fields/initializers =====================================================================================
50  
51      private static final Log logger = LogFactory.getLog(MapBasedMethodDefinitionSource.class);
52  
53      //~ Instance fields ================================================================================================
54      private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader();
55  
56      /** Map from RegisteredMethod to ConfigAttributeDefinition */
57      protected Map methodMap = new HashMap();
58  
59      /** Map from RegisteredMethod to name pattern used for registration */
60      private Map nameMap = new HashMap();
61  
62      //~ Methods ========================================================================================================
63  
64      public MapBasedMethodDefinitionSource() {
65      }
66  
67      /**
68       * Creates the MapBasedMethodDefinitionSource from a
69       * @param methodMap map of method names to <tt>ConfigAttributeDefinition</tt>s.
70       */
71      public MapBasedMethodDefinitionSource(Map methodMap) {
72          Iterator iterator = methodMap.entrySet().iterator();
73  
74          while (iterator.hasNext()) {
75              Map.Entry entry = (Map.Entry) iterator.next();
76              addSecureMethod((String)entry.getKey(), (ConfigAttributeDefinition)entry.getValue());
77          }
78      }
79  
80      /**
81       * Implementation does not support class-level attributes.
82       */
83      protected ConfigAttributeDefinition findAttributes(Class clazz) {
84          return null;
85      }
86  
87      /**
88       * Will walk the method inheritance tree to find the most specific declaration applicable.
89       */
90      protected ConfigAttributeDefinition findAttributes(Method method, Class targetClass) {
91          if (targetClass == null) {
92              return null;
93          }
94  
95          return findAttributesSpecifiedAgainst(method, targetClass);
96      }
97  
98      private ConfigAttributeDefinition findAttributesSpecifiedAgainst(Method method, Class clazz) {
99          RegisteredMethod registeredMethod = new RegisteredMethod(method, clazz);
100         if (methodMap.containsKey(registeredMethod)) {
101             return (ConfigAttributeDefinition) methodMap.get(registeredMethod);
102         }
103         // Search superclass
104         if (clazz.getSuperclass() != null) {
105             return findAttributesSpecifiedAgainst(method, clazz.getSuperclass());
106         }
107         return null;
108     }
109 
110     /**
111      * Add configuration attributes for a secure method. Method names can end or start with <code>&#42</code>
112      * for matching multiple methods.
113      *
114      * @param name type and method name, separated by a dot
115      * @param attr required authorities associated with the method
116      */
117     public void addSecureMethod(String name, ConfigAttributeDefinition attr) {
118         int lastDotIndex = name.lastIndexOf(".");
119 
120         if (lastDotIndex == -1) {
121             throw new IllegalArgumentException("'" + name + "' is not a valid method name: format is FQN.methodName");
122         }
123 
124         String methodName = name.substring(lastDotIndex + 1);
125         Assert.hasText(methodName, "Method not found for '" + name + "'");
126 
127         String typeName = name.substring(0, lastDotIndex);
128         Class type = ClassUtils.resolveClassName(typeName, this.beanClassLoader);
129 
130         addSecureMethod(type, methodName, attr);
131     }
132 
133     /**
134      * Add configuration attributes for a secure method. Mapped method names can end or start with <code>&#42</code>
135      * for matching multiple methods.
136      *
137      * @param javaType target interface or class the security configuration attribute applies to
138      * @param mappedName mapped method name, which the javaType has declared or inherited
139      * @param attr required authorities associated with the method
140      */
141     public void addSecureMethod(Class javaType, String mappedName, ConfigAttributeDefinition attr) {
142         String name = javaType.getName() + '.' + mappedName;
143 
144         if (logger.isDebugEnabled()) {
145             logger.debug("Request to add secure method [" + name + "] with attributes [" + attr + "]");
146         }
147 
148         Method[] methods = javaType.getMethods();
149         List matchingMethods = new ArrayList();
150 
151         for (int i = 0; i < methods.length; i++) {
152             if (methods[i].getName().equals(mappedName) || isMatch(methods[i].getName(), mappedName)) {
153                 matchingMethods.add(methods[i]);
154             }
155         }
156 
157         if (matchingMethods.isEmpty()) {
158             throw new IllegalArgumentException("Couldn't find method '" + mappedName + "' on '" + javaType + "'");
159         }
160 
161         // register all matching methods
162         for (Iterator it = matchingMethods.iterator(); it.hasNext();) {
163             Method method = (Method) it.next();
164             RegisteredMethod registeredMethod = new RegisteredMethod(method, javaType);
165             String regMethodName = (String) this.nameMap.get(registeredMethod);
166 
167             if ((regMethodName == null) || (!regMethodName.equals(name) && (regMethodName.length() <= name.length()))) {
168                 // no already registered method name, or more specific
169                 // method name specification now -> (re-)register method
170                 if (regMethodName != null) {
171                     logger.debug("Replacing attributes for secure method [" + method + "]: current name [" + name
172                         + "] is more specific than [" + regMethodName + "]");
173                 }
174 
175                 this.nameMap.put(registeredMethod, name);
176                 addSecureMethod(registeredMethod, attr);
177             } else {
178                 logger.debug("Keeping attributes for secure method [" + method + "]: current name [" + name
179                     + "] is not more specific than [" + regMethodName + "]");
180             }
181         }
182     }
183 
184     /**
185      * Adds configuration attributes for a specific method, for example where the method has been
186      * matched using a pointcut expression. If a match already exists in the map for the method, then
187      * the existing match will be retained, so that if this method is called for a more general pointcut
188      * it will not override a more specific one which has already been added. This
189      */
190     public void addSecureMethod(Class javaType, Method method, ConfigAttributeDefinition attr) {
191         RegisteredMethod key = new RegisteredMethod(method, javaType);
192 
193         if (methodMap.containsKey(key)) {
194             logger.debug("Method [" + method + "] is already registered with attributes [" + methodMap.get(key) + "]");
195             return;
196         }
197 
198         methodMap.put(key, attr);
199     }
200 
201     /**
202      * Add configuration attributes for a secure method.
203      *
204      * @param method the method to be secured
205      * @param attr required authorities associated with the method
206      */
207     private void addSecureMethod(RegisteredMethod method, ConfigAttributeDefinition attr) {
208         Assert.notNull(method, "RegisteredMethod required");
209         Assert.notNull(attr, "Configuration attribute required");
210         if (logger.isInfoEnabled()) {
211             logger.info("Adding secure method [" + method + "] with attributes [" + attr + "]");
212         }
213         this.methodMap.put(method, attr);
214     }
215 
216     /**
217      * Obtains the configuration attributes explicitly defined against this bean.
218      *
219      * @return the attributes explicitly defined against this bean
220      */
221     public Collection getConfigAttributeDefinitions() {
222         return Collections.unmodifiableCollection(methodMap.values());
223     }
224 
225     /**
226      * Return if the given method name matches the mapped name. The default implementation checks for "xxx" and
227      * "xxx" matches.
228      *
229      * @param methodName the method name of the class
230      * @param mappedName the name in the descriptor
231      *
232      * @return if the names match
233      */
234     private boolean isMatch(String methodName, String mappedName) {
235         return (mappedName.endsWith("*") && methodName.startsWith(mappedName.substring(0, mappedName.length() - 1)))
236         || (mappedName.startsWith("*") && methodName.endsWith(mappedName.substring(1, mappedName.length())));
237     }
238 
239     public void setBeanClassLoader(ClassLoader beanClassLoader) {
240         Assert.notNull(beanClassLoader, "Bean class loader required");
241         this.beanClassLoader = beanClassLoader;
242     }
243 
244     /**
245      * @return map size (for unit tests and diagnostics)
246      */
247     public int getMethodMapSize() {
248         return methodMap.size();
249     }
250 
251     /**
252      * Stores both the Java Method as well as the Class we obtained the Method from. This is necessary because Method only
253      * provides us access to the declaring class. It doesn't provide a way for us to introspect which Class the Method
254      * was registered against. If a given Class inherits and redeclares a method (i.e. calls super();) the registered Class
255      * and declaring Class are the same. If a given class merely inherits but does not redeclare a method, the registered
256      * Class will be the Class we're invoking against and the Method will provide details of the declared class.
257      */
258     private class RegisteredMethod {
259         private Method method;
260         private Class registeredJavaType;
261 
262         public RegisteredMethod(Method method, Class registeredJavaType) {
263             Assert.notNull(method, "Method required");
264             Assert.notNull(registeredJavaType, "Registered Java Type required");
265             this.method = method;
266             this.registeredJavaType = registeredJavaType;
267         }
268 
269         public boolean equals(Object obj) {
270             if (this == obj) {
271                 return true;
272             }
273             if (obj != null && obj instanceof RegisteredMethod) {
274                 RegisteredMethod rhs = (RegisteredMethod) obj;
275                 return method.equals(rhs.method) && registeredJavaType.equals(rhs.registeredJavaType);
276             }
277             return false;
278         }
279 
280         public int hashCode() {
281             return method.hashCode() * registeredJavaType.hashCode();
282         }
283 
284         public String toString() {
285             return "RegisteredMethod[" + registeredJavaType.getName() + "; " + method + "]";
286         }
287     }
288 
289 }