Browse Source

DefaultAuthorizationManagerFactory additionalAuthorization

This commit adds AuthorizationManager<T> additionalAuthorization to
DefaultAuthorizationManagerFactory which can be used for multi factor
authorization.

There is a builder that allows for creating an instance that requires
static additional authorities, but for more advanced cases users can
inject an additionalAuthorization that looks up if the user has settings
that enable additional required authorities.

The builder can later be updated to support checking that a particular
authority was granted within a specified amount of time.

Issue gh-17900
Rob Winch 1 week ago
parent
commit
1608465a38

+ 13 - 0
core/src/main/java/org/springframework/security/authorization/AllAuthoritiesAuthorizationManager.java

@@ -134,6 +134,19 @@ public final class AllAuthoritiesAuthorizationManager<T> implements Authorizatio
 		return new AllAuthoritiesAuthorizationManager<>(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(List<String> authorities) {
+		Assert.notEmpty(authorities, "authorities cannot be empty");
+		Assert.noNullElements(authorities, "authorities cannot contain null values");
+		return new AllAuthoritiesAuthorizationManager<>(authorities.toArray(new String[0]));
+	}
+
 	private static String[] toNamedRolesArray(String rolePrefix, String[] roles) {
 		String[] result = new String[roles.length];
 		for (int i = 0; i < roles.length; i++) {

+ 116 - 50
core/src/main/java/org/springframework/security/authorization/DefaultAuthorizationManagerFactory.java

@@ -16,6 +16,9 @@
 
 package org.springframework.security.authorization;
 
+import java.util.ArrayList;
+import java.util.List;
+
 import org.jspecify.annotations.Nullable;
 
 import org.springframework.security.access.hierarchicalroles.NullRoleHierarchy;
@@ -23,6 +26,7 @@ import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
 import org.springframework.security.authentication.AuthenticationTrustResolver;
 import org.springframework.security.authentication.AuthenticationTrustResolverImpl;
 import org.springframework.util.Assert;
+import org.springframework.util.CollectionUtils;
 
 /**
  * A factory for creating different kinds of {@link AuthorizationManager} instances.
@@ -30,6 +34,7 @@ import org.springframework.util.Assert;
  * @param <T> the type of object that the authorization check is being done on
  * @author Steve Riesenberg
  * @author Andrey Litvitski
+ * @author Rob Winch
  * @since 7.0
  */
 public final class DefaultAuthorizationManagerFactory<T extends @Nullable Object>
@@ -41,7 +46,7 @@ public final class DefaultAuthorizationManagerFactory<T extends @Nullable Object
 
 	private String rolePrefix = "ROLE_";
 
-	private String[] requiredAuthorities = new String[0];
+	private @Nullable AuthorizationManager<T> additionalAuthorization;
 
 	/**
 	 * Sets the {@link AuthenticationTrustResolver} used to check the user's
@@ -73,32 +78,29 @@ public final class DefaultAuthorizationManagerFactory<T extends @Nullable Object
 	}
 
 	/**
-	 * Sets authorities required for authorization managers that apply to authenticated
-	 * users.
-	 * <p>
-	 * Does not affect {@code anonymous}, {@code permitAll}, or {@code denyAll}.
-	 * <p>
-	 * Evaluated with the configured {@link RoleHierarchy}.
-	 * @param requiredAuthorities the required authorities (must not be {@code null})
-	 */
-	public void setRequiredAuthorities(String[] requiredAuthorities) {
-		Assert.notNull(requiredAuthorities, "requiredAuthorities cannot be null");
-		this.requiredAuthorities = requiredAuthorities;
-	}
-
-	/**
-	 * Creates a factory that requires the given authorities for authorization managers
-	 * that apply to authenticated users.
+	 * Sets additional authorization to be applied to the returned
+	 * {@link AuthorizationManager} for the following methods:
+	 *
+	 * <ul>
+	 * <li>{@link #hasRole(String)}</li>
+	 * <li>{@link #hasAnyRole(String...)}</li>
+	 * <li>{@link #hasAllRoles(String...)}</li>
+	 * <li>{@link #hasAuthority(String)}</li>
+	 * <li>{@link #hasAnyAuthority(String...)}</li>
+	 * <li>{@link #hasAllAuthorities(String...)}</li>
+	 * <li>{@link #authenticated()}</li>
+	 * <li>{@link #fullyAuthenticated()}</li>
+	 * <li>{@link #rememberMe()}</li>
+	 * </ul>
+	 *
 	 * <p>
-	 * Does not affect {@code anonymous}, {@code permitAll}, or {@code denyAll}.
-	 * @param authorities the required authorities
-	 * @param <T> the secured object type
-	 * @return a factory configured with the required authorities
+	 * This does not affect {@code anonymous}, {@code permitAll}, or {@code denyAll}.
+	 * </p>
+	 * @param additionalAuthorization the {@link AuthorizationManager} to be applied.
+	 * Default is null (no additional authorization).
 	 */
-	public static <T> AuthorizationManagerFactory<T> withAuthorities(String... authorities) {
-		DefaultAuthorizationManagerFactory<T> factory = new DefaultAuthorizationManagerFactory<>();
-		factory.setRequiredAuthorities(authorities);
-		return factory;
+	public void setAdditionalAuthorization(@Nullable AuthorizationManager<T> additionalAuthorization) {
+		this.additionalAuthorization = additionalAuthorization;
 	}
 
 	@Override
@@ -108,76 +110,140 @@ public final class DefaultAuthorizationManagerFactory<T extends @Nullable Object
 
 	@Override
 	public AuthorizationManager<T> hasAnyRole(String... roles) {
-		return withRequiredAuthorities(
-				withRoleHierarchy(AuthorityAuthorizationManager.hasAnyRole(this.rolePrefix, roles)));
+		return createManager(AuthorityAuthorizationManager.hasAnyRole(this.rolePrefix, roles));
 	}
 
 	@Override
 	public AuthorizationManager<T> hasAllRoles(String... roles) {
-		return withRequiredAuthorities(withRoleHierarchy(
-				AllAuthoritiesAuthorizationManager.hasAllPrefixedAuthorities(this.rolePrefix, roles)));
+		return createManager(AllAuthoritiesAuthorizationManager.hasAllPrefixedAuthorities(this.rolePrefix, roles));
 	}
 
 	@Override
 	public AuthorizationManager<T> hasAuthority(String authority) {
-		return withRequiredAuthorities(withRoleHierarchy(AuthorityAuthorizationManager.hasAuthority(authority)));
+		return createManager(AuthorityAuthorizationManager.hasAuthority(authority));
 	}
 
 	@Override
 	public AuthorizationManager<T> hasAnyAuthority(String... authorities) {
-		return withRequiredAuthorities(withRoleHierarchy(AuthorityAuthorizationManager.hasAnyAuthority(authorities)));
+		return createManager(AuthorityAuthorizationManager.hasAnyAuthority(authorities));
 	}
 
 	@Override
 	public AuthorizationManager<T> hasAllAuthorities(String... authorities) {
-		return withRequiredAuthorities(
-				withRoleHierarchy(AllAuthoritiesAuthorizationManager.hasAllAuthorities(authorities)));
+		return createManager(AllAuthoritiesAuthorizationManager.hasAllAuthorities(authorities));
 	}
 
 	@Override
 	public AuthorizationManager<T> authenticated() {
-		return withRequiredAuthorities(withTrustResolver(AuthenticatedAuthorizationManager.authenticated()));
+		return createManager(AuthenticatedAuthorizationManager.authenticated());
 	}
 
 	@Override
 	public AuthorizationManager<T> fullyAuthenticated() {
-		return withRequiredAuthorities(withTrustResolver(AuthenticatedAuthorizationManager.fullyAuthenticated()));
+		return createManager(AuthenticatedAuthorizationManager.fullyAuthenticated());
 	}
 
 	@Override
 	public AuthorizationManager<T> rememberMe() {
-		return withRequiredAuthorities(withTrustResolver(AuthenticatedAuthorizationManager.rememberMe()));
+		return createManager(AuthenticatedAuthorizationManager.rememberMe());
 	}
 
 	@Override
 	public AuthorizationManager<T> anonymous() {
-		return withTrustResolver(AuthenticatedAuthorizationManager.anonymous());
+		return createManager(AuthenticatedAuthorizationManager.anonymous());
 	}
 
-	private AuthorityAuthorizationManager<T> withRoleHierarchy(AuthorityAuthorizationManager<T> authorizationManager) {
+	/**
+	 * Creates a {@link Builder} that helps build an {@link AuthorizationManager} to set
+	 * on {@link #setAdditionalAuthorization(AuthorizationManager)} for common scenarios.
+	 * <p>
+	 * Does not affect {@code anonymous}, {@code permitAll}, or {@code denyAll}.
+	 * @param <T> the secured object type
+	 * @return a factory configured with the required authorities
+	 */
+	public static <T> Builder<T> builder() {
+		return new Builder<>();
+	}
+
+	private AuthorizationManager<T> createManager(AuthorityAuthorizationManager<T> authorizationManager) {
 		authorizationManager.setRoleHierarchy(this.roleHierarchy);
-		return authorizationManager;
+		return withAdditionalAuthorization(authorizationManager);
 	}
 
-	private AllAuthoritiesAuthorizationManager<T> withRoleHierarchy(
-			AllAuthoritiesAuthorizationManager<T> authorizationManager) {
+	private AuthorizationManager<T> createManager(AllAuthoritiesAuthorizationManager<T> authorizationManager) {
 		authorizationManager.setRoleHierarchy(this.roleHierarchy);
-		return authorizationManager;
+		return withAdditionalAuthorization(authorizationManager);
 	}
 
-	private AuthenticatedAuthorizationManager<T> withTrustResolver(
-			AuthenticatedAuthorizationManager<T> authorizationManager) {
+	private AuthorizationManager<T> createManager(AuthenticatedAuthorizationManager<T> authorizationManager) {
 		authorizationManager.setTrustResolver(this.trustResolver);
-		return authorizationManager;
+		return withAdditionalAuthorization(authorizationManager);
 	}
 
-	private AuthorizationManager<T> withRequiredAuthorities(AuthorizationManager<T> manager) {
-		if (this.requiredAuthorities == null || this.requiredAuthorities.length == 0) {
+	private AuthorizationManager<T> withAdditionalAuthorization(AuthorizationManager<T> manager) {
+		if (this.additionalAuthorization == null) {
 			return manager;
 		}
-		AuthorizationManager<T> required = withRoleHierarchy(
-				AllAuthoritiesAuthorizationManager.hasAllAuthorities(this.requiredAuthorities));
-		return AuthorizationManagers.allOf(new AuthorizationDecision(false), manager, required);
+		return AuthorizationManagers.allOf(new AuthorizationDecision(false), this.additionalAuthorization, manager);
+	}
+
+	/**
+	 * A builder that allows creating {@link DefaultAuthorizationManagerFactory} with
+	 * additional authorization for common scenarios.
+	 *
+	 * @param <T> the type for the {@link DefaultAuthorizationManagerFactory}
+	 * @author Rob Winch
+	 */
+	public static final class Builder<T> {
+
+		private final List<String> additionalAuthorities = new ArrayList<>();
+
+		private RoleHierarchy roleHierarchy = new NullRoleHierarchy();
+
+		/**
+		 * Add additional authorities that will be required.
+		 * @param additionalAuthorities the additional authorities.
+		 * @return the {@link Builder} to further customize.
+		 */
+		public Builder<T> requireAdditionalAuthorities(String... additionalAuthorities) {
+			Assert.notEmpty(additionalAuthorities, "additionalAuthorities cannot be empty");
+			for (String additionalAuthority : additionalAuthorities) {
+				this.additionalAuthorities.add(additionalAuthority);
+			}
+			return this;
+		}
+
+		/**
+		 * The {@link RoleHierarchy} to use.
+		 * @param roleHierarchy the non-null {@link RoleHierarchy} to use. Default is
+		 * {@link NullRoleHierarchy}.
+		 * @return the Builder to further customize.
+		 */
+		public Builder<T> roleHierarchy(RoleHierarchy roleHierarchy) {
+			Assert.notNull(roleHierarchy, "roleHierarchy cannot be null");
+			this.roleHierarchy = roleHierarchy;
+			return this;
+		}
+
+		/**
+		 * Builds a {@link DefaultAuthorizationManagerFactory} that has the
+		 * {@link #setAdditionalAuthorization(AuthorizationManager)} set.
+		 * @return the {@link DefaultAuthorizationManagerFactory}.
+		 */
+		public DefaultAuthorizationManagerFactory<T> build() {
+			Assert.state(!CollectionUtils.isEmpty(this.additionalAuthorities), "additionalAuthorities cannot be empty");
+			DefaultAuthorizationManagerFactory<T> result = new DefaultAuthorizationManagerFactory<>();
+			AllAuthoritiesAuthorizationManager<T> additionalChecks = AllAuthoritiesAuthorizationManager
+				.hasAllAuthorities(this.additionalAuthorities);
+			result.setRoleHierarchy(this.roleHierarchy);
+			additionalChecks.setRoleHierarchy(this.roleHierarchy);
+			result.setAdditionalAuthorization(additionalChecks);
+			return result;
+		}
+
+		private Builder() {
+		}
+
 	}
 
 }

+ 240 - 0
core/src/test/java/org/springframework/security/authorization/AuthorizationManagerFactoryTests.java

@@ -16,9 +16,23 @@
 
 package org.springframework.security.authorization;
 
+import java.util.Collection;
+
 import org.junit.jupiter.api.Test;
 
+import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
+import org.springframework.security.authentication.TestAuthentication;
+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.assertj.core.api.Assertions.assertThatIllegalStateException;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
 
 /**
  * Tests for {@link AuthorizationManagerFactory}.
@@ -111,4 +125,230 @@ public class AuthorizationManagerFactoryTests {
 		assertThat(authorizationManager).isInstanceOf(AuthenticatedAuthorizationManager.class);
 	}
 
+	@Test
+	public void anonymousWhenAdditionalAuthorizationThenNotInvoked() {
+		AuthorizationManager<String> additional = mock(AuthorizationManager.class);
+		DefaultAuthorizationManagerFactory<String> factory = new DefaultAuthorizationManagerFactory<>();
+		factory.setAdditionalAuthorization(additional);
+
+		factory.anonymous();
+
+		verifyNoInteractions(additional);
+	}
+
+	@Test
+	public void permitAllWhenAdditionalAuthorizationThenNotInvoked() {
+		AuthorizationManager<String> additional = mock(AuthorizationManager.class);
+		DefaultAuthorizationManagerFactory<String> factory = new DefaultAuthorizationManagerFactory<>();
+		factory.setAdditionalAuthorization(additional);
+
+		factory.permitAll();
+
+		verifyNoInteractions(additional);
+	}
+
+	@Test
+	public void denyAllAllWhenAdditionalAuthorizationThenNotInvoked() {
+		AuthorizationManager<String> additional = mock(AuthorizationManager.class);
+		DefaultAuthorizationManagerFactory<String> factory = new DefaultAuthorizationManagerFactory<>();
+		factory.setAdditionalAuthorization(additional);
+
+		factory.permitAll();
+
+		verifyNoInteractions(additional);
+	}
+
+	@Test
+	public void hasRoleWhenAdditionalAuthorizationThenInvoked() {
+		AuthorizationManager<String> additional = mock(AuthorizationManager.class);
+		given(additional.authorize(any(), any())).willReturn(new AuthorizationDecision(true),
+				new AuthorizationDecision(false));
+		DefaultAuthorizationManagerFactory<String> factory = new DefaultAuthorizationManagerFactory<>();
+		factory.setAdditionalAuthorization(additional);
+
+		assertUserGranted(factory.hasRole("USER"));
+		assertUserDenied(factory.hasRole("USER"));
+
+		verify(additional, times(2)).authorize(any(), any());
+
+	}
+
+	@Test
+	public void hasAnyRoleWhenAdditionalAuthorizationThenInvoked() {
+		AuthorizationManager<String> additional = mock(AuthorizationManager.class);
+		given(additional.authorize(any(), any())).willReturn(new AuthorizationDecision(true),
+				new AuthorizationDecision(false));
+		DefaultAuthorizationManagerFactory<String> factory = new DefaultAuthorizationManagerFactory<>();
+		factory.setAdditionalAuthorization(additional);
+
+		assertUserGranted(factory.hasAnyRole("USER"));
+		assertUserDenied(factory.hasAnyRole("USER"));
+
+		verify(additional, times(2)).authorize(any(), any());
+
+	}
+
+	@Test
+	public void hasAllRolesWhenAdditionalAuthorizationThenInvoked() {
+		AuthorizationManager<String> additional = mock(AuthorizationManager.class);
+		given(additional.authorize(any(), any())).willReturn(new AuthorizationDecision(true),
+				new AuthorizationDecision(false));
+		DefaultAuthorizationManagerFactory<String> factory = new DefaultAuthorizationManagerFactory<>();
+		factory.setAdditionalAuthorization(additional);
+
+		assertUserGranted(factory.hasAllRoles("USER"));
+		assertUserDenied(factory.hasAllRoles("USER"));
+
+		verify(additional, times(2)).authorize(any(), any());
+
+	}
+
+	@Test
+	public void hasAuthorityWhenAdditionalAuthorizationThenInvoked() {
+		AuthorizationManager<String> additional = mock(AuthorizationManager.class);
+		given(additional.authorize(any(), any())).willReturn(new AuthorizationDecision(true),
+				new AuthorizationDecision(false));
+		DefaultAuthorizationManagerFactory<String> factory = new DefaultAuthorizationManagerFactory<>();
+		factory.setAdditionalAuthorization(additional);
+
+		assertUserGranted(factory.hasAuthority("ROLE_USER"));
+		assertUserDenied(factory.hasAuthority("ROLE_USER"));
+
+		verify(additional, times(2)).authorize(any(), any());
+
+	}
+
+	@Test
+	public void hasAnyAuthorityWhenAdditionalAuthorizationThenInvoked() {
+		AuthorizationManager<String> additional = mock(AuthorizationManager.class);
+		given(additional.authorize(any(), any())).willReturn(new AuthorizationDecision(true),
+				new AuthorizationDecision(false));
+		DefaultAuthorizationManagerFactory<String> factory = new DefaultAuthorizationManagerFactory<>();
+		factory.setAdditionalAuthorization(additional);
+
+		assertUserGranted(factory.hasAnyAuthority("ROLE_USER"));
+		assertUserDenied(factory.hasAnyAuthority("ROLE_USER"));
+
+		verify(additional, times(2)).authorize(any(), any());
+
+	}
+
+	@Test
+	public void hasAllAuthoritiesWhenAdditionalAuthorizationThenInvoked() {
+		AuthorizationManager<String> additional = mock(AuthorizationManager.class);
+		given(additional.authorize(any(), any())).willReturn(new AuthorizationDecision(true),
+				new AuthorizationDecision(false));
+		DefaultAuthorizationManagerFactory<String> factory = new DefaultAuthorizationManagerFactory<>();
+		factory.setAdditionalAuthorization(additional);
+
+		assertUserGranted(factory.hasAllAuthorities("ROLE_USER"));
+		assertUserDenied(factory.hasAllAuthorities("ROLE_USER"));
+
+		verify(additional, times(2)).authorize(any(), any());
+	}
+
+	@Test
+	public void authenticatedWhenAdditionalAuthorizationThenInvoked() {
+		AuthorizationManager<String> additional = mock(AuthorizationManager.class);
+		given(additional.authorize(any(), any())).willReturn(new AuthorizationDecision(true),
+				new AuthorizationDecision(false));
+		DefaultAuthorizationManagerFactory<String> factory = new DefaultAuthorizationManagerFactory<>();
+		factory.setAdditionalAuthorization(additional);
+
+		assertUserGranted(factory.authenticated());
+		assertUserDenied(factory.authenticated());
+
+		verify(additional, times(2)).authorize(any(), any());
+	}
+
+	@Test
+	public void fullyAuthenticatedWhenAdditionalAuthorizationThenInvoked() {
+		AuthorizationManager<String> additional = mock(AuthorizationManager.class);
+		given(additional.authorize(any(), any())).willReturn(new AuthorizationDecision(true),
+				new AuthorizationDecision(false));
+		DefaultAuthorizationManagerFactory<String> factory = new DefaultAuthorizationManagerFactory<>();
+		factory.setAdditionalAuthorization(additional);
+
+		assertUserGranted(factory.fullyAuthenticated());
+		assertUserDenied(factory.fullyAuthenticated());
+
+		verify(additional, times(2)).authorize(any(), any());
+	}
+
+	@Test
+	public void rememberMeWhenAdditionalAuthorizationThenInvoked() {
+		AuthorizationManager<String> additional = mock(AuthorizationManager.class);
+		given(additional.authorize(any(), any())).willReturn(new AuthorizationDecision(true),
+				new AuthorizationDecision(false));
+		DefaultAuthorizationManagerFactory<String> factory = new DefaultAuthorizationManagerFactory<>();
+		factory.setAdditionalAuthorization(additional);
+
+		assertThat(factory.rememberMe().authorize(() -> TestAuthentication.rememberMeUser(), "").isGranted()).isTrue();
+		assertThat(factory.rememberMe().authorize(() -> TestAuthentication.rememberMeUser(), "").isGranted()).isFalse();
+
+		verify(additional, times(2)).authorize(any(), any());
+	}
+
+	@Test
+	public void builderWhenEmptyAdditionalAuthoritiesThenIllegalStateException() {
+		DefaultAuthorizationManagerFactory.Builder<Object> builder = DefaultAuthorizationManagerFactory.builder();
+		assertThatIllegalStateException().isThrownBy(() -> builder.build());
+	}
+
+	@Test
+	public void builderWhenAdditionalAuthorityThenRequired() {
+		AuthorizationManagerFactory<String> factory = DefaultAuthorizationManagerFactory.<String>builder()
+			.requireAdditionalAuthorities("ROLE_ADMIN")
+			.build();
+		assertUserDenied(factory.hasRole("USER"));
+		assertThat(factory.hasRole("USER").authorize(() -> TestAuthentication.authenticatedAdmin(), "").isGranted())
+			.isTrue();
+	}
+
+	@Test
+	public void builderWhenAdditionalAuthoritiesThenRequired() {
+		AuthorizationManagerFactory<String> factory = DefaultAuthorizationManagerFactory.<String>builder()
+			.requireAdditionalAuthorities("ROLE_ADMIN", "ROLE_USER")
+			.build();
+		assertUserDenied(factory.hasRole("USER"));
+		assertThat(factory.hasRole("USER").authorize(() -> TestAuthentication.authenticatedAdmin(), "").isGranted())
+			.isTrue();
+	}
+
+	@Test
+	public void builderWhenNullRoleHierachyThenIllegalArgumentException() {
+		DefaultAuthorizationManagerFactory.Builder<Object> builder = DefaultAuthorizationManagerFactory.builder();
+		assertThatIllegalArgumentException().isThrownBy(() -> builder.roleHierarchy(null));
+	}
+
+	@Test
+	public void builderWhenRoleHierarchyThenUsed() {
+
+		RoleHierarchy roleHierarchy = mock(RoleHierarchy.class);
+		String ROLE_HIERARCHY = "ROLE_HIERARCHY";
+		Collection authorityHierarchy = AuthorityUtils.createAuthorityList(ROLE_HIERARCHY, "ROLE_USER");
+		given(roleHierarchy.getReachableGrantedAuthorities(any())).willReturn(authorityHierarchy);
+		DefaultAuthorizationManagerFactory<String> factory = DefaultAuthorizationManagerFactory.<String>builder()
+			.requireAdditionalAuthorities(ROLE_HIERARCHY)
+			.roleHierarchy(roleHierarchy)
+			.build();
+
+		// ROLE_USER is replaced with the RoleHierarchy (ROLE_USER, ROLE_HIERARCHY)
+		assertUserGranted(factory.hasAuthority("ROLE_USER"));
+		// ROLE_ADMIN is replaced with the RoleHierarchy (ROLE_USER, ROLE_HIERARCHY)
+		assertThat(factory.hasAuthority("ROLE_ADMIN")
+			.authorize(() -> TestAuthentication.authenticatedAdmin(), "")
+			.isGranted()).isFalse();
+
+		verify(roleHierarchy, times(4)).getReachableGrantedAuthorities(any());
+	}
+
+	private void assertUserGranted(AuthorizationManager<String> manager) {
+		assertThat(manager.authorize(() -> TestAuthentication.authenticatedUser(), "").isGranted()).isTrue();
+	}
+
+	private void assertUserDenied(AuthorizationManager<String> manager) {
+		assertThat(manager.authorize(() -> TestAuthentication.authenticatedUser(), "").isGranted()).isFalse();
+	}
+
 }