diff --git a/core/src/main/java/org/springframework/security/access/hierarchicalroles/RoleHierarchyImpl.java b/core/src/main/java/org/springframework/security/access/hierarchicalroles/RoleHierarchyImpl.java
index e0988445fef..e4a5c77fe50 100755
--- a/core/src/main/java/org/springframework/security/access/hierarchicalroles/RoleHierarchyImpl.java
+++ b/core/src/main/java/org/springframework/security/access/hierarchicalroles/RoleHierarchyImpl.java
@@ -20,6 +20,8 @@
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -30,6 +32,7 @@
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.util.Assert;
/**
*
@@ -99,6 +102,29 @@ public class RoleHierarchyImpl implements RoleHierarchy {
*/
private Map> rolesReachableInOneOrMoreStepsMap = null;
+ /**
+ * Factory method that creates a {@link Builder} instance with the default role prefix
+ * "ROLE_"
+ * @return a {@link Builder} instance with the default role prefix "ROLE_"
+ * @since 6.3
+ */
+ public static Builder withDefaultRolePrefix() {
+ return withRolePrefix("ROLE_");
+ }
+
+ /**
+ * Factory method that creates a {@link Builder} instance with the specified role
+ * prefix.
+ * @param rolePrefix the prefix to be used for the roles in the hierarchy.
+ * @return a new {@link Builder} instance with the specified role prefix
+ * @throws IllegalArgumentException if the provided role prefix is null
+ * @since 6.3
+ */
+ public static Builder withRolePrefix(String rolePrefix) {
+ Assert.notNull(rolePrefix, "rolePrefix must not be null");
+ return new Builder(rolePrefix);
+ }
+
/**
* Set the role hierarchy and pre-calculate for every role the set of all reachable
* roles, i.e. all roles lower in the hierarchy of every given role. Pre-calculation
@@ -213,4 +239,82 @@ else if (roleName.equals(lowerRole.getAuthority())) {
}
+ /**
+ * Builder class for constructing a {@link RoleHierarchyImpl} based on a hierarchical
+ * role structure.
+ *
+ * @author Federico Herrera
+ * @since 6.3
+ */
+ public static final class Builder {
+
+ private final String rolePrefix;
+
+ private final Map> hierarchy;
+
+ private Builder(String rolePrefix) {
+ this.rolePrefix = rolePrefix;
+ this.hierarchy = new LinkedHashMap<>();
+ }
+
+ /**
+ * Creates a new hierarchy branch to define a role and its child roles.
+ * @param role the highest role in this branch
+ * @return a {@link ImpliedRoles} to define the child roles for the
+ * role
+ */
+ public ImpliedRoles role(String role) {
+ Assert.hasText(role, "role must not be empty");
+ return new ImpliedRoles(role);
+ }
+
+ /**
+ * Builds and returns a {@link RoleHierarchyImpl} describing the defined role
+ * hierarchy.
+ * @return a {@link RoleHierarchyImpl}
+ */
+ public RoleHierarchyImpl build() {
+ String roleHierarchyRepresentation = RoleHierarchyUtils.roleHierarchyFromMap(this.hierarchy);
+ RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
+ roleHierarchy.setHierarchy(roleHierarchyRepresentation);
+ return roleHierarchy;
+ }
+
+ private Builder addHierarchy(String role, String... impliedRoles) {
+ List withPrefix = new ArrayList<>();
+ for (String impliedRole : impliedRoles) {
+ withPrefix.add(this.rolePrefix.concat(impliedRole));
+ }
+ this.hierarchy.put(this.rolePrefix.concat(role), withPrefix);
+ return this;
+ }
+
+ /**
+ * Builder class for constructing child roles within a role hierarchy branch.
+ */
+ public final class ImpliedRoles {
+
+ private final String role;
+
+ private ImpliedRoles(String role) {
+ this.role = role;
+ }
+
+ /**
+ * Specifies implied role(s) for the current role in the hierarchy.
+ * @param impliedRoles role name(s) implied by the role.
+ * @return the same {@link Builder} instance
+ * @throws IllegalArgumentException if impliedRoles
is null,
+ * empty or contains any null element.
+ */
+ public Builder implies(String... impliedRoles) {
+ Assert.notEmpty(impliedRoles, "at least one implied role must be provided");
+ Assert.noNullElements(impliedRoles, "implied role name(s) cannot be empty");
+ return Builder.this.addHierarchy(this.role, impliedRoles);
+ }
+
+ }
+
+ }
+
}
diff --git a/core/src/test/java/org/springframework/security/access/hierarchicalroles/RoleHierarchyImplTests.java b/core/src/test/java/org/springframework/security/access/hierarchicalroles/RoleHierarchyImplTests.java
index c95024d6867..977781231ae 100644
--- a/core/src/test/java/org/springframework/security/access/hierarchicalroles/RoleHierarchyImplTests.java
+++ b/core/src/test/java/org/springframework/security/access/hierarchicalroles/RoleHierarchyImplTests.java
@@ -26,6 +26,7 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
+import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.assertThatNoException;
/**
@@ -205,4 +206,63 @@ public void singleLineLargeHierarchy() {
.containsExactlyInAnyOrderElementsOf(allAuthorities);
}
+ @Test
+ public void testBuilderWithDefaultRolePrefix() {
+ RoleHierarchyImpl roleHierarchyImpl = RoleHierarchyImpl.withDefaultRolePrefix()
+ .role("A")
+ .implies("B")
+ .role("B")
+ .implies("C", "D")
+ .build();
+ List flatAuthorities = AuthorityUtils.createAuthorityList("ROLE_A");
+ List allAuthorities = AuthorityUtils.createAuthorityList("ROLE_A", "ROLE_B", "ROLE_C",
+ "ROLE_D");
+
+ assertThat(roleHierarchyImpl).isNotNull();
+ assertThat(roleHierarchyImpl.getReachableGrantedAuthorities(flatAuthorities))
+ .containsExactlyInAnyOrderElementsOf(allAuthorities);
+ }
+
+ @Test
+ public void testBuilderWithRolePrefix() {
+ RoleHierarchyImpl roleHierarchyImpl = RoleHierarchyImpl.withRolePrefix("CUSTOM_PREFIX_")
+ .role("A")
+ .implies("B")
+ .build();
+ List flatAuthorities = AuthorityUtils.createAuthorityList("CUSTOM_PREFIX_A");
+ List allAuthorities = AuthorityUtils.createAuthorityList("CUSTOM_PREFIX_A",
+ "CUSTOM_PREFIX_B");
+
+ assertThat(roleHierarchyImpl).isNotNull();
+ assertThat(roleHierarchyImpl.getReachableGrantedAuthorities(flatAuthorities))
+ .containsExactlyInAnyOrderElementsOf(allAuthorities);
+ }
+
+ @Test
+ public void testBuilderThrowIllegalArgumentExceptionWhenPrefixRoleNull() {
+ assertThatIllegalArgumentException().isThrownBy(() -> RoleHierarchyImpl.withRolePrefix(null));
+ }
+
+ @Test
+ public void testBuilderThrowIllegalArgumentExceptionWhenRoleEmpty() {
+ assertThatIllegalArgumentException().isThrownBy(() -> RoleHierarchyImpl.withDefaultRolePrefix().role(""));
+ }
+
+ @Test
+ public void testBuilderThrowIllegalArgumentExceptionWhenRoleNull() {
+ assertThatIllegalArgumentException().isThrownBy(() -> RoleHierarchyImpl.withDefaultRolePrefix().role(null));
+ }
+
+ @Test
+ public void testBuilderThrowIllegalArgumentExceptionWhenImpliedRolesNull() {
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> RoleHierarchyImpl.withDefaultRolePrefix().role("A").implies((String) null));
+ }
+
+ @Test
+ public void testBuilderThrowIllegalArgumentExceptionWhenImpliedRolesEmpty() {
+ assertThatIllegalArgumentException()
+ .isThrownBy(() -> RoleHierarchyImpl.withDefaultRolePrefix().role("A").implies());
+ }
+
}
diff --git a/docs/modules/ROOT/pages/servlet/authorization/architecture.adoc b/docs/modules/ROOT/pages/servlet/authorization/architecture.adoc
index adda11bb476..b287ecad833 100644
--- a/docs/modules/ROOT/pages/servlet/authorization/architecture.adoc
+++ b/docs/modules/ROOT/pages/servlet/authorization/architecture.adoc
@@ -253,11 +253,10 @@ Java::
----
@Bean
static RoleHierarchy roleHierarchy() {
- RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
- hierarchy.setHierarchy("ROLE_ADMIN > ROLE_STAFF\n" +
- "ROLE_STAFF > ROLE_USER\n" +
- "ROLE_USER > ROLE_GUEST");
- return hierarchy;
+ return RoleHierarchyImpl.withDefaultRolePrefix()
+ .role("ADMIN").implies("STAFF")
+ .role("STAFF").implies("USER")
+ .role("USER").implies("GUEST");
}
// and, if using method security also add