X-Git-Url: http://git.cascardo.info/?p=cascardo%2Fipsilon.git;a=blobdiff_plain;f=ipsilon%2Finfo%2Finfoldap.py;h=a197157e369f226408c40537be87ae926fc3dd87;hp=6d710bd6606c8010f4da39060a0ed2bd9e4288e0;hb=HEAD;hpb=f8699581dbcf5ba39a93b6202e577260c498b102 diff --git a/ipsilon/info/infoldap.py b/ipsilon/info/infoldap.py old mode 100755 new mode 100644 index 6d710bd..a197157 --- a/ipsilon/info/infoldap.py +++ b/ipsilon/info/infoldap.py @@ -1,51 +1,64 @@ -#!/usr/bin/python -# -# Copyright (C) 2014 Ipsilon Project Contributors -# -# See the file named COPYING for the project license +# Copyright (C) 2014 Ipsilon project Contributors, for license see COPYING from ipsilon.info.common import InfoProviderBase from ipsilon.info.common import InfoProviderInstaller from ipsilon.util.plugin import PluginObject -from ipsilon.util.log import Log +from ipsilon.util.policy import Policy +from ipsilon.util import config as pconfig import ldap +import subprocess -class InfoProvider(InfoProviderBase, Log): +# TODO: fetch mapping from configuration +ldap_mapping = [ + ['cn', 'fullname'], + ['commonname', 'fullname'], + ['sn', 'surname'], + ['mail', 'email'], + ['destinationindicator', 'country'], + ['postalcode', 'postcode'], + ['st', 'state'], + ['statetorprovincename', 'state'], + ['streetaddress', 'street'], + ['telephonenumber', 'phone'], +] - def __init__(self): - super(InfoProvider, self).__init__() + +class InfoProvider(InfoProviderBase): + + def __init__(self, *pargs): + super(InfoProvider, self).__init__(*pargs) + self.mapper = Policy(ldap_mapping) self.name = 'ldap' self.description = """ Info plugin that uses LDAP to retrieve user data. """ - self._options = { - 'server url': [ - """ The LDAP server url """, - 'string', - 'ldap://example.com' - ], - 'tls': [ - " What TLS level show be required " + - "(Demand, Allow, Try, Never, NoTLS) ", - 'string', - 'Demand' - ], - 'bind dn': [ - """ User DN to bind as, if empty uses anonymous bind. """, - 'string', - 'uid=ipsilon,ou=People,dc=example,dc=com' - ], - 'bind password': [ - """ Password to use for bind operation """, - 'string', - 'Password' - ], - 'user dn template': [ - """ Template to turn username into DN. """, - 'string', - 'uid=%(username)s,ou=People,dc=example,dc=com' - ], - } + self.new_config( + self.name, + pconfig.String( + 'server url', + 'The LDAP server url.', + 'ldap://example.com'), + pconfig.Template( + 'user dn template', + 'Template to turn username into DN.', + 'uid=%(username)s,ou=People,dc=example,dc=com'), + pconfig.Pick( + 'tls', + 'What TLS level show be required', + ['Demand', 'Allow', 'Try', 'Never', 'NoTLS'], + 'Demand'), + pconfig.String( + 'bind dn', + 'DN to bind as, if empty uses anonymous bind.', + 'uid=ipsilon,ou=People,dc=example,dc=com'), + pconfig.String( + 'bind password', + 'Password to use for bind operation'), + pconfig.String( + 'base dn', + 'The base dn to look for users and groups', + 'dc=example,dc=com'), + ) @property def server_url(self): @@ -67,6 +80,10 @@ Info plugin that uses LDAP to retrieve user data. """ def user_dn_tmpl(self): return self.get_config_value('user dn template') + @property + def base_dn(self): + return self.get_config_value('base dn') + def _ldap_bind(self): tls = self.tls.lower() @@ -92,31 +109,87 @@ Info plugin that uses LDAP to retrieve user data. """ return conn - def get_user_data_from_conn(self, conn, dn): + def _get_user_data(self, conn, dn): result = conn.search_s(dn, ldap.SCOPE_BASE) if result is None or result == []: raise Exception('User object could not be found!') elif len(result) > 1: raise Exception('No unique user object could be found!') - return result[0][1] + data = dict() + for name, value in result[0][1].iteritems(): + if isinstance(value, list) and len(value) == 1: + value = value[0] + data[name] = value + return data + + def _get_user_groups(self, conn, base, username): + # TODO: fixme to support RFC2307bis schemas + results = conn.search_s(base, ldap.SCOPE_SUBTREE, + filterstr='memberuid=%s' % username) + if results is None or results == []: + self.debug('No groups for %s' % username) + return [] + groups = [] + for r in results: + if 'cn' in r[1]: + groups.append(r[1]['cn'][0]) + return groups + + def get_user_data_from_conn(self, conn, dn, base, username): + reply = dict() + try: + ldapattrs = self._get_user_data(conn, dn) + self.debug('LDAP attrs for %s: %s' % (dn, ldapattrs)) + userattrs, extras = self.mapper.map_attributes(ldapattrs) + groups = self._get_user_groups(conn, base, username) + reply = userattrs + reply['_groups'] = groups + reply['_extras'] = {'ldap': extras} + except Exception, e: # pylint: disable=broad-except + self.error('Error fetching/mapping LDAP user data: %s' % e) + + return reply def get_user_attrs(self, user): - userattrs = None try: - conn = self._ldap_bind() dn = self.user_dn_tmpl % {'username': user} - userattrs = self.get_user_data_from_conn(conn, dn) - except Exception, e: # pylint: disable=broad-except - self.error(e) + except ValueError as e: + self.error( + 'DN generation failed with template %s, user %s: %s' + % (self.user_dn_tmpl, user, e) + ) + return {} + except Exception as e: # pylint: disable=broad-except + self.error( + 'Unhandled error generating DN from %s, user %s: %s' + % (self.user_dn_tmpl, user, e) + ) + return {} - return userattrs + try: + conn = self._ldap_bind() + base = self.base_dn + return self.get_user_data_from_conn(conn, dn, base, user) + except ldap.LDAPError as e: + self.error( + 'LDAP search failed for DN %s on base %s: %s' % + (dn, base, e) + ) + return {} + except Exception as e: # pylint: disable=broad-except + self.error( + 'Unhandled LDAP error for DN %s on base %s: %s' % + (dn, base, e) + ) + return {} class Installer(InfoProviderInstaller): - def __init__(self): + def __init__(self, *pargs): super(Installer, self).__init__() - self.name = 'nss' + self.name = 'ldap' + self.pargs = pargs def install_args(self, group): group.add_argument('--info-ldap', choices=['yes', 'no'], default='no', @@ -129,24 +202,23 @@ class Installer(InfoProviderInstaller): help='LDAP Bind Password') group.add_argument('--info-ldap-user-dn-template', action='store', help='LDAP User DN Template') + group.add_argument('--info-ldap-base-dn', action='store', + help='LDAP Base DN') - def configure(self, opts): + def configure(self, opts, changes): if opts['info_ldap'] != 'yes': return # Add configuration data to database - po = PluginObject() + po = PluginObject(*self.pargs) po.name = 'ldap' po.wipe_data() - po.wipe_config_values(self.facility) + po.wipe_config_values() config = dict() if 'info_ldap_server_url' in opts: config['server url'] = opts['info_ldap_server_url'] elif 'ldap_server_url' in opts: config['server url'] = opts['ldap_server_url'] - config = {'bind dn': opts['info_ldap_bind_dn']} - config = {'bind password': opts['info_ldap_bind_pwd']} - config = {'user dn template': opts['info_ldap_user_dn_template']} if 'info_ldap_bind_dn' in opts: config['bind dn'] = opts['info_ldap_bind_dn'] if 'info_ldap_bind_pwd' in opts: @@ -155,18 +227,26 @@ class Installer(InfoProviderInstaller): config['user dn template'] = opts['info_ldap_user_dn_template'] elif 'ldap_bind_dn_template' in opts: config['user dn template'] = opts['ldap_bind_dn_template'] - config['tls'] = 'Demand' - po.set_config(config) - po.save_plugin_config(self.facility) - - # Replace global config, only one plugin info can be used - po.name = 'global' - globalconf = po.get_plugin_config(self.facility) - if 'order' in globalconf: - order = globalconf['order'].split(',') + if 'info_ldap_tls_level' in opts and opts['info_ldap_tls_level']: + config['tls'] = opts['info_ldap_tls_level'] + elif 'ldap_tls_level' in opts and opts['ldap_tls_level']: + config['tls'] = opts['ldap_tls_level'] else: - order = [] - order.append('ldap') - globalconf['order'] = ','.join(order) - po.set_config(globalconf) - po.save_plugin_config(self.facility) + config['tls'] = 'Demand' + if 'info_ldap_base_dn' in opts and opts['info_ldap_base_dn']: + config['base dn'] = opts['info_ldap_base_dn'] + elif 'ldap_base_dn' in opts and opts['ldap_base_dn']: + config['base dn'] = opts['ldap_base_dn'] + po.save_plugin_config(config) + + # Update global config to add info plugin + po.is_enabled = True + po.save_enabled_state() + + # For selinux enabled platforms permit httpd to connect to ldap, + # ignore if it fails + try: + subprocess.call(['/usr/sbin/setsebool', '-P', + 'httpd_can_connect_ldap=on']) + except Exception: # pylint: disable=broad-except + pass