diff --git a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java index 1b3d4c1bfae..4278e5d0848 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfiguration.java @@ -51,6 +51,7 @@ import org.springframework.security.authorization.method.PreFilterAuthorizationMethodInterceptor; import org.springframework.security.authorization.method.PrePostTemplateDefaults; import org.springframework.security.config.core.GrantedAuthorityDefaults; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.util.function.SingletonSupplier; @@ -72,6 +73,7 @@ final class PrePostMethodSecurityConfiguration implements ImportAware, AopInfras @Role(BeanDefinition.ROLE_INFRASTRUCTURE) static MethodInterceptor preFilterAuthorizationMethodInterceptor( ObjectProvider defaultsProvider, + ObjectProvider templateExpressionDefaultsProvider, ObjectProvider methodSecurityDefaultsProvider, ObjectProvider expressionHandlerProvider, ObjectProvider strategyProvider, @@ -80,6 +82,7 @@ static MethodInterceptor preFilterAuthorizationMethodInterceptor( PreFilterAuthorizationMethodInterceptor preFilter = new PreFilterAuthorizationMethodInterceptor(); preFilter.setOrder(preFilter.getOrder() + configuration.interceptorOrderOffset); return new DeferringMethodInterceptor<>(preFilter, (f) -> { + templateExpressionDefaultsProvider.ifAvailable(f::setTemplateDefaults); methodSecurityDefaultsProvider.ifAvailable(f::setTemplateDefaults); f.setExpressionHandler(expressionHandlerProvider .getIfAvailable(() -> defaultExpressionHandler(defaultsProvider, roleHierarchyProvider, context))); @@ -91,6 +94,7 @@ static MethodInterceptor preFilterAuthorizationMethodInterceptor( @Role(BeanDefinition.ROLE_INFRASTRUCTURE) static MethodInterceptor preAuthorizeAuthorizationMethodInterceptor( ObjectProvider defaultsProvider, + ObjectProvider templateExpressionDefaultsProvider, ObjectProvider methodSecurityDefaultsProvider, ObjectProvider expressionHandlerProvider, ObjectProvider strategyProvider, @@ -103,6 +107,7 @@ static MethodInterceptor preAuthorizeAuthorizationMethodInterceptor( .preAuthorize(manager(manager, registryProvider)); preAuthorize.setOrder(preAuthorize.getOrder() + configuration.interceptorOrderOffset); return new DeferringMethodInterceptor<>(preAuthorize, (f) -> { + templateExpressionDefaultsProvider.ifAvailable(manager::setTemplateDefaults); methodSecurityDefaultsProvider.ifAvailable(manager::setTemplateDefaults); manager.setExpressionHandler(expressionHandlerProvider .getIfAvailable(() -> defaultExpressionHandler(defaultsProvider, roleHierarchyProvider, context))); @@ -115,6 +120,7 @@ static MethodInterceptor preAuthorizeAuthorizationMethodInterceptor( @Role(BeanDefinition.ROLE_INFRASTRUCTURE) static MethodInterceptor postAuthorizeAuthorizationMethodInterceptor( ObjectProvider defaultsProvider, + ObjectProvider templateExpressionDefaultsProvider, ObjectProvider methodSecurityDefaultsProvider, ObjectProvider expressionHandlerProvider, ObjectProvider strategyProvider, @@ -127,6 +133,7 @@ static MethodInterceptor postAuthorizeAuthorizationMethodInterceptor( .postAuthorize(manager(manager, registryProvider)); postAuthorize.setOrder(postAuthorize.getOrder() + configuration.interceptorOrderOffset); return new DeferringMethodInterceptor<>(postAuthorize, (f) -> { + templateExpressionDefaultsProvider.ifAvailable(manager::setTemplateDefaults); methodSecurityDefaultsProvider.ifAvailable(manager::setTemplateDefaults); manager.setExpressionHandler(expressionHandlerProvider .getIfAvailable(() -> defaultExpressionHandler(defaultsProvider, roleHierarchyProvider, context))); @@ -139,6 +146,7 @@ static MethodInterceptor postAuthorizeAuthorizationMethodInterceptor( @Role(BeanDefinition.ROLE_INFRASTRUCTURE) static MethodInterceptor postFilterAuthorizationMethodInterceptor( ObjectProvider defaultsProvider, + ObjectProvider templateExpressionDefaultsProvider, ObjectProvider methodSecurityDefaultsProvider, ObjectProvider expressionHandlerProvider, ObjectProvider strategyProvider, @@ -147,6 +155,7 @@ static MethodInterceptor postFilterAuthorizationMethodInterceptor( PostFilterAuthorizationMethodInterceptor postFilter = new PostFilterAuthorizationMethodInterceptor(); postFilter.setOrder(postFilter.getOrder() + configuration.interceptorOrderOffset); return new DeferringMethodInterceptor<>(postFilter, (f) -> { + templateExpressionDefaultsProvider.ifAvailable(f::setTemplateDefaults); methodSecurityDefaultsProvider.ifAvailable(f::setTemplateDefaults); f.setExpressionHandler(expressionHandlerProvider .getIfAvailable(() -> defaultExpressionHandler(defaultsProvider, roleHierarchyProvider, context))); diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfiguration.java index ec33c902f53..4c4617964c6 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfiguration.java @@ -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. @@ -40,6 +40,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.expression.BeanFactoryResolver; import org.springframework.expression.BeanResolver; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.web.FilterChainProxy; @@ -82,12 +83,15 @@ class WebMvcSecurityConfiguration implements WebMvcConfigurer, ApplicationContex private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder .getContextHolderStrategy(); + private AnnotationTemplateExpressionDefaults templateDefaults; + @Override @SuppressWarnings("deprecation") public void addArgumentResolvers(List argumentResolvers) { AuthenticationPrincipalArgumentResolver authenticationPrincipalResolver = new AuthenticationPrincipalArgumentResolver(); authenticationPrincipalResolver.setBeanResolver(this.beanResolver); authenticationPrincipalResolver.setSecurityContextHolderStrategy(this.securityContextHolderStrategy); + authenticationPrincipalResolver.setTemplateDefaults(this.templateDefaults); argumentResolvers.add(authenticationPrincipalResolver); argumentResolvers .add(new org.springframework.security.web.bind.support.AuthenticationPrincipalArgumentResolver()); @@ -109,6 +113,9 @@ public void setApplicationContext(ApplicationContext applicationContext) throws if (applicationContext.getBeanNamesForType(SecurityContextHolderStrategy.class).length == 1) { this.securityContextHolderStrategy = applicationContext.getBean(SecurityContextHolderStrategy.class); } + if (applicationContext.getBeanNamesForType(AnnotationTemplateExpressionDefaults.class).length == 1) { + this.templateDefaults = applicationContext.getBean(AnnotationTemplateExpressionDefaults.class); + } } /** diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java index 69c6e75c512..74c832ae427 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -34,6 +34,7 @@ import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager; import org.springframework.security.authentication.password.ReactiveCompromisedPasswordChecker; import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.userdetails.ReactiveUserDetailsPasswordService; import org.springframework.security.core.userdetails.ReactiveUserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; @@ -120,12 +121,14 @@ public void configureArgumentResolvers(ArgumentResolverConfigurer configurer) { } @Bean - AuthenticationPrincipalArgumentResolver authenticationPrincipalArgumentResolver() { + AuthenticationPrincipalArgumentResolver authenticationPrincipalArgumentResolver( + ObjectProvider templateDefaults) { AuthenticationPrincipalArgumentResolver resolver = new AuthenticationPrincipalArgumentResolver( this.adapterRegistry); if (this.beanFactory != null) { resolver.setBeanResolver(new BeanFactoryResolver(this.beanFactory)); } + templateDefaults.ifAvailable(resolver::setTemplateDefaults); return resolver; } diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfiguration.java b/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfiguration.java index ca0d49d97fe..b9b0aec90e4 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfiguration.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfiguration.java @@ -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. @@ -35,6 +35,7 @@ import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.authorization.ObservationAuthorizationManager; import org.springframework.security.authorization.SpringAuthorizationEventPublisher; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.messaging.access.intercept.AuthorizationChannelInterceptor; @@ -82,6 +83,8 @@ final class WebSocketMessageBrokerSecurityConfiguration private ApplicationContext context; + private AnnotationTemplateExpressionDefaults templateDefaults; + WebSocketMessageBrokerSecurityConfiguration(ApplicationContext context) { this.context = context; } @@ -90,6 +93,7 @@ final class WebSocketMessageBrokerSecurityConfiguration public void addArgumentResolvers(List argumentResolvers) { AuthenticationPrincipalArgumentResolver resolver = new AuthenticationPrincipalArgumentResolver(); resolver.setSecurityContextHolderStrategy(this.securityContextHolderStrategy); + resolver.setTemplateDefaults(this.templateDefaults); argumentResolvers.add(resolver); } @@ -128,6 +132,11 @@ void setObservationRegistry(ObservationRegistry observationRegistry) { this.observationRegistry = observationRegistry; } + @Autowired(required = false) + void setTemplateDefaults(AnnotationTemplateExpressionDefaults templateDefaults) { + this.templateDefaults = templateDefaults; + } + @Override public void afterSingletonsInstantiated() { SimpleUrlHandlerMapping mapping = getBeanOrNull(SIMPLE_URL_HANDLER_MAPPING_BEAN_NAME, diff --git a/config/src/main/java/org/springframework/security/config/websocket/WebSocketMessageBrokerSecurityBeanDefinitionParser.java b/config/src/main/java/org/springframework/security/config/websocket/WebSocketMessageBrokerSecurityBeanDefinitionParser.java index e833c6f3f77..edb6646b363 100644 --- a/config/src/main/java/org/springframework/security/config/websocket/WebSocketMessageBrokerSecurityBeanDefinitionParser.java +++ b/config/src/main/java/org/springframework/security/config/websocket/WebSocketMessageBrokerSecurityBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 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. @@ -305,6 +305,8 @@ static class MessageSecurityPostProcessor implements BeanDefinitionRegistryPostP private static final String CUSTOM_ARG_RESOLVERS_PROP = "customArgumentResolvers"; + private static final String TEMPLATE_EXPRESSION_BEAN_ID = "annotationExpressionTemplateDefaults"; + private final String inboundSecurityInterceptorId; private final boolean sameOriginDisabled; @@ -327,7 +329,13 @@ public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) t if (current != null) { argResolvers.addAll((ManagedList) current.getValue()); } - argResolvers.add(new RootBeanDefinition(AuthenticationPrincipalArgumentResolver.class)); + RootBeanDefinition beanDefinition = new RootBeanDefinition( + AuthenticationPrincipalArgumentResolver.class); + if (registry.containsBeanDefinition(TEMPLATE_EXPRESSION_BEAN_ID)) { + beanDefinition.getPropertyValues() + .add("templateDefaults", new RuntimeBeanReference(TEMPLATE_EXPRESSION_BEAN_ID)); + } + argResolvers.add(beanDefinition); bd.getPropertyValues().add(CUSTOM_ARG_RESOLVERS_PROP, argResolvers); if (!registry.containsBeanDefinition(PATH_MATCHER_BEAN_NAME)) { PropertyValue pathMatcherProp = bd.getPropertyValues().getPropertyValue("pathMatcher"); diff --git a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java index a2f82afe6d6..5d0dbaee369 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/method/configuration/PrePostMethodSecurityConfigurationTests.java @@ -33,6 +33,8 @@ import org.aopalliance.intercept.MethodInvocation; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.aop.Advisor; import org.springframework.aop.support.DefaultPointcutAdvisor; @@ -78,6 +80,7 @@ import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.config.test.SpringTestParentApplicationContextExecutionListener; import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.security.test.context.support.WithAnonymousUser; import org.springframework.security.test.context.support.WithMockUser; @@ -607,69 +610,77 @@ public void allAnnotationsWhenAdviceAfterAllOffsetThenReturnsFilteredList() { assertThat(filtered).containsExactly("DoNotDrop"); } - @Test + @ParameterizedTest + @ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class }) @WithMockUser - public void methodeWhenParameterizedPreAuthorizeMetaAnnotationThenPasses() { - this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire(); + public void methodeWhenParameterizedPreAuthorizeMetaAnnotationThenPasses(Class config) { + this.spring.register(config).autowire(); MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class); assertThat(service.hasRole("USER")).isTrue(); } - @Test + @ParameterizedTest + @ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class }) @WithMockUser - public void methodRoleWhenPreAuthorizeMetaAnnotationHardcodedParameterThenPasses() { - this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire(); + public void methodRoleWhenPreAuthorizeMetaAnnotationHardcodedParameterThenPasses(Class config) { + this.spring.register(config).autowire(); MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class); assertThat(service.hasUserRole()).isTrue(); } - @Test - public void methodWhenParameterizedAnnotationThenFails() { - this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire(); + @ParameterizedTest + @ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class }) + public void methodWhenParameterizedAnnotationThenFails(Class config) { + this.spring.register(config).autowire(); MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class); assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(service::placeholdersOnlyResolvedByMetaAnnotations); } - @Test + @ParameterizedTest + @ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class }) @WithMockUser(authorities = "SCOPE_message:read") - public void methodWhenMultiplePlaceholdersHasAuthorityThenPasses() { - this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire(); + public void methodWhenMultiplePlaceholdersHasAuthorityThenPasses(Class config) { + this.spring.register(config).autowire(); MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class); assertThat(service.readMessage()).isEqualTo("message"); } - @Test + @ParameterizedTest + @ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class }) @WithMockUser(roles = "ADMIN") - public void methodWhenMultiplePlaceholdersHasRoleThenPasses() { - this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire(); + public void methodWhenMultiplePlaceholdersHasRoleThenPasses(Class config) { + this.spring.register(config).autowire(); MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class); assertThat(service.readMessage()).isEqualTo("message"); } - @Test + @ParameterizedTest + @ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class }) @WithMockUser - public void methodWhenPostAuthorizeMetaAnnotationThenAuthorizes() { - this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire(); + public void methodWhenPostAuthorizeMetaAnnotationThenAuthorizes(Class config) { + this.spring.register(config).autowire(); MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class); service.startsWithDave("daveMatthews"); assertThatExceptionOfType(AccessDeniedException.class) .isThrownBy(() -> service.startsWithDave("jenniferHarper")); } - @Test + @ParameterizedTest + @ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class }) @WithMockUser - public void methodWhenPreFilterMetaAnnotationThenFilters() { - this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire(); + public void methodWhenPreFilterMetaAnnotationThenFilters(Class config) { + this.spring.register(config).autowire(); MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class); assertThat(service.parametersContainDave(new ArrayList<>(List.of("dave", "carla", "vanessa", "paul")))) .containsExactly("dave"); } - @Test + @ParameterizedTest + @ValueSource(classes = { LegacyMetaAnnotationPlaceholderConfig.class, MetaAnnotationPlaceholderConfig.class }) @WithMockUser - public void methodWhenPostFilterMetaAnnotationThenFilters() { - this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire(); + public void methodWhenPostFilterMetaAnnotationThenFilters(Class config) { + this.spring.register(config).autowire(); MetaAnnotationService service = this.spring.getContext().getBean(MetaAnnotationService.class); assertThat(service.resultsContainDave(new ArrayList<>(List.of("dave", "carla", "vanessa", "paul")))) .containsExactly("dave"); @@ -827,7 +838,7 @@ void preAuthorizeWhenDeniedAndHandlerWithCustomAnnotationInClassThenHandlerCanUs @WithMockUser void postAuthorizeWhenNullDeniedMetaAnnotationThanWorks() { this.spring - .register(MethodSecurityServiceEnabledConfig.class, MetaAnnotationPlaceholderConfig.class, + .register(MethodSecurityServiceEnabledConfig.class, LegacyMetaAnnotationPlaceholderConfig.class, MethodSecurityService.NullPostProcessor.class) .autowire(); MethodSecurityService service = this.spring.getContext().getBean(MethodSecurityService.class); @@ -1268,7 +1279,7 @@ Authz authz() { @Configuration @EnableMethodSecurity - static class MetaAnnotationPlaceholderConfig { + static class LegacyMetaAnnotationPlaceholderConfig { @Bean PrePostTemplateDefaults methodSecurityDefaults() { @@ -1282,6 +1293,22 @@ MetaAnnotationService metaAnnotationService() { } + @Configuration + @EnableMethodSecurity + static class MetaAnnotationPlaceholderConfig { + + @Bean + AnnotationTemplateExpressionDefaults methodSecurityDefaults() { + return new AnnotationTemplateExpressionDefaults(); + } + + @Bean + MetaAnnotationService metaAnnotationService() { + return new MetaAnnotationService(); + } + + } + static class MetaAnnotationService { @RequireRole(role = "#role") diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfigurationTests.java index 322dd22dea6..2578aa3b180 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configuration/WebMvcSecurityConfigurationTests.java @@ -16,6 +16,11 @@ package org.springframework.security.config.annotation.web.configuration; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -26,6 +31,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolder; @@ -39,12 +45,15 @@ import org.springframework.test.web.servlet.ResultMatcher; import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; @@ -97,10 +106,28 @@ public void csrfToken() throws Exception { this.mockMvc.perform(request).andExpect(assertResult(csrfToken)); } + @Test + public void metaAnnotationWhenTemplateDefaultsBeanThenResolvesExpression() throws Exception { + this.mockMvc.perform(get("/hi")).andExpect(content().string("Hi, Stranger!")); + Authentication harold = new TestingAuthenticationToken("harold", "password", + AuthorityUtils.createAuthorityList("ROLE_USER")); + SecurityContextHolder.getContext().setAuthentication(harold); + this.mockMvc.perform(get("/hi")).andExpect(content().string("Hi, Harold!")); + } + private ResultMatcher assertResult(Object expected) { return model().attribute("result", expected); } + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.PARAMETER) + @AuthenticationPrincipal(expression = "#this.equals('{value}')") + @interface IsUser { + + String value() default "user"; + + } + @Controller static class TestController { @@ -120,6 +147,17 @@ ModelAndView csrf(CsrfToken token) { return new ModelAndView("view", "result", token); } + @GetMapping("/hi") + @ResponseBody + String ifUser(@IsUser("harold") boolean isHarold) { + if (isHarold) { + return "Hi, Harold!"; + } + else { + return "Hi, Stranger!"; + } + } + } @Configuration @@ -132,6 +170,11 @@ TestController testController() { return new TestController(); } + @Bean + AnnotationTemplateExpressionDefaults templateExpressionDefaults() { + return new AnnotationTemplateExpressionDefaults(); + } + } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfigurationTests.java index 972a6eb539c..9743782942d 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/reactive/ServerHttpSecurityConfigurationTests.java @@ -16,6 +16,10 @@ package org.springframework.security.config.annotation.web.reactive; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.net.URI; import org.junit.jupiter.api.Test; @@ -26,6 +30,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authentication.password.CompromisedPasswordDecision; import org.springframework.security.authentication.password.CompromisedPasswordException; import org.springframework.security.authentication.password.ReactiveCompromisedPasswordChecker; @@ -34,8 +39,12 @@ import org.springframework.security.config.test.SpringTestContextExtension; import org.springframework.security.config.users.ReactiveAuthenticationTestConfiguration; import org.springframework.security.config.web.server.ServerHttpSecurity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.userdetails.MapReactiveUserDetailsService; import org.springframework.security.core.userdetails.PasswordEncodedUser; +import org.springframework.security.core.userdetails.ReactiveUserDetailsService; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.server.DefaultServerRedirectStrategy; @@ -43,12 +52,16 @@ import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.reactive.config.EnableWebFlux; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.csrf; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.mockAuthentication; +import static org.springframework.security.test.web.reactive.server.SecurityMockServerConfigurers.springSecurity; /** * Tests for {@link ServerHttpSecurityConfiguration}. @@ -67,7 +80,10 @@ void setup(ApplicationContext context) { if (!context.containsBean(WebHttpHandlerBuilder.WEB_HANDLER_BEAN_NAME)) { return; } - this.webClient = WebTestClient.bindToApplicationContext(context).configureClient().build(); + this.webClient = WebTestClient.bindToApplicationContext(context) + .apply(springSecurity()) + .configureClient() + .build(); } @Test @@ -146,6 +162,27 @@ void loginWhenCompromisedPasswordAndRedirectIfPasswordExceptionThenRedirectedToR // @formatter:on } + @Test + public void metaAnnotationWhenTemplateDefaultsBeanThenResolvesExpression() throws Exception { + this.spring.register(MetaAnnotationPlaceholderConfig.class).autowire(); + Authentication user = new TestingAuthenticationToken("user", "password", "ROLE_USER"); + this.webClient.mutateWith(mockAuthentication(user)) + .get() + .uri("/hi") + .exchange() + .expectStatus() + .isOk() + .expectBody(String.class) + .isEqualTo("Hi, Stranger!"); + Authentication harold = new TestingAuthenticationToken("harold", "password", "ROLE_USER"); + this.webClient.mutateWith(mockAuthentication(harold)) + .get() + .uri("/hi") + .exchange() + .expectBody(String.class) + .isEqualTo("Hi, Harold!"); + } + @Configuration static class SubclassConfig extends ServerHttpSecurityConfiguration { @@ -237,4 +274,61 @@ public Mono check(String password) { } + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.PARAMETER) + @AuthenticationPrincipal(expression = "#this.equals('{value}')") + @interface IsUser { + + String value() default "user"; + + } + + @RestController + static class TestController { + + @GetMapping("/hi") + String ifUser(@IsUser("harold") boolean isHarold) { + if (isHarold) { + return "Hi, Harold!"; + } + else { + return "Hi, Stranger!"; + } + } + + } + + @Configuration + @EnableWebFlux + @EnableWebFluxSecurity + static class MetaAnnotationPlaceholderConfig { + + @Bean + SecurityWebFilterChain filterChain(ServerHttpSecurity http) { + // @formatter:off + http + .authorizeExchange((authorize) -> authorize.anyExchange().authenticated()) + .httpBasic(Customizer.withDefaults()); + // @formatter:on + return http.build(); + } + + @Bean + ReactiveUserDetailsService userDetailsService() { + return new MapReactiveUserDetailsService( + User.withUsername("user").password("password").authorities("app").build()); + } + + @Bean + TestController testController() { + return new TestController(); + } + + @Bean + AnnotationTemplateExpressionDefaults templateExpressionDefaults() { + return new AnnotationTemplateExpressionDefaults(); + } + + } + } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationTests.java index a14986b1ee5..2210288b573 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/socket/WebSocketMessageBrokerSecurityConfigurationTests.java @@ -16,6 +16,10 @@ package org.springframework.security.config.annotation.web.socket; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -59,6 +63,7 @@ import org.springframework.security.config.annotation.SecurityContextChangedListenerConfig; import org.springframework.security.config.annotation.web.messaging.MessageSecurityMetadataSourceRegistry; import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolderStrategy; @@ -164,6 +169,17 @@ public void addsAuthenticationPrincipalResolverWhenNoAuthorization() { .isEqualTo((String) this.messageUser.getPrincipal()); } + @Test + public void sendMessageWhenMetaAnnotationThenParsesExpression() { + loadConfig(NoInboundSecurityConfig.class); + this.messageUser = new TestingAuthenticationToken("harold", "password", "ROLE_USER"); + clientInboundChannel().send(message("/permitAll/hi")); + assertThat(this.context.getBean(MyController.class).message).isEqualTo("Hi, Harold!"); + this.messageUser = new TestingAuthenticationToken("user", "password", "ROLE_USER"); + clientInboundChannel().send(message("/permitAll/hi")); + assertThat(this.context.getBean(MyController.class).message).isEqualTo("Hi, Stranger!"); + } + @Test public void addsCsrfProtectionWhenNoAuthorization() { loadConfig(NoInboundSecurityConfig.class); @@ -365,15 +381,6 @@ public void sendMessageWhenAnonymousConfiguredAndAnonymousUserThenPasses() { clientInboundChannel().send(message("/anonymous")); } - @Test - public void sendMessageWhenAnonymousConfiguredAndLoggedInUserThenAccessDeniedException() { - loadConfig(WebSocketSecurityConfig.class); - assertThatExceptionOfType(MessageDeliveryException.class) - .isThrownBy(() -> clientInboundChannel().send(message("/anonymous"))) - .withCauseInstanceOf(AccessDeniedException.class); - - } - private void assertHandshake(HttpServletRequest request) { TestHandshakeHandler handshakeHandler = this.context.getBean(TestHandshakeHandler.class); assertThatCsrfToken(handshakeHandler.attributes.get(CsrfToken.class.getName())).isEqualTo(this.token); @@ -585,6 +592,15 @@ TestHandshakeHandler testHandshakeHandler() { } + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.PARAMETER) + @AuthenticationPrincipal(expression = "#this.equals('{value}')") + @interface IsUser { + + String value() default "user"; + + } + @Controller static class MyController { @@ -592,6 +608,8 @@ static class MyController { MyCustomArgument myCustomArgument; + String message; + @MessageMapping("/authentication") void authentication(@AuthenticationPrincipal String un) { this.authenticationPrincipal = un; @@ -602,6 +620,11 @@ void myCustom(MyCustomArgument myCustomArgument) { this.myCustomArgument = myCustomArgument; } + @MessageMapping("/hi") + void sayHello(@IsUser("harold") boolean isHarold) { + this.message = isHarold ? "Hi, Harold!" : "Hi, Stranger!"; + } + } static class MyCustomArgument { @@ -735,6 +758,11 @@ MyController myController() { return new MyController(); } + @Bean + AnnotationTemplateExpressionDefaults templateExpressionDefaults() { + return new AnnotationTemplateExpressionDefaults(); + } + } @Configuration diff --git a/config/src/test/java/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests.java b/config/src/test/java/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests.java index c8bf1d8eb1e..b0d37eff2ab 100644 --- a/config/src/test/java/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests.java +++ b/config/src/test/java/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests.java @@ -16,6 +16,10 @@ package org.springframework.security.config.websocket; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; import java.util.HashMap; import java.util.Map; import java.util.function.Supplier; @@ -47,6 +51,7 @@ import org.springframework.messaging.support.GenericMessage; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.expression.SecurityExpressionOperations; +import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.config.test.SpringTestContext; @@ -55,6 +60,7 @@ import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; +import org.springframework.security.core.context.SecurityContextImpl; import org.springframework.security.messaging.access.expression.DefaultMessageSecurityExpressionHandler; import org.springframework.security.messaging.access.expression.MessageSecurityExpressionRoot; import org.springframework.security.test.context.annotation.SecurityTestExecutionListeners; @@ -376,6 +382,24 @@ public void sendWhenNoIdMessageThenAuthenticationPrincipalResolved() { assertThat(this.messageController.username).isEqualTo("anonymous"); } + @Test + public void sendMessageWhenMetaAnnotationThenAuthenticationPrincipalResolved() { + this.spring.configLocations(xml("SyncConfig")).autowire(); + Authentication harold = new TestingAuthenticationToken("harold", "password", "ROLE_USER"); + try { + getSecurityContextHolderStrategy().setContext(new SecurityContextImpl(harold)); + this.clientInboundChannel.send(message("/hi")); + assertThat(this.spring.getContext().getBean(MessageController.class).message).isEqualTo("Hi, Harold!"); + Authentication user = new TestingAuthenticationToken("user", "password", "ROLE_USER"); + getSecurityContextHolderStrategy().setContext(new SecurityContextImpl(user)); + this.clientInboundChannel.send(message("/hi")); + assertThat(this.spring.getContext().getBean(MessageController.class).message).isEqualTo("Hi, Stranger!"); + } + finally { + getSecurityContextHolderStrategy().clearContext(); + } + } + @Test public void requestWhenConnectMessageThenUsesCsrfTokenHandshakeInterceptor() throws Exception { this.spring.configLocations(xml("SyncConfig")).autowire(); @@ -553,16 +577,32 @@ public boolean isGenerated() { } + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.PARAMETER) + @AuthenticationPrincipal(expression = "#this.equals('{value}')") + @interface IsUser { + + String value() default "user"; + + } + @Controller static class MessageController { String username; + String message; + @MessageMapping("/message") void authentication(@AuthenticationPrincipal String username) { this.username = username; } + @MessageMapping("/hi") + void sayHello(@IsUser("harold") boolean isHarold) { + this.message = isHarold ? "Hi, Harold!" : "Hi, Stranger!"; + } + } @Controller diff --git a/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-SyncConfig.xml b/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-SyncConfig.xml index 54dfdc79ef4..667bdd73cbd 100644 --- a/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-SyncConfig.xml +++ b/config/src/test/resources/org/springframework/security/config/websocket/WebSocketMessageBrokerConfigTests-SyncConfig.xml @@ -28,4 +28,5 @@ + diff --git a/core/src/main/java/org/springframework/security/authorization/method/AbstractExpressionAttributeRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/AbstractExpressionAttributeRegistry.java index ba87efa9dc4..ef6d00ac79d 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/AbstractExpressionAttributeRegistry.java +++ b/core/src/main/java/org/springframework/security/authorization/method/AbstractExpressionAttributeRegistry.java @@ -26,6 +26,7 @@ import org.springframework.lang.NonNull; import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler; import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.util.Assert; /** @@ -76,6 +77,15 @@ void setExpressionHandler(MethodSecurityExpressionHandler expressionHandler) { this.expressionHandler = expressionHandler; } + @Deprecated + void setTemplateDefaults(PrePostTemplateDefaults defaults) { + AnnotationTemplateExpressionDefaults adapter = new AnnotationTemplateExpressionDefaults(); + adapter.setIgnoreUnknown(defaults.isIgnoreUnknown()); + setTemplateDefaults(adapter); + } + + abstract void setTemplateDefaults(AnnotationTemplateExpressionDefaults adapter); + /** * Subclasses should implement this method to provide the non-null * {@link ExpressionAttribute} for the method and the target class. diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java index 61953730366..09d696b1714 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeAuthorizationManager.java @@ -28,6 +28,7 @@ import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.authorization.AuthorizationResult; import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; /** * An {@link AuthorizationManager} which can determine if an {@link Authentication} may @@ -57,11 +58,26 @@ public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandl * not be resolved. * @param defaults - whether to resolve pre/post-authorization templates parameters * @since 6.3 + * @deprecated Please use + * {@link #setTemplateDefaults(AnnotationTemplateExpressionDefaults)} instead */ + @Deprecated public void setTemplateDefaults(PrePostTemplateDefaults defaults) { this.registry.setTemplateDefaults(defaults); } + /** + * Configure pre/post-authorization template resolution + *

+ * By default, this value is null, which indicates that templates should + * not be resolved. + * @param defaults - whether to resolve pre/post-authorization templates parameters + * @since 6.4 + */ + public void setTemplateDefaults(AnnotationTemplateExpressionDefaults defaults) { + this.registry.setTemplateDefaults(defaults); + } + /** * Invokes * {@link PostAuthorizeExpressionAttributeRegistry#setApplicationContext(ApplicationContext)} diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttributeRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttributeRegistry.java index a42aef345b9..05f17ee93df 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttributeRegistry.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PostAuthorizeExpressionAttributeRegistry.java @@ -27,6 +27,7 @@ import org.springframework.security.access.prepost.PostAuthorize; import org.springframework.security.core.annotation.AnnotationSynthesizer; import org.springframework.security.core.annotation.AnnotationSynthesizers; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.util.Assert; /** @@ -90,7 +91,7 @@ void setApplicationContext(ApplicationContext context) { this.handlerResolver = (clazz) -> resolveHandler(context, clazz); } - void setTemplateDefaults(PrePostTemplateDefaults templateDefaults) { + void setTemplateDefaults(AnnotationTemplateExpressionDefaults templateDefaults) { this.postAuthorizeSynthesizer = AnnotationSynthesizers.requireUnique(PostAuthorize.class, templateDefaults); } diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java index aa96de670da..5019e8fa474 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PostFilterAuthorizationMethodInterceptor.java @@ -28,6 +28,7 @@ import org.springframework.security.access.prepost.PostFilter; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; @@ -73,11 +74,25 @@ public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandl * not be resolved. * @param defaults - whether to resolve pre/post-authorization templates parameters * @since 6.3 + * @deprecated Please use {@link AnnotationTemplateExpressionDefaults} instead */ + @Deprecated public void setTemplateDefaults(PrePostTemplateDefaults defaults) { this.registry.setTemplateDefaults(defaults); } + /** + * Configure pre/post-authorization template resolution + *

+ * By default, this value is null, which indicates that templates should + * not be resolved. + * @param defaults - whether to resolve pre/post-authorization templates parameters + * @since 6.4 + */ + public void setTemplateDefaults(AnnotationTemplateExpressionDefaults defaults) { + this.registry.setTemplateDefaults(defaults); + } + /** * {@inheritDoc} */ diff --git a/core/src/main/java/org/springframework/security/authorization/method/PostFilterExpressionAttributeRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/PostFilterExpressionAttributeRegistry.java index 2d8e316e71e..b9d678c7495 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PostFilterExpressionAttributeRegistry.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PostFilterExpressionAttributeRegistry.java @@ -23,6 +23,7 @@ import org.springframework.security.access.prepost.PostFilter; import org.springframework.security.core.annotation.AnnotationSynthesizer; import org.springframework.security.core.annotation.AnnotationSynthesizers; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; /** * For internal use only, as this contract is likely to change. @@ -47,7 +48,7 @@ ExpressionAttribute resolveAttribute(Method method, Class targetClass) { return new ExpressionAttribute(postFilterExpression); } - void setTemplateDefaults(PrePostTemplateDefaults defaults) { + void setTemplateDefaults(AnnotationTemplateExpressionDefaults defaults) { this.synthesizer = AnnotationSynthesizers.requireUnique(PostFilter.class, defaults); } diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java index 02c26ebf34b..0960c7a2dbe 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeAuthorizationManager.java @@ -28,6 +28,7 @@ import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.authorization.AuthorizationResult; import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; /** * An {@link AuthorizationManager} which can determine if an {@link Authentication} may @@ -57,11 +58,26 @@ public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandl * not be resolved. * @param defaults - whether to resolve pre/post-authorization templates parameters * @since 6.3 + * @deprecated Please use + * {@link #setTemplateDefaults(AnnotationTemplateExpressionDefaults)} instead */ + @Deprecated public void setTemplateDefaults(PrePostTemplateDefaults defaults) { this.registry.setTemplateDefaults(defaults); } + /** + * Configure pre/post-authorization template resolution + *

+ * By default, this value is null, which indicates that templates should + * not be resolved. + * @param defaults - whether to resolve pre/post-authorization templates parameters + * @since 6.4 + */ + public void setTemplateDefaults(AnnotationTemplateExpressionDefaults defaults) { + this.registry.setTemplateDefaults(defaults); + } + public void setApplicationContext(ApplicationContext context) { this.registry.setApplicationContext(context); } diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttributeRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttributeRegistry.java index 8e02071c7f1..d5146749417 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttributeRegistry.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PreAuthorizeExpressionAttributeRegistry.java @@ -27,6 +27,7 @@ import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.annotation.AnnotationSynthesizer; import org.springframework.security.core.annotation.AnnotationSynthesizers; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.util.Assert; /** @@ -90,7 +91,7 @@ void setApplicationContext(ApplicationContext context) { this.handlerResolver = (clazz) -> resolveHandler(context, clazz); } - void setTemplateDefaults(PrePostTemplateDefaults defaults) { + void setTemplateDefaults(AnnotationTemplateExpressionDefaults defaults) { this.preAuthorizeSynthesizer = AnnotationSynthesizers.requireUnique(PreAuthorize.class, defaults); } diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java b/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java index a00e22f2534..28d4344ba9b 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PreFilterAuthorizationMethodInterceptor.java @@ -28,6 +28,7 @@ import org.springframework.security.access.prepost.PreFilter; import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; import org.springframework.util.Assert; @@ -74,11 +75,26 @@ public void setExpressionHandler(MethodSecurityExpressionHandler expressionHandl * not be resolved. * @param defaults - whether to resolve pre/post-authorization templates parameters * @since 6.3 + * @deprecated Please use + * {@link #setTemplateDefaults(AnnotationTemplateExpressionDefaults)} instead */ + @Deprecated public void setTemplateDefaults(PrePostTemplateDefaults defaults) { this.registry.setTemplateDefaults(defaults); } + /** + * Configure pre/post-authorization template resolution + *

+ * By default, this value is null, which indicates that templates should + * not be resolved. + * @param defaults - whether to resolve pre/post-authorization templates parameters + * @since 6.4 + */ + public void setTemplateDefaults(AnnotationTemplateExpressionDefaults defaults) { + this.registry.setTemplateDefaults(defaults); + } + /** * {@inheritDoc} */ diff --git a/core/src/main/java/org/springframework/security/authorization/method/PreFilterExpressionAttributeRegistry.java b/core/src/main/java/org/springframework/security/authorization/method/PreFilterExpressionAttributeRegistry.java index eda03744864..6d52698850d 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PreFilterExpressionAttributeRegistry.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PreFilterExpressionAttributeRegistry.java @@ -23,6 +23,7 @@ import org.springframework.security.access.prepost.PreFilter; import org.springframework.security.core.annotation.AnnotationSynthesizer; import org.springframework.security.core.annotation.AnnotationSynthesizers; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; /** * For internal use only, as this contract is likely to change. @@ -48,7 +49,7 @@ PreFilterExpressionAttribute resolveAttribute(Method method, Class targetClas return new PreFilterExpressionAttribute(preFilterExpression, preFilter.filterTarget()); } - void setTemplateDefaults(PrePostTemplateDefaults defaults) { + void setTemplateDefaults(AnnotationTemplateExpressionDefaults defaults) { this.synthesizer = AnnotationSynthesizers.requireUnique(PreFilter.class, defaults); } diff --git a/core/src/main/java/org/springframework/security/authorization/method/PrePostTemplateDefaults.java b/core/src/main/java/org/springframework/security/authorization/method/PrePostTemplateDefaults.java index e31ac4aca98..4ffef548342 100644 --- a/core/src/main/java/org/springframework/security/authorization/method/PrePostTemplateDefaults.java +++ b/core/src/main/java/org/springframework/security/authorization/method/PrePostTemplateDefaults.java @@ -27,7 +27,32 @@ * @see org.springframework.security.access.prepost.PostAuthorize * @see org.springframework.security.access.prepost.PreFilter * @see org.springframework.security.access.prepost.PostFilter + * @deprecated Please use {@link AnnotationTemplateExpressionDefaults} instead */ -public final class PrePostTemplateDefaults extends AnnotationTemplateExpressionDefaults { +@Deprecated +public final class PrePostTemplateDefaults { + + private boolean ignoreUnknown = true; + + /** + * Whether template resolution should ignore placeholders it doesn't recognize. + *

+ * By default, this value is true. + */ + public boolean isIgnoreUnknown() { + return this.ignoreUnknown; + } + + /** + * Configure template resolution to ignore unknown placeholders. When set to + * false, template resolution will throw an exception for unknown + * placeholders. + *

+ * By default, this value is true. + * @param ignoreUnknown - whether to ignore unknown placeholders parameters + */ + public void setIgnoreUnknown(boolean ignoreUnknown) { + this.ignoreUnknown = ignoreUnknown; + } } diff --git a/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc b/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc index 2af4db987b7..8eeddf5efc7 100644 --- a/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc +++ b/docs/modules/ROOT/pages/servlet/authorization/method-security.adoc @@ -1012,8 +1012,8 @@ Java:: [source,java,role="primary"] ---- @Bean -static PrePostTemplateDefaults prePostTemplateDefaults() { - return new PrePostTemplateDefaults(); +static AnnotationTemplateExpressionDefaults templateExpressionDefaults() { + return new AnnotationTemplateExpressionDefaults(); } ---- @@ -1023,8 +1023,8 @@ Kotlin:: ---- companion object { @Bean - fun prePostTemplateDefaults(): PrePostTemplateDefaults { - return PrePostTemplateDefaults() + fun templateExpressionDefaults(): AnnotationTemplateExpressionDefaults { + return AnnotationTemplateExpressionDefaults() } } ---- diff --git a/docs/modules/ROOT/pages/servlet/integrations/mvc.adoc b/docs/modules/ROOT/pages/servlet/integrations/mvc.adoc index 604f8219cf7..92223407e21 100644 --- a/docs/modules/ROOT/pages/servlet/integrations/mvc.adoc +++ b/docs/modules/ROOT/pages/servlet/integrations/mvc.adoc @@ -503,6 +503,126 @@ open fun findMessagesForUser(@CurrentUser customUser: CustomUser?): ModelAndView ---- ====== +Once it is a meta-annotation, parameterization is also available to you. + +For example, consider when you have a JWT as your principal and you want to say which claim to retrieve. +As a meta-annotation, you might do: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Target({ElementType.PARAMETER, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@AuthenticationPrincipal(expression = "claims['sub']") +public @interface CurrentUser {} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +@AuthenticationPrincipal(expression = "claims['sub']") +annotation class CurrentUser +---- +====== + +which is already quite powerful. +But, it is also limited to retrieving the `sub` claim. + +To make this more flexible, first publish the `AnnotationTemplateExpressionDefaults` bean like so: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Bean +public AnnotationTemplateExpressionDefaults templateDefaults() { + return new AnnotationTemplateExpressionDeafults(); +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Bean +fun templateDefaults(): AnnotationTemplateExpressionDefaults { + return AnnotationTemplateExpressionDeafults() +} +---- + +Xml:: ++ +[source,xml,role="secondary"] +---- + +---- +====== + +and then you can supply a parameter to `@CurrentUser` like so: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@Target({ElementType.PARAMETER, ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@AuthenticationPrincipal(expression = "claims['{claim}']") +public @interface CurrentUser { + String claim() default 'sub'; +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.TYPE) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +@AuthenticationPrincipal(expression = "claims['{claim}']") +annotation class CurrentUser(val claim: String = "sub") +---- +====== + +This will allow you more flexibility across your set of applications in the following way: + +[tabs] +====== +Java:: ++ +[source,java,role="primary"] +---- +@RequestMapping("/messages/inbox") +public ModelAndView findMessagesForUser(@CurrentUser("user_id") String userId) { + + // .. find messages for this user and return them ... +} +---- + +Kotlin:: ++ +[source,kotlin,role="secondary"] +---- +@RequestMapping("/messages/inbox") +open fun findMessagesForUser(@CurrentUser("user_id") userId: String?): ModelAndView { + + // .. find messages for this user and return them ... +} +---- +====== [[mvc-async]] == Spring MVC Async Integration diff --git a/messaging/src/main/java/org/springframework/security/messaging/context/AuthenticationPrincipalArgumentResolver.java b/messaging/src/main/java/org/springframework/security/messaging/context/AuthenticationPrincipalArgumentResolver.java index 8cd93d095da..482434db547 100644 --- a/messaging/src/main/java/org/springframework/security/messaging/context/AuthenticationPrincipalArgumentResolver.java +++ b/messaging/src/main/java/org/springframework/security/messaging/context/AuthenticationPrincipalArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 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. @@ -17,9 +17,10 @@ package org.springframework.security.messaging.context; import java.lang.annotation.Annotation; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.springframework.core.MethodParameter; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; @@ -27,6 +28,9 @@ import org.springframework.messaging.Message; import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AnnotationSynthesizer; +import org.springframework.security.core.annotation.AnnotationSynthesizers; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; @@ -83,6 +87,7 @@ * * * @author Rob Winch + * @author DingHao * @since 4.0 */ public final class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver { @@ -90,11 +95,16 @@ public final class AuthenticationPrincipalArgumentResolver implements HandlerMet private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder .getContextHolderStrategy(); + private final Map cachedAttributes = new ConcurrentHashMap<>(); + private ExpressionParser parser = new SpelExpressionParser(); + private AnnotationSynthesizer synthesizer = AnnotationSynthesizers + .requireUnique(AuthenticationPrincipal.class); + @Override public boolean supportsParameter(MethodParameter parameter) { - return findMethodAnnotation(AuthenticationPrincipal.class, parameter) != null; + return findMethodAnnotation(parameter) != null; } @Override @@ -104,7 +114,7 @@ public Object resolveArgument(MethodParameter parameter, Message message) { return null; } Object principal = authentication.getPrincipal(); - AuthenticationPrincipal authPrincipal = findMethodAnnotation(AuthenticationPrincipal.class, parameter); + AuthenticationPrincipal authPrincipal = findMethodAnnotation(parameter); String expressionToParse = authPrincipal.expression(); if (StringUtils.hasLength(expressionToParse)) { StandardEvaluationContext context = new StandardEvaluationContext(); @@ -133,26 +143,29 @@ public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy secur this.securityContextHolderStrategy = securityContextHolderStrategy; } + /** + * Configure AuthenticationPrincipal template resolution + *

+ * By default, this value is null, which indicates that templates should + * not be resolved. + * @param templateDefaults - whether to resolve AuthenticationPrincipal templates + * parameters + * @since 6.4 + */ + public void setTemplateDefaults(AnnotationTemplateExpressionDefaults templateDefaults) { + this.synthesizer = AnnotationSynthesizers.requireUnique(AuthenticationPrincipal.class, templateDefaults); + } + /** * Obtains the specified {@link Annotation} on the specified {@link MethodParameter}. - * @param annotationClass the class of the {@link Annotation} to find on the * {@link MethodParameter} * @param parameter the {@link MethodParameter} to search for an {@link Annotation} * @return the {@link Annotation} that was found or null. */ - private T findMethodAnnotation(Class annotationClass, MethodParameter parameter) { - T annotation = parameter.getParameterAnnotation(annotationClass); - if (annotation != null) { - return annotation; - } - Annotation[] annotationsToSearch = parameter.getParameterAnnotations(); - for (Annotation toSearch : annotationsToSearch) { - annotation = AnnotationUtils.findAnnotation(toSearch.annotationType(), annotationClass); - if (annotation != null) { - return annotation; - } - } - return null; + @SuppressWarnings("unchecked") + private T findMethodAnnotation(MethodParameter parameter) { + return (T) this.cachedAttributes.computeIfAbsent(parameter, + (methodParameter) -> this.synthesizer.synthesize(methodParameter.getParameter())); } } diff --git a/messaging/src/main/java/org/springframework/security/messaging/handler/invocation/reactive/AuthenticationPrincipalArgumentResolver.java b/messaging/src/main/java/org/springframework/security/messaging/handler/invocation/reactive/AuthenticationPrincipalArgumentResolver.java index f6baa3a328d..45adb233b5a 100644 --- a/messaging/src/main/java/org/springframework/security/messaging/handler/invocation/reactive/AuthenticationPrincipalArgumentResolver.java +++ b/messaging/src/main/java/org/springframework/security/messaging/handler/invocation/reactive/AuthenticationPrincipalArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2021 the original author or authors. + * Copyright 2019-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. @@ -17,6 +17,8 @@ package org.springframework.security.messaging.handler.invocation.reactive; import java.lang.annotation.Annotation; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; @@ -25,7 +27,6 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.expression.BeanResolver; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; @@ -34,6 +35,9 @@ import org.springframework.messaging.Message; import org.springframework.messaging.handler.invocation.reactive.HandlerMethodArgumentResolver; import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AnnotationSynthesizer; +import org.springframework.security.core.annotation.AnnotationSynthesizers; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.SecurityContext; @@ -90,12 +94,18 @@ * * * @author Rob Winch + * @author DingHao * @since 5.2 */ public class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver { + private final Map cachedAttributes = new ConcurrentHashMap<>(); + private ExpressionParser parser = new SpelExpressionParser(); + private AnnotationSynthesizer synthesizer = AnnotationSynthesizers + .requireUnique(AuthenticationPrincipal.class); + private BeanResolver beanResolver; private ReactiveAdapterRegistry adapterRegistry = ReactiveAdapterRegistry.getSharedInstance(); @@ -120,7 +130,7 @@ public void setAdapterRegistry(ReactiveAdapterRegistry adapterRegistry) { @Override public boolean supportsParameter(MethodParameter parameter) { - return findMethodAnnotation(AuthenticationPrincipal.class, parameter) != null; + return findMethodAnnotation(parameter) != null; } @Override @@ -138,7 +148,7 @@ public Mono resolveArgument(MethodParameter parameter, Message messag } private Object resolvePrincipal(MethodParameter parameter, Object principal) { - AuthenticationPrincipal authPrincipal = findMethodAnnotation(AuthenticationPrincipal.class, parameter); + AuthenticationPrincipal authPrincipal = findMethodAnnotation(parameter); String expressionToParse = authPrincipal.expression(); if (StringUtils.hasLength(expressionToParse)) { StandardEvaluationContext context = new StandardEvaluationContext(); @@ -174,26 +184,29 @@ private boolean isInvalidType(MethodParameter parameter, Object principal) { return !ClassUtils.isAssignable(typeToCheck, principal.getClass()); } + /** + * Configure AuthenticationPrincipal template resolution + *

+ * By default, this value is null, which indicates that templates should + * not be resolved. + * @param templateDefaults - whether to resolve AuthenticationPrincipal templates + * parameters + * @since 6.4 + */ + public void setTemplateDefaults(AnnotationTemplateExpressionDefaults templateDefaults) { + this.synthesizer = AnnotationSynthesizers.requireUnique(AuthenticationPrincipal.class, templateDefaults); + } + /** * Obtains the specified {@link Annotation} on the specified {@link MethodParameter}. - * @param annotationClass the class of the {@link Annotation} to find on the * {@link MethodParameter} * @param parameter the {@link MethodParameter} to search for an {@link Annotation} * @return the {@link Annotation} that was found or null. */ - private T findMethodAnnotation(Class annotationClass, MethodParameter parameter) { - T annotation = parameter.getParameterAnnotation(annotationClass); - if (annotation != null) { - return annotation; - } - Annotation[] annotationsToSearch = parameter.getParameterAnnotations(); - for (Annotation toSearch : annotationsToSearch) { - annotation = AnnotationUtils.findAnnotation(toSearch.annotationType(), annotationClass); - if (annotation != null) { - return annotation; - } - } - return null; + @SuppressWarnings("unchecked") + private T findMethodAnnotation(MethodParameter parameter) { + return (T) this.cachedAttributes.computeIfAbsent(parameter, + (methodParameter) -> this.synthesizer.synthesize(methodParameter.getParameter())); } } diff --git a/messaging/src/test/java/org/springframework/security/messaging/context/AuthenticationPrincipalArgumentResolverTests.java b/messaging/src/test/java/org/springframework/security/messaging/context/AuthenticationPrincipalArgumentResolverTests.java index acd41aba8ef..ce1437f0b06 100644 --- a/messaging/src/test/java/org/springframework/security/messaging/context/AuthenticationPrincipalArgumentResolverTests.java +++ b/messaging/src/test/java/org/springframework/security/messaging/context/AuthenticationPrincipalArgumentResolverTests.java @@ -27,7 +27,9 @@ import org.junit.jupiter.api.Test; import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.AliasFor; import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolder; @@ -167,6 +169,23 @@ public void resolveArgumentObject() throws Exception { assertThat(this.resolver.resolveArgument(showUserAnnotationObject(), null)).isEqualTo(this.expectedPrincipal); } + @Test + public void resolveArgumentCustomMetaAnnotation() throws Exception { + CustomUserPrincipal principal = new CustomUserPrincipal(); + setAuthenticationPrincipal(principal); + this.expectedPrincipal = principal.id; + assertThat(this.resolver.resolveArgument(showUserCustomMetaAnnotation(), null)).isEqualTo(principal.id); + } + + @Test + public void resolveArgumentCustomMetaAnnotationTpl() throws Exception { + CustomUserPrincipal principal = new CustomUserPrincipal(); + setAuthenticationPrincipal(principal); + this.resolver.setTemplateDefaults(new AnnotationTemplateExpressionDefaults()); + this.expectedPrincipal = principal.id; + assertThat(this.resolver.resolveArgument(showUserCustomMetaAnnotationTpl(), null)).isEqualTo(principal.id); + } + private MethodParameter showUserNoAnnotation() { return getMethodParameter("showUserNoAnnotation", String.class); } @@ -195,6 +214,14 @@ private MethodParameter showUserCustomAnnotation() { return getMethodParameter("showUserCustomAnnotation", CustomUserPrincipal.class); } + private MethodParameter showUserCustomMetaAnnotation() { + return getMethodParameter("showUserCustomMetaAnnotation", int.class); + } + + private MethodParameter showUserCustomMetaAnnotationTpl() { + return getMethodParameter("showUserCustomMetaAnnotationTpl", int.class); + } + private MethodParameter showUserSpel() { return getMethodParameter("showUserSpel", String.class); } @@ -236,6 +263,23 @@ private void setAuthenticationPrincipal(Object principal) { } + @Retention(RetentionPolicy.RUNTIME) + @AuthenticationPrincipal + public @interface CurrentUser2 { + + @AliasFor(annotation = AuthenticationPrincipal.class) + String expression() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + @AuthenticationPrincipal(expression = "principal.{property}") + public @interface CurrentUser3 { + + String property() default ""; + + } + public static class TestController { public void showUserNoAnnotation(String user) { @@ -260,6 +304,12 @@ public void showUserAnnotation(@AuthenticationPrincipal CustomUserPrincipal user public void showUserCustomAnnotation(@CurrentUser CustomUserPrincipal user) { } + public void showUserCustomMetaAnnotation(@CurrentUser2(expression = "principal.id") int userId) { + } + + public void showUserCustomMetaAnnotationTpl(@CurrentUser3(property = "id") int userId) { + } + public void showUserAnnotation(@AuthenticationPrincipal Object user) { } @@ -281,6 +331,10 @@ static class CustomUserPrincipal { public final int id = 1; + public Object getPrincipal() { + return this; + } + } public static class CopyUserPrincipal { diff --git a/messaging/src/test/java/org/springframework/security/messaging/handler/invocation/reactive/AuthenticationPrincipalArgumentResolverTests.java b/messaging/src/test/java/org/springframework/security/messaging/handler/invocation/reactive/AuthenticationPrincipalArgumentResolverTests.java index 04b66869201..bf66448af13 100644 --- a/messaging/src/test/java/org/springframework/security/messaging/handler/invocation/reactive/AuthenticationPrincipalArgumentResolverTests.java +++ b/messaging/src/test/java/org/springframework/security/messaging/handler/invocation/reactive/AuthenticationPrincipalArgumentResolverTests.java @@ -23,10 +23,12 @@ import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.AliasFor; import org.springframework.core.annotation.SynthesizingMethodParameter; import org.springframework.security.authentication.TestAuthentication; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; @@ -141,10 +143,56 @@ private MethodParameter arg0(String methodName) { } + @Test + public void resolveArgumentCustomMetaAnnotation() { + CustomUserPrincipal principal = new CustomUserPrincipal(); + Mono result = this.resolver.resolveArgument(arg0("showUserCustomMetaAnnotation"), null) + .contextWrite(ReactiveSecurityContextHolder + .withAuthentication(new TestingAuthenticationToken(principal, "password", "ROLE_USER"))); + assertThat(result.block()).isEqualTo(principal.id); + } + + @Test + public void resolveArgumentCustomMetaAnnotationTpl() { + CustomUserPrincipal principal = new CustomUserPrincipal(); + this.resolver.setTemplateDefaults(new AnnotationTemplateExpressionDefaults()); + Mono result = this.resolver.resolveArgument(arg0("showUserCustomMetaAnnotationTpl"), null) + .contextWrite(ReactiveSecurityContextHolder + .withAuthentication(new TestingAuthenticationToken(principal, "password", "ROLE_USER"))); + assertThat(result.block()).isEqualTo(principal.id); + } + + public void showUserCustomMetaAnnotation(@CurrentUser2(expression = "principal.id") int userId) { + } + + public void showUserCustomMetaAnnotationTpl(@CurrentUser3(property = "id") int userId) { + } + static class CustomUserPrincipal { public final int id = 1; + public Object getPrincipal() { + return this; + } + + } + + @Retention(RetentionPolicy.RUNTIME) + @AuthenticationPrincipal + public @interface CurrentUser2 { + + @AliasFor(annotation = AuthenticationPrincipal.class) + String expression() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + @AuthenticationPrincipal(expression = "principal.{property}") + public @interface CurrentUser3 { + + String property() default ""; + } } diff --git a/web/src/main/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolver.java b/web/src/main/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolver.java index aa5db12100e..d3d34fe74f1 100644 --- a/web/src/main/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolver.java +++ b/web/src/main/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 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. @@ -17,15 +17,19 @@ package org.springframework.security.web.method.annotation; import java.lang.annotation.Annotation; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.springframework.core.MethodParameter; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.expression.BeanResolver; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AnnotationSynthesizer; +import org.springframework.security.core.annotation.AnnotationSynthesizers; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolderStrategy; @@ -86,6 +90,7 @@ * * * @author Rob Winch + * @author DingHao * @since 4.0 */ public final class AuthenticationPrincipalArgumentResolver implements HandlerMethodArgumentResolver { @@ -93,13 +98,18 @@ public final class AuthenticationPrincipalArgumentResolver implements HandlerMet private SecurityContextHolderStrategy securityContextHolderStrategy = SecurityContextHolder .getContextHolderStrategy(); + private final Map cachedAttributes = new ConcurrentHashMap<>(); + private ExpressionParser parser = new SpelExpressionParser(); + private AnnotationSynthesizer synthesizer = AnnotationSynthesizers + .requireUnique(AuthenticationPrincipal.class); + private BeanResolver beanResolver; @Override public boolean supportsParameter(MethodParameter parameter) { - return findMethodAnnotation(AuthenticationPrincipal.class, parameter) != null; + return findMethodAnnotation(parameter) != null; } @Override @@ -110,7 +120,7 @@ public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer m return null; } Object principal = authentication.getPrincipal(); - AuthenticationPrincipal annotation = findMethodAnnotation(AuthenticationPrincipal.class, parameter); + AuthenticationPrincipal annotation = findMethodAnnotation(parameter); String expressionToParse = annotation.expression(); if (StringUtils.hasLength(expressionToParse)) { StandardEvaluationContext context = new StandardEvaluationContext(); @@ -148,26 +158,29 @@ public void setSecurityContextHolderStrategy(SecurityContextHolderStrategy secur this.securityContextHolderStrategy = securityContextHolderStrategy; } + /** + * Configure AuthenticationPrincipal template resolution + *

+ * By default, this value is null, which indicates that templates should + * not be resolved. + * @param templateDefaults - whether to resolve AuthenticationPrincipal templates + * parameters + * @since 6.4 + */ + public void setTemplateDefaults(AnnotationTemplateExpressionDefaults templateDefaults) { + this.synthesizer = AnnotationSynthesizers.requireUnique(AuthenticationPrincipal.class, templateDefaults); + } + /** * Obtains the specified {@link Annotation} on the specified {@link MethodParameter}. - * @param annotationClass the class of the {@link Annotation} to find on the * {@link MethodParameter} * @param parameter the {@link MethodParameter} to search for an {@link Annotation} * @return the {@link Annotation} that was found or null. */ - private T findMethodAnnotation(Class annotationClass, MethodParameter parameter) { - T annotation = parameter.getParameterAnnotation(annotationClass); - if (annotation != null) { - return annotation; - } - Annotation[] annotationsToSearch = parameter.getParameterAnnotations(); - for (Annotation toSearch : annotationsToSearch) { - annotation = AnnotationUtils.findAnnotation(toSearch.annotationType(), annotationClass); - if (annotation != null) { - return annotation; - } - } - return null; + @SuppressWarnings("unchecked") + private T findMethodAnnotation(MethodParameter parameter) { + return (T) this.cachedAttributes.computeIfAbsent(parameter, + (methodParameter) -> this.synthesizer.synthesize(methodParameter.getParameter())); } } diff --git a/web/src/main/java/org/springframework/security/web/reactive/result/method/annotation/AuthenticationPrincipalArgumentResolver.java b/web/src/main/java/org/springframework/security/web/reactive/result/method/annotation/AuthenticationPrincipalArgumentResolver.java index 1e2d3d5556c..f4e99c4f020 100644 --- a/web/src/main/java/org/springframework/security/web/reactive/result/method/annotation/AuthenticationPrincipalArgumentResolver.java +++ b/web/src/main/java/org/springframework/security/web/reactive/result/method/annotation/AuthenticationPrincipalArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 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. @@ -17,6 +17,8 @@ package org.springframework.security.web.reactive.result.method.annotation; import java.lang.annotation.Annotation; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; @@ -25,12 +27,14 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.expression.BeanResolver; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; +import org.springframework.security.core.annotation.AnnotationSynthesizer; +import org.springframework.security.core.annotation.AnnotationSynthesizers; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.SecurityContext; @@ -44,12 +48,18 @@ * Resolves the Authentication * * @author Rob Winch + * @author DingHao * @since 5.0 */ public class AuthenticationPrincipalArgumentResolver extends HandlerMethodArgumentResolverSupport { + private final Map cachedAttributes = new ConcurrentHashMap<>(); + private ExpressionParser parser = new SpelExpressionParser(); + private AnnotationSynthesizer synthesizer = AnnotationSynthesizers + .requireUnique(AuthenticationPrincipal.class); + private BeanResolver beanResolver; public AuthenticationPrincipalArgumentResolver(ReactiveAdapterRegistry adapterRegistry) { @@ -66,7 +76,7 @@ public void setBeanResolver(BeanResolver beanResolver) { @Override public boolean supportsParameter(MethodParameter parameter) { - return findMethodAnnotation(AuthenticationPrincipal.class, parameter) != null; + return findMethodAnnotation(parameter) != null; } @Override @@ -82,7 +92,7 @@ public Mono resolveArgument(MethodParameter parameter, BindingContext bi } private Object resolvePrincipal(MethodParameter parameter, Object principal) { - AuthenticationPrincipal annotation = findMethodAnnotation(AuthenticationPrincipal.class, parameter); + AuthenticationPrincipal annotation = findMethodAnnotation(parameter); String expressionToParse = annotation.expression(); if (StringUtils.hasLength(expressionToParse)) { StandardEvaluationContext context = new StandardEvaluationContext(); @@ -118,26 +128,29 @@ private boolean isInvalidType(MethodParameter parameter, Object principal) { return !ClassUtils.isAssignable(typeToCheck, principal.getClass()); } + /** + * Configure AuthenticationPrincipal template resolution + *

+ * By default, this value is null, which indicates that templates should + * not be resolved. + * @param templateDefaults - whether to resolve AuthenticationPrincipal templates + * parameters + * @since 6.4 + */ + public void setTemplateDefaults(AnnotationTemplateExpressionDefaults templateDefaults) { + this.synthesizer = AnnotationSynthesizers.requireUnique(AuthenticationPrincipal.class, templateDefaults); + } + /** * Obtains the specified {@link Annotation} on the specified {@link MethodParameter}. - * @param annotationClass the class of the {@link Annotation} to find on the * {@link MethodParameter} * @param parameter the {@link MethodParameter} to search for an {@link Annotation} * @return the {@link Annotation} that was found or null. */ - private T findMethodAnnotation(Class annotationClass, MethodParameter parameter) { - T annotation = parameter.getParameterAnnotation(annotationClass); - if (annotation != null) { - return annotation; - } - Annotation[] annotationsToSearch = parameter.getParameterAnnotations(); - for (Annotation toSearch : annotationsToSearch) { - annotation = AnnotationUtils.findAnnotation(toSearch.annotationType(), annotationClass); - if (annotation != null) { - return annotation; - } - } - return null; + @SuppressWarnings("unchecked") + private T findMethodAnnotation(MethodParameter parameter) { + return (T) this.cachedAttributes.computeIfAbsent(parameter, + (methodParameter) -> this.synthesizer.synthesize(methodParameter.getParameter())); } } diff --git a/web/src/test/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolverTests.java b/web/src/test/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolverTests.java index 6b80024df54..b4542230a49 100644 --- a/web/src/test/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolverTests.java +++ b/web/src/test/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolverTests.java @@ -27,8 +27,10 @@ import org.junit.jupiter.api.Test; import org.springframework.core.MethodParameter; +import org.springframework.core.annotation.AliasFor; import org.springframework.expression.BeanResolver; import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.context.SecurityContextHolder; @@ -193,6 +195,25 @@ public void resolveArgumentObject() throws Exception { .isEqualTo(this.expectedPrincipal); } + @Test + public void resolveArgumentCustomMetaAnnotation() throws Exception { + CustomUserPrincipal principal = new CustomUserPrincipal(); + setAuthenticationPrincipal(principal); + this.expectedPrincipal = principal.id; + assertThat(this.resolver.resolveArgument(showUserCustomMetaAnnotation(), null, null, null)) + .isEqualTo(this.expectedPrincipal); + } + + @Test + public void resolveArgumentCustomMetaAnnotationTpl() throws Exception { + CustomUserPrincipal principal = new CustomUserPrincipal(); + setAuthenticationPrincipal(principal); + this.resolver.setTemplateDefaults(new AnnotationTemplateExpressionDefaults()); + this.expectedPrincipal = principal.id; + assertThat(this.resolver.resolveArgument(showUserCustomMetaAnnotationTpl(), null, null, null)) + .isEqualTo(this.expectedPrincipal); + } + private MethodParameter showUserNoAnnotation() { return getMethodParameter("showUserNoAnnotation", String.class); } @@ -241,6 +262,14 @@ private MethodParameter showUserAnnotationObject() { return getMethodParameter("showUserAnnotation", Object.class); } + private MethodParameter showUserCustomMetaAnnotation() { + return getMethodParameter("showUserCustomMetaAnnotation", int.class); + } + + private MethodParameter showUserCustomMetaAnnotationTpl() { + return getMethodParameter("showUserCustomMetaAnnotationTpl", int.class); + } + private MethodParameter getMethodParameter(String methodName, Class... paramTypes) { Method method = ReflectionUtils.findMethod(TestController.class, methodName, paramTypes); return new MethodParameter(method, 0); @@ -266,6 +295,23 @@ private void setAuthenticationPrincipal(Object principal) { } + @Retention(RetentionPolicy.RUNTIME) + @AuthenticationPrincipal + public @interface CurrentUser2 { + + @AliasFor(annotation = AuthenticationPrincipal.class) + String expression() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + @AuthenticationPrincipal(expression = "principal.{property}") + public @interface CurrentUser3 { + + String property() default ""; + + } + public static class TestController { public void showUserNoAnnotation(String user) { @@ -290,6 +336,12 @@ public void showUserAnnotation(@AuthenticationPrincipal CustomUserPrincipal user public void showUserCustomAnnotation(@CurrentUser CustomUserPrincipal user) { } + public void showUserCustomMetaAnnotation(@CurrentUser2(expression = "principal.id") int userId) { + } + + public void showUserCustomMetaAnnotationTpl(@CurrentUser3(property = "id") int userId) { + } + public void showUserAnnotation(@AuthenticationPrincipal Object user) { } @@ -314,6 +366,10 @@ static class CustomUserPrincipal { public final int id = 1; + public Object getPrincipal() { + return this; + } + } public static class CopyUserPrincipal { diff --git a/web/src/test/java/org/springframework/security/web/reactive/result/method/annotation/AuthenticationPrincipalArgumentResolverTests.java b/web/src/test/java/org/springframework/security/web/reactive/result/method/annotation/AuthenticationPrincipalArgumentResolverTests.java index 3c370ffa733..59bf28be852 100644 --- a/web/src/test/java/org/springframework/security/web/reactive/result/method/annotation/AuthenticationPrincipalArgumentResolverTests.java +++ b/web/src/test/java/org/springframework/security/web/reactive/result/method/annotation/AuthenticationPrincipalArgumentResolverTests.java @@ -31,8 +31,11 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ReactiveAdapterRegistry; +import org.springframework.core.annotation.AliasFor; +import org.springframework.core.annotation.SynthesizingMethodParameter; import org.springframework.expression.BeanResolver; import org.springframework.security.core.Authentication; +import org.springframework.security.core.annotation.AnnotationTemplateExpressionDefaults; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.web.method.ResolvableMethod; @@ -206,6 +209,38 @@ public void resolveArgumentWhenErrorOnInvalidTypeExplicitTrue() { assertThatExceptionOfType(ClassCastException.class).isThrownBy(() -> argument.block()); } + @Test + public void resolveArgumentCustomMetaAnnotation() { + CustomUserPrincipal principal = new CustomUserPrincipal(); + given(this.authentication.getPrincipal()).willReturn(principal); + Mono result = this.resolver + .resolveArgument(arg0("showUserCustomMetaAnnotation"), this.bindingContext, this.exchange) + .contextWrite(ReactiveSecurityContextHolder.withAuthentication(this.authentication)); + assertThat(result.block()).isEqualTo(principal.id); + } + + @Test + public void resolveArgumentCustomMetaAnnotationTpl() { + CustomUserPrincipal principal = new CustomUserPrincipal(); + given(this.authentication.getPrincipal()).willReturn(principal); + this.resolver.setTemplateDefaults(new AnnotationTemplateExpressionDefaults()); + Mono result = this.resolver + .resolveArgument(arg0("showUserCustomMetaAnnotationTpl"), this.bindingContext, this.exchange) + .contextWrite(ReactiveSecurityContextHolder.withAuthentication(this.authentication)); + assertThat(result.block()).isEqualTo(principal.id); + } + + private MethodParameter arg0(String methodName) { + ResolvableMethod method = ResolvableMethod.on(getClass()).named(methodName).build(); + return new SynthesizingMethodParameter(method.method(), 0); + } + + public void showUserCustomMetaAnnotation(@CurrentUser2(expression = "principal.id") int userId) { + } + + public void showUserCustomMetaAnnotationTpl(@CurrentUser3(property = "id") int userId) { + } + void authenticationPrincipal(@AuthenticationPrincipal String principal, @AuthenticationPrincipal Mono monoPrincipal) { } @@ -278,4 +313,31 @@ public int getId() { } + static class CustomUserPrincipal { + + public final int id = 1; + + public Object getPrincipal() { + return this; + } + + } + + @Retention(RetentionPolicy.RUNTIME) + @AuthenticationPrincipal + public @interface CurrentUser2 { + + @AliasFor(annotation = AuthenticationPrincipal.class) + String expression() default ""; + + } + + @Retention(RetentionPolicy.RUNTIME) + @AuthenticationPrincipal(expression = "principal.{property}") + public @interface CurrentUser3 { + + String property() default ""; + + } + }