Skip to content

Commit

Permalink
Polish
Browse files Browse the repository at this point in the history
  • Loading branch information
jzheaux committed Apr 4, 2024
1 parent 9dba87c commit 7e69289
Show file tree
Hide file tree
Showing 15 changed files with 150 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

import reactor.core.publisher.Mono;

import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

Expand Down Expand Up @@ -45,4 +47,20 @@ public boolean check(Authentication authentication, String message) {
return message != null && message.contains(authentication.getName());
}

public AuthorizationResult checkResult(boolean result) {
return new AuthzResult(result);
}

public Mono<AuthorizationResult> checkReactiveResult(boolean result) {
return Mono.just(checkResult(result));
}

public static class AuthzResult extends AuthorizationDecision {

public AuthzResult(boolean granted) {
super(granted);
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,11 @@ public interface MethodSecurityService {
@PreAuthorize(value = "hasRole('ADMIN')", handlerClass = UserFallbackDeniedHandler.class)
UserRecordWithEmailProtected getUserWithFallbackWhenUnauthorized();

@PreAuthorize(value = "@authz.checkResult(#result)", handlerClass = MethodAuthorizationDeniedHandler.class)
@PostAuthorize(value = "@authz.checkResult(!#result)",
postProcessorClass = MethodAuthorizationDeniedPostProcessor.class)
String checkCustomResult(boolean result);

class StarMaskingHandler implements MethodAuthorizationDeniedHandler {

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,14 @@ MethodSecurityService service() {
return new MethodSecurityServiceImpl();
}

@Bean
ReactiveMethodSecurityService reactiveService() {
return new ReactiveMethodSecurityServiceImpl();
}

@Bean
Authz authz() {
return new Authz();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -197,4 +197,9 @@ public UserRecordWithEmailProtected getUserWithFallbackWhenUnauthorized() {
return new UserRecordWithEmailProtected("username", "[email protected]");
}

@Override
public String checkCustomResult(boolean result) {
return "ok";
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@
import org.springframework.security.authorization.method.AuthorizationInterceptorsOrder;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
import org.springframework.security.authorization.method.AuthorizeReturnObject;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor;
import org.springframework.security.authorization.method.MethodInvocationResult;
import org.springframework.security.authorization.method.PrePostTemplateDefaults;
import org.springframework.security.config.Customizer;
Expand All @@ -92,6 +94,8 @@
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;

/**
* Tests for {@link PrePostMethodSecurityConfiguration}.
Expand Down Expand Up @@ -925,6 +929,23 @@ void getUserWhenNotAuthorizedAndHandlerFallbackValueThenReturnFallbackValue() {
assertThat(user.name()).isEqualTo("Protected");
}

@Test
@WithMockUser
void getUserWhenNotAuthorizedThenHandlerUsesCustomAuthorizationDecision() {
this.spring.register(MethodSecurityServiceConfig.class, CustomResultConfig.class).autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
MethodAuthorizationDeniedHandler handler = this.spring.getContext()
.getBean(MethodAuthorizationDeniedHandler.class);
MethodAuthorizationDeniedPostProcessor postProcessor = this.spring.getContext()
.getBean(MethodAuthorizationDeniedPostProcessor.class);
assertThat(service.checkCustomResult(false)).isNull();
verify(handler).handle(any(), any(Authz.AuthzResult.class));
verifyNoInteractions(postProcessor);
assertThat(service.checkCustomResult(true)).isNull();
verify(postProcessor).postProcessResult(any(), any(Authz.AuthzResult.class));
verifyNoMoreInteractions(handler);
}

private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
}
Expand Down Expand Up @@ -1449,4 +1470,23 @@ public String getName() {

}

@EnableMethodSecurity
static class CustomResultConfig {

MethodAuthorizationDeniedHandler handler = mock(MethodAuthorizationDeniedHandler.class);

MethodAuthorizationDeniedPostProcessor postProcessor = mock(MethodAuthorizationDeniedPostProcessor.class);

@Bean
MethodAuthorizationDeniedHandler methodAuthorizationDeniedHandler() {
return this.handler;
}

@Bean
MethodAuthorizationDeniedPostProcessor methodAuthorizationDeniedPostProcessor() {
return this.postProcessor;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,23 @@
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory;
import org.springframework.security.authorization.method.AuthorizationAdvisorProxyFactory.TargetVisitor;
import org.springframework.security.authorization.method.AuthorizeReturnObject;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedHandler;
import org.springframework.security.authorization.method.MethodAuthorizationDeniedPostProcessor;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.test.SpringTestContext;
import org.springframework.security.config.test.SpringTestContextExtension;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.test.context.support.WithMockUser;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;

/**
* @author Tadaya Tsuyukubo
Expand All @@ -65,7 +73,7 @@ public class ReactiveMethodSecurityConfigurationTests {

public final SpringTestContext spring = new SpringTestContext(this);

@Autowired
@Autowired(required = false)
DefaultMethodSecurityExpressionHandler methodSecurityExpressionHandler;

@Test
Expand Down Expand Up @@ -212,6 +220,23 @@ public void findAllWhenNestedPreAuthorizeThenAuthorizes() {
.verifyError(AccessDeniedException.class);
}

@Test
@WithMockUser
void getUserWhenNotAuthorizedThenHandlerUsesCustomAuthorizationDecision() {
this.spring.register(MethodSecurityServiceConfig.class, CustomResultConfig.class).autowire();
ReactiveMethodSecurityService service = this.spring.getContext().getBean(ReactiveMethodSecurityService.class);
MethodAuthorizationDeniedHandler handler = this.spring.getContext()
.getBean(MethodAuthorizationDeniedHandler.class);
MethodAuthorizationDeniedPostProcessor postProcessor = this.spring.getContext()
.getBean(MethodAuthorizationDeniedPostProcessor.class);
assertThat(service.checkCustomResult(false).block()).isNull();
verify(handler).handle(any(), any(Authz.AuthzResult.class));
verifyNoInteractions(postProcessor);
assertThat(service.checkCustomResult(true).block()).isNull();
verify(postProcessor).postProcessResult(any(), any(Authz.AuthzResult.class));
verifyNoMoreInteractions(handler);
}

private static Consumer<User.UserBuilder> authorities(String... authorities) {
return (builder) -> builder.authorities(authorities);
}
Expand Down Expand Up @@ -353,4 +378,23 @@ public Mono<String> getName() {

}

@EnableReactiveMethodSecurity
static class CustomResultConfig {

MethodAuthorizationDeniedHandler handler = mock(MethodAuthorizationDeniedHandler.class);

MethodAuthorizationDeniedPostProcessor postProcessor = mock(MethodAuthorizationDeniedPostProcessor.class);

@Bean
MethodAuthorizationDeniedHandler methodAuthorizationDeniedHandler() {
return this.handler;
}

@Bean
MethodAuthorizationDeniedPostProcessor methodAuthorizationDeniedPostProcessor() {
return this.postProcessor;
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ public interface ReactiveMethodSecurityService {
@Mask(expression = "@myMasker.getMask(returnObject)")
Mono<String> postAuthorizeWithMaskAnnotationUsingBean();

@PreAuthorize(value = "@authz.checkReactiveResult(#result)", handlerClass = MethodAuthorizationDeniedHandler.class)
@PostAuthorize(value = "@authz.checkReactiveResult(!#result)",
postProcessorClass = MethodAuthorizationDeniedPostProcessor.class)
Mono<String> checkCustomResult(boolean result);

class StarMaskingHandler implements MethodAuthorizationDeniedHandler {

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,9 @@ public Mono<String> postAuthorizeWithMaskAnnotationUsingBean() {
return Mono.just("ok");
}

@Override
public Mono<String> checkCustomResult(boolean result) {
return Mono.just("ok");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.springframework.expression.EvaluationException;
import org.springframework.expression.Expression;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.ExpressionAuthorizationDecision;

public final class ExpressionUtils {
Expand All @@ -34,10 +35,10 @@ private ExpressionUtils() {
* @return the resulting {@link AuthorizationDecision}
* @since 6.3
*/
public static AuthorizationDecision evaluate(Expression expr, EvaluationContext ctx) {
public static AuthorizationResult evaluate(Expression expr, EvaluationContext ctx) {
try {
Object result = expr.getValue(ctx);
if (result instanceof AuthorizationDecision decision) {
if (result instanceof AuthorizationResult decision) {
return decision;
}
if (result instanceof Boolean granted) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ public AuthorizationDecision check(Supplier<Authentication> authentication, Meth
MethodSecurityExpressionHandler expressionHandler = this.registry.getExpressionHandler();
EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication, mi.getMethodInvocation());
expressionHandler.setReturnObject(mi.getResult(), ctx);
return ExpressionUtils.evaluate(attribute.getExpression(), ctx);
return (AuthorizationDecision) ExpressionUtils.evaluate(attribute.getExpression(), ctx);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, Me
return authentication
.map((auth) -> expressionHandler.createEvaluationContext(auth, mi))
.doOnNext((ctx) -> expressionHandler.setReturnObject(result.getResult(), ctx))
.flatMap((ctx) -> ReactiveExpressionUtils.evaluate(attribute.getExpression(), ctx));
.flatMap((ctx) -> ReactiveExpressionUtils.evaluate(attribute.getExpression(), ctx))
.cast(AuthorizationDecision.class);
// @formatter:on
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public AuthorizationDecision check(Supplier<Authentication> authentication, Meth
return null;
}
EvaluationContext ctx = this.registry.getExpressionHandler().createEvaluationContext(authentication, mi);
return ExpressionUtils.evaluate(attribute.getExpression(), ctx);
return (AuthorizationDecision) ExpressionUtils.evaluate(attribute.getExpression(), ctx);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, Me
// @formatter:off
return authentication
.map((auth) -> this.registry.getExpressionHandler().createEvaluationContext(auth, mi))
.flatMap((ctx) -> ReactiveExpressionUtils.evaluate(attribute.getExpression(), ctx));
.flatMap((ctx) -> ReactiveExpressionUtils.evaluate(attribute.getExpression(), ctx))
.cast(AuthorizationDecision.class);
// @formatter:on
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.Expression;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.ExpressionAuthorizationDecision;

/**
Expand All @@ -32,7 +32,7 @@
*/
final class ReactiveExpressionUtils {

static Mono<AuthorizationDecision> evaluate(Expression expr, EvaluationContext ctx) {
static Mono<AuthorizationResult> evaluate(Expression expr, EvaluationContext ctx) {
return Mono.defer(() -> {
Object value;
try {
Expand All @@ -49,11 +49,11 @@ static Mono<AuthorizationDecision> evaluate(Expression expr, EvaluationContext c
});
}

private static Mono<AuthorizationDecision> adapt(Expression expr, Object value) {
private static Mono<AuthorizationResult> adapt(Expression expr, Object value) {
if (value instanceof Boolean granted) {
return Mono.just(new ExpressionAuthorizationDecision(granted, expr));
}
if (value instanceof AuthorizationDecision decision) {
if (value instanceof AuthorizationResult decision) {
return Mono.just(decision);
}
return createInvalidReturnTypeMono(expr);
Expand Down Expand Up @@ -86,8 +86,8 @@ static Mono<Boolean> evaluateAsBoolean(Expression expr, EvaluationContext ctx) {
}

private static <T> Mono<T> createInvalidReturnTypeMono(Expression expr) {
return Mono.error(() -> new IllegalStateException(
"Expression: '" + expr.getExpressionString() + "' must return boolean or Mono<Boolean>"));
return Mono.error(() -> new IllegalStateException("Expression: '" + expr.getExpressionString()
+ "' must return boolean, Mono<Boolean>, AuthorizationResult, or Mono<AuthorizationResult>"));
}

private ReactiveExpressionUtils() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1218,7 +1218,7 @@ It also has access to the full Java language.
[TIP]
In addition to returning a `Boolean`, you can also return `null` to indicate that the code abstains from making a decision.

If you want to include more information about the nature of the decision, you can instead return a a custom `AuthorizationDecision` like this:
If you want to include more information about the nature of the decision, you can instead return a custom `AuthorizationDecision` like this:

[tabs]
======
Expand Down Expand Up @@ -1249,7 +1249,7 @@ open class AuthorizationLogic {
----
======


Then, you can access the custom details when you <<fallback-values-authorization-denied, customize how the authorization result is handled>>.

[[custom-authorization-managers]]
=== Using a Custom Authorization Manager
Expand Down

0 comments on commit 7e69289

Please sign in to comment.