1
0
mirror of synced 2026-08-04 17:27:13 +00:00

SEC-2151: Support binding method arguments with Annotations

This allow utilizing method arguments for method access control on
interfaces prior to JDK 8.
This commit is contained in:
Rob Winch
2013-09-27 11:18:37 -05:00
parent fb0a8d19e8
commit a09756745f
9 changed files with 547 additions and 36 deletions
@@ -0,0 +1,47 @@
/*
* Copyright 2002-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.springframework.security.access.method;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.security.core.parameters.AnnotationParameterNameDiscoverer;
/**
* An annotation that can be used along with
* {@link AnnotationParameterNameDiscoverer} to specify parameter names. This is
* useful for interfaces prior to JDK 8 which cannot contain the parameter
* names.
*
* @see AnnotationParameterNameDiscoverer
*
* @author Rob Winch
* @since 3.2
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface P {
/**
* The parameter name
* @return
*/
String value();
}
@@ -0,0 +1,228 @@
/*
* Copyright 2002-2013 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.security.core.parameters;
import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.PrioritizedParameterNameDiscoverer;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.security.access.method.P;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* Allows finding parameter names using the value attribute of any number of
* {@link Annotation} instances. This is useful when needing to discover the
* parameter names of interfaces with Spring Security's method level security.
* For example, consider the following:
*
* <pre>
* import org.springframework.security.access.method.P;
*
* @PostAuthorize("#to == returnObject.to")
* public Message findMessageByTo(@P("to") String to);
* </pre>
*
* We can make this possible using the following
* {@link AnnotationParameterNameDiscoverer}:
*
* <pre>
* ParameterAnnotationsNameDiscoverer discoverer = new ParameterAnnotationsNameDiscoverer(
* &quot;org.springframework.security.access.method.P&quot;);
* </pre>
*
* <p>
* It is common for users to use {@link AnnotationParameterNameDiscoverer} in
* conjuction with {@link PrioritizedParameterNameDiscoverer}. In fact, Spring
* Security's {@link DefaultSecurityParameterNameDiscoverer} (which is used by
* default with method level security) extends
* {@link PrioritizedParameterNameDiscoverer} and will automatically support
* both {@link P} and Spring Data's Param annotation if it is found on the
* classpath.
* </p>
*
* <p>
* It is important that all the parameter names have a supported annotation on
* them. Otherwise, the result will be null. For example, consider the
* following:
* </p>
*
* <pre>
* import org.springframework.security.access.method.P;
*
* @PostAuthorize("#to == returnObject.to")
* public Message findMessageByToAndFrom(@P("to") User to, User from);
* </pre>
*
* <p>
* The result of finding parameters on the previous sample will be a null
* String[] since only a single parameter contains an annotation. This is mostly
* due to the fact that the fallbacks for
* {@link PrioritizedParameterNameDiscoverer} are an all or nothing operation.
* </p>
*
* @see DefaultSecurityParameterNameDiscoverer
*
* @author Rob Winch
* @since 3.2
*/
public class AnnotationParameterNameDiscoverer implements
ParameterNameDiscoverer {
private final Set<String> annotationClassesToUse;
public AnnotationParameterNameDiscoverer(String... annotationClassToUse) {
this(new HashSet<String>(Arrays.asList(annotationClassToUse)));
}
public AnnotationParameterNameDiscoverer(Set<String> annotationClassesToUse) {
Assert.notEmpty(annotationClassesToUse,
"annotationClassesToUse cannot be null or empty");
this.annotationClassesToUse = annotationClassesToUse;
}
/*
* (non-Javadoc)
*
* @see
* org.springframework.core.ParameterNameDiscoverer#getParameterNames(java
* .lang.reflect.Method)
*/
public String[] getParameterNames(Method method) {
Method originalMethod = BridgeMethodResolver.findBridgedMethod(method);
String[] paramNames = lookupParameterNames(METHOD_METHODPARAM_FACTORY, originalMethod);
if(paramNames != null) {
return paramNames;
}
Class<?> declaringClass = method.getDeclaringClass();
Class<?>[] interfaces = declaringClass.getInterfaces();
for(Class<?> intrfc : interfaces) {
Method intrfcMethod = ReflectionUtils.findMethod(intrfc, method.getName(), method.getParameterTypes());
if(intrfcMethod != null) {
return lookupParameterNames(METHOD_METHODPARAM_FACTORY, intrfcMethod);
}
}
return paramNames;
}
/*
* (non-Javadoc)
*
* @see
* org.springframework.core.ParameterNameDiscoverer#getParameterNames(java
* .lang.reflect.Constructor)
*/
public String[] getParameterNames(Constructor<?> constructor) {
return lookupParameterNames(CONSTRUCTOR_METHODPARAM_FACTORY,
constructor);
}
/**
* Gets the parameter names or null if not found.
*
* @param parameterNameFactory the {@link ParameterNameFactory} to use
* @param t the {@link AccessibleObject} to find the parameter names on (i.e. Method or Constructor)
* @return the parameter names or null
*/
private <T extends AccessibleObject> String[] lookupParameterNames(
ParameterNameFactory<T> parameterNameFactory, T t) {
int parameterCount = parameterNameFactory.getParamCount(t);
String[] paramNames = new String[parameterCount];
for (int i = 0; i < parameterCount; i++) {
Annotation[] annotations = parameterNameFactory.findAnnotationsAt(t, i);
String parameterName = findParameterName(annotations);
if (parameterName == null) {
return null;
} else {
paramNames[i] = parameterName;
}
}
return paramNames;
}
/**
* Finds the parameter name from the provided {@link Annotation}s or null if
* it could not find it. The search is done by looking at the value property
* of the {@link #annotationClassesToUse}.
*
* @param parameterAnnotations
* the {@link Annotation}'s to search.
* @return
*/
private String findParameterName(Annotation[] parameterAnnotations) {
for (Annotation paramAnnotation : parameterAnnotations) {
if (annotationClassesToUse.contains(paramAnnotation
.annotationType().getName())) {
return (String) AnnotationUtils.getValue(paramAnnotation,
"value");
}
}
return null;
}
private static final ParameterNameFactory<Constructor<?>> CONSTRUCTOR_METHODPARAM_FACTORY = new ParameterNameFactory<Constructor<?>>() {
public int getParamCount(Constructor<?> constructor) {
return constructor.getParameterTypes().length;
}
public Annotation[] findAnnotationsAt(Constructor<?> constructor, int index) {
return constructor.getParameterAnnotations()[index];
}
};
private static final ParameterNameFactory<Method> METHOD_METHODPARAM_FACTORY = new ParameterNameFactory<Method>() {
public int getParamCount(Method method) {
return method.getParameterTypes().length;
}
public Annotation[] findAnnotationsAt(Method method, int index) {
return method.getParameterAnnotations()[index];
}
};
/**
* Strategy interface for looking up the parameter names.
*
* @author Rob Winch
* @since 3.2
*
* @param <T> the type to inspect (i.e. {@link Method} or {@link Constructor})
*/
private interface ParameterNameFactory<T extends AccessibleObject> {
/**
* Gets the parameter count
* @param t
* @return
*/
int getParamCount(T t);
/**
* Gets the {@link Annotation}s at a specified index
* @param t
* @param index
* @return
*/
Annotation[] findAnnotationsAt(T t, int index);
}
}
@@ -16,13 +16,16 @@
package org.springframework.security.core.parameters;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.PrioritizedParameterNameDiscoverer;
import org.springframework.security.access.method.P;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
@@ -33,6 +36,9 @@ import org.springframework.util.ClassUtils;
* classpath.
*
* <ul>
* <li>Will use an instance of {@link AnnotationParameterNameDiscoverer} with
* {@link P} as a valid annotation. If, Spring Data is on the classpath will
* also add Param annotation.</li>
* <li>If Spring 4 is on the classpath, then DefaultParameterNameDiscoverer is
* added. This attempts to use JDK 8 information first and falls back to
* {@link LocalVariableTableParameterNameDiscoverer}.</li>
@@ -40,6 +46,8 @@ import org.springframework.util.ClassUtils;
* {@link LocalVariableTableParameterNameDiscoverer} is added directly.</li>
* </ul>
*
* @see AnnotationParameterNameDiscoverer
*
* @author Rob Winch
* @since 3.2
*/
@@ -53,6 +61,10 @@ public class DefaultSecurityParameterNameDiscoverer extends
private static final boolean DEFAULT_PARAM_DISCOVERER_PRESENT =
ClassUtils.isPresent(DEFAULT_PARAMETER_NAME_DISCOVERER_CLASSNAME, DefaultSecurityParameterNameDiscoverer.class.getClassLoader());
private static final String DATA_PARAM_CLASSNAME = "org.springframework.data.repository.query.Param";
private static final boolean DATA_PARAM_PRESENT =
ClassUtils.isPresent(DATA_PARAM_CLASSNAME, DefaultSecurityParameterNameDiscoverer.class.getClassLoader());
/**
* Creates a new instance with only the default
* {@link ParameterNameDiscoverer} instances.
@@ -71,6 +83,15 @@ public class DefaultSecurityParameterNameDiscoverer extends
for(ParameterNameDiscoverer discover : parameterNameDiscovers) {
addDiscoverer(discover);
}
Set<String> annotationClassesToUse = new HashSet<String>(2);
annotationClassesToUse.add(P.class.getName());
if(DATA_PARAM_PRESENT) {
annotationClassesToUse.add(DATA_PARAM_CLASSNAME);
}
addDiscoverer(new AnnotationParameterNameDiscoverer(annotationClassesToUse));
if (DEFAULT_PARAM_DISCOVERER_PRESENT) {
try {
Class<? extends ParameterNameDiscoverer> paramNameDiscoverClass = (Class<? extends ParameterNameDiscoverer>) ClassUtils
@@ -0,0 +1,106 @@
package org.springframework.security.core.parameters;
import static org.fest.assertions.Assertions.assertThat;
import org.junit.Before;
import org.junit.Test;
import org.springframework.security.access.method.P;
import org.springframework.util.ReflectionUtils;
public class AnnotationParameterNameDiscovererTests {
private AnnotationParameterNameDiscoverer discoverer;
@Before
public void setup() {
discoverer = new AnnotationParameterNameDiscoverer(P.class.getName());
}
@Test
public void getParameterNamesInterfaceSingleParam() {
assertThat(discoverer.getParameterNames(ReflectionUtils.findMethod(Dao.class, "findMessageByTo", String.class))).isEqualTo(new String [] { "to"});
}
@Test
public void getParameterNamesInterfaceSingleParamAnnotatedWithMultiParams() {
assertThat(discoverer.getParameterNames(ReflectionUtils.findMethod(Dao.class, "findMessageByToAndFrom", String.class, String.class))).isNull();
}
@Test
public void getParameterNamesInterfaceNoAnnotation() {
assertThat(discoverer.getParameterNames(ReflectionUtils.findMethod(Dao.class, "findMessageByIdNoAnnotation", String.class))).isNull();
}
@Test
public void getParameterNamesClassSingleParam() {
assertThat(discoverer.getParameterNames(ReflectionUtils.findMethod(Dao.class, "findMessageByTo", String.class))).isEqualTo(new String [] { "to"});
}
@Test
public void getParameterNamesClassSingleParamAnnotatedWithMultiParams() {
assertThat(discoverer.getParameterNames(ReflectionUtils.findMethod(Dao.class, "findMessageByToAndFrom", String.class, String.class))).isNull();
}
@Test
public void getParameterNamesClassNoAnnotation() {
assertThat(discoverer.getParameterNames(ReflectionUtils.findMethod(Dao.class, "findMessageByIdNoAnnotation", String.class))).isNull();
}
@Test
public void getParameterNamesConstructor() throws Exception {
assertThat(discoverer.getParameterNames(Impl.class.getConstructor(String.class))).isEqualTo(new String[] { "id"});
}
@Test
public void getParameterNamesConstructorNoAnnotation() throws Exception {
assertThat(discoverer.getParameterNames(Impl.class.getConstructor(Long.class))).isNull();
}
@Test
public void getParameterNamesClassAnnotationOnInterface() throws Exception {
assertThat(discoverer.getParameterNames(ReflectionUtils.findMethod(DaoImpl.class, "findMessageByTo", String.class))).isEqualTo(new String[] {"to"});
assertThat(discoverer.getParameterNames(ReflectionUtils.findMethod(Dao.class, "findMessageByTo", String.class))).isEqualTo(new String[] {"to"});
}
@Test
public void getParameterNamesClassAnnotationOnImpl() throws Exception {
assertThat(discoverer.getParameterNames(ReflectionUtils.findMethod(Dao.class, "findMessageByToAndFrom", String.class, String.class))).isNull();
assertThat(discoverer.getParameterNames(ReflectionUtils.findMethod(DaoImpl.class, "findMessageByToAndFrom", String.class, String.class))).isEqualTo(new String[] {"to", "from"});
}
@Test
public void getParameterNamesClassAnnotationOnBaseClass() throws Exception {
assertThat(discoverer.getParameterNames(ReflectionUtils.findMethod(Dao.class, "findMessageByIdNoAnnotation", String.class))).isNull();
assertThat(discoverer.getParameterNames(ReflectionUtils.findMethod(DaoImpl.class, "findMessageByIdNoAnnotation", String.class))).isEqualTo(new String[] {"id"});
}
interface Dao {
String findMessageByTo(@P("to") String to);
String findMessageByToAndFrom(@P("to") String to, String from);
String findMessageByIdNoAnnotation(String id);
}
static class BaseDaoImpl {
public String findMessageByIdNoAnnotation(@P("id") String id) { return null; }
}
static class DaoImpl extends BaseDaoImpl implements Dao {
public String findMessageByTo(String to) { return null; }
public String findMessageByToAndFrom(@P("to") String to, @P("from") String from) { return null; }
}
static class Impl {
public Impl(Long dataSourceId) {}
public Impl(@P("id") String dataSourceId) {}
String findMessageByTo(@P("to") String to) { return null; }
String findMessageByToAndFrom(@P("to") String to, String from) { return null; }
String findMessageByIdNoAnnotation(String id) { return null; }
}
}
@@ -19,12 +19,14 @@ import static org.fest.assertions.Assertions.assertThat;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.security.access.method.P;
import org.springframework.test.util.ReflectionTestUtils;
/**
@@ -45,8 +47,16 @@ public class DefaultSecurityParameterNameDiscovererTests {
public void constructorDefault() {
List<ParameterNameDiscoverer> discoverers = (List<ParameterNameDiscoverer>) ReflectionTestUtils
.getField(discoverer, "parameterNameDiscoverers");
assertThat(discoverers.size()).isEqualTo(1);
assertThat(discoverers.get(0)).isInstanceOf(
assertThat(discoverers.size()).isEqualTo(2);
ParameterNameDiscoverer annotationDisc = discoverers.get(0);
assertThat(annotationDisc).isInstanceOf(
AnnotationParameterNameDiscoverer.class);
Set<String> annotationsToUse = (Set<String>)ReflectionTestUtils.getField(annotationDisc, "annotationClassesToUse");
assertThat(annotationsToUse).containsOnly(P.class.getName());
assertThat(discoverers.get(1)).isInstanceOf(
DefaultParameterNameDiscoverer.class);
}
@@ -57,10 +67,17 @@ public class DefaultSecurityParameterNameDiscovererTests {
List<ParameterNameDiscoverer> discoverers = (List<ParameterNameDiscoverer>) ReflectionTestUtils
.getField(discoverer, "parameterNameDiscoverers");
assertThat(discoverers.size()).isEqualTo(2);
assertThat(discoverers.size()).isEqualTo(3);
assertThat(discoverers.get(0)).isInstanceOf(
LocalVariableTableParameterNameDiscoverer.class);
assertThat(discoverers.get(1)).isInstanceOf(
ParameterNameDiscoverer annotationDisc = discoverers.get(1);
assertThat(annotationDisc).isInstanceOf(
AnnotationParameterNameDiscoverer.class);
Set<String> annotationsToUse = (Set<String>)ReflectionTestUtils.getField(annotationDisc, "annotationClassesToUse");
assertThat(annotationsToUse).containsOnly(P.class.getName());
assertThat(discoverers.get(2)).isInstanceOf(
DefaultParameterNameDiscoverer.class);
}
}