diff options
Diffstat (limited to 'lib/modules/openldap')
-rw-r--r-- | lib/modules/openldap | 82 |
1 files changed, 41 insertions, 41 deletions
diff --git a/lib/modules/openldap b/lib/modules/openldap index 5178033..c09e791 100644 --- a/lib/modules/openldap +++ b/lib/modules/openldap @@ -1,21 +1,21 @@ -#!/usr/bin/python +#!/usr/bin/python3 # Manage OpenLDAP databases # Copyright (c) 2013 Guilhem Moulin <guilhem@fripost.org> # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import ldap, ldap.sasl from ldap.filter import filter_format from ldap.dn import dn2str,explode_dn,str2dn @@ -32,162 +32,162 @@ import tempfile, atexit # explicit indices to what we find in the LDIF. indexedAttributes = frozenset([ 'olcAttributeTypes', 'olcObjectClasses', 'olcAccess', 'olcSyncrepl', 'olcOverlay', 'olcLimits', 'olcAuthzRegexp', 'olcDbConfig', ]) # Another hack. Configuration entries sometimes pollutes the DNs with # indices, thus it's not possible to directly use them as base. # Instead, we use their parent as a base, and search for the *unique* # match with the same ObjectClass and the matching extra attributes. # ('%s' in the attribute value is replaced with the value of the source # entry.) indexedDN = { - 'olcSchemaConfig': [('cn', '{*}%s')], - 'olcMdbConfig': [('olcDbDirectory', '%s' )], - 'olcOverlayConfig': [('olcOverlay', '%s' )], - 'olcMonitorConfig': [], + b'olcSchemaConfig': [('cn', '{*}%s')], + b'olcMdbConfig': [('olcDbDirectory', '%s' )], + b'olcOverlayConfig': [('olcOverlay', '%s' )], + b'olcMonitorConfig': [], } # Allow for flexible ACLs for user using SASL's EXTERNAL mechanism. # "username=postfix,cn=peercred,cn=external,cn=auth" is replaced by # "gidNumber=106+uidNumber=102,cn=peercred,cn=external,cn=auth" where # 102 is postfix's UID and 106 its primary GID. # (Regular expressions are not allowed.) -sasl_ext_re = re.compile( r"""(?P<start>\sby\s+dn(?:\.exact)?)= - (?P<quote>['\"]?)username=(?P<user>[a-z][-a-z0-9_]*), +sasl_ext_re = re.compile( b"""(?P<start>\sby\s+dn(?:\.exact)?)= + (?P<quote>['\"]?)username=(?P<user>_?[a-z][-a-z0-9_]*), (?P<end>cn=peercred,cn=external,cn=auth) (?P=quote)\s""" , re.VERBOSE ) -multispaces = re.compile( r"\s+" ) +multispaces = re.compile( b"\s+" ) pwd_dict = {} def acl_sasl_ext(m): - u = m.group('user') + u = m.group('user').decode("utf-8") if u not in pwd_dict.keys(): pwd_dict[u] = pwd.getpwnam(u) - return '%s="gidNumber=%d+uidNumber=%d,%s" ' % ( m.group('start') - , pwd_dict[u].pw_gid - , pwd_dict[u].pw_uid - , m.group('end') - ) + return b'%s="gidNumber=%d+uidNumber=%d,%s" ' % ( m.group('start') + , pwd_dict[u].pw_gid + , pwd_dict[u].pw_uid + , m.group('end') + ) # Run the given callback on each DN seen. If its return value is not # None, update the changed variable. class LDIFCallback(LDIFParser): def __init__(self, module, input, callback): LDIFParser.__init__(self,input) self.callback = callback self.changed = False def handle(self,dn,entry): b = self.callback(dn,entry) if b is not None: self.changed |= b # Check if the given dn is already present in the directory. # Returns None if doesn't exist, and give the dn,entry otherwise def flexibleSearch(module, l, dn, entry): idxClasses = set(entry['objectClass']).intersection(indexedDN.keys()) if not idxClasses: base = dn scope = ldap.SCOPE_BASE f = 'objectClass=*' else: # Search on the parent instead, and try to use a precise filter dn = str2dn(dn) h,t,_ = dn.pop(0)[0] base = dn2str(dn) scope = ldap.SCOPE_ONELEVEL f = [] for c in idxClasses: - f.append ( filter_format('objectClass=%s', [c]) ) + f.append ( filter_format('objectClass=%s', [c.decode("utf-8")]) ) for a,v in indexedDN[c]: if a == h: v2 = t elif a not in entry.keys() or len(entry[a]) > 1: module.fail_json(msg="Multiple values found! This is a bug. Please report.") else: - v2 = entry[a][0] + v2 = entry[a][0].decode("utf-8") f.append ( filter_format(a+'='+v, [v2]) ) if len(f) == 1: f = f[0] else: f = '(&(' + ')('.join(f) + '))' r = l.search_s( base, scope, filterstr=f ) if len(r) > 1: module.fail_json(msg="Multiple results found! This is a bug. Please report.") elif r: return r.pop() # Add or modify (only the attributes that differ from those in the # directory) the entry for that DN. # l must be an LDAPObject, and should provide an open connection to the # directory with disclose/search/write access. def processEntry(module, l, dn, entry): changed = False for x in indexedAttributes.intersection(entry.keys()): # remove useless extra spaces in ACLs etc - entry[x] = map( partial(multispaces.sub, ' '), entry[x] ) + entry[x] = list(map( partial(multispaces.sub, b' '), entry[x] )) r = flexibleSearch( module, l, dn, entry ) if r is None: changed = True if module.check_mode: module.exit_json(changed=changed, msg="add DN %s" % dn) if 'olcAccess' in entry.keys(): # replace "username=...,cn=peercred,cn=external,cn=auth" # by a DN with proper gidNumber and uidNumber - entry['olcAccess'] = map ( partial(sasl_ext_re.sub, acl_sasl_ext) - , entry['olcAccess'] ) + entry['olcAccess'] = list(map ( partial(sasl_ext_re.sub, acl_sasl_ext) + , entry['olcAccess'] )) l.add_s( dn, addModlist(entry) ) else: d,e = r fst = str2dn(dn).pop(0)[0][0] diff = [] - for a,v in e.iteritems(): + for a,v in e.items(): if a not in entry.keys(): if a != fst: # delete all values except for the first attribute, # which is implicit diff.append(( ldap.MOD_DELETE, a, None )) elif a in indexedAttributes: if a == 'olcAccess': # replace "username=...,cn=peercred,cn=external,cn=auth" # by a DN with proper gidNumber and uidNumber - entry[a] = map ( partial(sasl_ext_re.sub, acl_sasl_ext) - , entry[a] ) + entry[a] = list(map ( partial(sasl_ext_re.sub, acl_sasl_ext) + , entry[a] )) # add explicit indices in the entry from the LDIF - entry[a] = map( (lambda x: '{%d}%s' % x) - , zip(range(len(entry[a])),entry[a]) ) + entry[a] = list(map( (lambda x: b'{%d}%s' % x) + , zip(range(len(entry[a])),entry[a]))) if v != entry[a]: diff.append(( ldap.MOD_REPLACE, a, entry[a] )) elif v != entry[a]: # for non-indexed attribute, we update values in the # symmetric difference only s1 = set(v) s2 = set(entry[a]) if s1.isdisjoint(s2): # replace the former values with the new ones diff.append(( ldap.MOD_REPLACE, a, entry[a] )) else: x = list(s1.difference(s2)) if x: diff.append(( ldap.MOD_DELETE, a, x )) y = list(s2.difference(s1)) if y: diff.append(( ldap.MOD_ADD, a, y )) # add attributes that weren't in e for a in set(entry).difference(e.keys()): @@ -214,65 +214,65 @@ def loadModule(module, l, name): module.exit_json(changed=changed, msg="add module %s" % name) l.modify_s( 'cn=module{0},cn=config' , [(ldap.MOD_ADD, 'olcModuleLoad', name)] ) return changed # Find the database associated with a given attribute (eg, # olcDbDirectory or olcSuffix). def getDN_DB(module, l, a, v, attrlist=['']): f = filter_format( '(&(objectClass=olcDatabaseConfig)('+a+'=%s))', [v] ) return l.search_s( 'cn=config' , ldap.SCOPE_ONELEVEL , filterstr = f , attrlist = attrlist ) # Convert a *.schema file into *.ldif format. The algorithm can be found # in /etc/ldap/schema/openldap.ldif . def slapd_to_ldif(src, name): - s = open( src, 'r' ) + s = open( src, 'rb' ) d = tempfile.NamedTemporaryFile(delete=False) atexit.register(lambda: os.unlink( d.name )) - d.write('dn: cn=%s,cn=schema,cn=config\n' % name) - d.write('objectClass: olcSchemaConfig\n') + d.write(b'dn: cn=%s,cn=schema,cn=config\n' % name.encode("utf-8")) + d.write(b'objectClass: olcSchemaConfig\n') - re1 = re.compile( r'^objectIdentifier\s(.*)', re.I ) - re2 = re.compile( r'^objectClass\s(.*)', re.I ) - re3 = re.compile( r'^attributeType\s(.*)', re.I ) - reSp = re.compile( r'^\s+' ) + re1 = re.compile( b'^objectIdentifier\s(.*)', re.I ) + re2 = re.compile( b'^objectClass\s(.*)', re.I ) + re3 = re.compile( b'^attributeType\s(.*)', re.I ) + reSp = re.compile( b'^\s+' ) for line in s.readlines(): - if line == '\n': - line = '#\n' + if line == b'\n': + line = b'#\n' m1 = re1.match(line) m2 = re2.match(line) m3 = re3.match(line) if m1 is not None: - line = 'olcObjectIdentifier: %s' % m1.group(1) + line = b'olcObjectIdentifier: %s' % m1.group(1) elif m2 is not None: - line = 'olcObjectClasses: %s' % m2.group(1) + line = b'olcObjectClasses: %s' % m2.group(1) elif m3 is not None: - line = 'olcAttributeTypes: %s' % m3.group(1) + line = b'olcAttributeTypes: %s' % m3.group(1) - d.write( reSp.sub(line, ' ') ) + d.write( reSp.sub(line, b' ') ) s.close() d.close() return d.name def main(): module = AnsibleModule( argument_spec = dict( target = dict( default=None ), module = dict( default=None ), suffix = dict( default=None ), format = dict( default="ldif", choices=["ldif","slapd.conf"] ), name = dict( default=None ), local = dict( default="no", choices=["no","file","template"] ), delete = dict( default=None ), ), supports_check_mode=True ) @@ -286,41 +286,41 @@ def main(): delete = params['delete'] changed = False try: if delete is not None: if name is None: module.fail_json(msg="missing name") l = ldap.initialize( 'ldapi://' ) l.sasl_interactive_bind_s('', ldap.sasl.external()) if delete == 'entry': filterStr = '(objectClass=*)' else: filterStr = [ '(%s=*)' % x for x in delete.split(',') ] if len(filterStr) > 1: filterStr = '(|' + ''.join(filterStr) + ')' else: filterStr = filterStr[0] try: r = l.search_s( name, ldap.SCOPE_BASE, filterStr, attrsonly=1 ) - except ldap.LDAPError, ldap.NO_SUCH_OBJECT: + except (ldap.LDAPError, ldap.NO_SUCH_OBJECT): r = None if r: changed = True if module.check_mode: module.exit_json(changed=changed) if delete == 'entry': l.delete_s(r[0][0]) else: attrlist = list(set(r[0][1].keys()) & set(delete.split(','))) l.modify_s(r[0][0], [ (ldap.MOD_DELETE, x, None) for x in attrlist ]) l.unbind_s() else: if form == 'slapd.conf': if name is None: module.fail_json(msg="missing name") target = slapd_to_ldif(target, name) if target is None and mod is None: @@ -335,40 +335,40 @@ def main(): changed |= loadModule (module, l, '%s.la' % mod) if target is None and suffix is None: l.unbind_s() module.exit_json(changed=changed) if target is None or suffix is None: module.fail_json(msg="missing target or suffix") r = getDN_DB(module, l, 'olcSuffix', suffix) if not r: module.fail_json(msg="No database found for suffix %s" % suffix) elif len(r) > 1: module.fail_json(msg="Multiple results found! This is a bug. Please report.") else: d = 'olcOverlay=%s,%s' % (mod, r.pop()[0]) callback = lambda _,e: processEntry(module,l,d,e) parser = LDIFCallback( module, open(target, 'r'), callback ) parser.parse() changed = parser.changed l.unbind_s() - except subprocess.CalledProcessError, e: + except subprocess.CalledProcessError as e: module.fail_json(rv=e.returncode, msg=e.output.rstrip()) - except ldap.LDAPError, e: + except ldap.LDAPError as e: e = e.args[0] if 'info' in e.keys(): msg = e['info'] elif 'desc' in e.keys(): msg = e['desc'] else: msg = str(e) module.fail_json(msg=msg) - except KeyError, e: + except KeyError as e: module.fail_json(msg=str(e)) module.exit_json(changed=changed) # import module snippets from ansible.module_utils.basic import * main() |