Skip to content

Commit

Permalink
Add Authorization Denied Handlers for Method Security
Browse files Browse the repository at this point in the history
Closes gh-14601
  • Loading branch information
marcusdacoregio committed Apr 3, 2024
1 parent 19d66c0 commit d85857f
Show file tree
Hide file tree
Showing 36 changed files with 2,461 additions and 61 deletions.
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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ static MethodInterceptor preAuthorizeAuthorizationMethodInterceptor(
ObjectProvider<ObservationRegistry> registryProvider, ObjectProvider<RoleHierarchy> roleHierarchyProvider,
PrePostMethodSecurityConfiguration configuration, ApplicationContext context) {
PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
manager.setApplicationContext(context);
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
.preAuthorize(manager(manager, registryProvider));
preAuthorize.setOrder(preAuthorize.getOrder() + configuration.interceptorOrderOffset);
Expand All @@ -121,6 +122,7 @@ static MethodInterceptor postAuthorizeAuthorizationMethodInterceptor(
ObjectProvider<ObservationRegistry> registryProvider, ObjectProvider<RoleHierarchy> roleHierarchyProvider,
PrePostMethodSecurityConfiguration configuration, ApplicationContext context) {
PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager();
manager.setApplicationContext(context);
AuthorizationManagerAfterMethodInterceptor postAuthorize = AuthorizationManagerAfterMethodInterceptor
.postAuthorize(manager(manager, registryProvider));
postAuthorize.setOrder(postAuthorize.getOrder() + configuration.interceptorOrderOffset);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Role;
Expand Down Expand Up @@ -74,9 +75,10 @@ static MethodInterceptor preFilterAuthorizationMethodInterceptor(MethodSecurityE
static MethodInterceptor preAuthorizeAuthorizationMethodInterceptor(
MethodSecurityExpressionHandler expressionHandler,
ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider,
ObjectProvider<ObservationRegistry> registryProvider) {
ObjectProvider<ObservationRegistry> registryProvider, ApplicationContext context) {
PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager(
expressionHandler);
manager.setApplicationContext(context);
ReactiveAuthorizationManager<MethodInvocation> authorizationManager = manager(manager, registryProvider);
AuthorizationAdvisor interceptor = AuthorizationManagerBeforeReactiveMethodInterceptor
.preAuthorize(authorizationManager);
Expand All @@ -99,9 +101,10 @@ static MethodInterceptor postFilterAuthorizationMethodInterceptor(MethodSecurity
static MethodInterceptor postAuthorizeAuthorizationMethodInterceptor(
MethodSecurityExpressionHandler expressionHandler,
ObjectProvider<PrePostTemplateDefaults> defaultsObjectProvider,
ObjectProvider<ObservationRegistry> registryProvider) {
ObjectProvider<ObservationRegistry> registryProvider, ApplicationContext context) {
PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager(
expressionHandler);
manager.setApplicationContext(context);
ReactiveAuthorizationManager<MethodInvocationResult> authorizationManager = manager(manager, registryProvider);
AuthorizationAdvisor interceptor = AuthorizationManagerAfterReactiveMethodInterceptor
.postAuthorize(authorizationManager);
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 @@ -16,23 +16,42 @@

package org.springframework.security.config.annotation.method.configuration;

import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.List;

import jakarta.annotation.security.DenyAll;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import org.aopalliance.intercept.MethodInvocation;

import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
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.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.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.parameters.P;
import org.springframework.util.StringUtils;

/**
* @author Rob Winch
*/
@MethodSecurityService.Mask("classmask")
public interface MethodSecurityService {

@PreAuthorize("denyAll")
Expand Down Expand Up @@ -108,4 +127,196 @@ public interface MethodSecurityService {
@RequireAdminRole
void repeatedAnnotations();

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

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

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

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

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

@PreAuthorize(value = "denyAll()", handlerClass = MaskAnnotationHandler.class)
@Mask("methodmask")
String preAuthorizeDeniedMethodWithMaskAnnotation();

@PreAuthorize(value = "denyAll()", handlerClass = MaskAnnotationHandler.class)
String preAuthorizeDeniedMethodWithNoMaskAnnotation();

@NullDenied(role = "ADMIN")
String postAuthorizeDeniedWithNullDenied();

@PostAuthorize(value = "denyAll()", postProcessorClass = MaskAnnotationPostProcessor.class)
@Mask("methodmask")
String postAuthorizeDeniedMethodWithMaskAnnotation();

@PostAuthorize(value = "denyAll()", postProcessorClass = MaskAnnotationPostProcessor.class)
String postAuthorizeDeniedMethodWithNoMaskAnnotation();

@PreAuthorize(value = "hasRole('ADMIN')", handlerClass = MaskAnnotationHandler.class)
@Mask(expression = "@myMasker.getMask()")
String preAuthorizeWithMaskAnnotationUsingBean();

@PostAuthorize(value = "hasRole('ADMIN')", postProcessorClass = MaskAnnotationPostProcessor.class)
@Mask(expression = "@myMasker.getMask(returnObject)")
String postAuthorizeWithMaskAnnotationUsingBean();

@AuthorizeReturnObject
UserRecordWithEmailProtected getUserRecordWithEmailProtected();

@PreAuthorize(value = "hasRole('ADMIN')", handlerClass = UserFallbackDeniedHandler.class)
UserRecordWithEmailProtected getUserWithFallbackWhenUnauthorized();

class StarMaskingHandler implements MethodAuthorizationDeniedHandler {

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

}

class StartMaskingHandlerChild extends StarMaskingHandler {

@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult result) {
return super.handle(methodInvocation, result) + "-child";
}

}

class MaskAnnotationHandler implements MethodAuthorizationDeniedHandler {

MaskValueResolver maskValueResolver;

MaskAnnotationHandler(ApplicationContext context) {
this.maskValueResolver = new MaskValueResolver(context);
}

@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult result) {
Mask mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod(), Mask.class);
if (mask == null) {
mask = AnnotationUtils.getAnnotation(methodInvocation.getMethod().getDeclaringClass(), Mask.class);
}
return this.maskValueResolver.resolveValue(mask, methodInvocation, null);
}

}

class MaskAnnotationPostProcessor implements MethodAuthorizationDeniedPostProcessor {

MaskValueResolver maskValueResolver;

MaskAnnotationPostProcessor(ApplicationContext context) {
this.maskValueResolver = new MaskValueResolver(context);
}

@Override
public Object postProcessResult(MethodInvocationResult methodInvocationResult,
AuthorizationResult authorizationResult) {
MethodInvocation mi = methodInvocationResult.getMethodInvocation();
Mask mask = AnnotationUtils.getAnnotation(mi.getMethod(), Mask.class);
if (mask == null) {
mask = AnnotationUtils.getAnnotation(mi.getMethod().getDeclaringClass(), Mask.class);
}
return this.maskValueResolver.resolveValue(mask, mi, methodInvocationResult.getResult());
}

}

class MaskValueResolver {

DefaultMethodSecurityExpressionHandler expressionHandler;

MaskValueResolver(ApplicationContext context) {
this.expressionHandler = new DefaultMethodSecurityExpressionHandler();
this.expressionHandler.setApplicationContext(context);
}

String resolveValue(Mask mask, MethodInvocation mi, Object returnObject) {
if (StringUtils.hasText(mask.value())) {
return mask.value();
}
Expression expression = this.expressionHandler.getExpressionParser().parseExpression(mask.expression());
EvaluationContext evaluationContext = this.expressionHandler
.createEvaluationContext(() -> SecurityContextHolder.getContext().getAuthentication(), mi);
if (returnObject != null) {
this.expressionHandler.setReturnObject(returnObject, evaluationContext);
}
return expression.getValue(evaluationContext, String.class);
}

}

class PostMaskingPostProcessor implements MethodAuthorizationDeniedPostProcessor {

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

}

class CardNumberMaskingPostProcessor implements MethodAuthorizationDeniedPostProcessor {

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

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

}

class NullPostProcessor implements MethodAuthorizationDeniedPostProcessor {

@Override
public Object postProcessResult(MethodInvocationResult methodInvocationResult,
AuthorizationResult authorizationResult) {
return null;
}

}

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@interface Mask {

String value() default "";

String expression() default "";

}

@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@PostAuthorize(value = "hasRole('{value}')", postProcessorClass = NullPostProcessor.class)
@interface NullDenied {

String role();

}

class UserFallbackDeniedHandler implements MethodAuthorizationDeniedHandler {

private static final UserRecordWithEmailProtected FALLBACK = new UserRecordWithEmailProtected("Protected",
"Protected");

@Override
public Object handle(MethodInvocation methodInvocation, AuthorizationResult authorizationResult) {
return FALLBACK;
}

}

}
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,74 @@ 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");
}

@Override
public String preAuthorizeDeniedMethodWithMaskAnnotation() {
return "ok";
}

@Override
public String preAuthorizeDeniedMethodWithNoMaskAnnotation() {
return "ok";
}

@Override
public String postAuthorizeDeniedWithNullDenied() {
return "ok";
}

@Override
public String postAuthorizeDeniedMethodWithMaskAnnotation() {
return "ok";
}

@Override
public String postAuthorizeDeniedMethodWithNoMaskAnnotation() {
return "ok";
}

@Override
public String preAuthorizeWithMaskAnnotationUsingBean() {
return "ok";
}

@Override
public String postAuthorizeWithMaskAnnotationUsingBean() {
return "ok";
}

@Override
public UserRecordWithEmailProtected getUserRecordWithEmailProtected() {
return new UserRecordWithEmailProtected("username", "[email protected]");
}

@Override
public UserRecordWithEmailProtected getUserWithFallbackWhenUnauthorized() {
return new UserRecordWithEmailProtected("username", "[email protected]");
}

}
Loading

0 comments on commit d85857f

Please sign in to comment.