Skip to content

Commit

Permalink
Feedback changes
Browse files Browse the repository at this point in the history
  • Loading branch information
marcusdacoregio committed Apr 2, 2024
1 parent 6197405 commit 44d8c3e
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public class AuthorizationDeniedException extends AccessDeniedException {
public AuthorizationDeniedException(String msg, AuthorizationResult authorizationResult) {
super(msg);
Assert.notNull(authorizationResult, "authorizationResult cannot be null");
Assert.state(!authorizationResult.isGranted(), "Granted authorization results are not supported");
Assert.isTrue(!authorizationResult.isGranted(), "Granted authorization results are not supported");
this.result = authorizationResult;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@

import org.aopalliance.intercept.MethodInvocation;

import org.springframework.context.ApplicationContext;
import org.springframework.core.MethodClassKey;
import org.springframework.lang.NonNull;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
Expand All @@ -46,8 +45,6 @@ abstract class AbstractExpressionAttributeRegistry<T extends ExpressionAttribute

private PrePostTemplateDefaults defaults;

private ApplicationContext applicationContext;

/**
* Returns an {@link ExpressionAttribute} for the {@link MethodInvocation}.
* @param mi the {@link MethodInvocation} to use
Expand Down Expand Up @@ -93,14 +90,6 @@ void setTemplateDefaults(PrePostTemplateDefaults defaults) {
this.defaults = defaults;
}

void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}

ApplicationContext getApplicationContext() {
return this.applicationContext;
}

/**
* Subclasses should implement this method to provide the non-null
* {@link ExpressionAttribute} for the method and the target class.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,14 @@ public void setTemplateDefaults(PrePostTemplateDefaults defaults) {
this.registry.setTemplateDefaults(defaults);
}

/**
* Invokes
* {@link PostAuthorizeExpressionAttributeRegistry#setApplicationContext(ApplicationContext)}
* with the provided {@link ApplicationContext}.
* @param context the {@link ApplicationContext}
* @since 6.3
* @see PreAuthorizeExpressionAttributeRegistry#setApplicationContext(ApplicationContext)
*/
public void setApplicationContext(ApplicationContext context) {
this.registry.setApplicationContext(context);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@
import reactor.util.annotation.NonNull;

import org.springframework.aop.support.AopUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.expression.Expression;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.util.Assert;

/**
* For internal use only, as this contract is likely to change.
Expand All @@ -38,6 +40,12 @@ final class PostAuthorizeExpressionAttributeRegistry extends AbstractExpressionA

private final MethodAuthorizationDeniedPostProcessor defaultPostProcessor = new ThrowingMethodAuthorizationDeniedPostProcessor();

private Function<Class<? extends MethodAuthorizationDeniedPostProcessor>, MethodAuthorizationDeniedPostProcessor> postProcessorResolver;

PostAuthorizeExpressionAttributeRegistry() {
this.postProcessorResolver = (clazz) -> this.defaultPostProcessor;
}

@NonNull
@Override
ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
Expand All @@ -47,7 +55,8 @@ ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
return ExpressionAttribute.NULL_ATTRIBUTE;
}
Expression expression = getExpressionHandler().getExpressionParser().parseExpression(postAuthorize.value());
MethodAuthorizationDeniedPostProcessor postProcessor = resolvePostProcessor(postAuthorize.postProcessorClass());
MethodAuthorizationDeniedPostProcessor postProcessor = this.postProcessorResolver
.apply(postAuthorize.postProcessorClass());
return new PostAuthorizeExpressionAttribute(expression, postProcessor);
}

Expand All @@ -57,23 +66,30 @@ private PostAuthorize findPostAuthorizeAnnotation(Method method, Class<?> target
return (postAuthorize != null) ? postAuthorize : lookup.apply(targetClass(method, targetClass));
}

private MethodAuthorizationDeniedPostProcessor resolvePostProcessor(
/**
* Uses the provided {@link ApplicationContext} to resolve the
* {@link MethodAuthorizationDeniedPostProcessor} from {@link PostAuthorize}
* @param context the {@link ApplicationContext} to use
*/
void setApplicationContext(ApplicationContext context) {
Assert.notNull(context, "context cannot be null");
this.postProcessorResolver = (postProcessorClass) -> resolvePostProcessor(context, postProcessorClass);
}

private MethodAuthorizationDeniedPostProcessor resolvePostProcessor(ApplicationContext context,
Class<? extends MethodAuthorizationDeniedPostProcessor> postProcessorClass) {
if (getApplicationContext() == null) {
return this.defaultPostProcessor;
}
if (postProcessorClass == this.defaultPostProcessor.getClass()) {
return this.defaultPostProcessor;
}
String[] beanNames = getApplicationContext().getBeanNamesForType(postProcessorClass);
String[] beanNames = context.getBeanNamesForType(postProcessorClass);
if (beanNames.length == 0) {
throw new IllegalStateException("Could not find a bean of type " + postProcessorClass.getName());
}
if (beanNames.length > 1) {
throw new IllegalStateException("Expected to find a single bean of type " + postProcessorClass.getName()
+ " but found " + Arrays.toString(beanNames));
}
return getApplicationContext().getBean(beanNames[0], postProcessorClass);
return context.getBean(beanNames[0], postProcessorClass);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,10 @@
import reactor.util.annotation.NonNull;

import org.springframework.aop.support.AopUtils;
import org.springframework.context.ApplicationContext;
import org.springframework.expression.Expression;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.util.Assert;

/**
* For internal use only, as this contract is likely to change.
Expand All @@ -38,6 +40,12 @@ final class PreAuthorizeExpressionAttributeRegistry extends AbstractExpressionAt

private final MethodAuthorizationDeniedHandler defaultHandler = new ThrowingMethodAuthorizationDeniedHandler();

private Function<Class<? extends MethodAuthorizationDeniedHandler>, MethodAuthorizationDeniedHandler> handlerResolver;

PreAuthorizeExpressionAttributeRegistry() {
this.handlerResolver = (clazz) -> this.defaultHandler;
}

@NonNull
@Override
ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
Expand All @@ -47,7 +55,7 @@ ExpressionAttribute resolveAttribute(Method method, Class<?> targetClass) {
return ExpressionAttribute.NULL_ATTRIBUTE;
}
Expression expression = getExpressionHandler().getExpressionParser().parseExpression(preAuthorize.value());
MethodAuthorizationDeniedHandler handler = resolveHandler(preAuthorize.handlerClass());
MethodAuthorizationDeniedHandler handler = this.handlerResolver.apply(preAuthorize.handlerClass());
return new PreAuthorizeExpressionAttribute(expression, handler);
}

Expand All @@ -57,23 +65,30 @@ private PreAuthorize findPreAuthorizeAnnotation(Method method, Class<?> targetCl
return (preAuthorize != null) ? preAuthorize : lookup.apply(targetClass(method, targetClass));
}

private MethodAuthorizationDeniedHandler resolveHandler(
/**
* Uses the provided {@link ApplicationContext} to resolve the
* {@link MethodAuthorizationDeniedHandler} from {@link PreAuthorize}.
* @param context the {@link ApplicationContext} to use
*/
void setApplicationContext(ApplicationContext context) {
Assert.notNull(context, "context cannot be null");
this.handlerResolver = (clazz) -> resolveHandler(context, clazz);
}

private MethodAuthorizationDeniedHandler resolveHandler(ApplicationContext context,
Class<? extends MethodAuthorizationDeniedHandler> handlerClass) {
if (getApplicationContext() == null) {
return this.defaultHandler;
}
if (handlerClass == this.defaultHandler.getClass()) {
return this.defaultHandler;
}
String[] beanNames = getApplicationContext().getBeanNamesForType(handlerClass);
String[] beanNames = context.getBeanNamesForType(handlerClass);
if (beanNames.length == 0) {
throw new IllegalStateException("Could not find a bean of type " + handlerClass.getName());
}
if (beanNames.length > 1) {
throw new IllegalStateException("Expected to find a single bean of type " + handlerClass.getName()
+ " but found " + Arrays.toString(beanNames));
}
return getApplicationContext().getBean(beanNames[0], handlerClass);
return context.getBean(beanNames[0], handlerClass);
}

}
69 changes: 67 additions & 2 deletions docs/modules/ROOT/pages/servlet/authorization/method-security.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,14 @@ Consider learning about the following use cases:
* Understanding <<method-security-architecture,how method security works>> and reasons to use it
* Comparing <<request-vs-method,request-level and method-level authorization>>
* Authorizing methods with <<use-preauthorize,`@PreAuthorize`>> and <<use-postauthorize,`@PostAuthorize`>>
* Providing <<fallback-values-authorization-denied,fallback values when authorization is denied>>
* Filtering methods with <<use-prefilter,`@PreFilter`>> and <<use-postfilter,`@PostFilter`>>
* Authorizing methods with <<use-jsr250,JSR-250 annotations>>
* Authorizing methods with <<use-aspectj,AspectJ expressions>>
* Integrating with <<weave-aspectj,AspectJ byte-code weaving>>
* Coordinating with <<changing-the-order,@Transactional and other AOP-based annotations>>
* Customizing <<customizing-expression-handling,SpEL expression handling>>
* Integrating with <<custom-authorization-managers,custom authorization systems>>
* Providing <<fallback-values-authorization-denied,fallback values when authorization is denied>>

[[method-security-architecture]]
== How Method Security Works
Expand Down Expand Up @@ -2215,7 +2215,7 @@ You can also add the Spring Boot property `spring.jackson.default-property-inclu
There are some scenarios where you may not wish to throw an `AccessDeniedException` when a method is invoked without the required permissions.
Instead, you might wish to return a post-processed result, like a masked result, or a default value in cases where access denied happened before invoking the method.

Spring Security provides support for handling and post-processing method access denied with the `@PreAuthorize` and `@PostAuthorize` annotations respectively.
Spring Security provides support for handling and post-processing method access denied with the <<authorizing-with-annotations,`@PreAuthorize` and `@PostAuthorize` annotations>> respectively.
The `@PreAuthorize` annotation works with implementations of `MethodAuthorizationDeniedHandler` while the `@PostAuthorize` annotation works with implementations of `MethodAuthorizationDeniedPostProcessor`.

=== Using with `@PreAuthorize`
Expand Down Expand Up @@ -2596,6 +2596,71 @@ fun barWhenDeniedThenReturnQuestionMarks() {
----
======

=== Combining with Meta Annotation Support

Some authorization expressions may be long enough that it can become hard to read or to maintain.
For example, consider the following `@PreAuthorize` expression:

[tabs]
======
Java::
+
[source,java,role="primary"]
----
@PreAuthorize(value = "@myAuthorizationBean.check()", handlerClass = NullAuthorizationDeniedHandler.class)
public String myMethod() {
// ...
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@PreAuthorize(value = "@myAuthorizationBean.check()", handlerClass = NullAuthorizationDeniedHandler::class)
fun myMethod(): String {
// ...
}
----
======

The way it is, it is somewhat hard to read it, but we can do better.
By using the <<meta-annotations,meta annotation support>>, we can simplify it to:

[tabs]
======
Java::
+
[source,java,role="primary"]
----
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize(value = "@myAuthorizationBean.check()", handlerClass = NullAuthorizationDeniedHandler.class)
public @interface NullDenied {}
@NullDenied
public String myMethod() {
// ...
}
----
Kotlin::
+
[source,kotlin,role="secondary"]
----
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
@Retention(AnnotationRetention.RUNTIME)
@PreAuthorize(value = "@myAuthorizationBean.check()", handlerClass = NullAuthorizationDeniedHandler::class)
annotation class NullDenied
@NullDenied
fun myMethod(): String {
// ...
}
----
======

Make sure to read the <<meta-annotations,Meta Annotations Support>> section for more details on the usage.

[[migration-enableglobalmethodsecurity]]
== Migrating from `@EnableGlobalMethodSecurity`
Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/whats-new.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Below are the highlights of the release.

- https://github.com/spring-projects/spring-security/issues/14596[gh-14596] - xref:servlet/authorization/method-security.adoc[docs] - Add Programmatic Proxy Support for Method Security
- https://github.com/spring-projects/spring-security/issues/14597[gh-14597] - xref:servlet/authorization/method-security.adoc[docs] - Add Securing of Return Values
- https://github.com/spring-projects/spring-security/issues/14601[gh-14601] - xref:servlet/authorization/method-security.adoc#fallback-values-authorization-denied[docs] - Add Authorization Denied Handlers for Method Security

== Configuration

Expand Down

0 comments on commit 44d8c3e

Please sign in to comment.