Browse Source

Add DefaultAuthorizationManagerFactory.additionalAuthorization

Rob Winch 1 week ago
parent
commit
549569ea55

+ 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++) {

+ 127 - 17
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,12 +26,15 @@ 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.
  *
  * @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>
@@ -40,6 +46,8 @@ public final class DefaultAuthorizationManagerFactory<T extends @Nullable Object
 
 	private String rolePrefix = "ROLE_";
 
+	private @Nullable AuthorizationManager<T> additionalAuthorization;
+
 	/**
 	 * Sets the {@link AuthenticationTrustResolver} used to check the user's
 	 * authentication.
@@ -69,6 +77,32 @@ public final class DefaultAuthorizationManagerFactory<T extends @Nullable Object
 		this.rolePrefix = rolePrefix;
 	}
 
+	/**
+	 * 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>
+	 * 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 void setAdditionalAuthorization(@Nullable AuthorizationManager<T> additionalAuthorization) {
+		this.additionalAuthorization = additionalAuthorization;
+	}
+
 	@Override
 	public AuthorizationManager<T> hasRole(String role) {
 		return hasAnyRole(role);
@@ -76,64 +110,140 @@ public final class DefaultAuthorizationManagerFactory<T extends @Nullable Object
 
 	@Override
 	public AuthorizationManager<T> hasAnyRole(String... roles) {
-		return withRoleHierarchy(AuthorityAuthorizationManager.hasAnyRole(this.rolePrefix, roles));
+		return createManager(AuthorityAuthorizationManager.hasAnyRole(this.rolePrefix, roles));
 	}
 
 	@Override
 	public AuthorizationManager<T> hasAllRoles(String... roles) {
-		return withRoleHierarchy(AllAuthoritiesAuthorizationManager.hasAllPrefixedAuthorities(this.rolePrefix, roles));
+		return createManager(AllAuthoritiesAuthorizationManager.hasAllPrefixedAuthorities(this.rolePrefix, roles));
 	}
 
 	@Override
 	public AuthorizationManager<T> hasAuthority(String authority) {
-		return withRoleHierarchy(AuthorityAuthorizationManager.hasAuthority(authority));
+		return createManager(AuthorityAuthorizationManager.hasAuthority(authority));
 	}
 
 	@Override
 	public AuthorizationManager<T> hasAnyAuthority(String... authorities) {
-		return withRoleHierarchy(AuthorityAuthorizationManager.hasAnyAuthority(authorities));
+		return createManager(AuthorityAuthorizationManager.hasAnyAuthority(authorities));
 	}
 
 	@Override
 	public AuthorizationManager<T> hasAllAuthorities(String... authorities) {
-		return withRoleHierarchy(AllAuthoritiesAuthorizationManager.hasAllAuthorities(authorities));
+		return createManager(AllAuthoritiesAuthorizationManager.hasAllAuthorities(authorities));
 	}
 
 	@Override
 	public AuthorizationManager<T> authenticated() {
-		return withTrustResolver(AuthenticatedAuthorizationManager.authenticated());
+		return createManager(AuthenticatedAuthorizationManager.authenticated());
 	}
 
 	@Override
 	public AuthorizationManager<T> fullyAuthenticated() {
-		return withTrustResolver(AuthenticatedAuthorizationManager.fullyAuthenticated());
+		return createManager(AuthenticatedAuthorizationManager.fullyAuthenticated());
 	}
 
 	@Override
 	public AuthorizationManager<T> rememberMe() {
-		return 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> withAdditionalAuthorization(AuthorizationManager<T> manager) {
+		if (this.additionalAuthorization == null) {
+			return manager;
+		}
+		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();
+	}
+
 }