

from copy import copy
import ldap
import ldap.modlist
import ldap.sasl
import os
import re
from subprocess import Popen, PIPE
from types import ListType


OPENLDAPCONF = (os.path.join(os.getenv('HOME', ''), '.ldaprc'),
                '/usr/local/etc/openldap/ldap.conf',
                '/etc/openldap/ldap.conf')

SAMBASECRETS = '/var/lib/samba/private/secrets.tdb'


def auth_callback(conn, username=None, password=None):
    from os import getlogin
    from getpass import getpass
    if username is None:
        try:
            username = conn.searchone('(&(objectClass=posixAccount)(uid=%s))' \
                                      % getlogin(), attrs=[]).dn
        except IndexError:
            if getlogin() == 'root':
                username = 'cn=manager,%s' % conn.basedn
            else:
                username = raw_input('Enter bind DN: ')
    if password is None:
        password = getpass('Enter LDAP password: ')
    return (username, password)


def auth_samba_callback(conn, username=None, password=None):    
    backend = get_samba_conf_param('passdb backend')
    m = re.match('^ldapsam:"(.*)"$', backend)
    if m is None:
        raise ValueError('Samba not configured for LDAP backend')
    binddn = get_samba_conf_param('ldap admin dn')
    authpw = get_samba_secrets_param('SECRETS/LDAP_BIND_PW/%s' % binddn)
    return (binddn, authpw)


def get_samba_conf_param(key, section = 'global'):
    cmd = Popen(['testparm', '-s', '-v', '--section-name=%s' % section,
                 '-d', '0', '--parameter-name=%s' % key], stdout=PIPE,
                stderr=PIPE)
    cmd.wait()
    for line in cmd.stdout.readlines():
        m = re.match('^"(.*)"$', line)
        if m is not None:
            return m.group(1)
        return line.strip()
    for line in cmd.stderr.readlines():
        if re.search('Parameter .* unknown for section [^ ]', line):
            raise KeyError(line.strip())


def get_samba_secrets_param(key):
    cmd = Popen(['tdbdump', SAMBASECRETS], stdout=PIPE)
    cmd.wait()
    for line in cmd.stdout.readlines():
        line = line.strip()
        if line == '{':
            k = None
            v = None
            continue
        elif line == '}':
            if k == key:
                return v
            continue
        line = re.match('^(key|data)\(\d+\) = "(.*)"$', line)
        if line.group(1) == 'key':
            k = line.group(2)
        elif line.group(1) == 'data':
            v = line.group(2)
            if v[len(v)-3:] == '\\00':
                v = v[0:len(v)-3]
    raise KeyError()


def connect(uris = None, basedn = None):
    if uris is None:
        (uris, basedn) = parse_openldap_config()
    return Connection(uris, basedn)


def parse_openldap_config():
    for name in OPENLDAPCONF:
        ret = [None, None]
        try:
            f = open(name, 'r')
        except IOError:
            continue
        for line in f.readlines():
            m = re.match('^\s*(URI|BASE)\s+(.*)', line)
            if m is None:
                continue
            if m.group(1) == 'URI':
                ret[0] = m.group(2).split()
            elif m.group(1) == 'BASE':
                ret[1] = m.group(2)
        f.close()
        if ret[0] is not None:
            return tuple(ret)
    raise ldap.SERVER_DOWN('Could not determine LDAP server address')


class  LdapEntry:

    def __init__(self, conn, entry, new=False):
        for (attr, values) in entry[1].items():
            self._set(attr, values)
        if new:
            self._orig = {}
        else:
            self._orig = copy(entry[1])
        self._dn = entry[0]
        self._conn = conn

    def _del(self, key):
        if key in self._attrs():
            del(self.__dict__[key])

    __delitem__ = _del

    def _get(self, key):
        if key in self._attrs():
            keys = {}
            for v in self.__dict__[key]:
                keys[v] = True
            value = keys.keys()
            value.sort()
            return value
        return None

    __getitem__ = _get

    def __iter__(self):
        return self._attrs().__iter__()

    def __str__(self):
        ret = 'dn: %s\n' % self.dn
        for (key, values) in self.items():
            for value in values:
                ret += '%s: %s\n' % (key, value)
        return ret

    def _set(self, key, value):
        if key[0] == '_':
            self.__dict__[key] = value
        elif value is None:
            del(self.__dict__[key])
        else:
            if not isinstance(value, ListType):
                value = [ value ]
            self.__dict__[key] = map(lambda x: str(x), value)

    __setattr__ = _set
    __setitem__ = __setattr__

    @property
    def dn(self):
        return self._dn

    @property
    def modified(self):
        if len(self.changes()) > 0:
            return True
        return False

    def _attrs(self):
        return map(lambda (k, v): k, self.items())
    attrs = _attrs


    def changes(self):
        new = {}
        for k in self._attrs():
            new[k] = self._get(k)
        return ldap.modlist.modifyModlist(self._orig, new)

    def items(self):
        return filter(lambda (k, v): k[0] != '_', self.__dict__.items())

    def revert(self):
        for k in self._attrs():
            del(self.__dict__[k])
        for (k, v) in self._orig.items():
            self.__setattr__(k, v)

    def save(self):
        changes = self.changes()
        if len(changes) > 0:
            if self._orig == {}:
                self._conn._conn.add_s(self.dn, self.items())
            else:
                self._conn._conn.modify_s(self.dn, changes)
            self._orig = copy(self.__dict__)


