SEC-507: Initial support for JSR-250 "RolesAllowed" attributes.

Added jsr250 boolean to annotation-driven element to determine whether JSR-250 annotations should be used in preference to the traditional Acegi "Secured" attribute.
This commit is contained in:
Luke Taylor 2008-01-10 20:19:15 +00:00
parent fad2b597af
commit d66b9693ba
11 changed files with 344 additions and 26 deletions

View File

@ -33,6 +33,12 @@
<artifactId>spring-jdbc</artifactId> <artifactId>spring-jdbc</artifactId>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>annotations-api</artifactId>
<version>6.0.14</version>
<optional>true</optional>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@ -0,0 +1,101 @@
package org.springframework.security.annotation;
import org.springframework.security.SecurityConfig;
import org.springframework.metadata.Attributes;
import org.springframework.core.annotation.AnnotationUtils;
import javax.annotation.security.PermitAll;
import javax.annotation.security.RolesAllowed;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.lang.reflect.Method;
import java.lang.reflect.Field;
import java.lang.annotation.Annotation;
/**
* Java 5 Annotation <code>Attributes</code> metadata implementation used for secure method interception.
* <p>
* This <code>Attributes</code> implementation will return security configuration for classes described using the
* <code>RolesAllowed</code> Java JEE 5 annotation.
* <p>
* The <code>SecurityAnnotationAttributes</code> implementation can be used to configure a
* <code>MethodDefinitionAttributes</code> and <code>MethodSecurityInterceptor</code> bean definition.
*
* @author Mark St.Godard
* @author Usama Rashwan
* @author Luke Taylor
* @since 2.0
*
* @see javax.annotation.security.RolesAllowed
*/
public class Jsr250SecurityAnnotationAttributes implements Attributes {
//~ Methods ========================================================================================================
/**
* Get the <code>RolesAllowed</code> attributes for a given target class.
* This method will return an empty Collection because the call to getAttributes(method) will override the class
* annotation.
*
* @param target The target Object
* @return Empty Collection of <code>SecurityConfig</code>
*
* @see Attributes#getAttributes
*/
public Collection<SecurityConfig> getAttributes(Class target) {
return new HashSet<SecurityConfig>();
}
/**
* Get the <code>RolesAllowed</code> attributes for a given target method.
*
* @param method The target method
* @return Collection of <code>SecurityConfig</code>
* @see Attributes#getAttributes
*/
public Collection<SecurityConfig> getAttributes(Method method) {
Annotation[] annotations = AnnotationUtils.getAnnotations(method);
Collection<SecurityConfig> attributes = populateSecurityConfigWithRolesAllowed(annotations);
// if there is no RolesAllowed defined on the Method then we will use the one defined on the class
// level , according to JSR 250
if (attributes.size()==0 && !method.isAnnotationPresent(PermitAll.class)) {
attributes = populateSecurityConfigWithRolesAllowed(method.getDeclaringClass().getDeclaredAnnotations());
}
return attributes;
}
protected Collection<SecurityConfig> populateSecurityConfigWithRolesAllowed (Annotation[] annotations) {
Set<SecurityConfig> attributes = new HashSet<SecurityConfig>();
for (Annotation annotation : annotations) {
// check for RolesAllowed annotations
if (annotation instanceof RolesAllowed) {
RolesAllowed attr = (RolesAllowed) annotation;
for (String auth : attr.value()) {
attributes.add(new SecurityConfig(auth));
}
break;
}
}
return attributes;
}
public Collection getAttributes(Class clazz, Class filter) {
throw new UnsupportedOperationException();
}
public Collection getAttributes(Method method, Class clazz) {
throw new UnsupportedOperationException();
}
public Collection getAttributes(Field field) {
throw new UnsupportedOperationException();
}
public Collection getAttributes(Field field, Class clazz) {
throw new UnsupportedOperationException();
}
}

View File

