diff --git a/aspects/src/main/java/org/springframework/security/authorization/method/aspectj/AbstractMethodInterceptorAspect.aj b/aspects/src/main/java/org/springframework/security/authorization/method/aspectj/AbstractMethodInterceptorAspect.aj new file mode 100644 index 0000000000..ff750a6a1f --- /dev/null +++ b/aspects/src/main/java/org/springframework/security/authorization/method/aspectj/AbstractMethodInterceptorAspect.aj @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.authorization.method.aspectj; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.security.access.prepost.PostAuthorize; + +/** + * Abstract AspectJ aspect for adapting a {@link MethodInvocation} + * + * @author Josh Cummings + * @since 5.8 + */ +abstract aspect AbstractMethodInterceptorAspect { + + protected abstract pointcut executionOfAnnotatedMethod(); + + private MethodInterceptor securityInterceptor; + + Object around(): executionOfAnnotatedMethod() { + if (this.securityInterceptor == null) { + return proceed(); + } + MethodInvocation invocation = new JoinPointMethodInvocation(thisJoinPoint, () -> proceed()); + try { + return this.securityInterceptor.invoke(invocation); + } catch (Throwable t) { + throwUnchecked(t); + throw new IllegalStateException("Code unexpectedly reached", t); + } + } + + public void setSecurityInterceptor(MethodInterceptor securityInterceptor) { + this.securityInterceptor = securityInterceptor; + } + + private static void throwUnchecked(Throwable ex) { + AbstractMethodInterceptorAspect.throwAny(ex); + } + + @SuppressWarnings("unchecked") + private static void throwAny(Throwable ex) throws E { + throw (E) ex; + } +} diff --git a/aspects/src/main/java/org/springframework/security/authorization/method/aspectj/JoinPointMethodInvocation.aj b/aspects/src/main/java/org/springframework/security/authorization/method/aspectj/JoinPointMethodInvocation.aj new file mode 100644 index 0000000000..85b5d6d353 --- /dev/null +++ b/aspects/src/main/java/org/springframework/security/authorization/method/aspectj/JoinPointMethodInvocation.aj @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.authorization.method.aspectj; + +import java.lang.reflect.AccessibleObject; +import java.lang.reflect.Method; +import java.util.function.Supplier; + +import org.aopalliance.intercept.MethodInvocation; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.reflect.CodeSignature; + +import org.springframework.util.Assert; + +class JoinPointMethodInvocation implements MethodInvocation { + + private final JoinPoint jp; + + private final Method method; + + private final Object target; + + private final Supplier proceed; + + JoinPointMethodInvocation(JoinPoint jp, Supplier proceed) { + this.jp = jp; + if (jp.getTarget() != null) { + this.target = jp.getTarget(); + } + else { + // SEC-1295: target may be null if an ITD is in use + this.target = jp.getSignature().getDeclaringType(); + } + String targetMethodName = jp.getStaticPart().getSignature().getName(); + Class[] types = ((CodeSignature) jp.getStaticPart().getSignature()).getParameterTypes(); + Class declaringType = jp.getStaticPart().getSignature().getDeclaringType(); + this.method = findMethod(targetMethodName, declaringType, types); + Assert.notNull(this.method, () -> "Could not obtain target method from JoinPoint: '" + jp + "'"); + this.proceed = proceed; + } + + private Method findMethod(String name, Class declaringType, Class[] params) { + Method method = null; + try { + method = declaringType.getMethod(name, params); + } + catch (NoSuchMethodException ignored) { + } + if (method == null) { + try { + method = declaringType.getDeclaredMethod(name, params); + } + catch (NoSuchMethodException ignored) { + } + } + return method; + } + + @Override + public Method getMethod() { + return this.method; + } + + @Override + public Object[] getArguments() { + return this.jp.getArgs(); + } + + @Override + public AccessibleObject getStaticPart() { + return this.method; + } + + @Override + public Object getThis() { + return this.target; + } + + @Override + public Object proceed() throws Throwable { + return this.proceed.get(); + } + +} diff --git a/aspects/src/main/java/org/springframework/security/authorization/method/aspectj/PostAuthorizeAspect.aj b/aspects/src/main/java/org/springframework/security/authorization/method/aspectj/PostAuthorizeAspect.aj new file mode 100644 index 0000000000..70253f9d97 --- /dev/null +++ b/aspects/src/main/java/org/springframework/security/authorization/method/aspectj/PostAuthorizeAspect.aj @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.authorization.method.aspectj; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.security.access.prepost.PostAuthorize; + +/** + * Concrete AspectJ aspect using Spring Security @PostAuthorize annotation. + * + *

+ * When using this aspect, you must annotate the implementation class + * (and/or methods within that class), not the interface (if any) that + * the class implements. AspectJ follows Java's rule that annotations on + * interfaces are not inherited. This will vary from Spring AOP. + * + * @author Mike Wiesner + * @author Luke Taylor + * @author Josh Cummings + * @since 5.8 + */ +public aspect PostAuthorizeAspect extends AbstractMethodInterceptorAspect { + + /** + * Matches the execution of any method with a PostAuthorize annotation. + */ + protected pointcut executionOfAnnotatedMethod() : execution(* *(..)) && @annotation(PostAuthorize); +} diff --git a/aspects/src/main/java/org/springframework/security/authorization/method/aspectj/PostFilterAspect.aj b/aspects/src/main/java/org/springframework/security/authorization/method/aspectj/PostFilterAspect.aj new file mode 100644 index 0000000000..47a8e065cd --- /dev/null +++ b/aspects/src/main/java/org/springframework/security/authorization/method/aspectj/PostFilterAspect.aj @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.authorization.method.aspectj; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.security.access.prepost.PostFilter; + +/** + * Concrete AspectJ aspect using Spring Security @PostFilter annotation. + * + *

+ * When using this aspect, you must annotate the implementation class + * (and/or methods within that class), not the interface (if any) that + * the class implements. AspectJ follows Java's rule that annotations on + * interfaces are not inherited. This will vary from Spring AOP. + * + * @author Mike Wiesner + * @author Luke Taylor + * @author Josh Cummings + * @since 5.8 + */ +public aspect PostFilterAspect extends AbstractMethodInterceptorAspect { + + /** + * Matches the execution of any method with a PostFilter annotation. + */ + protected pointcut executionOfAnnotatedMethod() : execution(* *(..)) && @annotation(PostFilter); + +} diff --git a/aspects/src/main/java/org/springframework/security/authorization/method/aspectj/PreAuthorizeAspect.aj b/aspects/src/main/java/org/springframework/security/authorization/method/aspectj/PreAuthorizeAspect.aj new file mode 100644 index 0000000000..4d20128411 --- /dev/null +++ b/aspects/src/main/java/org/springframework/security/authorization/method/aspectj/PreAuthorizeAspect.aj @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.authorization.method.aspectj; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.security.access.prepost.PreAuthorize; + +/** + * Concrete AspectJ aspect using Spring Security @PreAuthorize annotation. + * + *

+ * When using this aspect, you must annotate the implementation class + * (and/or methods within that class), not the interface (if any) that + * the class implements. AspectJ follows Java's rule that annotations on + * interfaces are not inherited. This will vary from Spring AOP. + * + * @author Mike Wiesner + * @author Luke Taylor + * @author Josh Cummings + * @since 5.8 + */ +public aspect PreAuthorizeAspect extends AbstractMethodInterceptorAspect { + + /** + * Matches the execution of any method with a PreAuthorize annotation. + */ + protected pointcut executionOfAnnotatedMethod() : execution(* *(..)) && @annotation(PreAuthorize); +} diff --git a/aspects/src/main/java/org/springframework/security/authorization/method/aspectj/PreFilterAspect.aj b/aspects/src/main/java/org/springframework/security/authorization/method/aspectj/PreFilterAspect.aj new file mode 100644 index 0000000000..1987059d30 --- /dev/null +++ b/aspects/src/main/java/org/springframework/security/authorization/method/aspectj/PreFilterAspect.aj @@ -0,0 +1,45 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.authorization.method.aspectj; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.security.access.prepost.PreFilter; + +/** + * Concrete AspectJ aspect using Spring Security @PreFilter annotation. + * + *

+ * When using this aspect, you must annotate the implementation class + * (and/or methods within that class), not the interface (if any) that + * the class implements. AspectJ follows Java's rule that annotations on + * interfaces are not inherited. This will vary from Spring AOP. + * + * @author Mike Wiesner + * @author Luke Taylor + * @author Josh Cummings + * @since 5.8 + */ +public aspect PreFilterAspect extends AbstractMethodInterceptorAspect { + + /** + * Matches the execution of any method with a PreFilter annotation. + */ + protected pointcut executionOfAnnotatedMethod() : execution(* *(..)) && @annotation(PreFilter); + +} diff --git a/aspects/src/main/java/org/springframework/security/authorization/method/aspectj/SecuredAspect.aj b/aspects/src/main/java/org/springframework/security/authorization/method/aspectj/SecuredAspect.aj new file mode 100644 index 0000000000..4964a7195b --- /dev/null +++ b/aspects/src/main/java/org/springframework/security/authorization/method/aspectj/SecuredAspect.aj @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.authorization.method.aspectj; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.security.access.annotation.Secured; + +/** + * Concrete AspectJ aspect using Spring Security @Secured annotation. + * + *

+ * When using this aspect, you must annotate the implementation class + * (and/or methods within that class), not the interface (if any) that + * the class implements. AspectJ follows Java's rule that annotations on + * interfaces are not inherited. This will vary from Spring AOP. + * + * @author Mike Wiesner + * @author Luke Taylor + * @author Josh Cummings + * @since 5.8 + */ +public aspect SecuredAspect extends AbstractMethodInterceptorAspect { + + /** + * Matches the execution of any public method in a type with the Secured + * annotation, or any subtype of a type with the Secured annotation. + */ + private pointcut executionOfAnyPublicMethodInAtSecuredType() : + execution(public * ((@Secured *)+).*(..)) && @this(Secured); + + /** + * Matches the execution of any method with the Secured annotation. + */ + private pointcut executionOfSecuredMethod() : + execution(* *(..)) && @annotation(Secured); + + protected pointcut executionOfAnnotatedMethod() : + executionOfAnyPublicMethodInAtSecuredType() || + executionOfSecuredMethod(); +} diff --git a/aspects/src/test/java/org/springframework/security/authorization/method/aspectj/PostAuthorizeAspectTests.java b/aspects/src/test/java/org/springframework/security/authorization/method/aspectj/PostAuthorizeAspectTests.java new file mode 100644 index 0000000000..b8613217e4 --- /dev/null +++ b/aspects/src/test/java/org/springframework/security/authorization/method/aspectj/PostAuthorizeAspectTests.java @@ -0,0 +1,160 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.authorization.method.aspectj; + +import org.aopalliance.intercept.MethodInterceptor; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockitoAnnotations; + +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.access.prepost.PostAuthorize; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor; +import org.springframework.security.core.context.SecurityContextHolder; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Luke Taylor + * @since 3.0.3 + */ +public class PostAuthorizeAspectTests { + + private TestingAuthenticationToken anne = new TestingAuthenticationToken("anne", "", "ROLE_A"); + + private MethodInterceptor interceptor; + + private SecuredImpl secured = new SecuredImpl(); + + private SecuredImplSubclass securedSub = new SecuredImplSubclass(); + + private PrePostSecured prePostSecured = new PrePostSecured(); + + @BeforeEach + public final void setUp() { + MockitoAnnotations.initMocks(this); + this.interceptor = AuthorizationManagerAfterMethodInterceptor.postAuthorize(); + PostAuthorizeAspect secAspect = PostAuthorizeAspect.aspectOf(); + secAspect.setSecurityInterceptor(this.interceptor); + } + + @AfterEach + public void clearContext() { + SecurityContextHolder.clearContext(); + } + + @Test + public void securedInterfaceMethodAllowsAllAccess() { + this.secured.securedMethod(); + } + + @Test + public void securedClassMethodDeniesUnauthenticatedAccess() { + assertThatExceptionOfType(AuthenticationCredentialsNotFoundException.class) + .isThrownBy(() -> this.secured.securedClassMethod()); + } + + @Test + public void securedClassMethodAllowsAccessToRoleA() { + SecurityContextHolder.getContext().setAuthentication(this.anne); + this.secured.securedClassMethod(); + } + + @Test + public void internalPrivateCallIsIntercepted() { + SecurityContextHolder.getContext().setAuthentication(this.anne); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> this.secured.publicCallsPrivate()); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> this.securedSub.publicCallsPrivate()); + } + + @Test + public void protectedMethodIsIntercepted() { + SecurityContextHolder.getContext().setAuthentication(this.anne); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> this.secured.protectedMethod()); + } + + @Test + public void overriddenProtectedMethodIsNotIntercepted() { + // AspectJ doesn't inherit annotations + this.securedSub.protectedMethod(); + } + + // SEC-1262 + @Test + public void denyAllPreAuthorizeDeniesAccess() { + SecurityContextHolder.getContext().setAuthentication(this.anne); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.prePostSecured::denyAllMethod); + } + + interface SecuredInterface { + + @PostAuthorize("hasRole('X')") + void securedMethod(); + + } + + static class SecuredImpl implements SecuredInterface { + + // Not really secured because AspectJ doesn't inherit annotations from interfaces + @Override + public void securedMethod() { + } + + @PostAuthorize("hasRole('A')") + void securedClassMethod() { + } + + @PostAuthorize("hasRole('X')") + private void privateMethod() { + } + + @PostAuthorize("hasRole('X')") + protected void protectedMethod() { + } + + @PostAuthorize("hasRole('X')") + void publicCallsPrivate() { + privateMethod(); + } + + } + + static class SecuredImplSubclass extends SecuredImpl { + + @Override + protected void protectedMethod() { + } + + @Override + public void publicCallsPrivate() { + super.publicCallsPrivate(); + } + + } + + static class PrePostSecured { + + @PostAuthorize("denyAll") + void denyAllMethod() { + } + + } + +} diff --git a/aspects/src/test/java/org/springframework/security/authorization/method/aspectj/PostFilterAspectTests.java b/aspects/src/test/java/org/springframework/security/authorization/method/aspectj/PostFilterAspectTests.java new file mode 100644 index 0000000000..5194e30b38 --- /dev/null +++ b/aspects/src/test/java/org/springframework/security/authorization/method/aspectj/PostFilterAspectTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.authorization.method.aspectj; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.aopalliance.intercept.MethodInterceptor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockitoAnnotations; + +import org.springframework.security.access.prepost.PostFilter; +import org.springframework.security.authorization.method.PostFilterAuthorizationMethodInterceptor; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Luke Taylor + * @since 3.0.3 + */ +public class PostFilterAspectTests { + + private MethodInterceptor interceptor; + + private PrePostSecured prePostSecured = new PrePostSecured(); + + @BeforeEach + public final void setUp() { + MockitoAnnotations.initMocks(this); + this.interceptor = new PostFilterAuthorizationMethodInterceptor(); + PostFilterAspect secAspect = PostFilterAspect.aspectOf(); + secAspect.setSecurityInterceptor(this.interceptor); + } + + @Test + public void preFilterMethodWhenListThenFilters() { + List objects = new ArrayList<>(Arrays.asList("apple", "banana", "aubergine", "orange")); + assertThat(this.prePostSecured.postFilterMethod(objects)).containsExactly("apple", "aubergine"); + } + + static class PrePostSecured { + + @PostFilter("filterObject.startsWith('a')") + List postFilterMethod(List objects) { + return objects; + } + + } + +} diff --git a/aspects/src/test/java/org/springframework/security/authorization/method/aspectj/PreAuthorizeAspectTests.java b/aspects/src/test/java/org/springframework/security/authorization/method/aspectj/PreAuthorizeAspectTests.java new file mode 100644 index 0000000000..9e268f0fe7 --- /dev/null +++ b/aspects/src/test/java/org/springframework/security/authorization/method/aspectj/PreAuthorizeAspectTests.java @@ -0,0 +1,160 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.authorization.method.aspectj; + +import org.aopalliance.intercept.MethodInterceptor; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockitoAnnotations; + +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; +import org.springframework.security.core.context.SecurityContextHolder; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Luke Taylor + * @since 3.0.3 + */ +public class PreAuthorizeAspectTests { + + private TestingAuthenticationToken anne = new TestingAuthenticationToken("anne", "", "ROLE_A"); + + private MethodInterceptor interceptor; + + private SecuredImpl secured = new SecuredImpl(); + + private SecuredImplSubclass securedSub = new SecuredImplSubclass(); + + private PrePostSecured prePostSecured = new PrePostSecured(); + + @BeforeEach + public final void setUp() { + MockitoAnnotations.initMocks(this); + this.interceptor = AuthorizationManagerBeforeMethodInterceptor.preAuthorize(); + PreAuthorizeAspect secAspect = PreAuthorizeAspect.aspectOf(); + secAspect.setSecurityInterceptor(this.interceptor); + } + + @AfterEach + public void clearContext() { + SecurityContextHolder.clearContext(); + } + + @Test + public void securedInterfaceMethodAllowsAllAccess() { + this.secured.securedMethod(); + } + + @Test + public void securedClassMethodDeniesUnauthenticatedAccess() { + assertThatExceptionOfType(AuthenticationCredentialsNotFoundException.class) + .isThrownBy(() -> this.secured.securedClassMethod()); + } + + @Test + public void securedClassMethodAllowsAccessToRoleA() { + SecurityContextHolder.getContext().setAuthentication(this.anne); + this.secured.securedClassMethod(); + } + + @Test + public void internalPrivateCallIsIntercepted() { + SecurityContextHolder.getContext().setAuthentication(this.anne); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> this.secured.publicCallsPrivate()); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> this.securedSub.publicCallsPrivate()); + } + + @Test + public void protectedMethodIsIntercepted() { + SecurityContextHolder.getContext().setAuthentication(this.anne); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> this.secured.protectedMethod()); + } + + @Test + public void overriddenProtectedMethodIsNotIntercepted() { + // AspectJ doesn't inherit annotations + this.securedSub.protectedMethod(); + } + + // SEC-1262 + @Test + public void denyAllPreAuthorizeDeniesAccess() { + SecurityContextHolder.getContext().setAuthentication(this.anne); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(this.prePostSecured::denyAllMethod); + } + + interface SecuredInterface { + + @PreAuthorize("hasRole('X')") + void securedMethod(); + + } + + static class SecuredImpl implements SecuredInterface { + + // Not really secured because AspectJ doesn't inherit annotations from interfaces + @Override + public void securedMethod() { + } + + @PreAuthorize("hasRole('A')") + void securedClassMethod() { + } + + @PreAuthorize("hasRole('X')") + private void privateMethod() { + } + + @PreAuthorize("hasRole('X')") + protected void protectedMethod() { + } + + @PreAuthorize("hasRole('X')") + void publicCallsPrivate() { + privateMethod(); + } + + } + + static class SecuredImplSubclass extends SecuredImpl { + + @Override + protected void protectedMethod() { + } + + @Override + public void publicCallsPrivate() { + super.publicCallsPrivate(); + } + + } + + static class PrePostSecured { + + @PreAuthorize("denyAll") + void denyAllMethod() { + } + + } + +} diff --git a/aspects/src/test/java/org/springframework/security/authorization/method/aspectj/PreFilterAspectTests.java b/aspects/src/test/java/org/springframework/security/authorization/method/aspectj/PreFilterAspectTests.java new file mode 100644 index 0000000000..81654ac65c --- /dev/null +++ b/aspects/src/test/java/org/springframework/security/authorization/method/aspectj/PreFilterAspectTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.authorization.method.aspectj; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.aopalliance.intercept.MethodInterceptor; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockitoAnnotations; + +import org.springframework.security.access.prepost.PreFilter; +import org.springframework.security.authorization.method.PreFilterAuthorizationMethodInterceptor; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Luke Taylor + * @since 3.0.3 + */ +public class PreFilterAspectTests { + + private MethodInterceptor interceptor; + + private PrePostSecured prePostSecured = new PrePostSecured(); + + @BeforeEach + public final void setUp() { + MockitoAnnotations.initMocks(this); + this.interceptor = new PreFilterAuthorizationMethodInterceptor(); + PreFilterAspect secAspect = PreFilterAspect.aspectOf(); + secAspect.setSecurityInterceptor(this.interceptor); + } + + @Test + public void preFilterMethodWhenListThenFilters() { + List objects = new ArrayList<>(Arrays.asList("apple", "banana", "aubergine", "orange")); + assertThat(this.prePostSecured.preFilterMethod(objects)).containsExactly("apple", "aubergine"); + } + + static class PrePostSecured { + + @PreFilter("filterObject.startsWith('a')") + List preFilterMethod(List objects) { + return objects; + } + + } + +} diff --git a/aspects/src/test/java/org/springframework/security/authorization/method/aspectj/SecuredAspectTests.java b/aspects/src/test/java/org/springframework/security/authorization/method/aspectj/SecuredAspectTests.java new file mode 100644 index 0000000000..0099c28edc --- /dev/null +++ b/aspects/src/test/java/org/springframework/security/authorization/method/aspectj/SecuredAspectTests.java @@ -0,0 +1,143 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.authorization.method.aspectj; + +import org.aopalliance.intercept.MethodInterceptor; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockitoAnnotations; + +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor; +import org.springframework.security.core.context.SecurityContextHolder; + +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * @author Luke Taylor + * @since 3.0.3 + */ +public class SecuredAspectTests { + + private TestingAuthenticationToken anne = new TestingAuthenticationToken("anne", "", "ROLE_A"); + + private MethodInterceptor interceptor; + + private SecuredImpl secured = new SecuredImpl(); + + private SecuredImplSubclass securedSub = new SecuredImplSubclass(); + + @BeforeEach + public final void setUp() { + MockitoAnnotations.initMocks(this); + this.interceptor = AuthorizationManagerBeforeMethodInterceptor.secured(); + SecuredAspect secAspect = SecuredAspect.aspectOf(); + secAspect.setSecurityInterceptor(this.interceptor); + } + + @AfterEach + public void clearContext() { + SecurityContextHolder.clearContext(); + } + + @Test + public void securedInterfaceMethodAllowsAllAccess() { + this.secured.securedMethod(); + } + + @Test + public void securedClassMethodDeniesUnauthenticatedAccess() { + assertThatExceptionOfType(AuthenticationCredentialsNotFoundException.class) + .isThrownBy(() -> this.secured.securedClassMethod()); + } + + @Test + public void securedClassMethodAllowsAccessToRoleA() { + SecurityContextHolder.getContext().setAuthentication(this.anne); + this.secured.securedClassMethod(); + } + + @Test + public void internalPrivateCallIsIntercepted() { + SecurityContextHolder.getContext().setAuthentication(this.anne); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> this.secured.publicCallsPrivate()); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> this.securedSub.publicCallsPrivate()); + } + + @Test + public void protectedMethodIsIntercepted() { + SecurityContextHolder.getContext().setAuthentication(this.anne); + assertThatExceptionOfType(AccessDeniedException.class).isThrownBy(() -> this.secured.protectedMethod()); + } + + @Test + public void overriddenProtectedMethodIsNotIntercepted() { + // AspectJ doesn't inherit annotations + this.securedSub.protectedMethod(); + } + + interface SecuredInterface { + + @Secured("ROLE_X") + void securedMethod(); + + } + + static class SecuredImpl implements SecuredInterface { + + // Not really secured because AspectJ doesn't inherit annotations from interfaces + @Override + public void securedMethod() { + } + + @Secured("ROLE_A") + void securedClassMethod() { + } + + @Secured("ROLE_X") + private void privateMethod() { + } + + @Secured("ROLE_X") + protected void protectedMethod() { + } + + @Secured("ROLE_X") + void publicCallsPrivate() { + privateMethod(); + } + + } + + static class SecuredImplSubclass extends SecuredImpl { + + @Override + protected void protectedMethod() { + } + + @Override + public void publicCallsPrivate() { + super.publicCallsPrivate(); + } + + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityAspectJAutoProxyRegistrar.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityAspectJAutoProxyRegistrar.java new file mode 100644 index 0000000000..16a31924d9 --- /dev/null +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecurityAspectJAutoProxyRegistrar.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2022 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 + * + * https://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.config.annotation.method.configuration; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.type.AnnotationMetadata; + +/** + * Registers an + * {@link org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator + * AnnotationAwareAspectJAutoProxyCreator} against the current + * {@link BeanDefinitionRegistry} as appropriate based on a given @ + * {@link EnableMethodSecurity} annotation. + * + *

+ * Note: This class is necessary because AspectJAutoProxyRegistrar only supports + * EnableAspectJAutoProxy. + *

+ * + * @author Josh Cummings + * @since 5.8 + */ +class MethodSecurityAspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar { + + /** + * Register, escalate, and configure the AspectJ auto proxy creator based on the value + * of the @{@link EnableMethodSecurity#proxyTargetClass()} attribute on the importing + * {@code @Configuration} class. + */ + @Override + public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { + registerBeanDefinition("preFilterAuthorizationMethodInterceptor", + "org.springframework.security.authorization.method.aspectj.PreFilterAspect", "preFilterAspect$0", + registry); + registerBeanDefinition("postFilterAuthorizationMethodInterceptor", + "org.springframework.security.authorization.method.aspectj.PostFilterAspect", "postFilterAspect$0", + registry); + registerBeanDefinition("preAuthorizeAuthorizationMethodInterceptor", + "org.springframework.security.authorization.method.aspectj.PreAuthorizeAspect", "preAuthorizeAspect$0", + registry); + registerBeanDefinition("postAuthorizeAuthorizationMethodInterceptor", + "org.springframework.security.authorization.method.aspectj.PostAuthorizeAspect", + "postAuthorizeAspect$0", registry); + registerBeanDefinition("securedAuthorizationMethodInterceptor", + "org.springframework.security.authorization.method.aspectj.SecuredAspect", "securedAspect$0", registry); + } + + private void registerBeanDefinition(String beanName, String aspectClassName, String aspectBeanName, + BeanDefinitionRegistry registry) { + if (!registry.containsBeanDefinition(beanName)) { + return; + } + BeanDefinition interceptor = registry.getBeanDefinition(beanName); + BeanDefinitionBuilder aspect = BeanDefinitionBuilder.rootBeanDefinition(aspectClassName); + aspect.setFactoryMethod("aspectOf"); + aspect.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + aspect.addPropertyValue("securityInterceptor", interceptor); + registry.registerBeanDefinition(aspectBeanName, aspect.getBeanDefinition()); + } + +} diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java index 0443a03e1c..f0ee6d4630 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/MethodSecuritySelector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2022 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. @@ -62,11 +62,17 @@ final class MethodSecuritySelector implements ImportSelector { private static final String[] IMPORTS = new String[] { AutoProxyRegistrar.class.getName() }; + private static final String[] ASPECTJ_IMPORTS = new String[] { + MethodSecurityAspectJAutoProxyRegistrar.class.getName() }; + @Override protected String[] selectImports(@NonNull AdviceMode adviceMode) { if (adviceMode == AdviceMode.PROXY) { return IMPORTS; } + if (adviceMode == AdviceMode.ASPECTJ) { + return ASPECTJ_IMPORTS; + } throw new IllegalStateException("AdviceMode '" + adviceMode + "' is not supported"); } diff --git a/config/src/main/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParser.java index 2869e719e0..63f3614536 100644 --- a/config/src/main/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParser.java @@ -34,6 +34,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.parsing.CompositeComponentDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.ManagedMap; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; @@ -78,6 +79,8 @@ public class MethodSecurityBeanDefinitionParser implements BeanDefinitionParser private static final String ATT_EXPRESSION = "expression"; + private static final String ATT_MODE = "mode"; + private static final String ATT_SECURITY_CONTEXT_HOLDER_STRATEGY_REF = "security-context-holder-strategy-ref"; @Override @@ -88,6 +91,7 @@ public class MethodSecurityBeanDefinitionParser implements BeanDefinitionParser BeanMetadataElement securityContextHolderStrategy = getSecurityContextHolderStrategy(element); boolean prePostAnnotationsEnabled = !element.hasAttribute(ATT_USE_PREPOST) || "true".equals(element.getAttribute(ATT_USE_PREPOST)); + boolean useAspectJ = "aspectj".equals(element.getAttribute(ATT_MODE)); if (prePostAnnotationsEnabled) { BeanDefinitionBuilder preFilterInterceptor = BeanDefinitionBuilder .rootBeanDefinition(PreFilterAuthorizationMethodInterceptor.class) @@ -151,20 +155,29 @@ public class MethodSecurityBeanDefinitionParser implements BeanDefinitionParser } Map managers = new ManagedMap<>(); List methods = DomUtils.getChildElementsByTagName(element, Elements.PROTECT_POINTCUT); - if (!methods.isEmpty()) { - for (Element protectElt : methods) { - managers.put(pointcut(protectElt), authorizationManager(element, protectElt)); + if (useAspectJ) { + if (!methods.isEmpty()) { + pc.getReaderContext().error("Cannot use and mode='aspectj' together", + pc.extractSource(element)); } - BeanDefinitionBuilder protectPointcutInterceptor = BeanDefinitionBuilder - .rootBeanDefinition(AuthorizationManagerBeforeMethodInterceptor.class) - .setRole(BeanDefinition.ROLE_INFRASTRUCTURE) - .addPropertyValue("securityContextHolderStrategy", securityContextHolderStrategy) - .addConstructorArgValue(pointcut(managers.keySet())) - .addConstructorArgValue(authorizationManager(managers)); - pc.getRegistry().registerBeanDefinition("protectPointcutInterceptor", - protectPointcutInterceptor.getBeanDefinition()); + registerInterceptors(pc.getRegistry()); + } + else { + if (!methods.isEmpty()) { + for (Element protectElt : methods) { + managers.put(pointcut(protectElt), authorizationManager(element, protectElt)); + } + BeanDefinitionBuilder protectPointcutInterceptor = BeanDefinitionBuilder + .rootBeanDefinition(AuthorizationManagerBeforeMethodInterceptor.class) + .setRole(BeanDefinition.ROLE_INFRASTRUCTURE) + .addPropertyValue("securityContextHolderStrategy", securityContextHolderStrategy) + .addConstructorArgValue(pointcut(managers.keySet())) + .addConstructorArgValue(authorizationManager(managers)); + pc.getRegistry().registerBeanDefinition("protectPointcutInterceptor", + protectPointcutInterceptor.getBeanDefinition()); + } + AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(pc, element); } - AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(pc, element); pc.popAndRegisterContainingComponent(); return null; } @@ -218,6 +231,36 @@ public class MethodSecurityBeanDefinitionParser implements BeanDefinitionParser .addConstructorArgValue(managers).getBeanDefinition(); } + private void registerInterceptors(BeanDefinitionRegistry registry) { + registerBeanDefinition("preFilterAuthorizationMethodInterceptor", + "org.springframework.security.authorization.method.aspectj.PreFilterAspect", "preFilterAspect$0", + registry); + registerBeanDefinition("postFilterAuthorizationMethodInterceptor", + "org.springframework.security.authorization.method.aspectj.PostFilterAspect", "postFilterAspect$0", + registry); + registerBeanDefinition("preAuthorizeAuthorizationMethodInterceptor", + "org.springframework.security.authorization.method.aspectj.PreAuthorizeAspect", "preAuthorizeAspect$0", + registry); + registerBeanDefinition("postAuthorizeAuthorizationMethodInterceptor", + "org.springframework.security.authorization.method.aspectj.PostAuthorizeAspect", + "postAuthorizeAspect$0", registry); + registerBeanDefinition("securedAuthorizationMethodInterceptor", + "org.springframework.security.authorization.method.aspectj.SecuredAspect", "securedAspect$0", registry); + } + + private void registerBeanDefinition(String beanName, String aspectClassName, String aspectBeanName, + BeanDefinitionRegistry registry) { + if (!registry.containsBeanDefinition(beanName)) { + return; + } + BeanDefinition interceptor = registry.getBeanDefinition(beanName); + BeanDefinitionBuilder aspect = BeanDefinitionBuilder.rootBeanDefinition(aspectClassName); + aspect.setFactoryMethod("aspectOf"); + aspect.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + aspect.addPropertyValue("securityInterceptor", interceptor); + registry.registerBeanDefinition(aspectBeanName, aspect.getBeanDefinition()); + } + public static final class MethodSecurityExpressionHandlerBean implements FactoryBean, ApplicationContextAware { diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc index 99d750d80f..bc0c055762 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.8.rnc @@ -216,6 +216,9 @@ method-security.attlist &= method-security.attlist &= ## If true, class-based proxying will be used instead of interface-based proxying. attribute proxy-target-class {xsd:boolean}? +method-security.attlist &= + ## If set to aspectj, then use AspectJ to intercept method invocation + attribute mode {"aspectj"}? method-security.attlist &= ## Specifies the security context holder strategy to use, by default uses a ThreadLocal-based strategy attribute security-context-holder-strategy-ref {xsd:string}? diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd index 330518a666..37bda48acd 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.8.xsd @@ -677,6 +677,17 @@ + + + If set to aspectj, then use AspectJ to intercept method invocation + + + + + + + + Specifies the security context holder strategy to use, by default uses a ThreadLocal-based diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java index 3305895129..dc56480a09 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java @@ -32,6 +32,7 @@ import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.aop.support.JdkRegexpMethodPointcut; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.AdviceMode; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Role; @@ -406,6 +407,17 @@ public class PrePostMethodSecurityConfigurationTests { this.methodSecurityService.preAuthorizeBean(true); } + @Test + public void configureWhenAspectJThenRegistersAspects() { + this.spring.register(AspectJMethodSecurityServiceConfig.class).autowire(); + assertThat(this.spring.getContext().containsBean("preFilterAspect$0")).isTrue(); + assertThat(this.spring.getContext().containsBean("postFilterAspect$0")).isTrue(); + assertThat(this.spring.getContext().containsBean("preAuthorizeAspect$0")).isTrue(); + assertThat(this.spring.getContext().containsBean("postAuthorizeAspect$0")).isTrue(); + assertThat(this.spring.getContext().containsBean("securedAspect$0")).isTrue(); + assertThat(this.spring.getContext().containsBean("annotationSecurityAspect$0")).isFalse(); + } + @Configuration @EnableMethodSecurity static class MethodSecurityServiceConfig { @@ -556,4 +568,19 @@ public class PrePostMethodSecurityConfigurationTests { } + @EnableMethodSecurity(mode = AdviceMode.ASPECTJ, securedEnabled = true) + static class AspectJMethodSecurityServiceConfig { + + @Bean + MethodSecurityService methodSecurityService() { + return new MethodSecurityServiceImpl(); + } + + @Bean + Authz authz() { + return new Authz(); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests.java b/config/src/test/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests.java index e1fd044a50..f2e2556fb6 100644 --- a/config/src/test/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests.java +++ b/config/src/test/java/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests.java @@ -85,6 +85,17 @@ public class MethodSecurityBeanDefinitionParserTests { .withMessage("Access Denied"); } + @Test + public void configureWhenAspectJThenRegistersAspects() { + this.spring.configLocations(xml("AspectJMethodSecurityServiceEnabled")).autowire(); + assertThat(this.spring.getContext().containsBean("preFilterAspect$0")).isTrue(); + assertThat(this.spring.getContext().containsBean("postFilterAspect$0")).isTrue(); + assertThat(this.spring.getContext().containsBean("preAuthorizeAspect$0")).isTrue(); + assertThat(this.spring.getContext().containsBean("postAuthorizeAspect$0")).isTrue(); + assertThat(this.spring.getContext().containsBean("securedAspect$0")).isTrue(); + assertThat(this.spring.getContext().containsBean("annotationSecurityAspect$0")).isFalse(); + } + @WithAnonymousUser @Test public void preAuthorizePermitAllWhenRoleAnonymousThenPasses() { diff --git a/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-AspectJMethodSecurityServiceEnabled.xml b/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-AspectJMethodSecurityServiceEnabled.xml new file mode 100644 index 0000000000..63b50792d2 --- /dev/null +++ b/config/src/test/resources/org/springframework/security/config/method/MethodSecurityBeanDefinitionParserTests-AspectJMethodSecurityServiceEnabled.xml @@ -0,0 +1,28 @@ + + + + + + + + + + diff --git a/core/src/main/java/org/springframework/security/access/intercept/aspectj/AspectJCallback.java b/core/src/main/java/org/springframework/security/access/intercept/aspectj/AspectJCallback.java index f82855ac33..251992dabb 100644 --- a/core/src/main/java/org/springframework/security/access/intercept/aspectj/AspectJCallback.java +++ b/core/src/main/java/org/springframework/security/access/intercept/aspectj/AspectJCallback.java @@ -22,7 +22,11 @@ package org.springframework.security.access.intercept.aspectj; * simple return proceed(); statement. * * @author Ben Alex + * @deprecated This class will be removed from the public API. Please either use + * `spring-security-aspects`, Spring Security's method security support or create your own + * class that uses Spring AOP annotations. */ +@Deprecated public interface AspectJCallback { Object proceedWithObject(); diff --git a/core/src/main/java/org/springframework/security/access/intercept/aspectj/AspectJMethodSecurityInterceptor.java b/core/src/main/java/org/springframework/security/access/intercept/aspectj/AspectJMethodSecurityInterceptor.java index 78e450c4d1..7d33ad6af0 100644 --- a/core/src/main/java/org/springframework/security/access/intercept/aspectj/AspectJMethodSecurityInterceptor.java +++ b/core/src/main/java/org/springframework/security/access/intercept/aspectj/AspectJMethodSecurityInterceptor.java @@ -33,7 +33,11 @@ import org.springframework.security.access.intercept.aopalliance.MethodSecurityI * @author Luke Taylor * @author Rob Winch * @since 3.0.3 + * @deprecated This class will be removed from the public API. Please either use + * `spring-security-aspects`, Spring Security's method security support or create your own + * class that uses Spring AOP annotations. */ +@Deprecated public final class AspectJMethodSecurityInterceptor extends MethodSecurityInterceptor { /** diff --git a/core/src/main/java/org/springframework/security/access/intercept/aspectj/MethodInvocationAdapter.java b/core/src/main/java/org/springframework/security/access/intercept/aspectj/MethodInvocationAdapter.java index 7030f99ee4..da29032f78 100644 --- a/core/src/main/java/org/springframework/security/access/intercept/aspectj/MethodInvocationAdapter.java +++ b/core/src/main/java/org/springframework/security/access/intercept/aspectj/MethodInvocationAdapter.java @@ -32,7 +32,10 @@ import org.springframework.util.Assert; * * @author Luke Taylor * @since 3.0.3 + * @deprecated This class will be removed from the public API. See + * `JoinPointMethodInvocation` in `spring-security-aspects` for its replacement */ +@Deprecated public final class MethodInvocationAdapter implements MethodInvocation { private final ProceedingJoinPoint jp; diff --git a/docs/modules/ROOT/pages/servlet/appendix/namespace/method-security.adoc b/docs/modules/ROOT/pages/servlet/appendix/namespace/method-security.adoc index 0298175c05..0b53e631b1 100644 --- a/docs/modules/ROOT/pages/servlet/appendix/namespace/method-security.adoc +++ b/docs/modules/ROOT/pages/servlet/appendix/namespace/method-security.adoc @@ -23,6 +23,10 @@ Defaults to "false". Enables JSR-250 authorization annotations (@RolesAllowed, @PermitAll, @DenyAll) for this application context. Defaults to "false". +[[nsa-method-security-mode]] +* **mode** +If set to "aspectj", then uses AspectJ to intercept method invocations. + [[nsa-method-security-proxy-target-class]] * **proxy-target-class** If true, class based proxying will be used instead of interface based proxying.