diff --git a/core/src/main/java/org/springframework/security/access/hierarchicalroles/RoleHierarchyUtils.java b/core/src/main/java/org/springframework/security/access/hierarchicalroles/RoleHierarchyUtils.java new file mode 100644 index 0000000000..ff5ce22e5c --- /dev/null +++ b/core/src/main/java/org/springframework/security/access/hierarchicalroles/RoleHierarchyUtils.java @@ -0,0 +1,98 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.access.hierarchicalroles; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.List; +import java.util.Map; + +/** + * Utility method for working with {@link RoleHierarchy}. + * + * @author Thomas Darimont + * @since + */ +public class RoleHierarchyUtils { + + /** + * Builds a {@link RoleHierarchy} representation from the given {@link Map} of role name to implied roles. + * The map key is the role name and the map value is a {@link List} of implied role names. + * + *

+ * Here is an example configuration of a role hierarchy configured via yaml. + * wich follows the pattern: + * {@code ROLE_NAME: List of implied role names} + *

+ *
+	 * 
+	 *
+	 * security:
+	 *   roles:
+	 *     hierarchy:
+	 *       ROLE_ALL: ROLE_A, ROLE_C
+	 *       ROLE_A: ROLE_B
+	 *       ROLE_B: ROLE_AUTHENTICATED
+	 *       ROLE_C: ROLE_AUTHENTICATED
+	 *       ROLE_AUTHENTICATED: ROLE_UNAUTHENTICATED
+	 * 
+	 * 
+ *

This yaml configuration could then be mapped by the following {@literal ConfigurationProperties}

+ *
+	 * 
+	 *   {@literal @}ConfigurationProperties("security.roles")
+	 *   class SecurityPropertiesExtension {
+	 *     Map> hierarchy = new LinkedHashMap<>();
+	 *
+	 *     //getter | setter
+	 *   }
+	 * 
+	 * 
+ *

To define the role hierarchy just declare a {@link org.springframework.context.annotation.Bean} of + * type {@link RoleHierarchy} as follows:

+ *
+	 * 
+	 *   {@literal @}Bean
+	 *   RoleHierarchy roleHierarchy(SecurityPropertiesExtension spe) {
+	 *     return RoleHierarchyUtils.roleHierarchyFromMap(spe.getHierarchy());
+	 *   }
+	 * 
+	 * 
+ * + * @param roleHierarchyMapping the role name to implied role names mapping + * @return + */ + public static RoleHierarchy roleHierarchyFromMap(Map> roleHierarchyMapping) { + + StringWriter roleHierachyDescriptionBuffer = new StringWriter(); + PrintWriter roleHierarchyDescriptionWriter = new PrintWriter(roleHierachyDescriptionBuffer); + + for (Map.Entry> entry : roleHierarchyMapping.entrySet()) { + + String currentRole = entry.getKey(); + List impliedRoles = entry.getValue(); + + for (String impliedRole : impliedRoles) { + String roleMapping = currentRole + " > " + impliedRole; + roleHierarchyDescriptionWriter.println(roleMapping); + } + } + + RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl(); + roleHierarchy.setHierarchy(roleHierachyDescriptionBuffer.toString()); + return roleHierarchy; + } +} diff --git a/core/src/test/java/org/springframework/security/access/hierarchicalroles/RoleHierarchyUtilsTests.java b/core/src/test/java/org/springframework/security/access/hierarchicalroles/RoleHierarchyUtilsTests.java new file mode 100644 index 0000000000..b90e58f03b --- /dev/null +++ b/core/src/test/java/org/springframework/security/access/hierarchicalroles/RoleHierarchyUtilsTests.java @@ -0,0 +1,266 @@ +/* + * Copyright 2002-2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.security.access.hierarchicalroles; + +import org.junit.Test; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.AuthorityUtils; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.springframework.security.access.hierarchicalroles.RoleHierarchyUtils.roleHierarchyFromMap; + +/** + * Tests for {@link RoleHierarchyUtils}. + * + * Copied from {@link RoleHierarchyImplTests} with adaptations for {@link RoleHierarchyUtils}. + * + * @author Thomas Darimont + */ +public class RoleHierarchyUtilsTests { + + @Test + public void testRoleHierarchyWithNullOrEmptyAuthorities() { + + List authorities0 = null; + List authorities1 = new ArrayList(); + + RoleHierarchy roleHierarchy = roleHierarchyFromMap(singletonMap("ROLE_A", singletonList("ROLE_B"))); + + assertThat(roleHierarchy.getReachableGrantedAuthorities( + authorities0)).isNotNull(); + assertThat( + roleHierarchy.getReachableGrantedAuthorities(authorities0)).isEmpty(); + ; + assertThat(roleHierarchy.getReachableGrantedAuthorities( + authorities1)).isNotNull(); + assertThat( + roleHierarchy.getReachableGrantedAuthorities(authorities1)).isEmpty(); + ; + } + + @Test + public void testSimpleRoleHierarchy() { + + List authorities0 = AuthorityUtils.createAuthorityList( + "ROLE_0"); + List authorities1 = AuthorityUtils.createAuthorityList( + "ROLE_A"); + List authorities2 = AuthorityUtils.createAuthorityList("ROLE_A", + "ROLE_B"); + + RoleHierarchy roleHierarchy = roleHierarchyFromMap(singletonMap("ROLE_A", singletonList("ROLE_B"))); + + assertThat(HierarchicalRolesTestHelper.containTheSameGrantedAuthorities( + roleHierarchy.getReachableGrantedAuthorities(authorities0), + authorities0)).isTrue(); + assertThat(HierarchicalRolesTestHelper.containTheSameGrantedAuthorities( + roleHierarchy.getReachableGrantedAuthorities(authorities1), + authorities2)).isTrue(); + assertThat(HierarchicalRolesTestHelper.containTheSameGrantedAuthorities( + roleHierarchy.getReachableGrantedAuthorities(authorities2), + authorities2)).isTrue(); + } + + @Test + public void testTransitiveRoleHierarchies() { + List authorities1 = AuthorityUtils.createAuthorityList( + "ROLE_A"); + List authorities2 = AuthorityUtils.createAuthorityList("ROLE_A", + "ROLE_B", "ROLE_C"); + List authorities3 = AuthorityUtils.createAuthorityList("ROLE_A", + "ROLE_B", "ROLE_C", "ROLE_D"); + + RoleHierarchy roleHierarchy2Levels = roleHierarchyFromMap(new HashMap>(){ + { + put("ROLE_A", asList("ROLE_B")); + put("ROLE_B", asList("ROLE_C")); + } + }); + + assertThat(HierarchicalRolesTestHelper.containTheSameGrantedAuthorities( + roleHierarchy2Levels.getReachableGrantedAuthorities(authorities1), + authorities2)).isTrue(); + + RoleHierarchy roleHierarchy3Levels = roleHierarchyFromMap(new HashMap>(){ + { + put("ROLE_A", asList("ROLE_B")); + put("ROLE_B", asList("ROLE_C")); + put("ROLE_C", asList("ROLE_D")); + } + }); + + assertThat(HierarchicalRolesTestHelper.containTheSameGrantedAuthorities( + roleHierarchy3Levels.getReachableGrantedAuthorities(authorities1), + authorities3)).isTrue(); + } + + @Test + public void testComplexRoleHierarchy() { + List authoritiesInput1 = AuthorityUtils.createAuthorityList( + "ROLE_A"); + List authoritiesOutput1 = AuthorityUtils.createAuthorityList( + "ROLE_A", "ROLE_B", "ROLE_C", "ROLE_D"); + List authoritiesInput2 = AuthorityUtils.createAuthorityList( + "ROLE_B"); + List authoritiesOutput2 = AuthorityUtils.createAuthorityList( + "ROLE_B", "ROLE_D"); + List authoritiesInput3 = AuthorityUtils.createAuthorityList( + "ROLE_C"); + List authoritiesOutput3 = AuthorityUtils.createAuthorityList( + "ROLE_C", "ROLE_D"); + List authoritiesInput4 = AuthorityUtils.createAuthorityList( + "ROLE_D"); + List authoritiesOutput4 = AuthorityUtils.createAuthorityList( + "ROLE_D"); + + + RoleHierarchy roleHierarchy3LevelsMultipleRoles = roleHierarchyFromMap(new HashMap>(){ + { + put("ROLE_A", asList("ROLE_B","ROLE_C")); + put("ROLE_B", asList("ROLE_D")); + put("ROLE_C", asList("ROLE_D")); + } + }); + + assertThat(HierarchicalRolesTestHelper.containTheSameGrantedAuthorities( + roleHierarchy3LevelsMultipleRoles.getReachableGrantedAuthorities(authoritiesInput1), + authoritiesOutput1)).isTrue(); + assertThat(HierarchicalRolesTestHelper.containTheSameGrantedAuthorities( + roleHierarchy3LevelsMultipleRoles.getReachableGrantedAuthorities(authoritiesInput2), + authoritiesOutput2)).isTrue(); + assertThat(HierarchicalRolesTestHelper.containTheSameGrantedAuthorities( + roleHierarchy3LevelsMultipleRoles.getReachableGrantedAuthorities(authoritiesInput3), + authoritiesOutput3)).isTrue(); + assertThat(HierarchicalRolesTestHelper.containTheSameGrantedAuthorities( + roleHierarchy3LevelsMultipleRoles.getReachableGrantedAuthorities(authoritiesInput4), + authoritiesOutput4)).isTrue(); + } + + @Test + public void testCyclesInRoleHierarchy() { + + try { + roleHierarchyFromMap(singletonMap("ROLE_A", singletonList("ROLE_A"))); + fail("Cycle in role hierarchy was not detected!"); + } + catch (CycleInRoleHierarchyException e) { + } + + try { + + roleHierarchyFromMap(new HashMap>(){ + { + put("ROLE_A", asList("ROLE_B")); + put("ROLE_B", asList("ROLE_A")); + } + }); + + fail("Cycle in role hierarchy was not detected!"); + } + catch (CycleInRoleHierarchyException e) { + } + + try { + + roleHierarchyFromMap(new HashMap>(){ + { + put("ROLE_A", asList("ROLE_B")); + put("ROLE_B", asList("ROLE_C")); + put("ROLE_C", asList("ROLE_A")); + } + }); + + fail("Cycle in role hierarchy was not detected!"); + } + catch (CycleInRoleHierarchyException e) { + } + + try { + + roleHierarchyFromMap(new HashMap>(){ + { + put("ROLE_A", asList("ROLE_B")); + put("ROLE_B", asList("ROLE_C")); + put("ROLE_C", asList("ROLE_E")); + put("ROLE_E", asList("ROLE_D")); + put("ROLE_D", asList("ROLE_B")); + } + }); + + + fail("Cycle in role hierarchy was not detected!"); + } + catch (CycleInRoleHierarchyException e) { + } + } + + @Test + public void testNoCyclesInRoleHierarchy() { + RoleHierarchyImpl roleHierarchyImpl = new RoleHierarchyImpl(); + + try { + roleHierarchyImpl.setHierarchy( + "ROLE_A > ROLE_B\nROLE_A > ROLE_C\nROLE_C > ROLE_D\nROLE_B > ROLE_D"); + + roleHierarchyFromMap(new HashMap>(){ + { + put("ROLE_A", asList("ROLE_B")); + put("ROLE_A", asList("ROLE_C")); + put("ROLE_C", asList("ROLE_D")); + put("ROLE_B", asList("ROLE_D")); + } + }); + + } + catch (CycleInRoleHierarchyException e) { + fail("A cycle in role hierarchy was incorrectly detected!"); + } + } + + @Test + public void testSimpleRoleHierarchyWithCustomGrantedAuthorityImplementation() { + + List authorities0 = HierarchicalRolesTestHelper.createAuthorityList( + "ROLE_0"); + List authorities1 = HierarchicalRolesTestHelper.createAuthorityList( + "ROLE_A"); + List authorities2 = HierarchicalRolesTestHelper.createAuthorityList( + "ROLE_A", "ROLE_B"); + + RoleHierarchy roleHierarchy = roleHierarchyFromMap(singletonMap("ROLE_A", singletonList("ROLE_B"))); + + assertThat( + HierarchicalRolesTestHelper.containTheSameGrantedAuthoritiesCompareByAuthorityString( + roleHierarchy.getReachableGrantedAuthorities(authorities0), + authorities0)).isTrue(); + assertThat( + HierarchicalRolesTestHelper.containTheSameGrantedAuthoritiesCompareByAuthorityString( + roleHierarchy.getReachableGrantedAuthorities(authorities1), + authorities2)).isTrue(); + assertThat( + HierarchicalRolesTestHelper.containTheSameGrantedAuthoritiesCompareByAuthorityString( + roleHierarchy.getReachableGrantedAuthorities(authorities2), + authorities2)).isTrue(); + } +}