@ -15,26 +15,29 @@
package org.springframework.security.annotation; package org.springframework.security.annotation;
import javax.annotation.security.RolesAllowed;
/** /**
* DOCUMENT ME! * @version $Id$
*
* @author $author$
* @version $Revision: 2145 $
*/ */
@Secured({"ROLE_USER"}) @Secured({"ROLE_USER"})
public interface BusinessService { public interface BusinessService {
//~ Methods ======================================================================================================== //~ Methods ========================================================================================================
@Secured({"ROLE_ADMIN"}) @Secured({"ROLE_ADMIN"})
@RolesAllowed({"ROLE_ADMIN"})
public void someAdminMethod(); public void someAdminMethod();
@Secured({"ROLE_USER", "ROLE_ADMIN"}) @Secured({"ROLE_USER", "ROLE_ADMIN"})
@RolesAllowed({"ROLE_USER", "ROLE_ADMIN"})
public void someUserAndAdminMethod(); public void someUserAndAdminMethod();
@Secured({"ROLE_USER"}) @Secured({"ROLE_USER"})
@RolesAllowed({"ROLE_USER"})
public void someUserMethod1(); public void someUserMethod1();
@Secured({"ROLE_USER"}) @Secured({"ROLE_USER"})
@RolesAllowed({"ROLE_USER"})
public void someUserMethod2(); public void someUserMethod2();
public int someOther(int input); public int someOther(int input);

View File

@ -0,0 +1,31 @@
package org.springframework.security.annotation;
import javax.annotation.security.RolesAllowed;
/**
*
* @author Luke Taylor
* @version $Id$
*/
public class Jsr250BusinessServiceImpl implements BusinessService {
@RolesAllowed({"ROLE_USER"})
public void someUserMethod1() {
}
@RolesAllowed({"ROLE_USER"})
public void someUserMethod2() {
}
@RolesAllowed({"ROLE_USER", "ROLE_ADMIN"})
public void someUserAndAdminMethod() {
}
@RolesAllowed({"ROLE_ADMIN"})
public void someAdminMethod() {
}
public int someOther(int input) {
return input;
}
}

View File

@ -0,0 +1,85 @@
package org.springframework.security.annotation;
import org.springframework.security.SecurityConfig;
import org.junit.Test;
import static org.junit.Assert.*;
import java.util.List;
import java.util.ArrayList;
import javax.annotation.security.RolesAllowed;
import javax.annotation.security.PermitAll;
/**
* @author Luke Taylor
* @version $Id$
*/
public class Jsr250SecurityAnnotationAttributesTests {
Jsr250SecurityAnnotationAttributes attributes = new Jsr250SecurityAnnotationAttributes();
A a = new A();
B b = new B();
@Test
public void methodWithRolesAllowedHasCorrectAttribute() throws Exception {
// Method[] methods = a.getClass().getMethods();
List<SecurityConfig> accessAttributes =
new ArrayList<SecurityConfig>(attributes.getAttributes(a.getClass().getMethod("adminMethod")));
assertEquals(1, accessAttributes.size());
assertEquals("ADMIN", accessAttributes.get(0).getAttribute());
}
@Test
public void permitAllMethodHasNoAttributes() throws Exception {
List<SecurityConfig> accessAttributes =
new ArrayList<SecurityConfig>(attributes.getAttributes(a.getClass().getMethod("permitAllMethod")));
assertEquals(0, accessAttributes.size());
}
@Test
public void noRoleMethodHasNoAttributes() throws Exception {
List<SecurityConfig> accessAttributes =
new ArrayList<SecurityConfig>(attributes.getAttributes(a.getClass().getMethod("noRoleMethod")));
assertEquals(0, accessAttributes.size());
}
@Test
public void classRoleIsAppliedNoRoleMethod() throws Exception {
List<SecurityConfig> accessAttributes =
new ArrayList<SecurityConfig>(attributes.getAttributes(b.getClass().getMethod("noRoleMethod")));
assertEquals(1, accessAttributes.size());
assertEquals("USER", accessAttributes.get(0).getAttribute());
}
@Test
public void methodRoleOverridesClassRole() throws Exception {
List<SecurityConfig> accessAttributes =
new ArrayList<SecurityConfig>(attributes.getAttributes(b.getClass().getMethod("adminMethod")));
assertEquals(1, accessAttributes.size());
assertEquals("ADMIN", accessAttributes.get(0).getAttribute());
}
//~ Inner Classes ======================================================================================================
public static class A {
public void noRoleMethod() {}
@RolesAllowed("ADMIN")
public void adminMethod() {}
@PermitAll
public void permitAllMethod() {}
}
@RolesAllowed("USER")
public static class B {
public void noRoleMethod() {}
@RolesAllowed("ADMIN")
public void adminMethod() {}
}
}

View File

@ -1,7 +1,5 @@
package org.springframework.security.config; package org.springframework.security.config;
import static org.junit.Assert.fail;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -37,13 +35,9 @@ public class AnnotationDrivenBeanDefinitionParserTests {
SecurityContextHolder.clearContext(); SecurityContextHolder.clearContext();
} }
@Test @Test(expected=AuthenticationCredentialsNotFoundException.class)
public void targetShouldPreventProtectedMethodInvocationWithNoContext() { public void targetShouldPreventProtectedMethodInvocationWithNoContext() {
try {
target.someUserMethod1(); target.someUserMethod1();
fail("Expected AuthenticationCredentialsNotFoundException");
} catch (AuthenticationCredentialsNotFoundException expected) {
}
} }
@Test @Test
@ -55,16 +49,12 @@ public class AnnotationDrivenBeanDefinitionParserTests {
target.someUserMethod1(); target.someUserMethod1();
} }
@Test @Test(expected=AccessDeniedException.class)
public void targetShouldPreventProtectedMethodInvocationWithIncorrectRole() { public void targetShouldPreventProtectedMethodInvocationWithIncorrectRole() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("Test", "Password", UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("Test", "Password",
new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_SOMEOTHERROLE")}); new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_SOMEOTHERROLE")});
SecurityContextHolder.getContext().setAuthentication(token); SecurityContextHolder.getContext().setAuthentication(token);
try {
target.someAdminMethod(); target.someAdminMethod();
fail("Expected AccessDeniedException");
} catch (AccessDeniedException expected) {
}
} }
} }

