From 11589cdb1198752aebe98df76a17495bd20a7c20 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Fri, 1 Dec 2023 17:15:19 -0700 Subject: [PATCH] Add Not Support Closes gh-14058 --- .../AuthorizeHttpRequestsConfigurer.java | 18 +++++++++- .../AuthorizeHttpRequestsConfigurerTests.java | 32 +++++++++++++++++ .../authorization/AuthorizationManagers.java | 36 +++++++++++++++++++ .../AuthorizationManagersTests.java | 15 ++++++++ 4 files changed, 100 insertions(+), 1 deletion(-) diff --git a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java index 1de4750a494..7937143b318 100644 --- a/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java +++ b/config/src/main/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurer.java @@ -30,6 +30,7 @@ import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationEventPublisher; import org.springframework.security.authorization.AuthorizationManager; +import org.springframework.security.authorization.AuthorizationManagers; import org.springframework.security.authorization.ObservationAuthorizationManager; import org.springframework.security.authorization.SpringAuthorizationEventPublisher; import org.springframework.security.config.annotation.ObjectPostProcessor; @@ -244,11 +245,14 @@ public H and() { * {@link RequestMatcher}s. * * @author Evgeniy Cheban + * @author Josh Cummings */ public class AuthorizedUrl { private final List matchers; + private boolean not; + /** * Creates an instance. * @param matchers the {@link RequestMatcher} instances to map @@ -261,6 +265,16 @@ protected List getMatchers() { return this.matchers; } + /** + * Negates the following authorization rule. + * @return the {@link AuthorizedUrl} for further customization + * @since 6.3 + */ + public AuthorizedUrl not() { + this.not = true; + return this; + } + /** * Specify that URLs are allowed by anyone. * @return the {@link AuthorizationManagerRequestMatcherRegistry} for further @@ -382,7 +396,9 @@ public AuthorizationManagerRequestMatcherRegistry anonymous() { public AuthorizationManagerRequestMatcherRegistry access( AuthorizationManager manager) { Assert.notNull(manager, "manager cannot be null"); - return AuthorizeHttpRequestsConfigurer.this.addMapping(this.matchers, manager); + return (this.not) + ? AuthorizeHttpRequestsConfigurer.this.addMapping(this.matchers, AuthorizationManagers.not(manager)) + : AuthorizeHttpRequestsConfigurer.this.addMapping(this.matchers, manager); } } diff --git a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java index c2d99042b02..5f220a7a177 100644 --- a/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java +++ b/config/src/test/java/org/springframework/security/config/annotation/web/configurers/AuthorizeHttpRequestsConfigurerTests.java @@ -596,6 +596,20 @@ public void getWhenAnonymousConfiguredAndLoggedInUserThenRespondsWithForbidden() this.mvc.perform(requestWithUser).andExpect(status().isForbidden()); } + @Test + public void getWhenNotConfigAndAuthenticatedThenRespondsWithForbidden() throws Exception { + this.spring.register(NotConfig.class, BasicController.class).autowire(); + MockHttpServletRequestBuilder requestWithUser = get("/").with(user("user")); + this.mvc.perform(requestWithUser).andExpect(status().isForbidden()); + } + + @Test + public void getWhenNotConfigAndNotAuthenticatedThenRespondsWithOk() throws Exception { + this.spring.register(NotConfig.class, BasicController.class).autowire(); + MockHttpServletRequestBuilder requestWithUser = get("/"); + this.mvc.perform(requestWithUser).andExpect(status().isOk()); + } + @Configuration @EnableWebSecurity static class GrantedAuthorityDefaultHasRoleConfig { @@ -1136,6 +1150,24 @@ SecurityFilterChain chain(HttpSecurity http) throws Exception { } + @Configuration + @EnableWebSecurity + static class NotConfig { + + @Bean + SecurityFilterChain chain(HttpSecurity http) throws Exception { + // @formatter:off + http + .httpBasic(withDefaults()) + .authorizeHttpRequests((requests) -> requests + .anyRequest().not().authenticated() + ); + // @formatter:on + return http.build(); + } + + } + @Configuration static class AuthorizationEventPublisherConfig { diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorizationManagers.java b/core/src/main/java/org/springframework/security/authorization/AuthorizationManagers.java index 94a072b5a4c..b9031092050 100644 --- a/core/src/main/java/org/springframework/security/authorization/AuthorizationManagers.java +++ b/core/src/main/java/org/springframework/security/authorization/AuthorizationManagers.java @@ -23,6 +23,7 @@ * A factory class to create an {@link AuthorizationManager} instances. * * @author Evgeniy Cheban + * @author Josh Cummings * @since 5.8 */ public final class AuthorizationManagers { @@ -119,6 +120,25 @@ public static AuthorizationManager allOf(AuthorizationDecision allAbstain }; } + /** + * Creates an {@link AuthorizationManager} that reverses whatever decision the given + * {@link AuthorizationManager} granted. If the given {@link AuthorizationManager} + * abstains, then the returned manager also abstains. + * @param the type of object that is being authorized + * @param manager the {@link AuthorizationManager} to reverse + * @return the reversing {@link AuthorizationManager} + * @since 6.3 + */ + public static AuthorizationManager not(AuthorizationManager manager) { + return (authentication, object) -> { + AuthorizationDecision decision = manager.check(authentication, object); + if (decision == null) { + return null; + } + return new NotAuthorizationDecision(decision); + }; + } + private AuthorizationManagers() { } @@ -138,4 +158,20 @@ public String toString() { } + private static final class NotAuthorizationDecision extends AuthorizationDecision { + + private final AuthorizationDecision decision; + + private NotAuthorizationDecision(AuthorizationDecision decision) { + super(!decision.isGranted()); + this.decision = decision; + } + + @Override + public String toString() { + return "NotAuthorizationDecision [decision=" + this.decision + ']'; + } + + } + } diff --git a/core/src/test/java/org/springframework/security/authorization/AuthorizationManagersTests.java b/core/src/test/java/org/springframework/security/authorization/AuthorizationManagersTests.java index 9debc7c2478..ba32fb63bbd 100644 --- a/core/src/test/java/org/springframework/security/authorization/AuthorizationManagersTests.java +++ b/core/src/test/java/org/springframework/security/authorization/AuthorizationManagersTests.java @@ -224,4 +224,19 @@ void checkAllOfWhenAllAbstainDefaultDecisionIsAbstainAndAllManagersAbstainThenAb assertThat(decision).isNull(); } + @Test + void checkNotWhenEmptyThenAbstainedDecision() { + AuthorizationManager negated = AuthorizationManagers.not((a, o) -> null); + AuthorizationDecision decision = negated.check(null, null); + assertThat(decision).isNull(); + } + + @Test + void checkNotWhenGrantedThenDeniedDecision() { + AuthorizationManager negated = AuthorizationManagers.not((a, o) -> new AuthorizationDecision(true)); + AuthorizationDecision decision = negated.check(null, null); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + } + }