LdapTree.py 5.94 KB
from os.path import dirname, realpath

import ldap
import pygraphviz as pgv

from jinja2 import Environment, FileSystemLoader

class LdapTree(object):
    def __init__(self, hosturi, binddn, basedn, password, use_gssapi):
        #ldap.set_option(ldap.OPT_DEBUG_LEVEL, 1)
        self._ldap = ldap.initialize(hosturi)
        """
        Setting ldap.OPT_REFERRALS to 0 was neccessary to query a samba4
        active directory... Currently I don't know if it is a good idea
        to keep it generally here.
        """
        self._ldap.set_option(ldap.OPT_REFERRALS, 0)
        if use_gssapi:
            sasl_auth = ldap.sasl.sasl({},'GSSAPI')
            self._ldap.sasl_interactive_bind_s("", sasl_auth)
        else:
            self._ldap.bind(binddn, password, ldap.AUTH_SIMPLE)
        self._basedn = basedn
        self._ldap_result = []

        self._data = None

    def text(self, filename = None):
        """
        Returns a text representing the directory.
        If filename is given it will be written in that file.
        """
	env = Environment(loader=FileSystemLoader(
	    dirname(dirname(realpath(__file__))) + '/templates'))
	template = env.get_template('simple.txt.j2')
        text = template.render(ldaptree=self).encode('utf8')

        if filename:
            with open(filename, "w") as text_file:
                text_file.write(text)
        else:
            return text

    def graph(self, filename = None):
        """
        Returns an svg representing the directory.
        If filename is given it will be written in that file.
        """
        graph = pgv.AGraph(
                directed=True, charset='utf-8', fixedsize='true', ranksep=0.1)

        graph.node_attr.update(
                style='rounded,filled', width='0', height='0', shape='box',
                fillcolor='#E5E5E5', concentrate='true', fontsize='8.0',
                fontname='Arial', margin='0.03')

        graph.edge_attr.update(arrowsize='0.55')

        self._graph(graph, self._basedn)

        graph.layout(prog='dot')
        if filename:
            graph.draw(path=filename, format='svg')
            return None
        else:
            return graph.draw(format='svg')

    def _graph(self, graph, dn):
        """
        Recursive function creating a graphviz graph from the directory.
        """
        result = self.node(dn)
        minlen = thislen = 1
        edge_start = dn

        for entry in (entry[0] for entry in result):
            if entry:
                point = entry + '_p'
                sub = graph.add_subgraph()
                sub.graph_attr['rank'] = 'same'
                sub.add_node(
                        point, shape='circle', fixedsize='true', width='0.04',
                        label='', fillcolor='transparent')
                #sub.add_node(entry, URL='https://www.google.de/')
                sub.add_node(entry)
                graph.add_edge(edge_start, point, arrowhead='none',
                        minlen=str(minlen))
                graph.add_edge(point, entry)
                edge_start = point
                minlen = self._graph(graph, entry)
                thislen += minlen

        return thislen

    def _encode(self, data):
        if type(data) is str:
            try:
                unicode(data, 'utf-8')
            except UnicodeDecodeError:
                data = data.encode('base64')
        return data

    @property
    def all(self):
        if self._data == None:
            self._data = {}
            result = self._ldap.search_s(self._basedn, ldap.SCOPE_SUBTREE)
            for entry in result:
                if entry[1] is None:
                    self._data[entry[0]] = None
                elif type(entry[1]) is str:
                    self._data[entry[0]] = self._encode(entry[1])
                elif type(entry[1]) is list:
                    self._data[entry[0]] = [self._encode(v) for v in entry[1]]
                elif type(entry[1]) is dict:
                    self._data[entry[0]] = {}
                    for k in entry[1].keys():
                        if type(entry[1][k]) is str:
                            self._data[entry[0]][k] = self._encode(entry[1])
                        else:
                            self._data[entry[0]][k] = [
                                    self._encode(v) for v in entry[1][k]]
                else:
                    raise TypeError("unsupported ldap type")

        return self._data

    @property
    def dn_tree(self):
        retval = {}
        for d in self.all.keys():
            current = retval
            if d:
                for k in reversed(d.split(',')):
                    try:
                        current = current[k]
                    except:
                        current[k] = {}
                        current = current[k]
        return retval

    @property
    def hirarchy(self):
        return self._hirarchy(self.dn_tree)

    def _hirarchy(self, dn, base=[], depth=0):
        """
        Hirarchie generates a flat list where each parent is
        followed by all its childs, so that in a template one
        can simple iterate over it to display the complete tree.
        Recently I learned that "recursive loops" are possible
        within jinja2. So we can alter the template that it
        ensures child displays correctly
        """
	retval = []
	for d in dn.keys():
            base_name = ','.join(reversed(base))
            name = ','.join(reversed(base + [d]))
	    retval.append((depth, base_name, name))
	    retval += self._hirarchy(dn[d], base + [d], depth+1)
	return retval

    def childs(self, dn):
        """
        Recently I learned that "recursive loops" are possible
        within jinja2. So we can alter the template that it
	ensures child displays correctly. So this function should
        return all child dn's to a given dn.
        """
        return [d for d in self.hirarchy if d[1] == dn]

    def node(self, dn):
        if dn in self.all:
            return self.all[dn]
        else:
            return {}