From 096dfd4046963d05edb7815095b30ab6bf3d088a Mon Sep 17 00:00:00 2001 From: Rob Winch <362503+rwinch@users.noreply.github.com> Date: Tue, 16 Sep 2025 15:02:56 -0500 Subject: [PATCH] Add AllAuthoritiesAuthorizationManager Closes gh-17916 --- .../AllAuthoritiesAuthorizationManager.java | 145 ++++++++++++++++++ .../AuthoritiesAuthorizationManager.java | 1 + ...lAuthoritiesAuthorizationManagerTests.java | 133 ++++++++++++++++ 3 files changed, 279 insertions(+) create mode 100644 core/src/main/java/org/springframework/security/authorization/AllAuthoritiesAuthorizationManager.java create mode 100644 core/src/test/java/org/springframework/security/authorization/AllAuthoritiesAuthorizationManagerTests.java diff --git a/core/src/main/java/org/springframework/security/authorization/AllAuthoritiesAuthorizationManager.java b/core/src/main/java/org/springframework/security/authorization/AllAuthoritiesAuthorizationManager.java new file mode 100644 index 0000000000..f2adfc60ce --- /dev/null +++ b/core/src/main/java/org/springframework/security/authorization/AllAuthoritiesAuthorizationManager.java @@ -0,0 +1,145 @@ +/* + * 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.List; +import java.util.function.Supplier; + +import org.jspecify.annotations.Nullable; + +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; + +/** + * An {@link AuthorizationManager} 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 AuthoritiesAuthorizationManager + */ +public final class AllAuthoritiesAuthorizationManager implements AuthorizationManager { + + private static final String ROLE_PREFIX = "ROLE_"; + + private RoleHierarchy roleHierarchy = new NullRoleHierarchy(); + + private final List requiredAuthorities; + + /** + * Creates a new instance. + * @param requiredAuthorities the authorities that are required. + */ + private AllAuthoritiesAuthorizationManager(String... requiredAuthorities) { + Assert.notEmpty(requiredAuthorities, "requiredAuthorities cannot be empty"); + this.requiredAuthorities = Arrays.asList(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 AuthorityAuthorizationDecision authorize(Supplier authentication, + T object) { + List authenticatedAuthorities = getGrantedAuthorities(authentication.get()); + List missingAuthorities = new ArrayList<>(this.requiredAuthorities); + missingAuthorities.removeIf(authenticatedAuthorities::contains); + return new AuthorityAuthorizationDecision(missingAuthorities.isEmpty(), + AuthorityUtils.createAuthorityList(missingAuthorities)); + } + + private List getGrantedAuthorities(Authentication authentication) { + return this.roleHierarchy.getReachableGrantedAuthorities(authentication.getAuthorities()) + .stream() + .map(GrantedAuthority::getAuthority) + .toList(); + } + + /** + * Creates an instance of {@link AllAuthoritiesAuthorizationManager} 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 AllAuthoritiesAuthorizationManager hasAllRoles(String... roles) { + return hasAllPrefixedAuthorities(ROLE_PREFIX, roles); + } + + /** + * Creates an instance of {@link AllAuthoritiesAuthorizationManager} 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 AllAuthoritiesAuthorizationManager 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 AllAuthoritiesAuthorizationManager} with the provided + * authorities. + * @param authorities the authorities to check for + * @param the type of object being authorized + * @return the new instance + */ + public static AllAuthoritiesAuthorizationManager hasAllAuthorities(String... authorities) { + Assert.notEmpty(authorities, "authorities cannot be empty"); + Assert.noNullElements(authorities, "authorities cannot contain null values"); + return new AllAuthoritiesAuthorizationManager<>(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 37b51df3f7..373f5f0ec3 100644 --- a/core/src/main/java/org/springframework/security/authorization/AuthoritiesAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/AuthoritiesAuthorizationManager.java @@ -34,6 +34,7 @@ import org.springframework.util.Assert; * * @author Evgeniy Cheban * @since 6.1 + * @see AllAuthoritiesAuthorizationManager */ public final class AuthoritiesAuthorizationManager 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 new file mode 100644 index 0000000000..98d033c2c4 --- /dev/null +++ b/core/src/test/java/org/springframework/security/authorization/AllAuthoritiesAuthorizationManagerTests.java @@ -0,0 +1,133 @@ +/* + * 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 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 AllAuthoritiesAuthorizationManagerTests { + + 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(() -> AllAuthoritiesAuthorizationManager.hasAllAuthorities(requiredAuthorities)); + } + + @Test + void hasAllAuthortiesWhenEmptyAuthoritiesThenIllegalArgumentException() { + assertThatIllegalArgumentException() + .isThrownBy(() -> AllAuthoritiesAuthorizationManager.hasAllAuthorities((new String[0]))); + } + + @Test + void authorizeWhenGranted() { + Authentication authentication = new TestingAuthenticationToken("user", "password", ROLE_USER); + AllAuthoritiesAuthorizationManager manager = AllAuthoritiesAuthorizationManager.hasAllAuthorities(ROLE_USER); + assertThat(manager.authorize(() -> authentication, "").isGranted()).isTrue(); + } + + @Test + void hasAllRolesAuthorizeWhenGranted() { + Authentication authentication = new TestingAuthenticationToken("user", "password", ROLE_USER); + AllAuthoritiesAuthorizationManager manager = AllAuthoritiesAuthorizationManager.hasAllRoles("USER"); + assertThat(manager.authorize(() -> authentication, "").isGranted()).isTrue(); + } + + @Test + void hasAllPrefixedAuthoritiesAuthorizeWhenGranted() { + String prefix = "PREFIX_"; + String authority1 = "AUTHORITY1"; + String authority2 = "AUTHORITY2"; + Authentication authentication = new TestingAuthenticationToken("user", "password", prefix + authority1, + prefix + authority2); + AllAuthoritiesAuthorizationManager manager = AllAuthoritiesAuthorizationManager + .hasAllPrefixedAuthorities(prefix, authority1, authority2); + assertThat(manager.authorize(() -> authentication, "").isGranted()).isTrue(); + } + + @Test + void authorizeWhenSingleMissingThenDenied() { + Authentication authentication = new TestingAuthenticationToken("user", "password", ROLE_USER); + 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); + AuthorityAuthorizationDecision result = manager.authorize(() -> authentication, ""); + assertThat(result.isGranted()).isFalse(); + assertThat(result.getAuthorities()).hasSize(1); + assertThat(new ArrayList<>(result.getAuthorities()).get(0).getAuthority()).isEqualTo(ROLE_ADMIN); + } + + @Test + void setRoleHierarchyWhenNullThenIllegalArgumentException() { + AllAuthoritiesAuthorizationManager manager = AllAuthoritiesAuthorizationManager.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); + AllAuthoritiesAuthorizationManager manager = AllAuthoritiesAuthorizationManager + .hasAllAuthorities(ROLE_USER); + manager.setRoleHierarchy(this.roleHierarchy); + + Authentication authentication = new TestingAuthenticationToken("user", "password", ROLE_USER); + + AuthorityAuthorizationDecision authz = manager.authorize(() -> authentication, ""); + assertThat(authz.isGranted()).isTrue(); + verify(this.roleHierarchy).getReachableGrantedAuthorities(this.authoritiesCaptor.capture()); + assertThat(this.authoritiesCaptor.getValue()).map(GrantedAuthority::getAuthority).contains(ROLE_USER); + } + +}