diff --git a/bin/ldapuserdir-ctl b/bin/ldapuserdir-ctl index 8fedc2a..c051531 100755 --- a/bin/ldapuserdir-ctl +++ b/bin/ldapuserdir-ctl @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/env python import logging from ldapuserdir import LdapUserDir, __version__ as libversion, LdapUserDirError @@ -7,7 +7,7 @@ import sys import os import pwd from optparse import OptionParser -import ConfigParser +import configparser import getpass class MyOptParser(OptionParser): @@ -17,7 +17,7 @@ class MyOptParser(OptionParser): return self.epilog.replace('%prog',os.path.basename(sys.argv[0])) def read_cfg(filename): - cfg = ConfigParser.ConfigParser() + cfg = configparser.ConfigParser() try: mylogger.debug('reading config from %s' % filename) @@ -29,7 +29,7 @@ def read_cfg(filename): 'default_user_dn' : cfg.get('Ldap','default_user_dn'), 'default_user_pw' : cfg.get('Ldap','default_user_pw'), } - except Exception, err: + except Exception as err: sys.stderr.write("Error in reading configuration from %s\n" % filename) sys.stderr.write(str(err)+'\n') sys.exit(1) @@ -93,14 +93,15 @@ usage += """ usage_epilog = """ Examples: List group members - %prog svc-ra_x06sa - %prog 'svc-ra_*' + %prog svc-cluster_merlin5 + %prog 'svc-cluster_*' Get group memberships for user mueller (optionally with a group filter) %prog -g mueller - %prog -g mueller 'svc-ra*' + %prog -g mueller 'svc-cluster_merlin5' + %prog -g mueller 'svc-cluster_*' - Add/delete users to/from a group (requires access rights!) + Add/delete users to/from a group (requires access rights defined in LDAP service!) %prog -a svc-ra_x06sa user1 user2 user3 %prog -d svc-ra_x06sa user1 user2 @@ -108,8 +109,9 @@ usage_epilog = """ %prog -u 'mueller*' List users matching a mail address pattern - %prog -m '*mueller@psi* + %prog -m '*mueller@psi*' + Author: 2013-19 D. Feichtinger """ examplecfg = """# Configuration file example: @@ -305,7 +307,7 @@ if flag_needprivileges and user_dn == config['default_user_dn']: logger = mylogger) try: user_dn = l_unpriv.systemuser2dn(loginname) - except LdapUserDirError, err: + except LdapUserDirError as err: if str(err) == 'No such user': sys.stderr.write(''' Error: Need priviledged user and cannot map your system user "%s" @@ -315,7 +317,7 @@ to LDAP DN for binding (you may want to use the explicit -D user_dn option) else: sys.stderr.write('Uncaught Error: %s' % str(err)) - except ldap.LDAPError, e: + except ldap.LDAPError as e: sys.stderr.write('LDAP error: %s\n' % str(e)) sys.exit(1) @@ -352,7 +354,7 @@ try: elif mode == 'userlist': - records = ldapdir.get_users(userfilter, config['user_ou'], mssfu=flag_mssfu) + records = ldapdir.get_users(userfilter, config['user_ou'], mssfu=flag_mssfu) ldapdir.list_users_etcpwd(records, verbose = flag_verbose) elif mode == 'maillist': @@ -372,7 +374,7 @@ try: + '\n') #sys.stdout.write("\n".join(ldapdir.get_groups_for_user(user_to_group)) # + "\n") - except LdapUserDirError, err: + except LdapUserDirError as err: if str(err) == "No such user": sys.stderr.write('Error: No such user (%s)\n' % user_to_group) else: @@ -393,9 +395,9 @@ try: group = args.pop(0) ldapdir.del_groupmembers(group, args) -except ldap.INVALID_CREDENTIALS, e: +except ldap.INVALID_CREDENTIALS as e: sys.exit(1) -except ldap.LDAPError, e: +except ldap.LDAPError as e: sys.stderr.write('Unhandled LDAP error: %s\n' % str(e)) sys.exit(1) # except Exception, err: diff --git a/conda-recipe/ldapuserdir/meta.yaml b/conda-recipe/ldapuserdir/meta.yaml index 2c692ce..348202f 100644 --- a/conda-recipe/ldapuserdir/meta.yaml +++ b/conda-recipe/ldapuserdir/meta.yaml @@ -1,6 +1,6 @@ package: name: ldapuserdir - version: "2.1.5" + version: "2.2.0" source: path: ../../ @@ -15,6 +15,7 @@ requirements: run: - python - python-ldap + - configparser build: preserve_egg_dir: True diff --git a/ldapuserdir/__init__.py b/ldapuserdir/__init__.py index 0f24d43..076c186 100644 --- a/ldapuserdir/__init__.py +++ b/ldapuserdir/__init__.py @@ -1,2 +1,2 @@ -from ldapuserdir import LdapUserDir, LdapUserDirError -from version import __version__ +from ldapuserdir.ldapuserdir import LdapUserDir, LdapUserDirError +from ldapuserdir.version import __version__ diff --git a/ldapuserdir/ldapuserdir.py b/ldapuserdir/ldapuserdir.py index 93ddd47..a25efce 100755 --- a/ldapuserdir/ldapuserdir.py +++ b/ldapuserdir/ldapuserdir.py @@ -90,7 +90,8 @@ class LdapUserDir(object): self.logger = logger self._ldap = ldap.initialize(self.serverurl, trace_level=0, - trace_file=sys.stderr) + trace_file=sys.stderr, + bytes_mode=False) self.logger.debug('binding to: %s\n' % serverurl) self.logger.debug('binding as user: %s\n' % user_dn) @@ -108,6 +109,12 @@ class LdapUserDir(object): except ldap.LDAPError: raise + @staticmethod + def ensure_utf8(bstr): + if type(bstr) == bytes: + return bstr.decode('utf-8') + return bstr + @staticmethod def has_dn_format(name): """Returns true if name has the format of a distinguished name @@ -148,6 +155,11 @@ class LdapUserDir(object): """Helper for search_s_reconn. Wraps ldap.search_ext to use paged results if desired (see self.page_size). + + Returns + ------- + list of tuples + list of tuples of the form (dn, attributes) """ if self.page_size == 0: # Do not use paged results @@ -165,18 +177,22 @@ class LdapUserDir(object): serverctrls=[page_ctrl]) results = [] + npages=0 while True: _, rdata, _, resp_ctrls = self._ldap.result3(msgid) + npages += 1 results.extend(rdata) # Extract the SimplePagedResultsControl to get the cookie. - page_ctrls = [c for c in resp_ctrls if c.controlType == SimplePagedResultsControl.controlType] - if page_ctrls == [] or page_ctrls[0].cookie == '': + pagecontrols = [c for c in resp_ctrls if c.controlType == SimplePagedResultsControl.controlType] + self.logger.debug('Retrieved page %d of results' % npages) + if not pagecontrols: + raise RuntimeError("The server ignores RFC 2696 control (paged results)") + if not pagecontrols[0].cookie: # We're done. break - else: - # Update the cookie to retrieve the next page. - page_ctrl.cookie = page_ctrls[0].cookie + # Update the cookie to retrieve the next page. + page_ctrl.cookie = pagecontrols[0].cookie msgid = self._ldap.search_ext(base, scope, filterstr, attrlist, attrsonly, serverctrls=[page_ctrl]) @@ -226,7 +242,7 @@ class LdapUserDir(object): attempts += 1 repl = self._search_s(base, scope, filterstr, attrlist, attrsonly) - except Exception, err: + except Exception as err: ok = False self.logger.warning("Got ldap error: %s" % (err,)) @@ -236,7 +252,7 @@ class LdapUserDir(object): # we try to reconnect and rebind try: del self._ldap - except Exception, err: + except Exception as err: self.logger.warning("failed to delete LDAP object: %s" % (err,)) @@ -246,13 +262,14 @@ class LdapUserDir(object): try: self._ldap = ldap.initialize(self.serverurl, trace_level=0, - trace_file=sys.stderr) + trace_file=sys.stderr, + bytes_mode=False) except ldap.SERVER_DOWN: self.logger.warning("ldap initialization error" + ", server down (server: %s)" % self.serverurl + ": %s" % (err,)) - except Exception, err: + except Exception as err: self.logger.warning("ldap initialization error" + " (server: %s)" % self.serverurl @@ -263,7 +280,7 @@ class LdapUserDir(object): except ldap.INVALID_CREDENTIALS: self.logger.error('Authentication failure for dn:"%s"\n' % self.user_dn) - except Exception, err: + except Exception as err: self.logger.warning("ldap binding error" + " (server: %s)" % self.serverurl @@ -342,14 +359,14 @@ class LdapUserDir(object): if verbose: for k in fields + ['description', 'mail', 'mobile']: if k in entry: - sys.stdout.write('[%s:]%s:' % (k, entry[k][0])) + sys.stdout.write('[%s:]%s:' % (k, self.ensure_utf8(entry[k][0]))) else: sys.stdout.write('[%s:]N.A.:' % k) sys.stdout.write('\n') else: for k in fields: if k in entry: - sys.stdout.write('%s:' % (entry[k][0],)) + sys.stdout.write('%s:' % (self.ensure_utf8(entry[k][0]))) else: sys.stdout.write('N.A.:') sys.stdout.write('\n') @@ -384,8 +401,9 @@ class LdapUserDir(object): if len(r) == 0: raise LdapUserDirError("No such user") - self.logger.debug('systemuser2dn: dn = %s' % r[0][0]) - return r[0][0] + dn = self.ensure_utf8(r[0][0]) + self.logger.debug('systemuser2dn: dn = %s' % dn) + return dn def get_groups_struct(self, gfilter='*', ou=None, mssfu=False): """searches for groups that match filter @@ -463,7 +481,7 @@ class LdapUserDir(object): grplist = [] if 'memberOf' in r[0][1]: - grplist = r[0][1]['memberOf'] + grplist = [self.ensure_utf8(g) for g in r[0][1]['memberOf']] if mssfu: srch = '(msSFU30GidNumber=*)' @@ -522,7 +540,7 @@ class LdapUserDir(object): if not returndn: try: reslist = [self.dn_to_cn(grp) for grp in reslist] - except RuntimeError, e: + except RuntimeError as e: self.logger.error(str(e)) if gfilter: @@ -563,16 +581,16 @@ class LdapUserDir(object): indent_increment = 3 # amount to indent members for dn, entry in r: if returndn: - print("%sgroup: %s" % (' '*indent, dn)), + print("%sgroup: %s" % (' '*indent, dn), end=''), else: - print("%sgroup: %s" % (' '*indent, entry['cn'][0])), + print("%sgroup: %s" % (' '*indent, self.ensure_utf8(entry['cn'][0])), end='') if not 'msSFU30GidNumber' in entry: gid = '---' else: - gid = entry['msSFU30GidNumber'][0] - print "(%s)" % gid + gid = self.ensure_utf8(entry['msSFU30GidNumber'][0]) + print("(%s)" % gid) if 'member' in entry: - for member in entry['member']: + for member in (self.ensure_utf8(m) for m in entry['member']): # Check if member is itself a group. This might be PSI-specific is_group = self._is_group(member) if recursive and is_group: @@ -595,11 +613,11 @@ class LdapUserDir(object): if not 'msSFU30GidNumber' in entry: gid = '---' else: - gid = entry['msSFU30GidNumber'][0] + gid = self.ensure_utf8(entry['msSFU30GidNumber'][0]) - sys.stdout.write("%s:IGNORE:%s:" % (entry['cn'][0], gid)) + sys.stdout.write("%s:IGNORE:%s:" % (self.ensure_utf8(entry['cn'][0]), gid)) if 'member' in entry: - members = [self.dn_to_cn(dn) for dn in entry['member']] + members = [self.dn_to_cn(self.ensure_utf8(dn)) for dn in entry['member']] if recursive: members = [m for cn in members for m in self._get_all_members(cn)] sys.stdout.write(",".join(members) + "\n") @@ -641,6 +659,7 @@ class LdapUserDir(object): if "member" in entry: # not an empty group for member in entry["member"]: + member = self.ensure_utf8(member) # Shortcut recursion for known leaves #if not LdapUserDir._is_group(member): # yield member diff --git a/ldapuserdir/version.py b/ldapuserdir/version.py index 0b167e6..8a124bf 100644 --- a/ldapuserdir/version.py +++ b/ldapuserdir/version.py @@ -1 +1 @@ -__version__ = "2.1.5" +__version__ = "2.2.0" diff --git a/setup.py b/setup.py index fe206c4..fd692df 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,8 @@ from setuptools import setup # we get the package version from inside our package, since we then # can use it also from within the package #from ldapuserdir.version import __version__ -execfile('ldapuserdir/version.py') +# py2.7 way: execfile('ldapuserdir/version.py') +exec(open("ldapuserdir/version.py").read()) setup( name="ldapuserdir", @@ -25,5 +26,5 @@ setup( # following format is (targetdir,[list of files]) data_files=[('etc', ['etc/ldapuserdir-ctl.cfg'])], - install_requires=['python-ldap'] + #install_requires=['python-ldap'] ) diff --git a/todo.org b/todo.org index a7e3ab0..bdc03e3 100644 --- a/todo.org +++ b/todo.org @@ -384,3 +384,88 @@ KeyError: 'displayName' I implemented a workaround by filtering out the None elements. +** [2019-05-10 Fri] compatibility with python-3.6 +*** RESOLVED simple fixes + CLOSED: [2019-05-11 Sat 09:54] + :LOGBOOK: + - State "RESOLVED" from "BUG" [2019-05-11 Sat 09:54] + - State "BUG" from [2019-05-11 Sat 09:54] + :END: + - Exceptions: use new syntax + #+begin_src python + except SomeException as err + #+end_src + - print statements +*** RESOLVED importer namespace problem + CLOSED: [2019-05-11 Sat 09:55] + :LOGBOOK: + - State "RESOLVED" from "BUG" [2019-05-11 Sat 09:55] + - State "BUG" from [2019-05-11 Sat 09:54] + :END: + - __init__.py only works with changing to relative import + : from ldapuserdir import LdapUserDir, LdapUserDirError + now must be made explicit with + : from ldapuserdir.ldapuserdir import LdapUserDir, LdapUserDirError +*** RESOLVED hangs in LDAP paging call + CLOSED: [2019-05-11 Sat 12:28] + :LOGBOOK: + - State "RESOLVED" from "BUG" [2019-05-11 Sat 12:28] + - State "BUG" from [2019-05-11 Sat 10:05] + :END: + The loop for reading the paged results never reaches the break condition + + in ldapuserdir.py:_search_s + #+begin_src python + page_ctrl = SimplePagedResultsControl(criticality=True, + size=self.page_size, + cookie='') + msgid = self._ldap.search_ext(base, scope, filterstr, attrlist, + attrsonly, + serverctrls=[page_ctrl]) + + results = [] + while True: + _, rdata, _, resp_ctrls = self._ldap.result3(msgid) + results.extend(rdata) + self.logger.debug('DEREK: in paging result call: results= %s' % results) + # .... CUT .... + # Extract the SimplePagedResultsControl to get the cookie. + page_ctrls = [c for c in resp_ctrls if c.controlType == SimplePagedResultsControl.controlType] + if page_ctrls == [] or page_ctrls[0].cookie == '': + # We're done. + break + else: + # Update the cookie to retrieve the next page. + page_ctrl.cookie = page_ctrls[0].cookie + + #+end_src + + The conditions for the break need to be changed. + Good resource: https://medium.com/@alpolishchuk/pagination-of-ldap-search-results-with-python-ldap-845de60b90d2 + + #+begin_src python + if not page_ctrls: + raise RuntimeError("The server ignores RFC 2696 control (paged results)") + if not page_ctrls[0].cookie: + # We're done. + break + # Update the cookie to retrieve the next page. + page_ctrl.cookie = page_ctrls[0].cookie + #+end_src + +*** RESOLVED In python3 the ldap calls return bytestrings + CLOSED: [2019-05-11 Sat 12:28] + :LOGBOOK: + - State "RESOLVED" from "BUG" [2019-05-11 Sat 12:28] + - State "BUG" from [2019-05-11 Sat 12:28] + :END: + + : ldapuserdir-ctl --debug -u feichtinger + : b'feichtinger':b'3896':b'3896':b'840':b'Feichtinger Derek Heinrich':b'/bin/bash':b'/afs/psi.ch/user/f/feichtinger': + + The (dn, attributes) that are returned by _search_s contain attributes the + values of which all are bytestrings. + + python-ldap returns bytestrings and in py3 a standard string is now utf-8. + This leads to all kinds of problems. I define a function + ensure_utf8 ẗo fix the issue.