/* * Copyright 2016 Red Hat, Inc. and/or its affiliates * and other contributors as indicated by the @author tags. * * 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.keycloak.federation.ldap.mappers.membership.group; import java.util.Arrays; import java.util.Collection; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; /** * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> */ public class GroupTreeResolver { /** * Fully resolves list of group trees to be used in Keycloak. The input is group info (usually from LDAP) where each "Group" object contains * just it's name and direct children. * * The operation also performs validation as rules for LDAP are less strict than for Keycloak (In LDAP, the recursion is possible and multiple parents of single group is also allowed) * * @param groups * @return * @throws GroupTreeResolveException */ public List<GroupTreeEntry> resolveGroupTree(List<Group> groups) throws GroupTreeResolveException { // 1- Get parents of each group Map<String, List<String>> parentsTree = getParentsTree(groups); // 2 - Get rootGroups (groups without parent) and check if there is no group with multiple parents List<String> rootGroups = new LinkedList<>(); for (Map.Entry<String, List<String>> group : parentsTree.entrySet()) { int parentCount = group.getValue().size(); if (parentCount == 0) { rootGroups.add(group.getKey()); } else if (parentCount > 1) { throw new GroupTreeResolveException("Group '" + group.getKey() + "' detected to have multiple parents. This is not allowed in Keycloak. Parents are: " + group.getValue()); } } // 3 - Just convert to map for easier retrieval Map<String, Group> asMap = new TreeMap<>(); for (Group group : groups) { asMap.put(group.getGroupName(), group); } // 4 - Now we have rootGroups. Let's resolve them List<GroupTreeEntry> finalResult = new LinkedList<>(); Set<String> visitedGroups = new TreeSet<>(); for (String rootGroupName : rootGroups) { List<String> subtree = new LinkedList<>(); subtree.add(rootGroupName); GroupTreeEntry groupTree = resolveGroupTree(rootGroupName, asMap, visitedGroups, subtree); finalResult.add(groupTree); } // 5 - Check recursion if (visitedGroups.size() != asMap.size()) { // Recursion detected. Try to find where it is for (Map.Entry<String, Group> entry : asMap.entrySet()) { String groupName = entry.getKey(); if (!visitedGroups.contains(groupName)) { List<String> subtree = new LinkedList<>(); subtree.add(groupName); Set<String> newVisitedGroups = new TreeSet<>(); resolveGroupTree(groupName, asMap, newVisitedGroups, subtree); visitedGroups.addAll(newVisitedGroups); } } // Shouldn't happen throw new GroupTreeResolveException("Illegal state: Recursion detected, but wasn't able to find it"); } return finalResult; } private Map<String, List<String>> getParentsTree(List<Group> groups) throws GroupTreeResolveException { Map<String, List<String>> result = new TreeMap<>(); for (Group group : groups) { result.put(group.getGroupName(), new LinkedList<String>()); } for (Group group : groups) { for (String child : group.getChildrenNames()) { List<String> list = result.get(child); if (list == null) { throw new GroupTreeResolveException("Group '" + child + "' referenced as member of group '" + group.getGroupName() + "' doesn't exists"); } list.add(group.getGroupName()); } } return result; } private GroupTreeEntry resolveGroupTree(String groupName, Map<String, Group> asMap, Set<String> visitedGroups, List<String> currentSubtree) throws GroupTreeResolveException { if (visitedGroups.contains(groupName)) { throw new GroupTreeResolveException("Recursion detected when trying to resolve group '" + groupName + "'. Whole recursion path: " + currentSubtree); } visitedGroups.add(groupName); Group group = asMap.get(groupName); List<GroupTreeEntry> children = new LinkedList<>(); GroupTreeEntry result = new GroupTreeEntry(group.getGroupName(), children); for (String childrenName : group.getChildrenNames()) { List<String> subtreeCopy = new LinkedList<>(currentSubtree); subtreeCopy.add(childrenName); GroupTreeEntry childEntry = resolveGroupTree(childrenName, asMap, visitedGroups, subtreeCopy); children.add(childEntry); } return result; } // static classes public static class GroupTreeResolveException extends Exception { public GroupTreeResolveException(String message) { super(message); } } public static class Group { private final String groupName; private final List<String> childrenNames; public Group(String groupName, String... childrenNames) { this(groupName, Arrays.asList(childrenNames)); } public Group(String groupName, Collection<String> childrenNames) { this.groupName = groupName; this.childrenNames = new LinkedList<>(childrenNames); } public String getGroupName() { return groupName; } public List<String> getChildrenNames() { return childrenNames; } } public static class GroupTreeEntry { private final String groupName; private final List<GroupTreeEntry> children; public GroupTreeEntry(String groupName, List<GroupTreeEntry> children) { this.groupName = groupName; this.children = children; } public String getGroupName() { return groupName; } public List<GroupTreeEntry> getChildren() { return children; } @Override public String toString() { StringBuilder builder = new StringBuilder("{ " + groupName + " -> [ "); for (GroupTreeEntry child : children) { builder.append(child.toString()); } builder.append(" ]}"); return builder.toString(); } } }