Add very simple LDAP authentication plugin
authorSimo Sorce <simo@redhat.com>
Thu, 28 Aug 2014 18:59:13 +0000 (14:59 -0400)
committerPatrick Uiterwijk <puiterwijk@redhat.com>
Wed, 24 Sep 2014 18:51:53 +0000 (20:51 +0200)
Uses python-ldap to perform a simple bind after connecting to
the LDAP server using (by default) a TLS encrypted connection.

Signed-off-by: Simo Sorce <simo@redhat.com>
Reviewed-by: Patrick Uiterwijk <puiterwijk@redhat.com>
ipsilon/info/infoldap.py [new file with mode: 0755]
ipsilon/login/authldap.py [new file with mode: 0755]

diff --git a/ipsilon/info/infoldap.py b/ipsilon/info/infoldap.py
new file mode 100755 (executable)
index 0000000..6d710bd
--- /dev/null
@@ -0,0 +1,172 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2014 Ipsilon Project Contributors
+#
+# See the file named COPYING for the project license
+
+from ipsilon.info.common import InfoProviderBase
+from ipsilon.info.common import InfoProviderInstaller
+from ipsilon.util.plugin import PluginObject
+from ipsilon.util.log import Log
+import ldap
+
+
+class InfoProvider(InfoProviderBase, Log):
+
+    def __init__(self):
+        super(InfoProvider, self).__init__()
+        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'
+            ],
+        }
+
+    @property
+    def server_url(self):
+        return self.get_config_value('server url')
+
+    @property
+    def tls(self):
+        return self.get_config_value('tls')
+
+    @property
+    def bind_dn(self):
+        return self.get_config_value('bind dn')
+
+    @property
+    def bind_password(self):
+        return self.get_config_value('bind password')
+
+    @property
+    def user_dn_tmpl(self):
+        return self.get_config_value('user dn template')
+
+    def _ldap_bind(self):
+
+        tls = self.tls.lower()
+        tls_req_opt = None
+        if tls == "never":
+            tls_req_opt = ldap.OPT_X_TLS_NEVER
+        elif tls == "demand":
+            tls_req_opt = ldap.OPT_X_TLS_DEMAND
+        elif tls == "allow":
+            tls_req_opt = ldap.OPT_X_TLS_ALLOW
+        elif tls == "try":
+            tls_req_opt = ldap.OPT_X_TLS_TRY
+        if tls_req_opt is not None:
+            ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, tls_req_opt)
+
+        conn = ldap.initialize(self.server_url)
+
+        if tls != "notls":
+            if not self.server_url.startswith("ldaps"):
+                conn.start_tls_s()
+
+        conn.simple_bind_s(self.bind_dn, self.bind_password)
+
+        return conn
+
+    def get_user_data_from_conn(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]
+
+    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)
+
+        return userattrs
+
+
+class Installer(InfoProviderInstaller):
+
+    def __init__(self):
+        super(Installer, self).__init__()
+        self.name = 'nss'
+
+    def install_args(self, group):
+        group.add_argument('--info-ldap', choices=['yes', 'no'], default='no',
+                           help='Use LDAP to populate user attrs')
+        group.add_argument('--info-ldap-server-url', action='store',
+                           help='LDAP Server Url')
+        group.add_argument('--info-ldap-bind-dn', action='store',
+                           help='LDAP Bind DN')
+        group.add_argument('--info-ldap-bind-pwd', action='store',
+                           help='LDAP Bind Password')
+        group.add_argument('--info-ldap-user-dn-template', action='store',
+                           help='LDAP User DN Template')
+
+    def configure(self, opts):
+        if opts['info_ldap'] != 'yes':
+            return
+
+        # Add configuration data to database
+        po = PluginObject()
+        po.name = 'ldap'
+        po.wipe_data()
+        po.wipe_config_values(self.facility)
+        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:
+            config['bind password'] = opts['info_ldap_bind_pwd']
+        if 'info_ldap_user_dn_template' in opts:
+            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(',')
+        else:
+            order = []
+        order.append('ldap')
+        globalconf['order'] = ','.join(order)
+        po.set_config(globalconf)
+        po.save_plugin_config(self.facility)
diff --git a/ipsilon/login/authldap.py b/ipsilon/login/authldap.py
new file mode 100755 (executable)
index 0000000..0d70479
--- /dev/null
@@ -0,0 +1,221 @@
+#!/usr/bin/python
+#
+# Copyright (C) 2014  Ipsilon Contributors, see COPYING for license
+
+from ipsilon.login.common import LoginFormBase, LoginManagerBase
+from ipsilon.login.common import FACILITY
+from ipsilon.util.plugin import PluginObject
+from ipsilon.util.log import Log
+from ipsilon.info.infoldap import InfoProvider as LDAPInfo
+import ldap
+
+
+class LDAP(LoginFormBase, Log):
+
+    def __init__(self, site, mgr, page):
+        super(LDAP, self).__init__(site, mgr, page)
+        self.ldap_info = None
+
+    def _ldap_connect(self):
+
+        tls = self.lm.tls.lower()
+        tls_req_opt = None
+        if tls == "never":
+            tls_req_opt = ldap.OPT_X_TLS_NEVER
+        elif tls == "demand":
+            tls_req_opt = ldap.OPT_X_TLS_DEMAND
+        elif tls == "allow":
+            tls_req_opt = ldap.OPT_X_TLS_ALLOW
+        elif tls == "try":
+            tls_req_opt = ldap.OPT_X_TLS_TRY
+        if tls_req_opt is not None:
+            ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, tls_req_opt)
+
+        conn = ldap.initialize(self.lm.server_url)
+
+        if tls != "notls":
+            if not self.lm.server_url.startswith("ldaps"):
+                conn.start_tls_s()
+        return conn
+
+    def _authenticate(self, username, password):
+
+        conn = self._ldap_connect()
+        dn = self.lm.bind_dn_tmpl % {'username': username}
+        conn.simple_bind_s(dn, password)
+
+        # Bypass info plugins to optimize data retrieval
+        if self.lm.get_user_info:
+            self.lm.info = None
+
+            if not self.ldap_info:
+                self.ldap_info = LDAPInfo()
+
+            return self.ldap_info.get_user_data_from_conn(conn, dn)
+
+        return None
+
+    def POST(self, *args, **kwargs):
+        username = kwargs.get("login_name")
+        password = kwargs.get("login_password")
+        userattrs = None
+        authed = False
+        errmsg = None
+
+        if username and password:
+            try:
+                userattrs = self._authenticate(username, password)
+                authed = True
+            except Exception, e:  # pylint: disable=broad-except
+                errmsg = "Authentication failed"
+                self.error("Exception raised: [%s]" % repr(e))
+        else:
+            errmsg = "Username or password is missing"
+            self.error(errmsg)
+
+        if authed:
+            return self.lm.auth_successful(self.trans, username, 'password',
+                                           userdata=userattrs)
+
+        context = self.create_tmpl_context(
+            username=username,
+            error=errmsg,
+            error_password=not password,
+            error_username=not username
+        )
+        # pylint: disable=star-args
+        return self._template('login/form.html', **context)
+
+
+class LoginManager(LoginManagerBase):
+
+    def __init__(self, *args, **kwargs):
+        super(LoginManager, self).__init__(*args, **kwargs)
+        self.name = 'ldap'
+        self.path = 'ldap'
+        self.page = None
+        self.ldap_info = None
+        self.service_name = 'ldap'
+        self.description = """
+Form based login Manager that uses a simple bind LDAP operation to perform
+authentication. """
+        self._options = {
+            'help text': [
+                """ The text shown to guide the user at login time. """,
+                'string',
+                'Insert your Username and Password and then submit.'
+            ],
+            'username text': [
+                """ The text shown to ask for the username in the form. """,
+                'string',
+                'Username'
+            ],
+            'password text': [
+                """ The text shown to ask for the password in the form. """,
+                'string',
+                'Password'
+            ],
+            '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 template': [
+                """ Template to turn username into DN. """,
+                'string',
+                'uid=%(username)s,ou=People,dc=example,dc=com'
+            ],
+            'get user info': [
+                """ Get user info via ldap directly after auth (Yes/No) """,
+                'string',
+                'Yes'
+            ],
+        }
+        self.conf_opt_order = ['server url', 'bind dn template',
+                               'get user info', 'tls', 'username text',
+                               'password text', 'help text']
+
+    @property
+    def help_text(self):
+        return self.get_config_value('help text')
+
+    @property
+    def username_text(self):
+        return self.get_config_value('username text')
+
+    @property
+    def password_text(self):
+        return self.get_config_value('password text')
+
+    @property
+    def server_url(self):
+        return self.get_config_value('server url')
+
+    @property
+    def tls(self):
+        return self.get_config_value('tls')
+
+    @property
+    def get_user_info(self):
+        return (self.get_config_value('get user info').lower() == 'yes')
+
+    @property
+    def bind_dn_tmpl(self):
+        return self.get_config_value('bind dn template')
+
+    def get_tree(self, site):
+        self.page = LDAP(site, self, 'login/ldap')
+        return self.page
+
+
+class Installer(object):
+
+    def __init__(self):
+        self.name = 'ldap'
+        self.ptype = 'login'
+
+    def install_args(self, group):
+        group.add_argument('--ldap', choices=['yes', 'no'], default='no',
+                           help='Configure PAM authentication')
+        group.add_argument('--ldap-server-url', action='store',
+                           help='LDAP Server Url')
+        group.add_argument('--ldap-bind-dn-template', action='store',
+                           help='LDAP Bind DN Template')
+
+    def configure(self, opts):
+        if opts['ldap'] != 'yes':
+            return
+
+        # Add configuration data to database
+        po = PluginObject()
+        po.name = 'ldap'
+        po.wipe_data()
+
+        po.wipe_config_values(FACILITY)
+        config = dict()
+        if 'ldap_server_url' in opts:
+            config['server url'] = opts['ldap_server_url']
+        if 'ldap_bind_dn_template' in opts:
+            config['bind dn template'] = opts['ldap_bind_dn_template']
+        config['tls'] = 'Demand'
+        po.set_config(config)
+        po.save_plugin_config(FACILITY)
+
+        # Update global config to add login plugin
+        po = PluginObject()
+        po.name = 'global'
+        globalconf = po.get_plugin_config(FACILITY)
+        if 'order' in globalconf:
+            order = globalconf['order'].split(',')
+        else:
+            order = []
+        order.append('ldap')
+        globalconf['order'] = ','.join(order)
+        po.set_config(globalconf)
+        po.save_plugin_config(FACILITY)