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 e0988445fe..30ea1db90e 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,8 +20,11 @@ import java.util.ArrayList;
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;
+import java.util.stream.Stream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -30,6 +33,9 @@ import org.springframework.core.log.LogMessage;
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;
+
+import static org.springframework.security.access.hierarchicalroles.RoleHierarchyUtils.roleHierarchyFromMap;
/**
*
@@ -99,6 +105,35 @@ 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_"
+ */
+ 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
+ */
+ public static Builder withRolePrefix(String rolePrefix) {
+ Assert.notNull(rolePrefix, "rolePrefix must not be null");
+ return new Builder(rolePrefix);
+ }
+
+ /**
+ * Factory method that creates a {@link Builder} instance with no role prefix.
+ * @return a new {@link Builder} instance with no role prefix.
+ */
+ public static Builder withNoRolePrefix() {
+ return withRolePrefix("");
+ }
+
/**
* 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 +248,78 @@ public class RoleHierarchyImpl implements RoleHierarchy {
}
+ /**
+ * 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> roleBranches;
+
+ private Builder(String rolePrefix) {
+ this.rolePrefix = rolePrefix;
+ this.roleBranches = 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 RoleBranchBuilder} to define the child roles for the
+ * role
+ */
+ public RoleBranchBuilder role(String role) {
+ Assert.hasText(role, "role must not be empty");
+ return new RoleBranchBuilder(this, rolePrefix.concat(role));
+ }
+
+ /**
+ * Builds and returns a {@link RoleHierarchyImpl} describing the defined role
+ * hierarchy.
+ * @return a {@link RoleHierarchyImpl}
+ */
+ public RoleHierarchyImpl build() {
+ String roleHierarchyRepresentation = roleHierarchyFromMap(roleBranches);
+ RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
+ roleHierarchy.setHierarchy(roleHierarchyRepresentation);
+ return roleHierarchy;
+ }
+
+ /**
+ * Builder class for constructing child roles within a role hierarchy branch.
+ */
+ public static final class RoleBranchBuilder {
+
+ private final Builder parentBuilder;
+
+ private final String role;
+
+ private RoleBranchBuilder(Builder parentBuilder, String role) {
+ this.parentBuilder = parentBuilder;
+ 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");
+ parentBuilder.roleBranches.put(role,
+ Stream.of(impliedRoles).map(parentBuilder.rolePrefix::concat).toList());
+ return parentBuilder;
+ }
+
+ }
+
+ }
+
}
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 c95024d686..4e03b40d0f 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 org.springframework.security.core.authority.AuthorityUtils;
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,68 @@ public class RoleHierarchyImplTests {
.containsExactlyInAnyOrderElementsOf(allAuthorities);
}
+ @Test
+ public void testBuilderWithDefaultRolePrefix() {
+ RoleHierarchyImpl roleHierarchyImpl = RoleHierarchyImpl.withDefaultRolePrefix().role("A").implies("B").build();
+ List flatAuthorities = AuthorityUtils.createAuthorityList("ROLE_A");
+ List allAuthorities = AuthorityUtils.createAuthorityList("ROLE_A", "ROLE_B");
+
+ 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 testBuilderWithNoRolePrefix() {
+ RoleHierarchyImpl roleHierarchyImpl = RoleHierarchyImpl.withNoRolePrefix().role("A").implies("B").build();
+ List flatAuthorities = AuthorityUtils.createAuthorityList("A");
+ List allAuthorities = AuthorityUtils.createAuthorityList("A", "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());
+ }
+
}