summaryrefslogtreecommitdiffstats
path: root/lib/openldap
diff options
context:
space:
mode:
Diffstat (limited to 'lib/openldap')
-rw-r--r--lib/openldap434
1 files changed, 0 insertions, 434 deletions
diff --git a/lib/openldap b/lib/openldap
deleted file mode 100644
index 7293b23..0000000
--- a/lib/openldap
+++ /dev/null
@@ -1,434 +0,0 @@
-#!/usr/bin/python
-
-# 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
-from ldap.modlist import addModlist
-from ldif import LDIFParser
-from functools import partial
-import re, pwd
-import tempfile, atexit
-
-
-# Dirty hack to check equality between the targetted LDIF and that
-# currently in the directory. The value of some configuration (olc*)
-# attributes is automatically indexed when added; for those we'll add
-# explicit indices to what we find in the LDIF.
-indexedAttributes = frozenset([
- 'olcAttributeTypes',
- 'olcObjectClasses',
- 'olcAccess',
- 'olcSyncrepl',
- 'olcOverlay',
- 'olcLimits',
-])
-
-
-# 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 pase, 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')],
- 'olcHdbConfig': [('olcDbDirectory', '%s' )],
- 'olcOverlayConfig': [('olcOverlay', '%s' )],
-}
-
-# 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_]*),
- (?P<end>cn=peercred,cn=external,cn=auth)
- (?P=quote)\s"""
- , re.VERBOSE )
-multispaces = re.compile( r"\s+" )
-pwd_dict = {}
-
-def acl_sasl_ext(m):
- u = m.group('user')
- 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')
- )
-
-
-# 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
-
-
-# Run slapcat(8) on the given suffix or DB number (suffix takes
-# precedence) with an optional filter. (This is useful for offline
-# searches, or one needs to bypass ACLs.) Returns an open pipe to the
-# subprocess.
-def slapcat(filter=None, suffix=None, idx=0):
- cmd = [ os.path.join(os.sep, 'usr', 'sbin', 'slapcat') ]
-
- if filter is not None:
- cmd.extend([ '-a', filter ])
-
- if suffix is not None:
- if type(suffix) is not str:
- suffix = dn2str(suffix)
- cmd.extend([ '-b', suffix ])
- else:
- cmd.append( '-n%d' % idx )
-
- return subprocess.Popen( cmd, stdout=subprocess.PIPE
- , stderr=open(os.devnull, 'wb') )
-
-
-# Start / stop / whatever a service.
-def service(name, state):
- cmd = [ os.path.join(os.sep, 'usr', 'sbin', 'service'), name, state ]
- subprocess.check_call( cmd, stdout=open(os.devnull, 'wb')
- , stderr=subprocess.STDOUT )
-
-
-# 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]) )
- 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]
- 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] )
-
- 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'] )
- l.add_s( dn, addModlist(entry) )
- else:
- d,e = r
- fst = str2dn(dn).pop(0)[0][0]
- diff = []
- for a,v in e.iteritems():
- 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] )
- # add explicit indices in the entry from the LDIF
- entry[a] = map( (lambda x: '{%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()):
- diff.append(( ldap.MOD_ADD, a, entry[a] ))
-
- if diff:
- changed = True
- if module.check_mode:
- module.exit_json(changed=changed, msg="mod DN %s" % dn)
- l.modify_s( d, diff )
- return changed
-
-
-# Load the given module.
-def loadModule(module, l, name):
- changed = False
-
- f = filter_format( '(&(objectClass=olcModuleList)(olcModuleLoad=%s))', [name] )
- r = l.search_s( 'cn=config', ldap.SCOPE_ONELEVEL, filterstr = f, attrlist = [''] )
-
- if not r:
- changed = True
- if module.check_mode:
- 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 )
-
-
-# Clear the given DB directory and delete the associated database. Fail
-# if non empty, unless all existing DNS are in skipdns.
-def wontRemove(module, skipdns, d, _):
- if d not in skipdns:
- module.fail_json(msg="won't remove '%s'" % d)
-def removeDB(module, dbdir, skipdn=None):
- changed = False
- if not os.path.exists(dbdir):
- return False
-
- l = ldap.initialize( 'ldapi://' )
- l.sasl_interactive_bind_s('', ldap.sasl.external())
- r = getDN_DB( module, l, 'olcDbDirectory', dbdir, attrlist=['olcSuffix'] )
- l.unbind_s()
-
- if len(r) > 1:
- module.fail_json(msg="Multiple results found! This is a bug. Please report.")
- elif r:
- dn,entry = r.pop()
- suffix = entry['olcSuffix'][0]
-
- skipdns = [suffix]
- if skipdn is not None:
- skipdns.extend([ "%s,%s" % (s,suffix) for s in skipdn ])
- # here we need to use slapcat not search_s, because we may
- # not have read access on the database (even though we're
- # root!).
- p = slapcat( suffix=suffix )
- parser = LDIFCallback( module, p.stdout
- , partial(wontRemove,module,skipdns) )
- parser.parse()
-
- changed = True
- if module.check_mode:
- module.exit_json(changed=changed, msg="remove dir %s" % dbdir)
-
- # slapd doesn't support database deletion, so we need to turn it
- # off and remove it from slapd.d manually.
- service( 'slapd', 'stop' )
- path = [ os.sep, 'etc', 'ldap', 'slapd.d' ]
- ldif = explode_dn(dn)[::-1]
- ldif[-1] += ".ldif"
- path.extend( ldif )
- os.unlink( os.path.join(*path) )
-
- # delete all children in path, but not the path directory itself.
- for file in os.listdir(dbdir):
- os.unlink( os.path.join(dbdir, file) )
- service( 'slapd', 'start' )
- return changed
-
-
-# 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' )
- 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')
-
- 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+' )
- for line in s.readlines():
- if line == '\n':
- line = '#\n'
- m1 = re1.match(line)
- m2 = re2.match(line)
- m3 = re3.match(line)
- if m1 is not None:
- line = 'olcObjectIdentifier: %s' % m1.group(1)
- elif m2 is not None:
- line = 'olcObjectClasses: %s' % m2.group(1)
- elif m3 is not None:
- line = 'olcAttributeTypes: %s' % m3.group(1)
-
- d.write( reSp.sub(line, ' ') )
-
-
- s.close()
- d.close()
- return d.name
-
-
-def main():
- module = AnsibleModule(
- argument_spec = dict(
- dbdirectory = dict( default=None ),
- ignoredn = dict( default=None ),
- state = dict( default="present", choices=["absent", "present"]),
- target = dict( default=None ),
- module = dict( default=None ),
- suffix = dict( default=None ),
- format = dict( default="ldif", choices=["ldif","slapd.conf"] ),
- name = dict( default=None ),
- ),
- supports_check_mode=True
- )
-
- params = module.params
- state = params['state']
- dbdirectory = params['dbdirectory']
- ignoredn = params['ignoredn']
- target = params['target']
- mod = params['module']
- suffix = params['suffix']
- form = params['format']
- name = params['name']
-
- if ignoredn is not None:
- ignoredn = ignoredn.split(':')
-
- changed = False
- try:
- if state == "absent":
- if dbdirectory is not None:
- changed = removeDB(module,dbdirectory,skipdn=ignoredn)
- # TODO: might be useful to be able remove DNs
- else:
- module.fail_json(msg="missing dbdirectory")
-
- elif state == "present":
- 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:
- module.fail_json(msg="missing target or module")
- # bind only once per LDIF file for performance
- l = ldap.initialize( 'ldapi://' )
- l.sasl_interactive_bind_s('', ldap.sasl.external())
-
- if mod is None:
- callback = partial(processEntry,module,l)
- else:
- 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:
- module.fail_json(rv=e.returncode, msg=e.output.rstrip())
- except ldap.LDAPError, 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:
- module.fail_json(msg=str(e))
-
- module.exit_json(changed=changed)
-
-
-# this is magic, see lib/ansible/module_common.py
-#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
-main()