浏览代码

Add RoleHierarchyImpl.Builder

Closes gh-13300
Federico Herrera 1 年之前
父节点
当前提交
7d366242ce

+ 109 - 0
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;
 
 /**
  * <p>
@@ -99,6 +105,35 @@ public class RoleHierarchyImpl implements RoleHierarchy {
 	 */
 	private Map<String, Set<GrantedAuthority>> 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<String, List<String>> 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
+		 * <code>role</code>
+		 */
+		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 <code>impliedRoles</code> 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;
+			}
+
+		}
+
+	}
+
 }

+ 65 - 0
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<GrantedAuthority> flatAuthorities = AuthorityUtils.createAuthorityList("ROLE_A");
+		List<GrantedAuthority> 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<GrantedAuthority> flatAuthorities = AuthorityUtils.createAuthorityList("CUSTOM_PREFIX_A");
+		List<GrantedAuthority> 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<GrantedAuthority> flatAuthorities = AuthorityUtils.createAuthorityList("A");
+		List<GrantedAuthority> 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());
+	}
+
 }