Add AllAuthoritiesAuthorizationManager

Closes gh-17916
This commit is contained in:
Rob Winch 2025-09-16 15:02:56 -05:00
parent fdd2a91b68
commit 096dfd4046
No known key found for this signature in database
3 changed files with 279 additions and 0 deletions

View File

@ -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<T> implements AuthorizationManager<T> {
private static final String ROLE_PREFIX = "ROLE_";
private RoleHierarchy roleHierarchy = new NullRoleHierarchy();
private final List<String> 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<? extends @Nullable Authentication> authentication,
T object) {
List<String> authenticatedAuthorities = getGrantedAuthorities(authentication.get());
List<String> missingAuthorities = new ArrayList<>(this.requiredAuthorities);
missingAuthorities.removeIf(authenticatedAuthorities::contains);
return new AuthorityAuthorizationDecision(missingAuthorities.isEmpty(),
AuthorityUtils.createAuthorityList(missingAuthorities));
}
private List<String> 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 <T> the type of object being authorized
* @return the new instance
*/
public static <T> AllAuthoritiesAuthorizationManager<T> hasAllRoles(String... roles) {
return hasAllPrefixedAuthorities(ROLE_PREFIX, roles);
}
/**
* Creates an instance of {@link AllAuthoritiesAuthorizationManager} with the provided
* authorities.
* @param prefix the prefix for <code>authorities</code>
* @param authorities the authorities to check for prefixed with <code>prefix</code>
* @param <T> the type of object being authorized
* @return the new instance
*/
public static <T> AllAuthoritiesAuthorizationManager<T> 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 <T> the type of object being authorized
* @return the new instance
*/
public static <T> AllAuthoritiesAuthorizationManager<T> 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;
}
}

View File

@ -34,6 +34,7 @@ import org.springframework.util.Assert;
*
* @author Evgeniy Cheban
* @since 6.1
* @see AllAuthoritiesAuthorizationManager
*/
public final class AuthoritiesAuthorizationManager implements AuthorizationManager<Collection<String>> {

View File

@ -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<Collection<? extends GrantedAuthority>> 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<Object> 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);
}
}