From 4b342843f84ceafd7d404791753923e58f98b557 Mon Sep 17 00:00:00 2001 From: Kendal Montgomery Date: Mar 07 2018 23:27:44 +0000 Subject: Added configuration options and code for optional LDAP functionality to look up user's DN - search for user's DN based on a filter rather than assuming the DN by format string. Updated ldapauth plugin to make a configurable group filter template, and we now pass the group_filter to the info plugin. The info plugin now uses the group_filter if passed in or by default uses the default filter of memberuid=%s where %s is replaced by the username. Signed-off-by: Kendal Montgomery --- diff --git a/ipsilon/info/infoldap.py b/ipsilon/info/infoldap.py index c4b4888..3dce982 100644 --- a/ipsilon/info/infoldap.py +++ b/ipsilon/info/infoldap.py @@ -6,6 +6,7 @@ from ipsilon.util.plugin import PluginObject from ipsilon.util.policy import Policy from ipsilon.util import config as pconfig import ldap +import ldap.filter import subprocess @@ -42,6 +43,14 @@ Info plugin that uses LDAP to retrieve user data. """ 'user dn template', 'Template to turn username into DN.', 'uid=%(username)s,ou=People,dc=example,dc=com'), + pconfig.Condition( + 'lookup dn', + 'Look up user DNs?', + False), + pconfig.String( + 'lookup dn filter template', + 'Search filter template used to look up DNs.', + '(mail=%(username)s)'), pconfig.Pick( 'tls', 'What TLS level show be required', @@ -81,6 +90,14 @@ Info plugin that uses LDAP to retrieve user data. """ return self.get_config_value('user dn template') @property + def lookup_dn(self): + return self.get_config_value('lookup dn') + + @property + def lookup_dn_filter_template(self): + return self.get_config_value('lookup dn filter template') + + @property def base_dn(self): return self.get_config_value('base dn') @@ -126,10 +143,12 @@ Info plugin that uses LDAP to retrieve user data. """ data[name] = value return data - def _get_user_groups(self, conn, base, username): + def _get_user_groups(self, conn, base, username, group_filter=None): + if group_filter is None: + group_filter = "memberuid=%s" % username + self.debug("Searching for groups using filter %s" % group_filter) # TODO: fixme to support RFC2307bis schemas - results = conn.search_s(base, ldap.SCOPE_SUBTREE, - filterstr='memberuid=%s' % username) + results = conn.search_s(base, ldap.SCOPE_SUBTREE, filterstr=group_filter) if results is None or results == []: self.debug('No groups for %s' % username) return [] @@ -139,13 +158,13 @@ Info plugin that uses LDAP to retrieve user data. """ groups.append(r[1]['cn'][0]) return groups - def get_user_data_from_conn(self, conn, dn, base, username): + def get_user_data_from_conn(self, conn, dn, base, username, group_filter): 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) + groups = self._get_user_groups(conn, base, username, group_filter) reply = userattrs reply['_groups'] = groups reply['_extras'] = {'ldap': extras} @@ -155,8 +174,28 @@ Info plugin that uses LDAP to retrieve user data. """ return reply def get_user_attrs(self, user): + username = user try: - dn = self.user_dn_tmpl % {'username': user} + if self.lookup_dn: + conn = self._ldap_bind() + conn.simple_bind_s(self.lookup_bind_dn, self.lookup_bind_pw) + search_filter = self.lookup_dn_filter_template % { 'username': ldap.filter.escape_filter_chars(user) } + result = conn.search_s(self.base_dn, ldap.SCOPE_SUBTREE, search_filter, ["dn"]) + if result is not None and result != []: + if len(result) > 1: + self.error("User query failed to yeild a unique result.") + else: + dn = result[0][0] + # In case we're using get_user_info below, get the UID or CN attribute that uniquely identifies the user... + uidattr = dn.split(',')[0] + uid = uidattr.split('=')[1] + username = uid + else: + self.error("User not found.") + # reset the ldap connection + conn.unbind() + else: + dn = self.user_dn_tmpl % {'username': user} except ValueError as e: self.error( 'DN generation failed with template %s, user %s: %s' @@ -173,7 +212,7 @@ Info plugin that uses LDAP to retrieve user data. """ try: base = self.base_dn conn = self._ldap_bind() - return self.get_user_data_from_conn(conn, dn, base, user) + return self.get_user_data_from_conn(conn, dn, base, username) except ldap.LDAPError as e: self.error( 'LDAP search failed for DN %s on base %s: %s' % diff --git a/ipsilon/login/authldap.py b/ipsilon/login/authldap.py index 6e9afd3..4a0b9d7 100644 --- a/ipsilon/login/authldap.py +++ b/ipsilon/login/authldap.py @@ -7,6 +7,7 @@ from ipsilon.util.log import Log from ipsilon.util import config as pconfig from ipsilon.info.infoldap import InfoProvider as LDAPInfo import ldap +import ldap.filter import subprocess import logging @@ -33,6 +34,8 @@ def ldap_connect(server_url, tls): return conn +def ldap_escape(str_to_escape): + return ldap.filter.escape_filter_chars(str_to_escape) class LDAP(LoginFormBase, Log): @@ -47,6 +50,29 @@ class LDAP(LoginFormBase, Log): conn = self._ldap_connect() dn = self.lm.bind_dn_tmpl % {'username': username} + if self.lm.lookup_dn: + if self.lm.lookup_bind_dn == None or self.lm.lookup_bind_pw == None: + self.error('LDAP Lookup Bind DN or Bind PW value is missing.') + else: + conn.simple_bind_s(self.lm.lookup_bind_dn, self.lm.lookup_bind_pw) + search_filter = self.lm.lookup_dn_filter_template % {'username': ldap_escape(username)} + result = conn.search_s(self.lm.base_dn, ldap.SCOPE_SUBTREE, search_filter, ["dn"]) + if result is not None and result != []: + if len(result) > 1: + self.error("User query failed to yeild a unique result.") + else: + dn = result[0][0] + if self.lm.get_user_info: + # In case we're using get_user_info below, get the UID or CN attribute that uniquely identifies the user... + uidattr = dn.split(',')[0] + uid = uidattr.split('=')[1] + username = uid + else: + self.error("User not found.") + # reset the ldap connection + conn.unbind() + conn = self._ldap_connect() + conn.simple_bind_s(dn, password) # Bypass info plugins to optimize data retrieval @@ -57,8 +83,9 @@ class LDAP(LoginFormBase, Log): self.ldap_info = LDAPInfo(self._site) base = self.lm.base_dn + group_filter = self.lm.group_filter_template % { 'username': username } return self.ldap_info.get_user_data_from_conn(conn, dn, base, - username) + username, group_filter) return None @@ -123,10 +150,30 @@ authentication. """ 'server url', 'The LDAP server url.', 'ldap://example.com'), + pconfig.Condition( + 'lookup dn', + 'Look up user DNs?', + False), + pconfig.String( + 'lookup bind dn', + 'Proxy user used to look up DNs.', + ''), + pconfig.String( + 'lookup bind pw', + 'Password for proxy user used to look up DNs.', + ''), + pconfig.Template( + 'lookup dn filter template', + 'Search filter template used to look up DNs.', + '(mail=%(username)s)'), pconfig.Template( 'bind dn template', 'Template to turn username into DN.', 'uid=%(username)s,ou=People,dc=example,dc=com'), + pconfig.Template( + 'group filter template', + 'Search filter template used to find groups for user.', + '(memberuid=%(username)s)'), pconfig.String( 'base dn', 'The base dn to look for users and groups', @@ -183,6 +230,26 @@ authentication. """ return self.get_config_value('bind dn template') @property + def group_filter_template(self): + return self.get_config_value('group filter template') + + @property + def lookup_dn(self): + return self.get_config_value('lookup dn') + + @property + def lookup_bind_dn(self): + return self.get_config_value('lookup bind dn') + + @property + def lookup_bind_pw(self): + return self.get_config_value('lookup bind pw') + + @property + def lookup_dn_filter_template(self): + return self.get_config_value('lookup dn filter template') + + @property def base_dn(self): return self.get_config_value('base dn')