From d66b9693ba8c5136ffba402ceb2851591a3beb9d Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Thu, 10 Jan 2008 20:19:15 +0000 Subject: [PATCH] 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. --- core-tiger/pom.xml | 6 ++ .../Jsr250SecurityAnnotationAttributes.java | 101 ++++++++++++++++++ .../security/annotation/BusinessService.java | 13 ++- .../annotation/Jsr250BusinessServiceImpl.java | 31 ++++++ ...r250SecurityAnnotationAttributesTests.java | 85 +++++++++++++++ ...tationDrivenBeanDefinitionParserTests.java | 18 +--- ...tationDrivenBeanDefinitionParserTests.java | 60 +++++++++++ .../jsr250-annotated-method-security.xml | 20 ++++ .../AnnotationDrivenBeanDefinitionParser.java | 15 ++- .../security/config/spring-security-2.0.rnc | 4 +- .../security/config/spring-security-2.0.xsd | 17 ++- 11 files changed, 344 insertions(+), 26 deletions(-) create mode 100644 core-tiger/src/main/java/org/springframework/security/annotation/Jsr250SecurityAnnotationAttributes.java create mode 100644 core-tiger/src/test/java/org/springframework/security/annotation/Jsr250BusinessServiceImpl.java create mode 100644 core-tiger/src/test/java/org/springframework/security/annotation/Jsr250SecurityAnnotationAttributesTests.java create mode 100644 core-tiger/src/test/java/org/springframework/security/config/Jsr250AnnotationDrivenBeanDefinitionParserTests.java create mode 100644 core-tiger/src/test/resources/org/springframework/security/config/jsr250-annotated-method-security.xml diff --git a/core-tiger/pom.xml b/core-tiger/pom.xml index e0ae44d42e..d0ed826e09 100644 --- a/core-tiger/pom.xml +++ b/core-tiger/pom.xml @@ -33,6 +33,12 @@ spring-jdbc true + + org.apache.tomcat + annotations-api + 6.0.14 + true + diff --git a/core-tiger/src/main/java/org/springframework/security/annotation/Jsr250SecurityAnnotationAttributes.java b/core-tiger/src/main/java/org/springframework/security/annotation/Jsr250SecurityAnnotationAttributes.java new file mode 100644 index 0000000000..d593b8801c --- /dev/null +++ b/core-tiger/src/main/java/org/springframework/security/annotation/Jsr250SecurityAnnotationAttributes.java @@ -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 Attributes metadata implementation used for secure method interception. + *

+ * This Attributes implementation will return security configuration for classes described using the + * RolesAllowed Java JEE 5 annotation. + *

