pam: use a pam object method instead of pam module function
[cascardo/ipsilon.git] / ipsilon / info / infoldap.py
old mode 100755 (executable)
new mode 100644 (file)
index 6d710bd..a197157
@@ -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.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 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.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):
 
     @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')
 
     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()
     def _ldap_bind(self):
 
         tls = self.tls.lower()
@@ -92,31 +109,87 @@ Info plugin that uses LDAP to retrieve user data. """
 
         return conn
 
 
         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!')
         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):
 
     def get_user_attrs(self, user):
-        userattrs = None
         try:
         try:
-            conn = self._ldap_bind()
             dn = self.user_dn_tmpl % {'username': user}
             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):
 
 
 
 class Installer(InfoProviderInstaller):
 
-    def __init__(self):
+    def __init__(self, *pargs):
         super(Installer, self).__init__()
         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',
 
     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')
                            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
         if opts['info_ldap'] != 'yes':
             return
 
         # Add configuration data to database
-        po = PluginObject()
+        po = PluginObject(*self.pargs)
         po.name = 'ldap'
         po.wipe_data()
         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 = 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:
         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['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:
         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