diff --git a/core/src/main/java/org/springframework/security/authorization/AllAuthoritiesAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/AllAuthoritiesAuthorizationManager.java index f2adfc60ce..c78d7fa50c 100644 --- a/core/src/main/java/org/springframework/security/authorization/AllAuthoritiesAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/AllAuthoritiesAuthorizationManager.java @@ -18,6 +18,7 @@ package org.springframework.security.authorization; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.function.Supplier; @@ -36,7 +37,7 @@ import org.springframework.util.Assert; * * @author Rob Winch * @since 7.0 - * @see AuthoritiesAuthorizationManager + * @see AuthorityAuthorizationManager */ public final class AllAuthoritiesAuthorizationManager implements AuthorizationManager { @@ -83,6 +84,9 @@ public final class AllAuthoritiesAuthorizationManager implements Authorizatio } private List getGrantedAuthorities(Authentication authentication) { + if (authentication == null || !authentication.isAuthenticated()) { + return Collections.emptyList(); + } return this.roleHierarchy.getReachableGrantedAuthorities(authentication.getAuthorities()) .stream() .map(GrantedAuthority::getAuthority) diff --git a/core/src/main/java/org/springframework/security/authorization/AllAuthoritiesReactiveAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/AllAuthoritiesReactiveAuthorizationManager.java new file mode 100644 index 0000000000..ebf5264a79 --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/AllAuthoritiesReactiveAuthorizationManager.java @@ -0,0 +1,158 @@ +/* + * Copyright 2004-present 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +import reactor.core.publisher.Mono; + +import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy; +import org.springframework.security.access.hierarchicalroles.RoleHierarchy; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.util.Assert; + +/** + * A {@link ReactiveAuthorizationManager} that determines if the current user is + * authorized by evaluating if the {@link Authentication} contains all the specified + * authorities. + * + * @author Rob Winch + * @since 7.0 + * @see AuthorityReactiveAuthorizationManager + */ +public final class AllAuthoritiesReactiveAuthorizationManager implements ReactiveAuthorizationManager { + + private static final String ROLE_PREFIX = "ROLE_"; + + private RoleHierarchy roleHierarchy = new NullRoleHierarchy(); + + private final List requiredAuthorities; + + private final AuthorityAuthorizationDecision defaultDecision; + + /** + * Creates a new instance. + * @param requiredAuthorities the authorities that are required. + */ + private AllAuthoritiesReactiveAuthorizationManager(String... requiredAuthorities) { + Assert.notEmpty(requiredAuthorities, "requiredAuthorities cannot be empty"); + this.requiredAuthorities = Arrays.asList(requiredAuthorities); + this.defaultDecision = new AuthorityAuthorizationDecision(false, + AuthorityUtils.createAuthorityList(this.requiredAuthorities)); + } + + /** + * Sets the {@link RoleHierarchy} to be used. Default is {@link NullRoleHierarchy}. + * Cannot be null. + * @param roleHierarchy the {@link RoleHierarchy} to use + */ + public void setRoleHierarchy(RoleHierarchy roleHierarchy) { + Assert.notNull(roleHierarchy, "roleHierarchy cannot be null"); + this.roleHierarchy = roleHierarchy; + } + + /** + * Determines if the current user is authorized by evaluating if the + * {@link Authentication} contains any of specified authorities. + * @param authentication the {@link Supplier} of the {@link Authentication} to check + * @param object the object to check authorization on (not used). + * @return an {@link AuthorityAuthorizationDecision} + */ + @Override + public Mono authorize(Mono authentication, T object) { + // @formatter:off + return authentication + .filter(Authentication::isAuthenticated) + .map(this::getGrantedAuthorities) + .defaultIfEmpty(Collections.emptyList()) + .map((authenticatedAuthorities) -> { + List missingAuthorities = new ArrayList<>(this.requiredAuthorities); + missingAuthorities.removeIf(authenticatedAuthorities::contains); + return new AuthorityAuthorizationDecision(missingAuthorities.isEmpty(), + AuthorityUtils.createAuthorityList(missingAuthorities)); + }); + // @formatter:on + + } + + private List getGrantedAuthorities(Authentication authentication) { + return this.roleHierarchy.getReachableGrantedAuthorities(authentication.getAuthorities()) + .stream() + .map(GrantedAuthority::getAuthority) + .toList(); + } + + /** + * Creates an instance of {@link AllAuthoritiesReactiveAuthorizationManager} with the + * provided authorities. + * @param roles the authorities to check for prefixed with "ROLE_". Each role should + * not start with "ROLE_" since it is automatically prepended already. + * @param the type of object being authorized + * @return the new instance + */ + public static AllAuthoritiesReactiveAuthorizationManager hasAllRoles(String... roles) { + return hasAllPrefixedAuthorities(ROLE_PREFIX, roles); + } + + /** + * Creates an instance of {@link AllAuthoritiesReactiveAuthorizationManager} with the + * provided authorities. + * @param prefix the prefix for authorities + * @param authorities the authorities to check for prefixed with prefix + * @param the type of object being authorized + * @return the new instance + */ + public static AllAuthoritiesReactiveAuthorizationManager hasAllPrefixedAuthorities(String prefix, + String... authorities) { + Assert.notNull(prefix, "rolePrefix cannot be null"); + Assert.notEmpty(authorities, "roles cannot be empty"); + Assert.noNullElements(authorities, "roles cannot contain null values"); + return hasAllAuthorities(toNamedRolesArray(prefix, authorities)); + } + + /** + * Creates an instance of {@link AllAuthoritiesReactiveAuthorizationManager} with the + * provided authorities. + * @param authorities the authorities to check for + * @param the type of object being authorized + * @return the new instance + */ + public static AllAuthoritiesReactiveAuthorizationManager hasAllAuthorities(String... authorities) { + Assert.notEmpty(authorities, "authorities cannot be empty"); + Assert.noNullElements(authorities, "authorities cannot contain null values"); + return new AllAuthoritiesReactiveAuthorizationManager<>(authorities); + } + + private static String[] toNamedRolesArray(String rolePrefix, String[] roles) { + String[] result = new String[roles.length]; + for (int i = 0; i < roles.length; i++) { + String role = roles[i]; + Assert.isTrue(rolePrefix.isEmpty() || !role.startsWith(rolePrefix), () -> role + " should not start with " + + rolePrefix + " since " + rolePrefix + + " is automatically prepended when using hasAnyRole. Consider using hasAnyAuthority instead."); + result[i] = rolePrefix + role; + } + return result; + } + +} diff --git a/core/src/main/java/org/springframework/security/authorization/AuthoritiesAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/AuthoritiesAuthorizationManager.java index 373f5f0ec3..37b51df3f7 100644 --- a/core/src/main/java/org/springframework/security/authorization/AuthoritiesAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/AuthoritiesAuthorizationManager.java @@ -34,7 +34,6 @@ import org.springframework.util.Assert; * * @author Evgeniy Cheban * @since 6.1 - * @see AllAuthoritiesAuthorizationManager */ public final class AuthoritiesAuthorizationManager implements AuthorizationManager> { diff --git a/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java index 824ac376da..2f2bd818f2 100644 --- a/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/AuthorityAuthorizationManager.java @@ -33,6 +33,7 @@ import org.springframework.util.Assert; * @param the type of object being authorized. * @author Evgeniy Cheban * @since 5.5 + * @see AllAuthoritiesAuthorizationManager */ public final class AuthorityAuthorizationManager implements AuthorizationManager { diff --git a/core/src/test/java/org/springframework/security/authorization/AllAuthoritiesAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/AllAuthoritiesAuthorizationManagerTests.java index 98d033c2c4..7eb71d0cdc 100644 --- a/core/src/test/java/org/springframework/security/authorization/AllAuthoritiesAuthorizationManagerTests.java +++ b/core/src/test/java/org/springframework/security/authorization/AllAuthoritiesAuthorizationManagerTests.java @@ -67,14 +67,24 @@ class AllAuthoritiesAuthorizationManagerTests { @Test void authorizeWhenGranted() { Authentication authentication = new TestingAuthenticationToken("user", "password", ROLE_USER); - AllAuthoritiesAuthorizationManager manager = AllAuthoritiesAuthorizationManager.hasAllAuthorities(ROLE_USER); + AllAuthoritiesAuthorizationManager manager = AllAuthoritiesAuthorizationManager + .hasAllAuthorities(ROLE_USER); assertThat(manager.authorize(() -> authentication, "").isGranted()).isTrue(); } + @Test + void authorizeWhenNotAuthenticated() { + Authentication authentication = new TestingAuthenticationToken("user", "password", ROLE_USER); + authentication.setAuthenticated(false); + AllAuthoritiesAuthorizationManager manager = AllAuthoritiesAuthorizationManager + .hasAllAuthorities(ROLE_USER); + assertThat(manager.authorize(() -> authentication, "").isGranted()).isFalse(); + } + @Test void hasAllRolesAuthorizeWhenGranted() { Authentication authentication = new TestingAuthenticationToken("user", "password", ROLE_USER); - AllAuthoritiesAuthorizationManager manager = AllAuthoritiesAuthorizationManager.hasAllRoles("USER"); + AllAuthoritiesAuthorizationManager manager = AllAuthoritiesAuthorizationManager.hasAllRoles("USER"); assertThat(manager.authorize(() -> authentication, "").isGranted()).isTrue(); } @@ -85,7 +95,7 @@ class AllAuthoritiesAuthorizationManagerTests { String authority2 = "AUTHORITY2"; Authentication authentication = new TestingAuthenticationToken("user", "password", prefix + authority1, prefix + authority2); - AllAuthoritiesAuthorizationManager manager = AllAuthoritiesAuthorizationManager + AllAuthoritiesAuthorizationManager manager = AllAuthoritiesAuthorizationManager .hasAllPrefixedAuthorities(prefix, authority1, authority2); assertThat(manager.authorize(() -> authentication, "").isGranted()).isTrue(); } @@ -93,15 +103,16 @@ class AllAuthoritiesAuthorizationManagerTests { @Test void authorizeWhenSingleMissingThenDenied() { Authentication authentication = new TestingAuthenticationToken("user", "password", ROLE_USER); - AllAuthoritiesAuthorizationManager manager = AllAuthoritiesAuthorizationManager.hasAllAuthorities(ROLE_ADMIN); + AllAuthoritiesAuthorizationManager manager = AllAuthoritiesAuthorizationManager + .hasAllAuthorities(ROLE_ADMIN); assertThat(manager.authorize(() -> authentication, "").isGranted()).isFalse(); } @Test void authorizeWhenMultipleMissingOneThenDenied() { Authentication authentication = new TestingAuthenticationToken("user", "password", ROLE_USER); - AllAuthoritiesAuthorizationManager manager = AllAuthoritiesAuthorizationManager.hasAllAuthorities(ROLE_ADMIN, - ROLE_USER); + AllAuthoritiesAuthorizationManager manager = AllAuthoritiesAuthorizationManager + .hasAllAuthorities(ROLE_ADMIN, ROLE_USER); AuthorityAuthorizationDecision result = manager.authorize(() -> authentication, ""); assertThat(result.isGranted()).isFalse(); assertThat(result.getAuthorities()).hasSize(1); diff --git a/core/src/test/java/org/springframework/security/authorization/AllAuthoritiesReactiveAuthorizationManagerTests.java b/core/src/test/java/org/springframework/security/authorization/AllAuthoritiesReactiveAuthorizationManagerTests.java new file mode 100644 index 0000000000..7756a126d6 --- /dev/null +++ b/core/src/test/java/org/springframework/security/authorization/AllAuthoritiesReactiveAuthorizationManagerTests.java @@ -0,0 +1,151 @@ +/* + * Copyright 2004-present 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. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.security.authorization; + +import java.util.ArrayList; +import java.util.Collection; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import reactor.core.publisher.Mono; + +import org.springframework.security.access.hierarchicalroles.RoleHierarchy; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class AllAuthoritiesReactiveAuthorizationManagerTests { + + public static final String ROLE_USER = "ROLE_USER"; + + public static final String ROLE_ADMIN = "ROLE_ADMIN"; + + @Mock + private RoleHierarchy roleHierarchy; + + @Captor + private ArgumentCaptor> authoritiesCaptor; + + @Test + void hasAllAuthoritiesWhenNullAuthoritiesThenIllegalArgumentException() { + String[] requiredAuthorities = null; + assertThatIllegalArgumentException() + .isThrownBy(() -> AllAuthoritiesReactiveAuthorizationManager.hasAllAuthorities(requiredAuthorities)); + } + + @Test + void hasAllAuthortiesWhenEmptyAuthoritiesThenIllegalArgumentException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> AllAuthoritiesReactiveAuthorizationManager.hasAllAuthorities((new String[0]))); + } + + @Test + void authorizeWhenGranted() { + Authentication authentication = new TestingAuthenticationToken("user", "password", ROLE_USER); + AllAuthoritiesReactiveAuthorizationManager manager = AllAuthoritiesReactiveAuthorizationManager + .hasAllAuthorities(ROLE_USER); + assertThat(manager.authorize(Mono.just(authentication), "").block().isGranted()).isTrue(); + } + + @Test + void authorizeWhenNotAuthenticated() { + Authentication authentication = new TestingAuthenticationToken("user", "password", ROLE_USER); + authentication.setAuthenticated(false); + AllAuthoritiesReactiveAuthorizationManager manager = AllAuthoritiesReactiveAuthorizationManager + .hasAllAuthorities(ROLE_USER); + assertThat(manager.authorize(Mono.just(authentication), "").block().isGranted()).isFalse(); + } + + @Test + void hasAllRolesAuthorizeWhenGranted() { + Authentication authentication = new TestingAuthenticationToken("user", "password", ROLE_USER); + AllAuthoritiesReactiveAuthorizationManager manager = AllAuthoritiesReactiveAuthorizationManager + .hasAllRoles("USER"); + assertThat(manager.authorize(Mono.just(authentication), "").block().isGranted()).isTrue(); + } + + @Test + void hasAllPrefixedAuthoritiesAuthorizeWhenGranted() { + String prefix = "PREFIX_"; + String authority1 = "AUTHORITY1"; + String authority2 = "AUTHORITY2"; + Authentication authentication = new TestingAuthenticationToken("user", "password", prefix + authority1, + prefix + authority2); + AllAuthoritiesReactiveAuthorizationManager manager = AllAuthoritiesReactiveAuthorizationManager + .hasAllPrefixedAuthorities(prefix, authority1, authority2); + assertThat(manager.authorize(Mono.just(authentication), "").block().isGranted()).isTrue(); + } + + @Test + void authorizeWhenSingleMissingThenDenied() { + Authentication authentication = new TestingAuthenticationToken("user", "password", ROLE_USER); + AllAuthoritiesReactiveAuthorizationManager manager = AllAuthoritiesReactiveAuthorizationManager + .hasAllAuthorities(ROLE_ADMIN); + assertThat(manager.authorize(Mono.just(authentication), "").block().isGranted()).isFalse(); + } + + @Test + void authorizeWhenMultipleMissingOneThenDenied() { + Authentication authentication = new TestingAuthenticationToken("user", "password", ROLE_USER); + AllAuthoritiesReactiveAuthorizationManager manager = AllAuthoritiesReactiveAuthorizationManager + .hasAllAuthorities(ROLE_ADMIN, ROLE_USER); + AuthorityAuthorizationDecision result = manager.authorize(Mono.just(authentication), "") + .cast(AuthorityAuthorizationDecision.class) + .block(); + assertThat(result.isGranted()).isFalse(); + assertThat(result.getAuthorities()).hasSize(1); + assertThat(new ArrayList<>(result.getAuthorities()).get(0).getAuthority()).isEqualTo(ROLE_ADMIN); + } + + @Test + void setRoleHierarchyWhenNullThenIllegalArgumentException() { + AllAuthoritiesReactiveAuthorizationManager manager = AllAuthoritiesReactiveAuthorizationManager + .hasAllAuthorities(ROLE_USER); + assertThatIllegalArgumentException().isThrownBy(() -> manager.setRoleHierarchy(null)); + } + + @Test + void setRoleHierarchyThenUsesResult() { + Collection result = AuthorityUtils.createAuthorityList(ROLE_USER, ROLE_ADMIN); + given(this.roleHierarchy.getReachableGrantedAuthorities(any())).willReturn(result); + AllAuthoritiesReactiveAuthorizationManager manager = AllAuthoritiesReactiveAuthorizationManager + .hasAllAuthorities(ROLE_USER); + manager.setRoleHierarchy(this.roleHierarchy); + + Authentication authentication = new TestingAuthenticationToken("user", "password", ROLE_USER); + + AuthorityAuthorizationDecision authz = manager.authorize(Mono.just(authentication), "") + .cast(AuthorityAuthorizationDecision.class) + .block(); + assertThat(authz.isGranted()).isTrue(); + verify(this.roleHierarchy).getReachableGrantedAuthorities(this.authoritiesCaptor.capture()); + assertThat(this.authoritiesCaptor.getValue()).map(GrantedAuthority::getAuthority).contains(ROLE_USER); + } + +} diff --git a/docs/modules/ROOT/pages/whats-new.adoc b/docs/modules/ROOT/pages/whats-new.adoc index c150e56113..0f103b0e9f 100644 --- a/docs/modules/ROOT/pages/whats-new.adoc +++ b/docs/modules/ROOT/pages/whats-new.adoc @@ -16,6 +16,7 @@ Each section that follows will indicate the more notable removals as well as the == Core * Removed `AuthorizationManager#check` in favor of `AuthorizationManager#authorize` +* Added javadoc:org.springframework.security.authorization.AllAuthoritiesAuthorizationManager[] and javadoc:org.springframework.security.authorization.AllAuthoritiesReactiveAuthorizationManager[] * Added xref:servlet/authorization/architecture.adoc#authz-authorization-manager-factory[`AuthorizationManagerFactory`] for creating `AuthorizationManager` instances in xref:servlet/authorization/authorize-http-requests.adoc#customizing-authorization-managers[request-based] and xref:servlet/authorization/method-security.adoc#customizing-authorization-managers[method-based] authorization components * Added `Authentication.Builder` for mutating and merging `Authentication` instances * Moved Access API (`AccessDecisionManager`, `AccessDecisionVoter`, etc.) to a new module, `spring-security-access`