View File

@ -0,0 +1,60 @@
package org.springframework.security.config;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.security.AccessDeniedException;
import org.springframework.security.AuthenticationCredentialsNotFoundException;
import org.springframework.security.GrantedAuthority;
import org.springframework.security.GrantedAuthorityImpl;
import org.springframework.security.annotation.BusinessService;
import org.springframework.security.context.SecurityContextHolder;
import org.springframework.security.providers.UsernamePasswordAuthenticationToken;
/**
* @author Luke Taylor
* @version $Id$
*/
public class Jsr250AnnotationDrivenBeanDefinitionParserTests {
private ClassPathXmlApplicationContext appContext;
private BusinessService target;
@Before
public void loadContext() {
appContext = new ClassPathXmlApplicationContext("/org/springframework/security/config/jsr250-annotated-method-security.xml");
target = (BusinessService) appContext.getBean("target");
}
@After
public void closeAppContext() {
if (appContext != null) {
appContext.close();
}
SecurityContextHolder.clearContext();
}
@Test(expected=AuthenticationCredentialsNotFoundException.class)
public void targetShouldPreventProtectedMethodInvocationWithNoContext() {
target.someUserMethod1();
}
@Test
public void targetShouldAllowProtectedMethodInvocationWithCorrectRole() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("Test", "Password",
new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_USER")});
SecurityContextHolder.getContext().setAuthentication(token);
target.someUserMethod1();
}
@Test(expected=AccessDeniedException.class)
public void targetShouldPreventProtectedMethodInvocationWithIncorrectRole() {
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("Test", "Password",
new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_SOMEOTHERROLE")});
SecurityContextHolder.getContext().setAuthentication(token);
target.someAdminMethod();
}
}

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<b:beans xmlns="http://www.springframework.org/schema/security"
xmlns:b="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.xsd">
<b:bean id="target" class="org.springframework.security.annotation.Jsr250BusinessServiceImpl"/>
<annotation-driven jsr250="true"/>
<authentication-provider>
<user-service>
<user name="bob" password="bobspassword" authorities="ROLE_A,ROLE_B" />
<user name="bill" password="billspassword" authorities="ROLE_A,ROLE_B,AUTH_OTHER" />
</user-service>
</authentication-provider>
</b:beans>

View File

@ -23,17 +23,22 @@ import org.w3c.dom.Element;
* @version $Id$ * @version $Id$
*/ */
class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser { class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParser {
public static final String SECURITY_ANNOTATION_ATTRIBUTES_CLASS = "org.springframework.security.annotation.SecurityAnnotationAttributes"; public static final String SECURITY_ANNOTATION_ATTRIBUTES_CLASS = "org.springframework.security.annotation.SecurityAnnotationAttributes";
public static final String JSR_250_SECURITY_ANNOTATION_ATTRIBUTES_CLASS = "org.springframework.security.annotation.Jsr250SecurityAnnotationAttributes";
private static final String ATT_ACCESS_MGR = "access-decision-manager"; private static final String ATT_ACCESS_MGR = "access-decision-manager";
private static final String ATT_USE_JSR250 = "jsr250";
public BeanDefinition parse(Element element, ParserContext parserContext) { public BeanDefinition parse(Element element, ParserContext parserContext) {
String className = "true".equals(element.getAttribute(ATT_USE_JSR250)) ?
JSR_250_SECURITY_ANNOTATION_ATTRIBUTES_CLASS : SECURITY_ANNOTATION_ATTRIBUTES_CLASS;
// Reflectively obtain the Annotation-based ObjectDefinitionSource. // Reflectively obtain the Annotation-based ObjectDefinitionSource.
// Reflection is used to avoid a compile-time dependency on SECURITY_ANNOTATION_ATTRIBUTES_CLASS, as this parser is in the Java 4 project whereas the dependency is in the Tiger project. // Reflection is used to avoid a compile-time dependency on SECURITY_ANNOTATION_ATTRIBUTES_CLASS, as this parser is in the Java 4 project whereas the dependency is in the Tiger project.
Assert.isTrue(ClassUtils.isPresent(SECURITY_ANNOTATION_ATTRIBUTES_CLASS), "Could not locate class '" + SECURITY_ANNOTATION_ATTRIBUTES_CLASS + "' - please ensure the spring-security-tiger-xxx.jar is in your classpath and you are running Java 5 or above."); Assert.isTrue(ClassUtils.isPresent(className), "Could not locate class '" + className + "' - please ensure the spring-security-tiger-xxx.jar is in your classpath and you are running Java 5 or above.");
Class clazz = null; Class clazz = null;
try { try {
clazz = ClassUtils.forName(SECURITY_ANNOTATION_ATTRIBUTES_CLASS); clazz = ClassUtils.forName(className);
} catch (Exception ex) { } catch (Exception ex) {
ReflectionUtils.handleReflectionException(ex); ReflectionUtils.handleReflectionException(ex);
} }

View File

@ -90,7 +90,9 @@ protect.attlist &=
annotation-driven = annotation-driven =
## Activates security annotation scanning. All beans registered in the Spring application context will be scanned for Spring Security annotations. Where found, the beans will automatically be proxied and security authorization applied to the methods accordingly. Please ensure you have the spring-security-tiger-XXX.jar on your classpath. ## Activates security annotation scanning. All beans registered in the Spring application context will be scanned for Spring Security annotations. Where found, the beans will automatically be proxied and security authorization applied to the methods accordingly. Please ensure you have the spring-security-tiger-XXX.jar on your classpath.
element annotation-driven {annotation-driven.attlist} element annotation-driven {annotation-driven.attlist}
annotation-driven.attlist = empty annotation-driven.attlist &=
## Specifies that JSR-250 style attributes are to be used (for example "RolesAllowed" instead of "Secured"). This will require the javax.annotation.security classes on the classpath. Defaults to false.
attribute jsr250 {"true" | "false" }?
http = http =

View File

@ -228,8 +228,23 @@
<xs:annotation> <xs:annotation>
<xs:documentation>Activates security annotation scanning. All beans registered in the Spring application context will be scanned for Spring Security annotations. Where found, the beans will automatically be proxied and security authorization applied to the methods accordingly. Please ensure you have the spring-security-tiger-XXX.jar on your classpath.</xs:documentation> <xs:documentation>Activates security annotation scanning. All beans registered in the Spring application context will be scanned for Spring Security annotations. Where found, the beans will automatically be proxied and security authorization applied to the methods accordingly. Please ensure you have the spring-security-tiger-XXX.jar on your classpath.</xs:documentation>
</xs:annotation> </xs:annotation>
<xs:complexType/> <xs:complexType>
<xs:attributeGroup ref="security:annotation-driven.attlist"/>
</xs:complexType>
</xs:element> </xs:element>
<xs:attributeGroup name="annotation-driven.attlist">
<xs:attribute name="jsr250">
<xs:annotation>
<xs:documentation>Specifies that JSR-250 style attributes are to be used (for example "RolesAllowed" instead of "Secured"). This will require the javax.annotation.security classes on the classpath. Defaults to false.</xs:documentation>
</xs:annotation>
<xs:simpleType>
<xs:restriction base="xs:token">
<xs:enumeration value="true"/>
<xs:enumeration value="false"/>
</xs:restriction>
</xs:simpleType>
</xs:attribute>
</xs:attributeGroup>
<xs:element name="http"> <xs:element name="http">
<xs:annotation> <xs:annotation>
<xs:documentation>Container element for HTTP security configuration</xs:documentation> <xs:documentation>Container element for HTTP security configuration</xs:documentation>