Skip to content

Commit

Permalink
Allow post-processing of authorization denied results with @PreAuthorize
Browse files Browse the repository at this point in the history
  • Loading branch information
marcusdacoregio committed Mar 20, 2024
1 parent 7ce2b73 commit d11e4af
Show file tree
Hide file tree
Showing 22 changed files with 617 additions and 23 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2002-2024 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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authorization.method.DefaultPostInvocationAuthorizationDeniedPostProcessor;
import org.springframework.security.authorization.method.DefaultPreInvocationAuthorizationDeniedPostProcessor;

@Configuration(proxyBeanMethods = false)
class AuthorizationPostProcessorConfiguration {

@Bean
DefaultPreInvocationAuthorizationDeniedPostProcessor defaultPreAuthorizeMethodAccessDeniedHandler() {
return new DefaultPreInvocationAuthorizationDeniedPostProcessor();
}

@Bean
DefaultPostInvocationAuthorizationDeniedPostProcessor defaultPostAuthorizeMethodAccessDeniedHandler() {
return new DefaultPostInvocationAuthorizationDeniedPostProcessor();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ public String[] selectImports(@NonNull AnnotationMetadata importMetadata) {
imports.add(Jsr250MethodSecurityConfiguration.class.getName());
}
imports.add(AuthorizationProxyConfiguration.class.getName());
imports.add(AuthorizationPostProcessorConfiguration.class.getName());
return imports.toArray(new String[0]);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ static MethodInterceptor preAuthorizeAuthorizationMethodInterceptor(
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
.preAuthorize(manager(manager, registryProvider));
preAuthorize.setOrder(preAuthorize.getOrder() + configuration.interceptorOrderOffset);
preAuthorize.setApplicationContext(context);
return new DeferringMethodInterceptor<>(preAuthorize, (f) -> {
methodSecurityDefaultsProvider.ifAvailable(manager::setTemplateDefaults);
manager.setExpressionHandler(expressionHandlerProvider
Expand All @@ -124,6 +125,7 @@ static MethodInterceptor postAuthorizeAuthorizationMethodInterceptor(
AuthorizationManagerAfterMethodInterceptor postAuthorize = AuthorizationManagerAfterMethodInterceptor
.postAuthorize(manager(manager, registryProvider));
postAuthorize.setOrder(postAuthorize.getOrder() + configuration.interceptorOrderOffset);
postAuthorize.setApplicationContext(context);
return new DeferringMethodInterceptor<>(postAuthorize, (f) -> {
methodSecurityDefaultsProvider.ifAvailable(manager::setTemplateDefaults);
manager.setExpressionHandler(expressionHandlerProvider
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
Expand All @@ -21,12 +21,16 @@
import jakarta.annotation.security.DenyAll;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import org.aopalliance.intercept.MethodInvocation;

import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PostAuthorize;
import org.springframework.security.access.prepost.PostFilter;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.access.prepost.PreFilter;
import org.springframework.security.authorization.AuthorizationResult;
import org.springframework.security.authorization.method.AuthorizationDeniedPostProcessor;
import org.springframework.security.authorization.method.MethodInvocationResult;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.parameters.P;

Expand Down Expand Up @@ -108,4 +112,59 @@ public interface MethodSecurityService {
@RequireAdminRole
void repeatedAnnotations();

@PostAuthorize(value = "hasRole('ADMIN')", postProcessorClass = CardNumberMaskingPostProcessor.class)
String postAuthorizeGetCardNumberIfAdmin(String cardNumber);

@PreAuthorize(value = "hasRole('ADMIN')", postProcessorClass = MaskingPostProcessor.class)
String preAuthorizeGetCardNumberIfAdmin(String cardNumber);

@PreAuthorize(value = "hasRole('ADMIN')", postProcessorClass = MaskingPostProcessorChild.class)
String preAuthorizeWithHandlerChildGetCardNumberIfAdmin(String cardNumber);

@PreAuthorize(value = "hasRole('ADMIN')", postProcessorClass = MaskingPostProcessor.class)
String preAuthorizeThrowAccessDeniedManually();

@PostAuthorize(value = "hasRole('ADMIN')", postProcessorClass = PostMaskingPostProcessor.class)
String postAuthorizeThrowAccessDeniedManually();

class MaskingPostProcessor implements AuthorizationDeniedPostProcessor<MethodInvocation> {

@Override
public Object postProcessResult(MethodInvocation contextObject, AuthorizationResult result) {
return "***";
}

}

class MaskingPostProcessorChild extends MaskingPostProcessor {

@Override
public Object postProcessResult(MethodInvocation contextObject, AuthorizationResult result) {
Object mask = super.postProcessResult(contextObject, result);
return mask + "-child";
}

}

class PostMaskingPostProcessor implements AuthorizationDeniedPostProcessor<MethodInvocationResult> {

@Override
public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) {
return "***";
}

}

class CardNumberMaskingPostProcessor implements AuthorizationDeniedPostProcessor<MethodInvocationResult> {

static String MASK = "****-****-****-";

@Override
public Object postProcessResult(MethodInvocationResult contextObject, AuthorizationResult result) {
String cardNumber = (String) contextObject.getResult();
return MASK + cardNumber.substring(cardNumber.length() - 4);
}

}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
Expand All @@ -18,6 +18,7 @@

import java.util.List;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;

Expand Down Expand Up @@ -126,4 +127,29 @@ public List<String> allAnnotations(List<String> list) {
public void repeatedAnnotations() {
}

@Override
public String postAuthorizeGetCardNumberIfAdmin(String cardNumber) {
return cardNumber;
}

@Override
public String preAuthorizeGetCardNumberIfAdmin(String cardNumber) {
return cardNumber;
}

@Override
public String preAuthorizeWithHandlerChildGetCardNumberIfAdmin(String cardNumber) {
return cardNumber;
}

@Override
public String preAuthorizeThrowAccessDeniedManually() {
throw new AccessDeniedException("Access Denied");
}

@Override
public String postAuthorizeThrowAccessDeniedManually() {
throw new AccessDeniedException("Access Denied");
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,66 @@ public void methodWhenPostFilterMetaAnnotationThenFilters() {
.containsExactly("dave");
}

@Test
@WithMockUser
void getCardNumberWhenPostAuthorizeAndNotAdminThenReturnMasked() {
this.spring
.register(AuthzConfig.class, MethodSecurityServiceEnabledConfig.class,
MethodSecurityService.CardNumberMaskingPostProcessor.class,
MethodSecurityService.MaskingPostProcessor.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
String cardNumber = service.postAuthorizeGetCardNumberIfAdmin("4444-3333-2222-1111");
assertThat(cardNumber).isEqualTo("****-****-****-1111");
}

@Test
@WithMockUser
void getCardNumberWhenPreAuthorizeAndNotAdminThenReturnMasked() {
this.spring
.register(AuthzConfig.class, MethodSecurityServiceEnabledConfig.class,
MethodSecurityService.MaskingPostProcessor.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
String cardNumber = service.preAuthorizeGetCardNumberIfAdmin("4444-3333-2222-1111");
assertThat(cardNumber).isEqualTo("***");
}

@Test
@WithMockUser
void getCardNumberWhenPreAuthorizeAndNotAdminAndChildHandlerThenResolveCorrectHandlerAndReturnMasked() {
this.spring.register(AuthzConfig.class, MethodSecurityServiceEnabledConfig.class,
MethodSecurityService.MaskingPostProcessor.class, MethodSecurityService.MaskingPostProcessorChild.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
String cardNumber = service.preAuthorizeWithHandlerChildGetCardNumberIfAdmin("4444-3333-2222-1111");
assertThat(cardNumber).isEqualTo("***-child");
}

@Test
@WithMockUser(roles = "ADMIN")
void preAuthorizeWhenHandlerAndAccessDeniedNotThrownFromPreAuthorizeThenNotHandled() {
this.spring
.register(AuthzConfig.class, MethodSecurityServiceEnabledConfig.class,
MethodSecurityService.MaskingPostProcessor.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
assertThatExceptionOfType(AccessDeniedException.class)
.isThrownBy(service::preAuthorizeThrowAccessDeniedManually);
}

@Test
@WithMockUser(roles = "ADMIN")
void postAuthorizeWhenHandlerAndAccessDeniedNotThrownFromPostAuthorizeThenNotHandled() {
this.spring
.register(AuthzConfig.class, MethodSecurityServiceEnabledConfig.class,
MethodSecurityService.PostMaskingPostProcessor.class)
.autowire();
MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class);
assertThatExceptionOfType(AccessDeniedException.class)
.isThrownBy(service::postAuthorizeThrowAccessDeniedManually);
}

private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
}
Expand All @@ -675,6 +735,16 @@ private static Advisor returnAdvisor(int order) {
return advisor;
}

@Configuration
static class AuthzConfig {

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

}

@Configuration
@EnableCustomMethodSecurity
static class CustomMethodSecurityServiceConfig {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2024 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.
Expand All @@ -23,6 +23,10 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.security.authorization.method.AuthorizationDeniedPostProcessor;
import org.springframework.security.authorization.method.DefaultPostInvocationAuthorizationDeniedPostProcessor;
import org.springframework.security.authorization.method.MethodInvocationResult;

/**
* Annotation for specifying a method access-control expression which will be evaluated
* after a method has been invoked.
Expand All @@ -42,4 +46,6 @@
*/
String value();

Class<? extends AuthorizationDeniedPostProcessor<MethodInvocationResult>> postProcessorClass() default DefaultPostInvocationAuthorizationDeniedPostProcessor.class;

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2002-2016 the original author or authors.
* Copyright 2002-2024 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.
Expand All @@ -23,6 +23,11 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.aopalliance.intercept.MethodInvocation;

import org.springframework.security.authorization.method.AuthorizationDeniedPostProcessor;
import org.springframework.security.authorization.method.DefaultPreInvocationAuthorizationDeniedPostProcessor;

/**
* Annotation for specifying a method access-control expression which will be evaluated to
* decide whether a method invocation is allowed or not.
Expand All @@ -42,4 +47,6 @@
*/
String value();

Class<? extends AuthorizationDeniedPostProcessor<MethodInvocation>> postProcessorClass() default DefaultPreInvocationAuthorizationDeniedPostProcessor.class;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2002-2024 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;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.util.Assert;

public class AuthorizationException extends AccessDeniedException {

private final AuthorizationResult result;

public AuthorizationException(String msg, AuthorizationResult result) {
super(msg);
Assert.notNull(result, "decision cannot be null");
Assert.state(!result.isGranted(), "Granted decisions are not supported");
this.result = result;
}

public AuthorizationResult getResult() {
return this.result;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2002-2024 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;

import org.springframework.lang.Nullable;
import org.springframework.security.authorization.AuthorizationResult;

public interface AuthorizationDeniedPostProcessor<T> {

@Nullable
Object postProcessResult(T contextObject, AuthorizationResult result);

}
Loading

0 comments on commit d11e4af

Please sign in to comment.