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 23, 2024
1 parent e771267 commit ad01f33
Show file tree
Hide file tree
Showing 16 changed files with 609 additions and 115 deletions.
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,12 +51,21 @@
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;
import org.springframework.security.authorization.method.AuthorizationInterceptorsOrder;
import org.springframework.security.authorization.method.AuthorizationManagerAfterMethodInterceptor;
import org.springframework.security.authorization.method.AuthorizationManagerBeforeMethodInterceptor;
import org.springframework.security.authorization.method.MethodInvocationResult;
import org.springframework.security.authorization.method.PostAuthorizeAuthorizationManager;
import org.springframework.security.authorization.method.PostFilterAuthorizationMethodInterceptor;
import org.springframework.security.authorization.method.PreAuthorizeAuthorizationManager;
import org.springframework.security.authorization.method.PreFilterAuthorizationMethodInterceptor;
import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig;
import org.springframework.security.config.core.GrantedAuthorityDefaults;
import org.springframework.security.config.test.SpringTestContext;
Expand Down Expand Up @@ -587,6 +598,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 +969,135 @@ Authz authz() {

}

@Configuration
@EnableMethodSecurity(prePostEnabled = false)
static class MetaAnnotationPlaceholderConfig {

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor preAuthorize(SecurityContextHolderStrategy strategy) {
PreAuthorizeAuthorizationManager preAuthorize = new PreAuthorizeAuthorizationManager();
preAuthorize.setUseTemplates(true);
AuthorizationManagerBeforeMethodInterceptor interceptor = AuthorizationManagerBeforeMethodInterceptor
.preAuthorize(preAuthorize);
interceptor.setSecurityContextHolderStrategy(strategy);
return interceptor;
}

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor postAuthorize(SecurityContextHolderStrategy strategy) {
PostAuthorizeAuthorizationManager postAuthorize = new PostAuthorizeAuthorizationManager();
postAuthorize.setUseTemplates(true);
AuthorizationManagerAfterMethodInterceptor interceptor = AuthorizationManagerAfterMethodInterceptor
.postAuthorize(postAuthorize);
interceptor.setSecurityContextHolderStrategy(strategy);
return interceptor;
}

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor preFilter(SecurityContextHolderStrategy strategy) {
PreFilterAuthorizationMethodInterceptor preFilter = new PreFilterAuthorizationMethodInterceptor();
preFilter.setUseTemplates(true);
preFilter.setSecurityContextHolderStrategy(strategy);
return preFilter;
}

@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
Advisor postFilter(SecurityContextHolderStrategy strategy) {
PostFilterAuthorizationMethodInterceptor postFilter = new PostFilterAuthorizationMethodInterceptor();
postFilter.setUseTemplates(true);
postFilter.setSecurityContextHolderStrategy(strategy);
return postFilter;
}

@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();

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,20 @@

package org.springframework.security.authorization.method;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;

import org.aopalliance.intercept.MethodInvocation;

import org.springframework.core.MethodClassKey;
import org.springframework.lang.NonNull;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.util.Assert;

/**
* For internal use only, as this contract is likely to change
Expand All @@ -35,6 +41,10 @@ abstract class AbstractExpressionAttributeRegistry<T extends ExpressionAttribute

private final Map<MethodClassKey, T> cachedAttributes = new ConcurrentHashMap<>();

private MethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();

private boolean useTemplates;

/**
* Returns an {@link ExpressionAttribute} for the {@link MethodInvocation}.
* @param mi the {@link MethodInvocation} to use
Expand All @@ -58,6 +68,28 @@ final T getAttribute(Method method, Class<?> targetClass) {
return this.cachedAttributes.computeIfAbsent(cacheKey, (k) -> resolveAttribute(method, targetClass));
}

final <A extends Annotation> Function<AnnotatedElement, A> findUniqueAnnotation(Class<A> type) {
return (this.useTemplates) ? AuthorizationAnnotationUtils.withTemplateResolver(type)
: AuthorizationAnnotationUtils.withDefaults(type);
}

/**
* Returns the {@link MethodSecurityExpressionHandler}.
* @return the {@link MethodSecurityExpressionHandler} to use
*/
MethodSecurityExpressionHandler getExpressionHandler() {
return this.expressionHandler;
}

void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) {
Assert.notNull(expressionHandler, "expressionHandler cannot be null");
this.expressionHandler = expressionHandler;
}

void setUseTemplates(boolean useTemplates) {
this.useTemplates = useTemplates;
}

/**
* Subclasses should implement this method to provide the non-null
* {@link ExpressionAttribute} for the method and the target class.
Expand Down
Loading

0 comments on commit ad01f33

Please sign in to comment.