+ * The SecurityAnnotationAttributes implementation can be used to configure a + * MethodDefinitionAttributes and MethodSecurityInterceptor 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 RolesAllowed 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 SecurityConfig + * + * @see Attributes#getAttributes + */ + public Collection getAttributes(Class target) { + return new HashSet(); + } + + /** + * Get the RolesAllowed attributes for a given target method. + * + * @param method The target method + * @return Collection of SecurityConfig + * @see Attributes#getAttributes + */ + public Collection getAttributes(Method method) { + Annotation[] annotations = AnnotationUtils.getAnnotations(method); + Collection 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 populateSecurityConfigWithRolesAllowed (Annotation[] annotations) { + Set attributes = new HashSet(); + 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(); + } +} diff --git a/core-tiger/src/test/java/org/springframework/security/annotation/BusinessService.java b/core-tiger/src/test/java/org/springframework/security/annotation/BusinessService.java index 07caecdcbb..142b896d45 100644 --- a/core-tiger/src/test/java/org/springframework/security/annotation/BusinessService.java +++ b/core-tiger/src/test/java/org/springframework/security/annotation/BusinessService.java @@ -15,26 +15,29 @@ package org.springframework.security.annotation; +import javax.annotation.security.RolesAllowed; + /** - * DOCUMENT ME! - * - * @author $author$ - * @version $Revision: 2145 $ - */ + * @version $Id$ + */ @Secured({"ROLE_USER"}) public interface BusinessService { //~ Methods ======================================================================================================== @Secured({"ROLE_ADMIN"}) + @RolesAllowed({"ROLE_ADMIN"}) public void someAdminMethod(); @Secured({"ROLE_USER", "ROLE_ADMIN"}) + @RolesAllowed({"ROLE_USER", "ROLE_ADMIN"}) public void someUserAndAdminMethod(); @Secured({"ROLE_USER"}) + @RolesAllowed({"ROLE_USER"}) public void someUserMethod1(); @Secured({"ROLE_USER"}) + @RolesAllowed({"ROLE_USER"}) public void someUserMethod2(); public int someOther(int input); diff --git a/core-tiger/src/test/java/org/springframework/security/annotation/Jsr250BusinessServiceImpl.java b/core-tiger/src/test/java/org/springframework/security/annotation/Jsr250BusinessServiceImpl.java new file mode 100644 index 0000000000..88493a3cd9 --- /dev/null +++ b/core-tiger/src/test/java/org/springframework/security/annotation/Jsr250BusinessServiceImpl.java @@ -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; + } +} \ No newline at end of file diff --git a/core-tiger/src/test/java/org/springframework/security/annotation/Jsr250SecurityAnnotationAttributesTests.java b/core-tiger/src/test/java/org/springframework/security/annotation/Jsr250SecurityAnnotationAttributesTests.java new file mode 100644 index 0000000000..5dcd36865c --- /dev/null +++ b/core-tiger/src/test/java/org/springframework/security/annotation/Jsr250SecurityAnnotationAttributesTests.java @@ -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 accessAttributes = + new ArrayList(attributes.getAttributes(a.getClass().getMethod("adminMethod"))); + assertEquals(1, accessAttributes.size()); + assertEquals("ADMIN", accessAttributes.get(0).getAttribute()); + } + + @Test + public void permitAllMethodHasNoAttributes() throws Exception { + List accessAttributes = + new ArrayList(attributes.getAttributes(a.getClass().getMethod("permitAllMethod"))); + assertEquals(0, accessAttributes.size()); + } + + @Test + public void noRoleMethodHasNoAttributes() throws Exception { + List accessAttributes = + new ArrayList(attributes.getAttributes(a.getClass().getMethod("noRoleMethod"))); + assertEquals(0, accessAttributes.size()); + } + + @Test + public void classRoleIsAppliedNoRoleMethod() throws Exception { + List accessAttributes = + new ArrayList(attributes.getAttributes(b.getClass().getMethod("noRoleMethod"))); + assertEquals(1, accessAttributes.size()); + assertEquals("USER", accessAttributes.get(0).getAttribute()); + } + + @Test + public void methodRoleOverridesClassRole() throws Exception { + List accessAttributes = + new ArrayList(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() {} + } + +} diff --git a/core-tiger/src/test/java/org/springframework/security/config/AnnotationDrivenBeanDefinitionParserTests.java b/core-tiger/src/test/java/org/springframework/security/config/AnnotationDrivenBeanDefinitionParserTests.java index 5159dcc172..330c7e93f8 100644 --- a/core-tiger/src/test/java/org/springframework/security/config/AnnotationDrivenBeanDefinitionParserTests.java +++ b/core-tiger/src/test/java/org/springframework/security/config/AnnotationDrivenBeanDefinitionParserTests.java @@ -1,7 +1,5 @@ package org.springframework.security.config; -import static org.junit.Assert.fail; - import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -37,13 +35,9 @@ public class AnnotationDrivenBeanDefinitionParserTests { SecurityContextHolder.clearContext(); } - @Test + @Test(expected=AuthenticationCredentialsNotFoundException.class) public void targetShouldPreventProtectedMethodInvocationWithNoContext() { - try { - target.someUserMethod1(); - fail("Expected AuthenticationCredentialsNotFoundException"); - } catch (AuthenticationCredentialsNotFoundException expected) { - } + target.someUserMethod1(); } @Test @@ -55,16 +49,12 @@ public class AnnotationDrivenBeanDefinitionParserTests { target.someUserMethod1(); } - @Test + @Test(expected=AccessDeniedException.class) public void targetShouldPreventProtectedMethodInvocationWithIncorrectRole() { UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("Test", "Password", new GrantedAuthority[] {new GrantedAuthorityImpl("ROLE_SOMEOTHERROLE")}); SecurityContextHolder.getContext().setAuthentication(token); - try { - target.someAdminMethod(); - fail("Expected AccessDeniedException"); - } catch (AccessDeniedException expected) { - } + target.someAdminMethod(); } } diff --git a/core-tiger/src/test/java/org/springframework/security/config/Jsr250AnnotationDrivenBeanDefinitionParserTests.java b/core-tiger/src/test/java/org/springframework/security/config/Jsr250AnnotationDrivenBeanDefinitionParserTests.java new file mode 100644 index 0000000000..dcd432d464 --- /dev/null +++ b/core-tiger/src/test/java/org/springframework/security/config/Jsr250AnnotationDrivenBeanDefinitionParserTests.java @@ -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(); + } +} \ No newline at end of file diff --git a/core-tiger/src/test/resources/org/springframework/security/config/jsr250-annotated-method-security.xml b/core-tiger/src/test/resources/org/springframework/security/config/jsr250-annotated-method-security.xml new file mode 100644 index 0000000000..af075d606d --- /dev/null +++ b/core-tiger/src/test/resources/org/springframework/security/config/jsr250-annotated-method-security.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/core/src/main/java/org/springframework/security/config/AnnotationDrivenBeanDefinitionParser.java b/core/src/main/java/org/springframework/security/config/AnnotationDrivenBeanDefinitionParser.java index bf512cf34f..5c69378be8 100644 --- a/core/src/main/java/org/springframework/security/config/AnnotationDrivenBeanDefinitionParser.java +++ b/core/src/main/java/org/springframework/security/config/AnnotationDrivenBeanDefinitionParser.java @@ -23,17 +23,22 @@ import org.w3c.dom.Element; * @version $Id$ */ 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_USE_JSR250 = "jsr250"; 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. // 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; - try { - clazz = ClassUtils.forName(SECURITY_ANNOTATION_ATTRIBUTES_CLASS); + + try { + clazz = ClassUtils.forName(className); } catch (Exception ex) { ReflectionUtils.handleReflectionException(ex); } diff --git a/core/src/main/resources/org/springframework/security/config/spring-security-2.0.rnc b/core/src/main/resources/org/springframework/security/config/spring-security-2.0.rnc index f60363374b..3d9f3fc015 100644 --- a/core/src/main/resources/org/springframework/security/config/spring-security-2.0.rnc +++ b/core/src/main/resources/org/springframework/security/config/spring-security-2.0.rnc @@ -90,7 +90,9 @@ protect.attlist &= 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. 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 = diff --git a/core/src/main/resources/org/springframework/security/config/spring-security-2.0.xsd b/core/src/main/resources/org/springframework/security/config/spring-security-2.0.xsd index 30aa097127..672f1a7ed4 100644 --- a/core/src/main/resources/org/springframework/security/config/spring-security-2.0.xsd +++ b/core/src/main/resources/org/springframework/security/config/spring-security-2.0.xsd @@ -228,8 +228,23 @@ 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. - + + + + + + + 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. + + + + + + + + + Container element for HTTP security configuration