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 c78d7fa50c..badf55e719 100644 --- a/core/src/main/java/org/springframework/security/authorization/AllAuthoritiesAuthorizationManager.java +++ b/core/src/main/java/org/springframework/security/authorization/AllAuthoritiesAuthorizationManager.java @@ -134,6 +134,19 @@ public static AllAuthoritiesAuthorizationManager hasAllAuthorities(String return new AllAuthoritiesAuthorizationManager<>(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(List 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++) { diff --git a/core/src/main/java/org/springframework/security/authorization/DefaultAuthorizationManagerFactory.java b/core/src/main/java/org/springframework/security/authorization/DefaultAuthorizationManagerFactory.java index 09ad052a95..9b4892b556 100644 --- a/core/src/main/java/org/springframework/security/authorization/DefaultAuthorizationManagerFactory.java +++ b/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.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 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 @@ -40,6 +46,8 @@ public final class DefaultAuthorizationManagerFactory additionalAuthorization; + /** * Sets the {@link AuthenticationTrustResolver} used to check the user's * authentication. @@ -69,6 +77,32 @@ public void setRolePrefix(String rolePrefix) { this.rolePrefix = rolePrefix; } + /** + * Sets additional authorization to be applied to the returned + * {@link AuthorizationManager} for the following methods: + * + *
    + *
  • {@link #hasRole(String)}
  • + *
  • {@link #hasAnyRole(String...)}
  • + *
  • {@link #hasAllRoles(String...)}
  • + *
  • {@link #hasAuthority(String)}
  • + *
  • {@link #hasAnyAuthority(String...)}
  • + *
  • {@link #hasAllAuthorities(String...)}
  • + *
  • {@link #authenticated()}
  • + *
  • {@link #fullyAuthenticated()}
  • + *
  • {@link #rememberMe()}
  • + *
+ * + *

+ * This does not affect {@code anonymous}, {@code permitAll}, or {@code denyAll}. + *

+ * @param additionalAuthorization the {@link AuthorizationManager} to be applied. + * Default is null (no additional authorization). + */ + public void setAdditionalAuthorization(@Nullable AuthorizationManager additionalAuthorization) { + this.additionalAuthorization = additionalAuthorization; + } + @Override public AuthorizationManager hasRole(String role) { return hasAnyRole(role); @@ -76,64 +110,140 @@ public AuthorizationManager hasRole(String role) { @Override public AuthorizationManager hasAnyRole(String... roles) { - return withRoleHierarchy(AuthorityAuthorizationManager.hasAnyRole(this.rolePrefix, roles)); + return createManager(AuthorityAuthorizationManager.hasAnyRole(this.rolePrefix, roles)); } @Override public AuthorizationManager hasAllRoles(String... roles) { - return withRoleHierarchy(AllAuthoritiesAuthorizationManager.hasAllPrefixedAuthorities(this.rolePrefix, roles)); + return createManager(AllAuthoritiesAuthorizationManager.hasAllPrefixedAuthorities(this.rolePrefix, roles)); } @Override public AuthorizationManager hasAuthority(String authority) { - return withRoleHierarchy(AuthorityAuthorizationManager.hasAuthority(authority)); + return createManager(AuthorityAuthorizationManager.hasAuthority(authority)); } @Override public AuthorizationManager hasAnyAuthority(String... authorities) { - return withRoleHierarchy(AuthorityAuthorizationManager.hasAnyAuthority(authorities)); + return createManager(AuthorityAuthorizationManager.hasAnyAuthority(authorities)); } @Override public AuthorizationManager hasAllAuthorities(String... authorities) { - return withRoleHierarchy(AllAuthoritiesAuthorizationManager.hasAllAuthorities(authorities)); + return createManager(AllAuthoritiesAuthorizationManager.hasAllAuthorities(authorities)); } @Override public AuthorizationManager authenticated() { - return withTrustResolver(AuthenticatedAuthorizationManager.authenticated()); + return createManager(AuthenticatedAuthorizationManager.authenticated()); } @Override public AuthorizationManager fullyAuthenticated() { - return withTrustResolver(AuthenticatedAuthorizationManager.fullyAuthenticated()); + return createManager(AuthenticatedAuthorizationManager.fullyAuthenticated()); } @Override public AuthorizationManager rememberMe() { - return withTrustResolver(AuthenticatedAuthorizationManager.rememberMe()); + return createManager(AuthenticatedAuthorizationManager.rememberMe()); } @Override public AuthorizationManager anonymous() { - return withTrustResolver(AuthenticatedAuthorizationManager.anonymous()); + return createManager(AuthenticatedAuthorizationManager.anonymous()); } - private AuthorityAuthorizationManager withRoleHierarchy(AuthorityAuthorizationManager authorizationManager) { + /** + * Creates a {@link Builder} that helps build an {@link AuthorizationManager} to set + * on {@link #setAdditionalAuthorization(AuthorizationManager)} for common scenarios. + *

+ * Does not affect {@code anonymous}, {@code permitAll}, or {@code denyAll}. + * @param the secured object type + * @return a factory configured with the required authorities + */ + public static Builder builder() { + return new Builder<>(); + } + + private AuthorizationManager createManager(AuthorityAuthorizationManager authorizationManager) { authorizationManager.setRoleHierarchy(this.roleHierarchy); - return authorizationManager; + return withAdditionalAuthorization(authorizationManager); } - private AllAuthoritiesAuthorizationManager withRoleHierarchy( - AllAuthoritiesAuthorizationManager authorizationManager) { + private AuthorizationManager createManager(AllAuthoritiesAuthorizationManager authorizationManager) { authorizationManager.setRoleHierarchy(this.roleHierarchy); - return authorizationManager; + return withAdditionalAuthorization(authorizationManager); } - private AuthenticatedAuthorizationManager withTrustResolver( - AuthenticatedAuthorizationManager authorizationManager) { + private AuthorizationManager createManager(AuthenticatedAuthorizationManager authorizationManager) { authorizationManager.setTrustResolver(this.trustResolver); - return authorizationManager; + return withAdditionalAuthorization(authorizationManager); + } + + private AuthorizationManager withAdditionalAuthorization(AuthorizationManager 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 the type for the {@link DefaultAuthorizationManagerFactory} + * @author Rob Winch + */ + public static final class Builder { + + private final List 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 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 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 build() { + Assert.state(!CollectionUtils.isEmpty(this.additionalAuthorities), "additionalAuthorities cannot be empty"); + DefaultAuthorizationManagerFactory result = new DefaultAuthorizationManagerFactory<>(); + AllAuthoritiesAuthorizationManager additionalChecks = AllAuthoritiesAuthorizationManager + .hasAllAuthorities(this.additionalAuthorities); + result.setRoleHierarchy(this.roleHierarchy); + additionalChecks.setRoleHierarchy(this.roleHierarchy); + result.setAdditionalAuthorization(additionalChecks); + return result; + } + + private Builder() { + } + } } diff --git a/core/src/test/java/org/springframework/security/authorization/AuthorizationManagerFactoryTests.java b/core/src/test/java/org/springframework/security/authorization/AuthorizationManagerFactoryTests.java index c24c3f5982..cfbb3d4d33 100644 --- a/core/src/test/java/org/springframework/security/authorization/AuthorizationManagerFactoryTests.java +++ b/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 void anonymousReturnsAuthenticatedAuthorizationManagerByDefault() { assertThat(authorizationManager).isInstanceOf(AuthenticatedAuthorizationManager.class); } + @Test + public void anonymousWhenAdditionalAuthorizationThenNotInvoked() { + AuthorizationManager additional = mock(AuthorizationManager.class); + DefaultAuthorizationManagerFactory factory = new DefaultAuthorizationManagerFactory<>(); + factory.setAdditionalAuthorization(additional); + + factory.anonymous(); + + verifyNoInteractions(additional); + } + + @Test + public void permitAllWhenAdditionalAuthorizationThenNotInvoked() { + AuthorizationManager additional = mock(AuthorizationManager.class); + DefaultAuthorizationManagerFactory factory = new DefaultAuthorizationManagerFactory<>(); + factory.setAdditionalAuthorization(additional); + + factory.permitAll(); + + verifyNoInteractions(additional); + } + + @Test + public void denyAllAllWhenAdditionalAuthorizationThenNotInvoked() { + AuthorizationManager additional = mock(AuthorizationManager.class); + DefaultAuthorizationManagerFactory factory = new DefaultAuthorizationManagerFactory<>(); + factory.setAdditionalAuthorization(additional); + + factory.permitAll(); + + verifyNoInteractions(additional); + } + + @Test + public void hasRoleWhenAdditionalAuthorizationThenInvoked() { + AuthorizationManager additional = mock(AuthorizationManager.class); + given(additional.authorize(any(), any())).willReturn(new AuthorizationDecision(true), + new AuthorizationDecision(false)); + DefaultAuthorizationManagerFactory 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 additional = mock(AuthorizationManager.class); + given(additional.authorize(any(), any())).willReturn(new AuthorizationDecision(true), + new AuthorizationDecision(false)); + DefaultAuthorizationManagerFactory 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 additional = mock(AuthorizationManager.class); + given(additional.authorize(any(), any())).willReturn(new AuthorizationDecision(true), + new AuthorizationDecision(false)); + DefaultAuthorizationManagerFactory 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 additional = mock(AuthorizationManager.class); + given(additional.authorize(any(), any())).willReturn(new AuthorizationDecision(true), + new AuthorizationDecision(false)); + DefaultAuthorizationManagerFactory 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 additional = mock(AuthorizationManager.class); + given(additional.authorize(any(), any())).willReturn(new AuthorizationDecision(true), + new AuthorizationDecision(false)); + DefaultAuthorizationManagerFactory 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 additional = mock(AuthorizationManager.class); + given(additional.authorize(any(), any())).willReturn(new AuthorizationDecision(true), + new AuthorizationDecision(false)); + DefaultAuthorizationManagerFactory 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 additional = mock(AuthorizationManager.class); + given(additional.authorize(any(), any())).willReturn(new AuthorizationDecision(true), + new AuthorizationDecision(false)); + DefaultAuthorizationManagerFactory factory = new DefaultAuthorizationManagerFactory<>(); + factory.setAdditionalAuthorization(additional); + + assertUserGranted(factory.authenticated()); + assertUserDenied(factory.authenticated()); + + verify(additional, times(2)).authorize(any(), any()); + } + + @Test + public void fullyAuthenticatedWhenAdditionalAuthorizationThenInvoked() { + AuthorizationManager additional = mock(AuthorizationManager.class); + given(additional.authorize(any(), any())).willReturn(new AuthorizationDecision(true), + new AuthorizationDecision(false)); + DefaultAuthorizationManagerFactory factory = new DefaultAuthorizationManagerFactory<>(); + factory.setAdditionalAuthorization(additional); + + assertUserGranted(factory.fullyAuthenticated()); + assertUserDenied(factory.fullyAuthenticated()); + + verify(additional, times(2)).authorize(any(), any()); + } + + @Test + public void rememberMeWhenAdditionalAuthorizationThenInvoked() { + AuthorizationManager additional = mock(AuthorizationManager.class); + given(additional.authorize(any(), any())).willReturn(new AuthorizationDecision(true), + new AuthorizationDecision(false)); + DefaultAuthorizationManagerFactory 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 builder = DefaultAuthorizationManagerFactory.builder(); + assertThatIllegalStateException().isThrownBy(() -> builder.build()); + } + + @Test + public void builderWhenAdditionalAuthorityThenRequired() { + AuthorizationManagerFactory factory = DefaultAuthorizationManagerFactory.builder() + .requireAdditionalAuthorities("ROLE_ADMIN") + .build(); + assertUserDenied(factory.hasRole("USER")); + assertThat(factory.hasRole("USER").authorize(() -> TestAuthentication.authenticatedAdmin(), "").isGranted()) + .isTrue(); + } + + @Test + public void builderWhenAdditionalAuthoritiesThenRequired() { + AuthorizationManagerFactory factory = DefaultAuthorizationManagerFactory.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 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 factory = DefaultAuthorizationManagerFactory.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 manager) { + assertThat(manager.authorize(() -> TestAuthentication.authenticatedUser(), "").isGranted()).isTrue(); + } + + private void assertUserDenied(AuthorizationManager manager) { + assertThat(manager.authorize(() -> TestAuthentication.authenticatedUser(), "").isGranted()).isFalse(); + } + }