Skip to content

Commit

Permalink
Add meta-annotation parameter support
Browse files Browse the repository at this point in the history
  • Loading branch information
jzheaux committed Feb 22, 2024
1 parent e771267 commit 3a85172
Show file tree
Hide file tree
Showing 21 changed files with 582 additions and 127 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,11 @@
*/
int offset() default 0;

/**
* Indicate whether to resolve authorization templates with annotation parameters
* @return the resolution policy to use
* @since 6.3
*/
boolean useAnnotationParameters() default false;

}
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,11 @@
*/
boolean useAuthorizationManager() default true;

/**
* Indicate whether to resolve authorization templates with annotation parameters
* @return the resolution policy to use
* @since 6.3
*/
boolean useAnnotationParameters() default false;

}
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ final class PrePostMethodSecurityConfiguration implements ImportAware {

private int interceptorOrderOffset;

private boolean useAnnotationParameters;

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static MethodInterceptor preFilterAuthorizationMethodInterceptor(
Expand All @@ -77,6 +79,7 @@ static MethodInterceptor preFilterAuthorizationMethodInterceptor(
strategyProvider.ifAvailable(preFilter::setSecurityContextHolderStrategy);
preFilter.setExpressionHandler(new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider,
defaultsProvider, roleHierarchyProvider, context));
preFilter.setUseAnnotationParameters(configuration.useAnnotationParameters);
return preFilter;
}

Expand All @@ -92,6 +95,7 @@ static MethodInterceptor preAuthorizeAuthorizationMethodInterceptor(
PreAuthorizeAuthorizationManager manager = new PreAuthorizeAuthorizationManager();
manager.setExpressionHandler(new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider,
defaultsProvider, roleHierarchyProvider, context));
manager.setUseAnnotationParameters(configuration.useAnnotationParameters);
AuthorizationManagerBeforeMethodInterceptor preAuthorize = AuthorizationManagerBeforeMethodInterceptor
.preAuthorize(manager(manager, registryProvider));
preAuthorize.setOrder(preAuthorize.getOrder() + configuration.interceptorOrderOffset);
Expand All @@ -112,6 +116,7 @@ static MethodInterceptor postAuthorizeAuthorizationMethodInterceptor(
PostAuthorizeAuthorizationManager manager = new PostAuthorizeAuthorizationManager();
manager.setExpressionHandler(new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider,
defaultsProvider, roleHierarchyProvider, context));
manager.setUseAnnotationParameters(configuration.useAnnotationParameters);
AuthorizationManagerAfterMethodInterceptor postAuthorize = AuthorizationManagerAfterMethodInterceptor
.postAuthorize(manager(manager, registryProvider));
postAuthorize.setOrder(postAuthorize.getOrder() + configuration.interceptorOrderOffset);
Expand All @@ -133,6 +138,7 @@ static MethodInterceptor postFilterAuthorizationMethodInterceptor(
strategyProvider.ifAvailable(postFilter::setSecurityContextHolderStrategy);
postFilter.setExpressionHandler(new DeferringMethodSecurityExpressionHandler(expressionHandlerProvider,
defaultsProvider, roleHierarchyProvider, context));
postFilter.setUseAnnotationParameters(configuration.useAnnotationParameters);
return postFilter;
}

Expand All @@ -156,6 +162,7 @@ static <T> AuthorizationManager<T> manager(AuthorizationManager<T> delegate,
public void setImportMetadata(AnnotationMetadata importMetadata) {
EnableMethodSecurity annotation = importMetadata.getAnnotations().get(EnableMethodSecurity.class).synthesize();
this.interceptorOrderOffset = annotation.offset();
this.useAnnotationParameters = annotation.useAnnotationParameters();
}

private static final class DeferringMethodSecurityExpressionHandler implements MethodSecurityExpressionHandler {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportAware;
import org.springframework.context.annotation.Role;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
Expand All @@ -45,37 +47,55 @@
* @since 5.8
*/
@Configuration(proxyBeanMethods = false)
final class ReactiveAuthorizationManagerMethodSecurityConfiguration {
final class ReactiveAuthorizationManagerMethodSecurityConfiguration implements ImportAware {

private boolean useAnnotationParameters;

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static PreFilterAuthorizationReactiveMethodInterceptor preFilterInterceptor(
MethodSecurityExpressionHandler expressionHandler) {
return new PreFilterAuthorizationReactiveMethodInterceptor(expressionHandler);
MethodSecurityExpressionHandler expressionHandler,
ReactiveAuthorizationManagerMethodSecurityConfiguration configuration) {
PreFilterAuthorizationReactiveMethodInterceptor interceptor = new PreFilterAuthorizationReactiveMethodInterceptor(
expressionHandler);
interceptor.setUseAnnotationParameters(configuration.useAnnotationParameters);
return interceptor;
}

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static AuthorizationManagerBeforeReactiveMethodInterceptor preAuthorizeInterceptor(
MethodSecurityExpressionHandler expressionHandler, ObjectProvider<ObservationRegistry> registryProvider) {
ReactiveAuthorizationManager<MethodInvocation> authorizationManager = manager(
new PreAuthorizeReactiveAuthorizationManager(expressionHandler), registryProvider);
MethodSecurityExpressionHandler expressionHandler,
ReactiveAuthorizationManagerMethodSecurityConfiguration configuration,
ObjectProvider<ObservationRegistry> registryProvider) {
PreAuthorizeReactiveAuthorizationManager manager = new PreAuthorizeReactiveAuthorizationManager(
expressionHandler);
manager.setUseAnnotationParameters(configuration.useAnnotationParameters);
ReactiveAuthorizationManager<MethodInvocation> authorizationManager = manager(manager, registryProvider);
return AuthorizationManagerBeforeReactiveMethodInterceptor.preAuthorize(authorizationManager);
}

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static PostFilterAuthorizationReactiveMethodInterceptor postFilterInterceptor(
MethodSecurityExpressionHandler expressionHandler) {
return new PostFilterAuthorizationReactiveMethodInterceptor(expressionHandler);
MethodSecurityExpressionHandler expressionHandler,
ReactiveAuthorizationManagerMethodSecurityConfiguration configuration) {
PostFilterAuthorizationReactiveMethodInterceptor interceptor = new PostFilterAuthorizationReactiveMethodInterceptor(
expressionHandler);
interceptor.setUseAnnotationParameters(configuration.useAnnotationParameters);
return interceptor;
}

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
static AuthorizationManagerAfterReactiveMethodInterceptor postAuthorizeInterceptor(
MethodSecurityExpressionHandler expressionHandler, ObjectProvider<ObservationRegistry> registryProvider) {
ReactiveAuthorizationManager<MethodInvocationResult> authorizationManager = manager(
new PostAuthorizeReactiveAuthorizationManager(expressionHandler), registryProvider);
MethodSecurityExpressionHandler expressionHandler,
ReactiveAuthorizationManagerMethodSecurityConfiguration configuration,
ObjectProvider<ObservationRegistry> registryProvider) {
PostAuthorizeReactiveAuthorizationManager manager = new PostAuthorizeReactiveAuthorizationManager(
expressionHandler);
manager.setUseAnnotationParameters(configuration.useAnnotationParameters);
ReactiveAuthorizationManager<MethodInvocationResult> authorizationManager = manager(manager, registryProvider);
return AuthorizationManagerAfterReactiveMethodInterceptor.postAuthorize(authorizationManager);
}

Expand All @@ -95,4 +115,12 @@ static <T> ReactiveAuthorizationManager<T> manager(ReactiveAuthorizationManager<
return new DeferringObservationReactiveAuthorizationManager<>(registryProvider, delegate);
}

@Override
public void setImportMetadata(AnnotationMetadata importMetadata) {
EnableReactiveMethodSecurity annotation = importMetadata.getAnnotations()
.get(EnableReactiveMethodSecurity.class)
.synthesize();
this.useAnnotationParameters = annotation.useAnnotationParameters();
}

}
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 @@ -17,6 +17,8 @@
package org.springframework.security.config.annotation.method.configuration;

import java.io.Serializable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
Expand Down Expand Up @@ -49,6 +51,10 @@
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
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.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationEventPublisher;
import org.springframework.security.authorization.AuthorizationManager;
Expand Down Expand Up @@ -587,6 +593,74 @@ public void allAnnotationsWhenAdviceAfterAllOffsetThenReturnsFilteredList() {
assertThat(filtered).containsExactly("DoNotDrop");
}

@Test
@WithMockUser
public void methodeWhenParameterizedPreAuthorizeMetaAnnotationThenPasses() {
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
assertThat(service.hasRole("USER")).isTrue();
}

@Test
@WithMockUser
public void methodRoleWhenPreAuthorizeMetaAnnotationHardcodedParameterThenPasses() {
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
assertThat(service.hasUserRole()).isTrue();
}

@Test
public void methodWhenParameterizedAnnotationThenFails() {
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
assertThatExceptionOfType(IllegalArgumentException.class)
.isThrownBy(service::placeholdersOnlyResolvedByMetaAnnotations);
}

@Test
@WithMockUser(authorities = "SCOPE_message:read")
public void methodWhenMultiplePlaceholdersHasAuthorityThenPasses() {
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
assertThat(service.readMessage()).isEqualTo("message");
}

@Test
@WithMockUser(roles = "ADMIN")
public void methodWhenMultiplePlaceholdersHasRoleThenPasses() {
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
assertThat(service.readMessage()).isEqualTo("message");
}

@Test
@WithMockUser
public void methodWhenPostAuthorizeMetaAnnotationThenAuthorizes() {
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
service.startsWithDave("daveMatthews");
assertThatExceptionOfType(AccessDeniedException.class)
.isThrownBy(() -> service.startsWithDave("jenniferHarper"));
}

@Test
@WithMockUser
public void methodWhenPreFilterMetaAnnotationThenFilters() {
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
assertThat(service.parametersContainDave(new ArrayList<>(List.of("dave", "carla", "vanessa", "paul"))))
.containsExactly("dave");
}

@Test
@WithMockUser
public void methodWhenPostFilterMetaAnnotationThenFilters() {
this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire();
MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class);
assertThat(service.resultsContainDave(new ArrayList<>(List.of("dave", "carla", "vanessa", "paul"))))
.containsExactly("dave");
}

private static Consumer<ConfigurableWebApplicationContext> disallowBeanOverriding() {
return (context) -> ((AnnotationConfigWebApplicationContext) context).setAllowBeanDefinitionOverriding(false);
}
Expand Down Expand Up @@ -890,4 +964,95 @@ Authz authz() {

}

@Configuration
@EnableMethodSecurity(useAnnotationParameters = true)
static class MetaAnnotationPlaceholderConfig {

@Bean
MetaAnnotationService methodSecurityService() {
return new MetaAnnotationService();
}

}

static class MetaAnnotationService {

@RequireRole(role = "#role")
boolean hasRole(String role) {
return true;
}

@RequireRole(role = "'USER'")
boolean hasUserRole() {
return true;
}

@PreAuthorize("hasRole({role})")
void placeholdersOnlyResolvedByMetaAnnotations() {
}

@HasClaim(claim = "message:read", roles = { "'ADMIN'" })
String readMessage() {
return "message";
}

@ResultStartsWith("dave")
String startsWithDave(String value) {
return value;
}

@ParameterContains("dave")
List<String> parametersContainDave(List<String> list) {
return list;
}

@ResultContains("dave")
List<String> resultsContainDave(List<String> list) {
return list;
}

}

@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole({role})")
@interface RequireRole {

String role();

}

@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAuthority('SCOPE_{claim}') || hasAnyRole({roles})")
@interface HasClaim {

String claim();

String[] roles() default {};

}

@Retention(RetentionPolicy.RUNTIME)
@PostAuthorize("returnObject.startsWith('{value}')")
@interface ResultStartsWith {

String value();

}

@Retention(RetentionPolicy.RUNTIME)
@PreFilter("filterObject.contains('{value}')")
@interface ParameterContains {

String value();

}

@Retention(RetentionPolicy.RUNTIME)
@PostFilter("filterObject.contains('{value}')")
@interface ResultContains {

String value();

}

}
Loading

0 comments on commit 3a85172

Please sign in to comment.