class Connection:

    def __init__(self, uri, basedn = None):
        self.auth = (None, None)
        self.reconnect(uri)
        if basedn is None:
            entry = self.get('', attrs=['namingContexts'])
            basedn = entry.namingContexts[0]
        self.basedn = basedn
        self.set_option(ldap.OPT_REFERRALS, False)

    def __del__(self):
        self.unbind()

    def reconnect(self, uris = None):
        if uris is None:
            uris = self.uri
        elif not hasattr(uris, '__iter__'):
            uris = [ uris ]
        for uri in uris:
            try:
                self._conn = ldap.initialize(uri)
                self.whoami()
                if self.auth[0] is not None and self.auth[1] is not None:
                    self.bind(self.auth[0], self.auth[1])
                self.uri = uri
                return
            except ldap.SERVER_DOWN:
                continue
        raise ldap.SERVER_DOWN({'desc': "Can't contact LDAP server"})
            

    def bind(self, username=None, password=None, callback=auth_callback):
        if username is None or password is None:
            (username, password) = callback(self, username, password)
        if password is '':
            raise ldap.INVALID_CREDENTIALS(
                {'desc': 'Empty passwords not allowed'})
        self._conn.bind_s(username, password)
        self.auth = (username, password)

    def delete(self, dn):
        return self._conn.delete_s(dn)

    def get(self, dn, attrs = None):
        return self.search(scope=ldap.SCOPE_BASE, basedn=dn, attrs=attrs)[0]

    def get_option(self, option):
        return self._conn.get_option(option)

    def new(self, dn, entry = {}, **kwargs):
        if not 'entryclass' in kwargs:
            kwargs['entryclass'] = LdapEntry
        return kwargs['entryclass'](self, [dn, entry], True)

    def passwd(self, newpw, **kwargs):
        if 'user' not in kwargs:
            kwargs['user'] = self.whoami()
        if 'oldpw' not in kwargs:
            kwargs['oldpw'] = None
        self._conn.passwd_s(kwargs['user'], kwargs['oldpw'], newpw)

    def ping(self, reconnect=True):
        try:
            self.whoami()
            return True
        except ldap.SERVER_DOWN:
            return False

    def search(self, filterstr = '(objectClass=*)', **kwargs):
        if not 'basedn' in kwargs:
            kwargs['basedn'] = self.basedn
        if not 'scope' in kwargs:
            kwargs['scope'] = ldap.SCOPE_SUBTREE
        if not 'attrs' in kwargs:
            kwargs['attrs'] = None
        if not 'timeout' in kwargs:
            kwargs['timeout'] = self._conn.timeout
        if not 'entryclass' in kwargs:
            kwargs['entryclass'] = LdapEntry
        msgid = self._conn.search_ext(kwargs['basedn'], kwargs['scope'],
                                      filterstr, kwargs['attrs'], 0, None,
                                      None, kwargs['timeout'], 1000000)
        return LdapResult(self, msgid, kwargs['entryclass'])

    def searchone(self, filterstr, **kwargs):
        result = self.search(filterstr, **kwargs)
        if len(result) == 0:
            raise ldap.NO_SUCH_OBJECT({'desc': 'No such object'})
        elif len(result) == 1:
            return result[0]
        else:
            raise ldap.NO_SUCH_OBJECT({'desc': 'Multiple objects matched'})

    def set_option(self, option, value):
        self._conn.set_option(option, value)

    def unbind(self):
        self._conn.unbind_s()
        self.auth = (None, None)

    def whoami(self):
        try:
            return self._conn.whoami_s().split(':')[1]
        except IndexError:
            return None


class LdapResult:

    def __init__(self, conn, msgid, entryclass = LdapEntry):
        self._conn = conn
        self._msgid = msgid
        self._entries = []
        self._entryclass = entryclass

    def __del__(self):
        try:
            self._conn._conn.abandon(self._msgid)
        except ldap.LDAPError:
            pass

    def __getitem__(self, idx):
        while len(self._entries)-1 < idx:
            try:
                self.next()
            except StopIteration:
                raise IndexError('list index out of range')
        return self._entries[idx]

    def __iter__(self):
        return self

    def __len__(self):
        try:
            self.next()
        except StopIteration:
            pass
        return len(self._entries)

    def __str__(self):
        return str(self.keys())

    def keys(self):
        r = []
        for entry in self:
            r.append(entry.dn)
        return r

    def next(self):
        try:
            (rtype, entry) = self._conn._conn.result(self._msgid, all=0,
                                                     timeout=-1)
            if rtype == ldap.RES_SEARCH_RESULT:
                raise StopIteration
        except IndexError:
            raise StopIteration
        self._entries.append(self._entryclass(self._conn, entry[0]))
        return self._entries[len(self._entries)-1]
