From f8699581dbcf5ba39a93b6202e577260c498b102 Mon Sep 17 00:00:00 2001 From: Simo Sorce Date: Thu, 28 Aug 2014 14:59:13 -0400 Subject: [PATCH] Add very simple LDAP authentication plugin 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 Reviewed-by: Patrick Uiterwijk --- ipsilon/info/infoldap.py | 172 +++++++++++++++++++++++++++++ ipsilon/login/authldap.py | 221 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 393 insertions(+) create mode 100755 ipsilon/info/infoldap.py create mode 100755 ipsilon/login/authldap.py diff --git a/ipsilon/info/infoldap.py b/ipsilon/info/infoldap.py new file mode 100755 index 0000000..6d710bd --- /dev/null +++ b/ipsilon/info/infoldap.py @@ -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 index 0000000..0d70479 --- /dev/null +++ b/ipsilon/login/authldap.py @@ -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) -- 2.20.1