diff --git a/core/src/test/java/org/springframework/security/authentication/TestAuthentication.java b/core/src/test/java/org/springframework/security/authentication/TestAuthentication.java index cdeb4ba1d82..a0faebee71c 100644 --- a/core/src/test/java/org/springframework/security/authentication/TestAuthentication.java +++ b/core/src/test/java/org/springframework/security/authentication/TestAuthentication.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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,23 @@ package org.springframework.security.authentication; import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; import org.springframework.security.core.userdetails.PasswordEncodedUser; import org.springframework.security.core.userdetails.UserDetails; /** * @author Rob Winch + * @author Evgeniy Cheban * @since 5.0 */ public class TestAuthentication extends PasswordEncodedUser { + private static final Authentication ANONYMOUS = new AnonymousAuthenticationToken("key", "anonymous", + AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); + + private static final RememberMeAuthenticationToken REMEMBER_ME = new RememberMeAuthenticationToken("key", "user", + AuthorityUtils.createAuthorityList("ROLE_USER")); + public static Authentication authenticatedAdmin() { return autheticated(admin()); } @@ -38,4 +46,12 @@ public static Authentication autheticated(UserDetails user) { return UsernamePasswordAuthenticationToken.authenticated(user, null, user.getAuthorities()); } + public static Authentication anonymousUser() { + return ANONYMOUS; + } + + public static Authentication rememberMeUser() { + return REMEMBER_ME; + } + } diff --git a/web/src/main/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManager.java b/web/src/main/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManager.java index 6192426ac3f..7f00779c4ed 100644 --- a/web/src/main/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManager.java +++ b/web/src/main/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -26,9 +26,12 @@ import org.apache.commons.logging.LogFactory; import org.springframework.core.log.LogMessage; +import org.springframework.security.authorization.AuthenticatedAuthorizationManager; +import org.springframework.security.authorization.AuthorityAuthorizationManager; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.authorization.AuthorizationManager; import org.springframework.security.core.Authentication; +import org.springframework.security.web.util.matcher.AnyRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher.MatchResult; import org.springframework.security.web.util.matcher.RequestMatcherEntry; @@ -102,6 +105,8 @@ public static Builder builder() { */ public static final class Builder { + private boolean anyRequestConfigured; + private final List>> mappings = new ArrayList<>(); /** @@ -111,6 +116,7 @@ public static final class Builder { * @return the {@link Builder} for further customizations */ public Builder add(RequestMatcher matcher, AuthorizationManager manager) { + Assert.state(!this.anyRequestConfigured, "Can't add mappings after anyRequest"); Assert.notNull(matcher, "matcher cannot be null"); Assert.notNull(manager, "manager cannot be null"); this.mappings.add(new RequestMatcherEntry<>(matcher, manager)); @@ -127,11 +133,34 @@ public Builder add(RequestMatcher matcher, AuthorizationManager>>> mappingsConsumer) { + Assert.state(!this.anyRequestConfigured, "Can't configure mappings after anyRequest"); Assert.notNull(mappingsConsumer, "mappingsConsumer cannot be null"); mappingsConsumer.accept(this.mappings); return this; } + /** + * Maps any request. + * @return the {@link AuthorizedUrl} for further customizations + * @since 6.2 + */ + public AuthorizedUrl anyRequest() { + Assert.state(!this.anyRequestConfigured, "Can't configure anyRequest after itself"); + this.anyRequestConfigured = true; + return new AuthorizedUrl(AnyRequestMatcher.INSTANCE); + } + + /** + * Maps {@link RequestMatcher}s to {@link AuthorizationManager}. + * @param matchers the {@link RequestMatcher}s to map + * @return the {@link AuthorizedUrl} for further customizations + * @since 6.2 + */ + public AuthorizedUrl requestMatchers(RequestMatcher... matchers) { + Assert.state(!this.anyRequestConfigured, "Can't configure requestMatchers after anyRequest"); + return new AuthorizedUrl(matchers); + } + /** * Creates a {@link RequestMatcherDelegatingAuthorizationManager} instance. * @return the {@link RequestMatcherDelegatingAuthorizationManager} instance @@ -140,6 +169,123 @@ public RequestMatcherDelegatingAuthorizationManager build() { return new RequestMatcherDelegatingAuthorizationManager(this.mappings); } + /** + * An object that allows configuring the {@link AuthorizationManager} for + * {@link RequestMatcher}s. + * + * @author Evgeniy Cheban + * @since 6.2 + */ + public final class AuthorizedUrl { + + private final List matchers; + + private AuthorizedUrl(RequestMatcher... matchers) { + this(List.of(matchers)); + } + + private AuthorizedUrl(List matchers) { + this.matchers = matchers; + } + + /** + * Specify that URLs are allowed by anyone. + * @return the {@link Builder} for further customizations + */ + public Builder permitAll() { + return access((a, o) -> new AuthorizationDecision(true)); + } + + /** + * Specify that URLs are not allowed by anyone. + * @return the {@link Builder} for further customizations + */ + public Builder denyAll() { + return access((a, o) -> new AuthorizationDecision(false)); + } + + /** + * Specify that URLs are allowed by any authenticated user. + * @return the {@link Builder} for further customizations + */ + public Builder authenticated() { + return access(AuthenticatedAuthorizationManager.authenticated()); + } + + /** + * Specify that URLs are allowed by users who have authenticated and were not + * "remembered". + * @return the {@link Builder} for further customization + */ + public Builder fullyAuthenticated() { + return access(AuthenticatedAuthorizationManager.fullyAuthenticated()); + } + + /** + * Specify that URLs are allowed by users that have been remembered. + * @return the {@link Builder} for further customization + */ + public Builder rememberMe() { + return access(AuthenticatedAuthorizationManager.rememberMe()); + } + + /** + * Specify that URLs are allowed by anonymous users. + * @return the {@link Builder} for further customization + */ + public Builder anonymous() { + return access(AuthenticatedAuthorizationManager.anonymous()); + } + + /** + * Specifies a user requires a role. + * @param role the role that should be required which is prepended with ROLE_ + * automatically (i.e. USER, ADMIN, etc). It should not start with ROLE_ + * @return {@link Builder} for further customizations + */ + public Builder hasRole(String role) { + return access(AuthorityAuthorizationManager.hasRole(role)); + } + + /** + * Specifies that a user requires one of many roles. + * @param roles the roles that the user should have at least one of (i.e. + * ADMIN, USER, etc). Each role should not start with ROLE_ since it is + * automatically prepended already + * @return the {@link Builder} for further customizations + */ + public Builder hasAnyRole(String... roles) { + return access(AuthorityAuthorizationManager.hasAnyRole(roles)); + } + + /** + * Specifies a user requires an authority. + * @param authority the authority that should be required + * @return the {@link Builder} for further customizations + */ + public Builder hasAuthority(String authority) { + return access(AuthorityAuthorizationManager.hasAuthority(authority)); + } + + /** + * Specifies that a user requires one of many authorities. + * @param authorities the authorities that the user should have at least one + * of (i.e. ROLE_USER, ROLE_ADMIN, etc) + * @return the {@link Builder} for further customizations + */ + public Builder hasAnyAuthority(String... authorities) { + return access(AuthorityAuthorizationManager.hasAnyAuthority(authorities)); + } + + private Builder access(AuthorizationManager manager) { + for (RequestMatcher matcher : this.matchers) { + Builder.this.mappings.add(new RequestMatcherEntry<>(matcher, manager)); + } + return Builder.this; + } + + } + } } diff --git a/web/src/test/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManagerTests.java b/web/src/test/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManagerTests.java index c84d37da6b6..dbc6daba3ac 100644 --- a/web/src/test/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManagerTests.java +++ b/web/src/test/java/org/springframework/security/web/access/intercept/RequestMatcherDelegatingAuthorizationManagerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 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. @@ -21,16 +21,20 @@ import org.junit.jupiter.api.Test; import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.security.authentication.TestAuthentication; import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.authorization.AuthenticatedAuthorizationManager; import org.springframework.security.authorization.AuthorityAuthorizationManager; import org.springframework.security.authorization.AuthorizationDecision; import org.springframework.security.core.Authentication; import org.springframework.security.web.servlet.util.matcher.MvcRequestMatcher; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AnyRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcherEntry; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; /** * Tests for {@link RequestMatcherDelegatingAuthorizationManager}. @@ -120,4 +124,225 @@ public void addWhenMappingsConsumerNullThenException() { .withMessage("mappingsConsumer cannot be null"); } + @Test + public void mappingsWhenConfiguredAfterAnyRequestThenException() { + assertThatIllegalStateException() + .isThrownBy(() -> RequestMatcherDelegatingAuthorizationManager.builder().anyRequest().authenticated() + .mappings((m) -> m.add(new RequestMatcherEntry<>(AnyRequestMatcher.INSTANCE, + AuthenticatedAuthorizationManager.authenticated())))) + .withMessage("Can't configure mappings after anyRequest"); + } + + @Test + public void addWhenConfiguredAfterAnyRequestThenException() { + assertThatIllegalStateException() + .isThrownBy(() -> RequestMatcherDelegatingAuthorizationManager.builder().anyRequest().authenticated() + .add(AnyRequestMatcher.INSTANCE, AuthenticatedAuthorizationManager.authenticated())) + .withMessage("Can't add mappings after anyRequest"); + } + + @Test + public void requestMatchersWhenConfiguredAfterAnyRequestThenException() { + assertThatIllegalStateException() + .isThrownBy(() -> RequestMatcherDelegatingAuthorizationManager.builder().anyRequest().authenticated() + .requestMatchers(new AntPathRequestMatcher("/authenticated")).authenticated().build()) + .withMessage("Can't configure requestMatchers after anyRequest"); + } + + @Test + public void anyRequestWhenConfiguredAfterAnyRequestThenException() { + assertThatIllegalStateException().isThrownBy(() -> RequestMatcherDelegatingAuthorizationManager.builder() + .anyRequest().authenticated().anyRequest().authenticated().build()) + .withMessage("Can't configure anyRequest after itself"); + } + + @Test + public void anyRequestWhenPermitAllThenGrantedDecision() { + RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .anyRequest().permitAll().build(); + AuthorizationDecision decision = manager.check(TestAuthentication::anonymousUser, null); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + + @Test + public void anyRequestWhenDenyAllThenDeniedDecision() { + RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .anyRequest().denyAll().build(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedAdmin, null); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + } + + @Test + public void authenticatedWhenAuthenticatedUserThenGrantedDecision() { + RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .anyRequest().authenticated().build(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, null); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + + @Test + public void authenticatedWhenAnonymousUserThenDeniedDecision() { + RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .anyRequest().authenticated().build(); + AuthorizationDecision decision = manager.check(TestAuthentication::anonymousUser, null); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + } + + @Test + public void fullyAuthenticatedWhenAuthenticatedUserThenGrantedDecision() { + RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .anyRequest().fullyAuthenticated().build(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, null); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + + @Test + public void fullyAuthenticatedWhenAnonymousUserThenDeniedDecision() { + RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .anyRequest().fullyAuthenticated().build(); + AuthorizationDecision decision = manager.check(TestAuthentication::anonymousUser, null); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + } + + @Test + public void fullyAuthenticatedWhenRememberMeUserThenDeniedDecision() { + RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .anyRequest().fullyAuthenticated().build(); + AuthorizationDecision decision = manager.check(TestAuthentication::rememberMeUser, null); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + } + + @Test + public void rememberMeWhenRememberMeUserThenGrantedDecision() { + RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .anyRequest().rememberMe().build(); + AuthorizationDecision decision = manager.check(TestAuthentication::rememberMeUser, null); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + + @Test + public void rememberMeWhenAuthenticatedUserThenDeniedDecision() { + RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .anyRequest().rememberMe().build(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, null); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + } + + @Test + public void anonymousWhenAnonymousUserThenGrantedDecision() { + RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .anyRequest().anonymous().build(); + AuthorizationDecision decision = manager.check(TestAuthentication::anonymousUser, null); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + + @Test + public void anonymousWhenAuthenticatedUserThenDeniedDecision() { + RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .anyRequest().anonymous().build(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, null); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + } + + @Test + public void hasRoleAdminWhenAuthenticatedUserThenDeniedDecision() { + RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .anyRequest().hasRole("ADMIN").build(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, null); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + } + + @Test + public void hasRoleAdminWhenAuthenticatedAdminThenGrantedDecision() { + RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .anyRequest().hasRole("ADMIN").build(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedAdmin, null); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + + @Test + public void hasAnyRoleUserOrAdminWhenAuthenticatedUserThenGrantedDecision() { + RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .anyRequest().hasAnyRole("USER", "ADMIN").build(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, null); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + + @Test + public void hasAnyRoleUserOrAdminWhenAuthenticatedAdminThenGrantedDecision() { + RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .anyRequest().hasAnyRole("USER", "ADMIN").build(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedAdmin, null); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + + @Test + public void hasAnyRoleUserOrAdminWhenAnonymousUserThenDeniedDecision() { + RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .anyRequest().hasAnyRole("USER", "ADMIN").build(); + AuthorizationDecision decision = manager.check(TestAuthentication::anonymousUser, null); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + } + + @Test + public void hasAuthorityRoleAdminWhenAuthenticatedUserThenDeniedDecision() { + RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .anyRequest().hasAuthority("ROLE_ADMIN").build(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, null); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + } + + @Test + public void hasAuthorityRoleAdminWhenAuthenticatedAdminThenGrantedDecision() { + RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .anyRequest().hasAuthority("ROLE_ADMIN").build(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedAdmin, null); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + + @Test + public void hasAnyAuthorityRoleUserOrAdminWhenAuthenticatedUserThenGrantedDecision() { + RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .anyRequest().hasAnyAuthority("ROLE_USER", "ROLE_ADMIN").build(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedUser, null); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + + @Test + public void hasAnyAuthorityRoleUserOrAdminWhenAuthenticatedAdminThenGrantedDecision() { + RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .anyRequest().hasAnyAuthority("ROLE_USER", "ROLE_ADMIN").build(); + AuthorizationDecision decision = manager.check(TestAuthentication::authenticatedAdmin, null); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isTrue(); + } + + @Test + public void hasAnyAuthorityRoleUserOrAdminWhenAnonymousUserThenDeniedDecision() { + RequestMatcherDelegatingAuthorizationManager manager = RequestMatcherDelegatingAuthorizationManager.builder() + .anyRequest().hasAnyRole("USER", "ADMIN").build(); + AuthorizationDecision decision = manager.check(TestAuthentication::anonymousUser, null); + assertThat(decision).isNotNull(); + assertThat(decision.isGranted()).isFalse(); + } + }