From 31434a1b0b9391d6db8dc8d06b82bb11d97ed174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Timo=20M=E4kinen?= Date: Wed, 20 Jun 2012 13:30:00 +0300 Subject: [PATCH] Added dynldap python library to ldap::client::python. --- ldap/files/dynldap.py | 363 +++++++++++++++++++++++++++++++++++++++++ ldap/manifests/init.pp | 10 ++ 2 files changed, 373 insertions(+) create mode 100644 ldap/files/dynldap.py diff --git a/ldap/files/dynldap.py b/ldap/files/dynldap.py new file mode 100644 index 0000000..5ea9d64 --- /dev/null +++ b/ldap/files/dynldap.py @@ -0,0 +1,363 @@ + + +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] diff --git a/ldap/manifests/init.pp b/ldap/manifests/init.pp index 40cf711..fe3b7f1 100644 --- a/ldap/manifests/init.pp +++ b/ldap/manifests/init.pp @@ -282,6 +282,16 @@ class ldap::client::python { ensure => installed, } + file { "${::pythonsitedir}/dynldap.py": + ensure => present, + source => "puppet:///modules/ldap/dynldap.py", + mode => "0644", + owner => "root", + group => "root", + require => Package["python-ldap"], + } + python::compile { "${::pythonsitedir}/dynldap.py": } + }