From 68742e170cc56b1ca6406da42c48d1872609f67a Mon Sep 17 00:00:00 2001 From: Andrey Litvitski Date: Mon, 22 Sep 2025 00:15:13 +0300 Subject: [PATCH 1/2] Support Automatically Checking for Required Authorities in Authorization Rules Closes: gh-17900 Signed-off-by: Andrey Litvitski --- .../DefaultAuthorizationManagerFactory.java | 60 ++++++++++++++++--- 1 file changed, 52 insertions(+), 8 deletions(-) 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..d9c21b9ead 100644 --- a/core/src/main/java/org/springframework/security/authorization/DefaultAuthorizationManagerFactory.java +++ b/core/src/main/java/org/springframework/security/authorization/DefaultAuthorizationManagerFactory.java @@ -29,6 +29,7 @@ * * @param the type of object that the authorization check is being done on * @author Steve Riesenberg + * @author Andrey Litvitski * @since 7.0 */ public final class DefaultAuthorizationManagerFactory @@ -40,6 +41,8 @@ public final class DefaultAuthorizationManagerFactory + * Does not affect {@code anonymous}, {@code permitAll}, or {@code denyAll}. + *

+ * 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. + *

+ * Does not affect {@code anonymous}, {@code permitAll}, or {@code denyAll}. + * @param authorities the required authorities + * @param the secured object type + * @return a factory configured with the required authorities + */ + public static AuthorizationManagerFactory withAuthorities(String... authorities) { + DefaultAuthorizationManagerFactory factory = new DefaultAuthorizationManagerFactory<>(); + factory.setRequiredAuthorities(authorities); + return factory; + } + @Override public AuthorizationManager hasRole(String role) { return hasAnyRole(role); @@ -76,42 +108,45 @@ public AuthorizationManager hasRole(String role) { @Override public AuthorizationManager hasAnyRole(String... roles) { - return withRoleHierarchy(AuthorityAuthorizationManager.hasAnyRole(this.rolePrefix, roles)); + return withRequiredAuthorities( + withRoleHierarchy(AuthorityAuthorizationManager.hasAnyRole(this.rolePrefix, roles))); } @Override public AuthorizationManager hasAllRoles(String... roles) { - return withRoleHierarchy(AllAuthoritiesAuthorizationManager.hasAllPrefixedAuthorities(this.rolePrefix, roles)); + return withRequiredAuthorities(withRoleHierarchy( + AllAuthoritiesAuthorizationManager.hasAllPrefixedAuthorities(this.rolePrefix, roles))); } @Override public AuthorizationManager hasAuthority(String authority) { - return withRoleHierarchy(AuthorityAuthorizationManager.hasAuthority(authority)); + return withRequiredAuthorities(withRoleHierarchy(AuthorityAuthorizationManager.hasAuthority(authority))); } @Override public AuthorizationManager hasAnyAuthority(String... authorities) { - return withRoleHierarchy(AuthorityAuthorizationManager.hasAnyAuthority(authorities)); + return withRequiredAuthorities(withRoleHierarchy(AuthorityAuthorizationManager.hasAnyAuthority(authorities))); } @Override public AuthorizationManager hasAllAuthorities(String... authorities) { - return withRoleHierarchy(AllAuthoritiesAuthorizationManager.hasAllAuthorities(authorities)); + return withRequiredAuthorities( + withRoleHierarchy(AllAuthoritiesAuthorizationManager.hasAllAuthorities(authorities))); } @Override public AuthorizationManager authenticated() { - return withTrustResolver(AuthenticatedAuthorizationManager.authenticated()); + return withRequiredAuthorities(withTrustResolver(AuthenticatedAuthorizationManager.authenticated())); } @Override public AuthorizationManager fullyAuthenticated() { - return withTrustResolver(AuthenticatedAuthorizationManager.fullyAuthenticated()); + return withRequiredAuthorities(withTrustResolver(AuthenticatedAuthorizationManager.fullyAuthenticated())); } @Override public AuthorizationManager rememberMe() { - return withTrustResolver(AuthenticatedAuthorizationManager.rememberMe()); + return withRequiredAuthorities(withTrustResolver(AuthenticatedAuthorizationManager.rememberMe())); } @Override @@ -136,4 +171,13 @@ private AuthenticatedAuthorizationManager withTrustResolver( return authorizationManager; } + private AuthorizationManager withRequiredAuthorities(AuthorizationManager manager) { + if (this.requiredAuthorities == null || this.requiredAuthorities.length == 0) { + return manager; + } + AuthorizationManager required = withRoleHierarchy( + AllAuthoritiesAuthorizationManager.hasAllAuthorities(this.requiredAuthorities)); + return AuthorizationManagers.allOf(new AuthorizationDecision(false), manager, required); + } + } From 1608465a38ec93551b920a29f653333cddd06cd4 Mon Sep 17 00:00:00 2001 From: Rob Winch <362503+rwinch@users.noreply.github.com> Date: Tue, 23 Sep 2025 14:49:09 -0500 Subject: [PATCH 2/2] DefaultAuthorizationManagerFactory additionalAuthorization This commit adds AuthorizationManager 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 --- .../AllAuthoritiesAuthorizationManager.java | 13 + .../DefaultAuthorizationManagerFactory.java | 166 ++++++++---- .../AuthorizationManagerFactoryTests.java | 240 ++++++++++++++++++ 3 files changed, 369 insertions(+), 50 deletions(-) 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 d9c21b9ead..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,6 +26,7 @@ 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 @@ * @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 @@ -41,7 +46,7 @@ public final class DefaultAuthorizationManagerFactory additionalAuthorization; /** * Sets the {@link AuthenticationTrustResolver} used to check the user's @@ -73,32 +78,29 @@ public void setRolePrefix(String rolePrefix) { } /** - * Sets authorities required for authorization managers that apply to authenticated - * users. - *

- * Does not affect {@code anonymous}, {@code permitAll}, or {@code denyAll}. - *

- * 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: + * + *

    + *
  • {@link #hasRole(String)}
  • + *
  • {@link #hasAnyRole(String...)}
  • + *
  • {@link #hasAllRoles(String...)}
  • + *
  • {@link #hasAuthority(String)}
  • + *
  • {@link #hasAnyAuthority(String...)}
  • + *
  • {@link #hasAllAuthorities(String...)}
  • + *
  • {@link #authenticated()}
  • + *
  • {@link #fullyAuthenticated()}
  • + *
  • {@link #rememberMe()}
  • + *
+ * *

- * Does not affect {@code anonymous}, {@code permitAll}, or {@code denyAll}. - * @param authorities the required authorities - * @param the secured object type - * @return a factory configured with the required authorities + * 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 static AuthorizationManagerFactory withAuthorities(String... authorities) { - DefaultAuthorizationManagerFactory factory = new DefaultAuthorizationManagerFactory<>(); - factory.setRequiredAuthorities(authorities); - return factory; + public void setAdditionalAuthorization(@Nullable AuthorizationManager additionalAuthorization) { + this.additionalAuthorization = additionalAuthorization; } @Override @@ -108,76 +110,140 @@ public AuthorizationManager hasRole(String role) { @Override public AuthorizationManager hasAnyRole(String... roles) { - return withRequiredAuthorities( - withRoleHierarchy(AuthorityAuthorizationManager.hasAnyRole(this.rolePrefix, roles))); + return createManager(AuthorityAuthorizationManager.hasAnyRole(this.rolePrefix, roles)); } @Override public AuthorizationManager hasAllRoles(String... roles) { - return withRequiredAuthorities(withRoleHierarchy( - AllAuthoritiesAuthorizationManager.hasAllPrefixedAuthorities(this.rolePrefix, roles))); + return createManager(AllAuthoritiesAuthorizationManager.hasAllPrefixedAuthorities(this.rolePrefix, roles)); } @Override public AuthorizationManager hasAuthority(String authority) { - return withRequiredAuthorities(withRoleHierarchy(AuthorityAuthorizationManager.hasAuthority(authority))); + return createManager(AuthorityAuthorizationManager.hasAuthority(authority)); } @Override public AuthorizationManager hasAnyAuthority(String... authorities) { - return withRequiredAuthorities(withRoleHierarchy(AuthorityAuthorizationManager.hasAnyAuthority(authorities))); + return createManager(AuthorityAuthorizationManager.hasAnyAuthority(authorities)); } @Override public AuthorizationManager hasAllAuthorities(String... authorities) { - return withRequiredAuthorities( - withRoleHierarchy(AllAuthoritiesAuthorizationManager.hasAllAuthorities(authorities))); + return createManager(AllAuthoritiesAuthorizationManager.hasAllAuthorities(authorities)); } @Override public AuthorizationManager authenticated() { - return withRequiredAuthorities(withTrustResolver(AuthenticatedAuthorizationManager.authenticated())); + return createManager(AuthenticatedAuthorizationManager.authenticated()); } @Override public AuthorizationManager fullyAuthenticated() { - return withRequiredAuthorities(withTrustResolver(AuthenticatedAuthorizationManager.fullyAuthenticated())); + return createManager(AuthenticatedAuthorizationManager.fullyAuthenticated()); } @Override public AuthorizationManager rememberMe() { - return withRequiredAuthorities(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 withRequiredAuthorities(AuthorizationManager manager) { - if (this.requiredAuthorities == null || this.requiredAuthorities.length == 0) { + private AuthorizationManager withAdditionalAuthorization(AuthorizationManager manager) { + if (this.additionalAuthorization == null) { return manager; } - AuthorizationManager 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 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(); + } + }