From 91a5a728607e30582e538b781fb50267c2208be2 Mon Sep 17 00:00:00 2001 From: Vyom Mani Tiwari Date: Wed, 21 Oct 2026 13:30:30 +0530 Subject: [PATCH] RANGER-5850: Validate nested role membership for cycles and avoid recursive role expansion in RoleREST --- .../org/apache/ranger/biz/RoleDBStore.java | 118 ++++++++++++++++ .../java/org/apache/ranger/rest/RoleREST.java | 108 +++++++++++---- .../apache/ranger/biz/TestRoleDBStore.java | 130 ++++++++++++++++++ .../org/apache/ranger/rest/TestRoleREST.java | 61 ++++++++ 4 files changed, 389 insertions(+), 28 deletions(-) diff --git a/security-admin/src/main/java/org/apache/ranger/biz/RoleDBStore.java b/security-admin/src/main/java/org/apache/ranger/biz/RoleDBStore.java index 4d06bfe67bd..d87def36069 100644 --- a/security-admin/src/main/java/org/apache/ranger/biz/RoleDBStore.java +++ b/security-admin/src/main/java/org/apache/ranger/biz/RoleDBStore.java @@ -51,9 +51,12 @@ import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; +import javax.ws.rs.WebApplicationException; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collections; +import java.util.Deque; import java.util.HashSet; import java.util.List; import java.util.Set; @@ -113,6 +116,8 @@ public RangerRole createRole(RangerRole role, Boolean createNonExistUserGroupRol throw restErrorUtil.createRESTException("role with name: " + role.getName() + " already exists", MessageEnums.ERROR_DUPLICATE_OBJECT); } + ensureRoleNotNestedInItself(role); + Runnable roleVersionUpdater = new RoleVersionUpdater(daoMgr); transactionSynchronizationAdapter.executeOnTransactionCommit(roleVersionUpdater); @@ -149,6 +154,8 @@ public RangerRole updateRole(RangerRole role, Boolean createNonExistUserGroupRol oldRole = JsonUtils.jsonToObject(xxRole.getRoleText(), RangerRole.class); } + ensureNoRoleNestingCycle(role, xxRole, oldRole); + Runnable roleVersionUpdater = new RoleVersionUpdater(daoMgr); transactionSynchronizationAdapter.executeOnTransactionCommit(roleVersionUpdater); @@ -454,6 +461,117 @@ private void ensureRoleNameUpdateAllowed(String roleName) throws Exception { } } + /** + * Rejects a new role whose nested roles contain it, directly or transitively. + * + * The ref table can't be used here: when an earlier save referenced a not-yet-existing role with + * createNonExistUserGroupRole=true (e.g. earlier in the same import), its x_role_ref_role row is only written + * after commit, so that edge exists only in the stored role JSON. So walk the new role's nested roles downwards + * using their stored definitions. Each role is loaded at most once; a role with no nested roles costs nothing. + */ + private void ensureRoleNotNestedInItself(RangerRole role) { + Set visited = new HashSet<>(); + Deque pending = new ArrayDeque<>(); + + for (RangerRole.RoleMember member : role.getRoles()) { + if (member != null && StringUtils.isNotEmpty(member.getName())) { + pending.add(member.getName()); + } + } + + while (!pending.isEmpty()) { + String nestedRoleName = pending.poll(); + + if (StringUtils.equals(nestedRoleName, role.getName())) { + throw createNestingCycleException(role.getName()); + } + + if (!visited.add(nestedRoleName)) { + continue; + } + + XXRole xxNestedRole = daoMgr.getXXRole().findByRoleName(nestedRoleName); + + if (xxNestedRole == null) { // will be created empty + continue; + } + + RangerRole nestedRole = roleService.read(xxNestedRole.getId()); + + if (nestedRole != null) { + for (RangerRole.RoleMember member : nestedRole.getRoles()) { + if (member != null && StringUtils.isNotEmpty(member.getName())) { + pending.add(member.getName()); + } + } + } + } + } + + /** + * Rejects an update that would make the role contain itself, directly or through nested roles. + * + * Only nested roles added by this update are checked: an edge role -> N closes a cycle iff N is the role + * itself or one of its ancestors. Ancestors are found by walking x_role_ref_role upwards by id (the same walk + * updatePolicyVersions() already performs on every update), so no role JSON is loaded. Updates that do not add + * a nested role (e.g. user/group membership changes) do no extra DB work. + */ + private void ensureNoRoleNestingCycle(RangerRole role, XXRole xxRole, RangerRole oldRole) { + Set addedRoleNames = new HashSet<>(); + + for (RangerRole.RoleMember member : role.getRoles()) { + if (member != null && StringUtils.isNotEmpty(member.getName())) { + addedRoleNames.add(member.getName()); + } + } + + if (oldRole != null) { + for (RangerRole.RoleMember member : oldRole.getRoles()) { + if (member != null) { + addedRoleNames.remove(member.getName()); + } + } + } + + if (addedRoleNames.isEmpty()) { + return; + } + + if (addedRoleNames.contains(role.getName()) || addedRoleNames.contains(xxRole.getName())) { + throw createNestingCycleException(role.getName()); + } + + // roles that don't exist yet are created empty, so they can't close a cycle + Set addedRoleIds = new HashSet<>(daoMgr.getXXRole().getIdsByRoleNames(addedRoleNames).values()); + + if (addedRoleIds.isEmpty()) { + return; + } + + Set visited = new HashSet<>(); + Deque pending = new ArrayDeque<>(); + + pending.add(xxRole.getId()); + + while (!pending.isEmpty()) { + Long roleId = pending.poll(); + + if (addedRoleIds.contains(roleId)) { + throw createNestingCycleException(role.getName()); + } + + if (visited.add(roleId)) { + pending.addAll(daoMgr.getXXRoleRefRole().getContainingRoles(roleId)); + } + } + } + + private WebApplicationException createNestingCycleException(String roleName) { + LOG.error("Role {} can not be saved: nested roles would make it contain itself", roleName); + + return restErrorUtil.createRESTException("Invalid role members for role " + roleName + ": a role can not contain itself, directly or through nested roles", MessageEnums.INVALID_INPUT_DATA); + } + private void ensureRoleDeleteAllowed(String roleName) throws Exception { boolean roleNotInPolicy = ensureRoleNotInPolicy(roleName); diff --git a/security-admin/src/main/java/org/apache/ranger/rest/RoleREST.java b/security-admin/src/main/java/org/apache/ranger/rest/RoleREST.java index 5df0c7ee524..cdc4aea265d 100644 --- a/security-admin/src/main/java/org/apache/ranger/rest/RoleREST.java +++ b/security-admin/src/main/java/org/apache/ranger/rest/RoleREST.java @@ -93,12 +93,16 @@ import java.io.IOException; import java.io.InputStream; +import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Set; @Path("roles") @@ -1007,15 +1011,16 @@ public List getUserRoles(@PathParam("user") String userName, @Context Ht Set roleList = roleStore.getRoleNames(userName, userMgr.getGroupsForUser(userName)); + // one visited set across all of the user's roles: a nested role shared by several of them is loaded once + Set nestedRoleNames = new HashSet<>(); + for (RangerRole role : roleList) { ret.add(role.getName()); - Set roleMembers = new HashSet<>(); - - getRoleMemberNames(roleMembers, role); - - ret.addAll(roleMembers); + getRoleMemberNames(nestedRoleNames, role); } + + ret.addAll(nestedRoleNames); } catch (WebApplicationException excp) { throw excp; } catch (Throwable excp) { @@ -1407,15 +1412,18 @@ private boolean ensureRoleAccess(String username, Set userGroups, Ranger } Set roleMemberList = new HashSet<>(); - - getRoleMembers(roleMemberList, role); + Map loadedRoles = getRoleMembers(roleMemberList, role); for (RangerRole.RoleMember roleMember : roleMemberList) { if (!roleMember.getIsAdmin()) { continue; } - RangerRole roleMemberObj = roleStore.getRole(roleMember.getName()); + RangerRole roleMemberObj = loadedRoles.get(roleMember.getName()); // already loaded while expanding + + if (roleMemberObj == null) { // nested role doesn't exist (yet) + continue; + } if (getUserNames(roleMemberObj).contains(username)) { isAccessible = true; @@ -1447,19 +1455,7 @@ private RangerRole addUsersGroupsAndRoles(RangerRole role, Set users, Se } try { - // Check for any cycling relationship between roles - for (String newRole : roles) { - //get members recursively and check if the grantor role is already a member - Set roleMembers = new HashSet<>(); - - getRoleMemberNames(roleMembers, roleStore.getRole(newRole)); - - LOG.debug("Role members for {} = {}", newRole, roleMembers); - - if (roleMembers.contains(role.getName())) { - throw new Exception("Invalid role grant"); - } - } + // nesting cycles, including granting a role into itself, are rejected by RoleDBStore.updateRole() Set roleUsers = new HashSet<>(); Set roleGroups = new HashSet<>(); @@ -1678,19 +1674,75 @@ private Set getRoleNames(RangerRole role) { return rolenames; } + /* + * Adds names of all roles nested, directly or transitively, under the given role. Iterative with a visited + * set: each role is loaded at most once and a cyclic role graph terminates instead of overflowing the stack. + */ private void getRoleMemberNames(Set roleMembers, RangerRole role) throws Exception { - for (RangerRole.RoleMember roleMember : role.getRoles()) { - roleMembers.add(roleMember.getName()); + Deque pending = new ArrayDeque<>(); + + pending.add(role); + + while (!pending.isEmpty()) { + RangerRole current = pending.poll(); + + for (RangerRole.RoleMember roleMember : current.getRoles()) { + if (roleMembers.add(roleMember.getName())) { + RangerRole memberRole = getNestedRoleIfExists(roleMember.getName()); + + if (memberRole != null) { + pending.add(memberRole); + } + } + } + } + } + + /* + * Adds all role members nested, directly or transitively, under the given role, and returns the nested roles + * loaded along the way (by name). Iterative: each nested role is loaded at most once, so a cyclic role graph + * terminates instead of overflowing the stack. + */ + private Map getRoleMembers(Set roleMembers, RangerRole role) throws Exception { + Map loaded = new HashMap<>(); + Deque pending = new ArrayDeque<>(); + + pending.add(role); + + while (!pending.isEmpty()) { + RangerRole current = pending.poll(); + + for (RangerRole.RoleMember roleMember : current.getRoles()) { + roleMembers.add(roleMember); + + if (!loaded.containsKey(roleMember.getName())) { + RangerRole memberRole = getNestedRoleIfExists(roleMember.getName()); - getRoleMemberNames(roleMembers, roleStore.getRole(roleMember.getName())); + loaded.put(roleMember.getName(), memberRole); + + if (memberRole != null) { + pending.add(memberRole); + } + } + } } + + return loaded; } - private void getRoleMembers(Set roleMembers, RangerRole role) throws Exception { - for (RangerRole.RoleMember roleMember : role.getRoles()) { - roleMembers.add(roleMember); + /* + * A nested role named in a stored role definition may not exist yet, e.g. when it is created after commit by + * createNonExistUserGroupRole. Skip it instead of failing the whole read. + * RoleDBStore.getRole(String) throws WebApplicationException only when the role is not found (by name, or by id + * in roleService.read() if deleted in between); DB failures surface as persistence exceptions and still propagate. + */ + private RangerRole getNestedRoleIfExists(String roleName) { + try { + return roleStore.getRole(roleName); + } catch (WebApplicationException excp) { + LOG.warn("Nested role {} does not exist; skipping it", roleName); - getRoleMembers(roleMembers, roleStore.getRole(roleMember.getName())); + return null; } } diff --git a/security-admin/src/test/java/org/apache/ranger/biz/TestRoleDBStore.java b/security-admin/src/test/java/org/apache/ranger/biz/TestRoleDBStore.java index 9548c14f5d9..694d59b4bdf 100644 --- a/security-admin/src/test/java/org/apache/ranger/biz/TestRoleDBStore.java +++ b/security-admin/src/test/java/org/apache/ranger/biz/TestRoleDBStore.java @@ -18,6 +18,7 @@ package org.apache.ranger.biz; import org.apache.ranger.authorization.hadoop.config.RangerAdminConfig; +import org.apache.ranger.authorization.utils.JsonUtils; import org.apache.ranger.common.ContextUtil; import org.apache.ranger.common.RESTErrorUtil; import org.apache.ranger.common.RangerConstants; @@ -592,6 +593,135 @@ public void testInitStore_setsPredicateUtilAndPopulateFlag() { } } + @Test + public void testCreateRole_selfNestingRejected() throws Exception { + RangerRole rangerRole = getRangerRole(); + XXRoleDao xxRoleDao = Mockito.mock(XXRoleDao.class); + + rangerRole.setRoles(Collections.singletonList(new RoleMember(roleName, false))); + + Mockito.when(daoMgr.getXXRole()).thenReturn(xxRoleDao); + Mockito.when(xxRoleDao.findByRoleName(roleName)).thenReturn(null); + Mockito.when(restErrorUtil.createRESTException(Mockito.anyString(), Mockito.any())).thenReturn(new WebApplicationException()); + + Assertions.assertThrows(WebApplicationException.class, () -> roleDBStore.createRole(rangerRole, false, false)); + + Mockito.verify(roleService, Mockito.never()).create(Mockito.any()); + } + + @Test + public void testCreateRole_nestedRoleAlreadyContainingNewRoleRejected() throws Exception { + // roleB (stored, e.g. earlier in the same import) lists test-role, whose ref row is still pending; + // creating test-role containing roleB must be rejected from the stored definitions + RangerRole rangerRole = getRangerRole(); + XXRole xxRoleB = new XXRole(); + XXRoleDao xxRoleDao = Mockito.mock(XXRoleDao.class); + + xxRoleB.setId(20L); + xxRoleB.setName("roleB"); + rangerRole.setRoles(Collections.singletonList(new RoleMember("roleB", false))); + + Mockito.when(daoMgr.getXXRole()).thenReturn(xxRoleDao); + Mockito.when(xxRoleDao.findByRoleName(roleName)).thenReturn(null); + Mockito.when(xxRoleDao.findByRoleName("roleB")).thenReturn(xxRoleB); + Mockito.when(roleService.read(20L)).thenReturn(new RangerRole("roleB", "roleB", null, null, null, Collections.singletonList(new RoleMember(roleName, false)))); + Mockito.when(restErrorUtil.createRESTException(Mockito.anyString(), Mockito.any())).thenReturn(new WebApplicationException()); + + Assertions.assertThrows(WebApplicationException.class, () -> roleDBStore.createRole(rangerRole, false, false)); + + Mockito.verify(roleService, Mockito.never()).create(Mockito.any()); + } + + @Test + public void testUpdateRole_selfNestingRejectedWithoutGraphWalk() throws Exception { + RangerRole rangerRole = getRangerRole(); + XXRoleDao xxRoleDao = Mockito.mock(XXRoleDao.class); + + rangerRole.setRoles(Collections.singletonList(new RoleMember(roleName, false))); + + Mockito.when(daoMgr.getXXRole()).thenReturn(xxRoleDao); + Mockito.when(xxRoleDao.findByRoleId(rangerRole.getId())).thenReturn(getTestRole()); + Mockito.when(restErrorUtil.createRESTException(Mockito.anyString(), Mockito.any())).thenReturn(new WebApplicationException()); + + Assertions.assertThrows(WebApplicationException.class, () -> roleDBStore.updateRole(rangerRole, false, true)); + + Mockito.verify(xxRoleDao, Mockito.never()).getIdsByRoleNames(Mockito.any()); + Mockito.verify(roleService, Mockito.never()).update(Mockito.any()); + } + + @Test + public void testUpdateRole_addingAncestorAsNestedRoleRejected() throws Exception { + // stored: roleB (id 20) contains test-role (id 9). Update adds roleB into test-role -> cycle. + RangerRole rangerRole = getRangerRole(); + XXRoleDao xxRoleDao = Mockito.mock(XXRoleDao.class); + XXRoleRefRoleDao xxRoleRefRoleDao = Mockito.mock(XXRoleRefRoleDao.class); + + rangerRole.setRoles(Collections.singletonList(new RoleMember("roleB", false))); + + Mockito.when(daoMgr.getXXRole()).thenReturn(xxRoleDao); + Mockito.when(daoMgr.getXXRoleRefRole()).thenReturn(xxRoleRefRoleDao); + Mockito.when(xxRoleDao.findByRoleId(rangerRole.getId())).thenReturn(getTestRole()); + Mockito.when(xxRoleDao.getIdsByRoleNames(Collections.singleton("roleB"))).thenReturn(Collections.singletonMap("roleB", 20L)); + Mockito.when(xxRoleRefRoleDao.getContainingRoles(roleId)).thenReturn(Collections.singleton(20L)); + Mockito.when(restErrorUtil.createRESTException(Mockito.anyString(), Mockito.any())).thenReturn(new WebApplicationException()); + + Assertions.assertThrows(WebApplicationException.class, () -> roleDBStore.updateRole(rangerRole, false, true)); + + Mockito.verify(roleService, Mockito.never()).update(Mockito.any()); + } + + @Test + public void testUpdateRole_unchangedNestedRolesSkipCycleCheck() throws Exception { + // user/group-only updates must not pay for a graph walk + RangerRole rangerRole = getRangerRole(); + XXRole xxRole = getTestRole(); + XXRoleDao xxRoleDao = Mockito.mock(XXRoleDao.class); + + rangerRole.setRoles(Collections.singletonList(new RoleMember("roleB", false))); + xxRole.setRoleText(JsonUtils.objectToJson(rangerRole)); + + Mockito.when(daoMgr.getXXRole()).thenReturn(xxRoleDao); + Mockito.when(xxRoleDao.findByRoleId(rangerRole.getId())).thenReturn(xxRole); + Mockito.doNothing().when(transactionSynchronizationAdapter).executeOnTransactionCommit(Mockito.any()); + Mockito.when(roleService.update(rangerRole)).thenReturn(rangerRole); + Mockito.doNothing().when(roleRefUpdater).createNewRoleMappingForRefTable(Mockito.any(), Mockito.anyBoolean(), Mockito.anyBoolean()); + Mockito.doNothing().when(roleService).updatePolicyVersions(rangerRole.getId()); + Mockito.doNothing().when(roleService).createTransactionLog(Mockito.any(), Mockito.any(), Mockito.anyInt()); + + roleDBStore.updateRole(rangerRole, false, true); + + Mockito.verify(xxRoleDao, Mockito.never()).getIdsByRoleNames(Mockito.any()); + Mockito.verify(daoMgr, Mockito.never()).getXXRoleRefRole(); + } + + @Test + public void testUpdateRole_preexistingAncestorCycleTerminatesAndSucceeds() throws Exception { + // ancestors: 9 -> {30}, 30 -> {31}, 31 -> {30} (existing cycle above test-role); added roleC (id 20) is unrelated + RangerRole rangerRole = getRangerRole(); + XXRoleDao xxRoleDao = Mockito.mock(XXRoleDao.class); + XXRoleRefRoleDao xxRoleRefRoleDao = Mockito.mock(XXRoleRefRoleDao.class); + + rangerRole.setRoles(Collections.singletonList(new RoleMember("roleC", false))); + + Mockito.when(daoMgr.getXXRole()).thenReturn(xxRoleDao); + Mockito.when(daoMgr.getXXRoleRefRole()).thenReturn(xxRoleRefRoleDao); + Mockito.when(xxRoleDao.findByRoleId(rangerRole.getId())).thenReturn(getTestRole()); + Mockito.when(xxRoleDao.getIdsByRoleNames(Collections.singleton("roleC"))).thenReturn(Collections.singletonMap("roleC", 20L)); + Mockito.when(xxRoleRefRoleDao.getContainingRoles(roleId)).thenReturn(Collections.singleton(30L)); + Mockito.when(xxRoleRefRoleDao.getContainingRoles(30L)).thenReturn(Collections.singleton(31L)); + Mockito.when(xxRoleRefRoleDao.getContainingRoles(31L)).thenReturn(Collections.singleton(30L)); + Mockito.doNothing().when(transactionSynchronizationAdapter).executeOnTransactionCommit(Mockito.any()); + Mockito.when(roleService.update(rangerRole)).thenReturn(rangerRole); + Mockito.doNothing().when(roleRefUpdater).createNewRoleMappingForRefTable(Mockito.any(), Mockito.anyBoolean(), Mockito.anyBoolean()); + Mockito.doNothing().when(roleService).updatePolicyVersions(rangerRole.getId()); + Mockito.doNothing().when(roleService).createTransactionLog(Mockito.any(), Mockito.any(), Mockito.anyInt()); + + Assertions.assertEquals(rangerRole, roleDBStore.updateRole(rangerRole, false, true)); + + Mockito.verify(xxRoleRefRoleDao, Mockito.times(1)).getContainingRoles(30L); + Mockito.verify(xxRoleRefRoleDao, Mockito.times(1)).getContainingRoles(31L); + } + private XXRole getTestRole() { return new XXRole() {{ setId(TestRoleDBStore.roleId); diff --git a/security-admin/src/test/java/org/apache/ranger/rest/TestRoleREST.java b/security-admin/src/test/java/org/apache/ranger/rest/TestRoleREST.java index 39790f7fbaf..3012abf2942 100644 --- a/security-admin/src/test/java/org/apache/ranger/rest/TestRoleREST.java +++ b/security-admin/src/test/java/org/apache/ranger/rest/TestRoleREST.java @@ -1623,6 +1623,67 @@ public void test22deleteRoleWithValidationError() { }); } + @Test + public void test36GetRoleByIdCyclicRoleDeniedWithoutRecursion() throws Exception { + // roleA <-> roleB cycle; a non-member, non-admin caller must get a clean denial, not a StackOverflowError + RangerRole roleA = createNestingRole("roleA", "roleB", false); + RangerRole roleB = createNestingRole("roleB", "roleA", false); + + roleA.setId(roleId); + + Mockito.when(bizUtil.isUserRangerAdmin(Mockito.anyString())).thenReturn(false); + Mockito.when(roleStore.getRole(roleId)).thenReturn(roleA); + Mockito.when(roleStore.getRole("roleA")).thenReturn(roleA); + Mockito.when(roleStore.getRole("roleB")).thenReturn(roleB); + Mockito.when(restErrorUtil.createRESTException(Mockito.anyString())).thenReturn(new WebApplicationException()); + + Assertions.assertThrows(WebApplicationException.class, () -> roleRest.getRole(roleId)); + + // each nested role is loaded at most once + Mockito.verify(roleStore, Mockito.atMost(1)).getRole("roleA"); + Mockito.verify(roleStore, Mockito.atMost(1)).getRole("roleB"); + } + + @Test + public void test37GetUserRolesCyclicRoleTerminates() throws Exception { + RangerRole roleA = createNestingRole("roleA", "roleB", false); + RangerRole roleB = createNestingRole("roleB", "roleA", false); + + Mockito.when(xUserService.getXUserByUserName(Mockito.anyString())).thenReturn(createVXUser()); + Mockito.when(roleStore.getRoleNames(Mockito.anyString(), Mockito.any())).thenReturn(new HashSet<>(Collections.singletonList(roleA))); + Mockito.when(roleStore.getRole("roleA")).thenReturn(roleA); + Mockito.when(roleStore.getRole("roleB")).thenReturn(roleB); + + List returnedRoles = roleRest.getUserRoles(adminLoginID, Mockito.mock(HttpServletRequest.class)); + + Assertions.assertEquals(new HashSet<>(Arrays.asList("roleA", "roleB")), new HashSet<>(returnedRoles)); + Mockito.verify(roleStore, Mockito.times(1)).getRole("roleA"); + Mockito.verify(roleStore, Mockito.times(1)).getRole("roleB"); + } + + @Test + public void test38MissingNestedRoleSkippedOnRead() throws Exception { + // roleA lists "pending" (not created yet) with admin option and roleB; reads must skip it, not fail + List members = new ArrayList<>(Arrays.asList(new RangerRole.RoleMember("pending", true), new RangerRole.RoleMember("roleB", true))); + RangerRole roleA = new RangerRole("roleA", "roleA", null, new ArrayList<>(), new ArrayList<>(), members); + RangerRole roleB = createRangerRole(adminLoginID, false); + + roleA.setId(roleId); + + Mockito.when(bizUtil.isUserRangerAdmin(Mockito.anyString())).thenReturn(false); + Mockito.when(roleStore.getRole(roleId)).thenReturn(roleA); + Mockito.when(roleStore.getRole("pending")).thenThrow(new WebApplicationException()); + Mockito.when(roleStore.getRole("roleB")).thenReturn(roleB); + + Assertions.assertEquals(roleA, roleRest.getRole(roleId)); + } + + private RangerRole createNestingRole(String name, String nestedRoleName, boolean isAdmin) { + List roles = new ArrayList<>(Collections.singletonList(new RangerRole.RoleMember(nestedRoleName, isAdmin))); + + return new RangerRole(name, name, null, new ArrayList<>(), new ArrayList<>(), roles); + } + private RangerRole createRole() { String name = "test-role"; String name2 = "admin";