# -*- coding: utf-8 -*- # vim:set expandtab tabstop=4 shiftwidth=4: # # The MIT License (MIT) # LdapCherry # Copyright (c) 2014 Carpentier Pierre-Francois import os import sys import copy from ldapcherry.pyyamlwrapper import loadNoDump from ldapcherry.pyyamlwrapper import DumplicatedKey from ldapcherry.exceptions import * import yaml if sys.version < '3': from sets import Set as set class CustomDumper(yaml.SafeDumper): "A custom YAML dumper that never emits aliases" def ignore_aliases(self, _data): return True class Roles: def __init__(self, role_file): self.role_file = role_file self.backends = set([]) try: stream = open(role_file, 'r') except Exception as e: raise MissingRolesFile(role_file) try: self.roles_raw = loadNoDump(stream) except DumplicatedKey as e: raise DumplicateRoleKey(e.key) stream.close() self.graph = {} self.roles = {} self.flatten = {} self.group2roles = {} self.admin_roles = [] self._nest() def _merge_groups(self, backends_list): """ merge a list backends_groups""" ret = {} for backends in backends_list: for b in backends: if b not in ret: ret[b] = set([]) for group in backends[b]: ret[b].add(group) for b in ret: ret[b] = list(ret[b]) ret[b].sort() return ret def _flatten(self, roles=None, groups=None): """ flatten a (semi) nest roles structure""" if roles is None: roles_in = copy.deepcopy(self.roles_raw) else: roles_in = roles for roleid in roles_in: role = roles_in[roleid] if groups is not None: role['backends_groups'] = self._merge_groups( [role['backends_groups'], groups], ) if 'subroles' in role: self._flatten( role['subroles'], role['backends_groups'], ) del role['subroles'] self.flatten[roleid] = role def _set_admin(self, role): for r in role['subroles']: self.admin_roles.append(r) self._set_admin(role['subroles'][r]) def _is_parent(self, roleid1, roleid2): """Test if roleid1 is contained inside roleid2""" role2 = copy.deepcopy(self.flatten[roleid2]) role1 = copy.deepcopy(self.flatten[roleid1]) if role1 == role2: return False # Check if role1 is contained by role2 for b1 in role1['backends_groups']: if b1 not in role2['backends_groups']: return False for group in role1['backends_groups'][b1]: if group not in role2['backends_groups'][b1]: return False # If role2 is inside role1, roles are equal, throw exception for b2 in role2['backends_groups']: if b2 not in role1['backends_groups']: return True for group in role2['backends_groups'][b2]: if group not in role1['backends_groups'][b2]: return True raise DumplicateRoleContent(roleid1, roleid2) def _nest(self): """nests the roles (creates roles hierarchy)""" self._flatten() parent_roles = {} for roleid in self.flatten: role = copy.deepcopy(self.flatten[roleid]) # Display name is mandatory if 'display_name' not in role: raise MissingKey('display_name', role, self.role_file) if 'description' not in role: raise MissingKey('description', role, self.role_file) # Backend is mandatory if 'backends_groups' not in role: raise MissingKey('backends_groups', role, self.role_file) # Create the list of backends for backend in role['backends_groups']: self.backends.add(backend) if roleid not in self.graph: self.graph[roleid] = { 'parent_roles': set([]), 'sub_roles': set([]) } # Create the nested groups for roleid in self.flatten: role = copy.deepcopy(self.flatten[roleid]) # create reverse groups 2 roles for b in role['backends_groups']: for g in role['backends_groups'][b]: if b not in self.group2roles: self.group2roles[b] = {} if g not in self.group2roles[b]: self.group2roles[b][g] = set([]) self.group2roles[b][g].add(roleid) parent_roles[roleid] = [] for roleid2 in self.flatten: role2 = copy.deepcopy(self.flatten[roleid2]) if self._is_parent(roleid, roleid2): parent_roles[roleid].append(roleid2) self.graph[roleid2]['parent_roles'].add(roleid) self.graph[roleid]['sub_roles'].add(roleid2) for r in parent_roles: for p in parent_roles[r]: for p2 in parent_roles[r]: if p != p2 and p in parent_roles[p2]: parent_roles[r].remove(p) def nest(p): ret = copy.deepcopy(self.flatten[p]) ret['subroles'] = {} if len(parent_roles[p]) == 0: return ret else: for i in parent_roles[p]: sub = nest(i) ret['subroles'][i] = sub return ret for p in parent_roles.keys(): if p in parent_roles: self.roles[p] = nest(p) for roleid in self.roles: role = self.roles[roleid] # Create the list of roles which are ldapcherry admins if 'LC_admins' in role and role['LC_admins']: self.admin_roles.append(roleid) self._set_admin(role) def get_admin_roles(self): return self.admin_roles def dump_nest(self): """dump the nested role hierarchy""" return yaml.dump(self.roles, Dumper=CustomDumper) def dump_flatten(self): """dump the nested role hierarchy""" return yaml.dump(self.flatten, Dumper=CustomDumper) def _check_member( self, role, groups, notroles, roles, parentroles, usedgroups): # if we have already calculate user is not member of role # return False if role in notroles: return False # if we have already calculate that user is already member, skip # role membership calculation # (parentroles is a list of roles that the user is member of by # being member of one of their subroles) if not (role in parentroles or role in roles): for b in self.roles[role]['backends_groups']: for g in self.roles[role]['backends_groups'][b]: if b not in groups: notroles.add(role) return False if g not in groups[b]: notroles.add(role) return False # add groups of the role to usedgroups for b in self.roles[role]['backends_groups']: if b not in usedgroups: usedgroups[b] = set([]) for g in self.roles[role]['backends_groups'][b]: usedgroups[b].add(g) flag = True # recursively determine if user is member of any subrole for subrole in self.roles[role]['subroles']: flag = flag and not \ self._check_member( subrole, groups, notroles, roles, parentroles, usedgroups, ) # if not, add role to the list of roles if flag: roles.add(role) # else remove it from the list of roles and add # it to the list of parentroles else: if role in roles: roles.remove(role) parentroles.add(role) return True def get_groups_to_remove(self, current_roles, roles_to_remove): """get groups to remove from list of roles to remove and current roles """ current_roles = set(current_roles) ret = {} roles_to_remove = set(roles_to_remove) tmp = set([]) # get sub roles of the role to remove that the user belongs to # if we remove a role, there is no reason to keep the sub roles for r in roles_to_remove: for sr in self._get_subroles(r): if sr not in roles_to_remove and sr in current_roles: tmp.add(sr) roles_to_remove = roles_to_remove.union(tmp) roles = current_roles.difference(set(roles_to_remove)) groups_roles = self._get_groups(roles) groups_roles_to_remove = self._get_groups(roles_to_remove) # if groups belongs to roles the user keeps, don't remove it for b in groups_roles_to_remove: if b in groups_roles: groups_roles_to_remove[b] = \ groups_roles_to_remove[b].difference(groups_roles[b]) return groups_roles_to_remove def _get_groups(self, roles): ret = {} for r in roles: for b in self.flatten[r]['backends_groups']: groups = self.flatten[r]['backends_groups'][b] if b not in ret: ret[b] = set(groups) ret[b] = ret[b].union(set(groups)) return ret def _get_subroles(self, role): ret = set([]) for sr in self.graph[role]['sub_roles']: tmp = self._get_subroles(sr) tmp.add(sr) ret = ret.union(tmp) return ret def get_roles(self, groups): """get list of roles and list of standalone groups""" roles = set([]) parentroles = set([]) notroles = set([]) tmp = set([]) usedgroups = {} unusedgroups = {} ret = {} # determine roles membership for role in self.roles: if self._check_member( role, groups, notroles, tmp, parentroles, usedgroups): roles.add(role) # determine standalone groups not matching any roles for b in groups: for g in groups[b]: if b not in usedgroups or g not in usedgroups[b]: if b not in unusedgroups: unusedgroups[b] = set([]) unusedgroups[b].add(g) ret['roles'] = roles ret['unusedgroups'] = unusedgroups return ret def get_allroles(self): """get the list of roles""" return self.flatten.keys() def get_display_name(self, role): """get the display name of a role""" if role not in self.flatten: raise MissingRole(role) return self.flatten[role]['display_name'] def get_groups(self, roles): """get the list of groups from role""" ret = {} for role in roles: if role not in self.flatten: raise MissingRole(role) for b in self.flatten[role]['backends_groups']: if b not in ret: ret[b] = [] ret[b] = ret[b] + self.flatten[role]['backends_groups'][b] return ret def is_admin(self, roles): """determine from a list of roles if is ldapcherry administrator""" for r in roles: if r in self.admin_roles: return True return False def get_backends(self): """return the list of backends in roles file""" return self.backends