def ldap_connect(settings, use_cache=True): """Establishes an LDAP connection. Establishes a connection to the LDAP server from the `uri` in the ``settings``. To establish a connection, the settings must be specified: - ``uri``: valid URI which points to a LDAP server, - ``bind_dn``: `dn` used to initially bind every LDAP connection - ``bind_password``" password used for the initial bind - ``tls``: ``True`` if the connection should use TLS encryption - ``starttls``: ``True`` to negotiate TLS with the server `Note`: ``starttls`` is ignored if the URI uses LDAPS and ``tls`` is set to ``True``. This function re-uses an existing LDAP connection if there is one available in the application context, unless caching is disabled. :param settings: dict -- The settings for a LDAP provider. :param use_cache: bool -- If the connection should be cached. :return: The ldap connection. """ if use_cache: cache = _get_ldap_cache() cache_key = frozenset((k, hash(v)) for k, v in iteritems(settings) if k in conn_keys) conn = cache.get(cache_key) if conn is not None: return conn uri_info = urlparse(settings['uri']) use_ldaps = uri_info.scheme == 'ldaps' credentials = (settings['bind_dn'], settings['bind_password']) ldap_connection = ReconnectLDAPObject(settings['uri']) ldap_connection.protocol_version = ldap.VERSION3 ldap_connection.set_option(ldap.OPT_REFERRALS, 0) ldap_connection.set_option(ldap.OPT_X_TLS, ldap.OPT_X_TLS_DEMAND if use_ldaps else ldap.OPT_X_TLS_NEVER) if settings['cert_file']: ldap_connection.set_option(ldap.OPT_X_TLS_CACERTFILE, settings['cert_file']) ldap_connection.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_DEMAND if settings['verify_cert'] else ldap.OPT_X_TLS_ALLOW) # force the creation of a new TLS context. This must be the last TLS option. # see: http://stackoverflow.com/a/27713355/298479 ldap_connection.set_option(ldap.OPT_X_TLS_NEWCTX, 0) if use_ldaps and settings['starttls']: warn("Unable to start TLS, LDAP connection already secured over SSL (LDAPS)") elif settings['starttls']: ldap_connection.start_tls_s() # TODO: allow anonymous bind ldap_connection.simple_bind_s(*credentials) if use_cache: cache[cache_key] = ldap_connection return ldap_connection
def ldap_connect(settings, use_cache=True): """Establishes an LDAP connection. Establishes a connection to the LDAP server from the `uri` in the ``settings``. To establish a connection, the settings must be specified: - ``uri``: valid URI which points to a LDAP server, - ``bind_dn``: `dn` used to initially bind every LDAP connection - ``bind_password``" password used for the initial bind - ``tls``: ``True`` if the connection should use TLS encryption - ``starttls``: ``True`` to negotiate TLS with the server `Note`: ``starttls`` is ignored if the URI uses LDAPS and ``tls`` is set to ``True``. This function re-uses an existing LDAP connection if there is one available in the application context, unless caching is disabled. :param settings: dict -- The settings for a LDAP provider. :param use_cache: bool -- If the connection should be cached. :return: The ldap connection. """ if use_cache: cache = _get_ldap_cache() cache_key = frozenset( (k, hash(v)) for k, v in iteritems(settings) if k in conn_keys) conn = cache.get(cache_key) if conn is not None: return conn uri_info = urlparse(settings['uri']) use_ldaps = uri_info.scheme == 'ldaps' credentials = (settings['bind_dn'], settings['bind_password']) ldap_connection = ReconnectLDAPObject(settings['uri']) ldap_connection.protocol_version = ldap.VERSION3 ldap_connection.set_option(ldap.OPT_REFERRALS, 0) ldap_connection.set_option( ldap.OPT_X_TLS, ldap.OPT_X_TLS_DEMAND if use_ldaps else ldap.OPT_X_TLS_NEVER) ldap_connection.set_option( ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_DEMAND if settings['verify_cert'] else ldap.OPT_X_TLS_ALLOW) # force the creation of a new TLS context. This must be the last TLS option. # see: http://stackoverflow.com/a/27713355/298479 ldap_connection.set_option(ldap.OPT_X_TLS_NEWCTX, 0) if use_ldaps and settings['starttls']: warn( "Unable to start TLS, LDAP connection already secured over SSL (LDAPS)" ) elif settings['starttls']: ldap_connection.start_tls_s() # TODO: allow anonymous bind ldap_connection.simple_bind_s(*credentials) if use_cache: cache[cache_key] = ldap_connection return ldap_connection
class CSHLDAP: __domain__ = "csh.rit.edu" def __init__(self, bind_dn, bind_pw, batch_mods=False, sasl=False, ro=False): """Handler for bindings to CSH LDAP. Keyword arguments: batch_mods -- whether or not to batch LDAP writes (default False) sasl -- whether or not to bypass bind_dn and bind_pw and use SASL bind ro -- whether or not CSH LDAP is in read only mode (default False) """ if ro: print("########################################\n" "# #\n" "# CSH LDAP IS IN READ ONLY MODE #\n" "# #\n" "########################################") ldap_srvs = srvlookup.lookup("ldap", "tcp", self.__domain__) ldap_uris = "" for uri in ldap_srvs: ldap_uris += "ldaps://"+uri.hostname+"," self.__con__ = ReconnectLDAPObject(ldap_uris) # Allow connections with self-signed certs self.__con__.set_option(self.__con__.OPT_X_TLS_REQUIRE_CERT, self.__con__.OPT_X_TLS_ALLOW) if sasl: self.__con__.sasl_non_interactive_bind_s('') else: self.__con__.simple_bind_s(bind_dn, bind_pw) self.__mod_queue__ = {} self.__pending_mod_dn__ = [] self.__batch_mods__ = batch_mods self.__ro__ = ro def get_member(self, val, uid=False): """Get a CSHMember object. Arguments: val -- the uuid (or uid) of the member Keyword arguments: uid -- whether or not val is a uid (default False) """ return CSHMember(self, val, uid) def get_member_ibutton(self, val): """Get a CSHMember object. Arguments: val -- the iButton ID of the member Returns: None if the iButton supplied does not correspond to a CSH Member """ members = self.__con__.search_s( CSHMember.__ldap_user_ou__, ldap.SCOPE_SUBTREE, "(ibutton=%s)" % val, ['ipaUniqueID']) if members: return CSHMember( self, members[0][1]['ipaUniqueID'][0].decode('utf-8'), False) return None def get_member_slackuid(self, slack): """Get a CSHMember object. Arguments: slack -- the Slack UID of the member Returns: None if the Slack UID provided does not correspond to a CSH Member """ members = self.__con__.search_s( CSHMember.__ldap_user_ou__, ldap.SCOPE_SUBTREE, "(slackuid=%s)" % slack, ['ipaUniqueID']) if members: return CSHMember( self, members[0][1]['ipaUniqueID'][0].decode('utf-8'), False) return None def get_group(self, val): """Get a CSHGroup object. Arguments: val -- the cn of the group """ return CSHGroup(self, val) def get_con(self): """Get the PyLDAP Connection""" return self.__con__ def get_directorship_heads(self, val): """Get the head of a directorship Arguments: val -- the cn of the directorship """ __ldap_group_ou__ = "cn=groups,cn=accounts,dc=csh,dc=rit,dc=edu" res = self.__con__.search_s( __ldap_group_ou__, ldap.SCOPE_SUBTREE, "(cn=eboard-%s)" % val, ['member']) ret = [] for member in res[0][1]['member']: try: ret.append(member.decode('utf-8')) except UnicodeDecodeError: ret.append(member) except KeyError: continue return [CSHMember(self, dn.split('=')[1].split(',')[0], True) for dn in ret] def enqueue_mod(self, dn, mod): """Enqueue a LDAP modification. Arguments: dn -- the distinguished name of the object to modify mod -- an ldap modfication entry to enqueue """ # mark for update if dn not in self.__pending_mod_dn__: self.__pending_mod_dn__.append(dn) self.__mod_queue__[dn] = [] self.__mod_queue__[dn].append(mod) def flush_mod(self): """Flush all pending LDAP modifications.""" for dn in self.__pending_mod_dn__: try: if self.__ro__: for mod in self.__mod_queue__[dn]: if mod[0] == ldap.MOD_DELETE: mod_str = "DELETE" elif mod[0] == ldap.MOD_ADD: mod_str = "ADD" else: mod_str = "REPLACE" print("{} VALUE {} = {} FOR {}".format(mod_str, mod[1], mod[2], dn)) else: self.__con__.modify_s(dn, self.__mod_queue__[dn]) except ldap.TYPE_OR_VALUE_EXISTS: print("Error! Conflicting Batch Modification: %s" % str(self.__mod_queue__[dn])) continue except ldap.NO_SUCH_ATTRIBUTE: print("Error! Conflicting Batch Modification: %s" % str(self.__mod_queue__[dn])) continue self.__mod_queue__[dn] = None self.__pending_mod_dn__ = []
class DatabaseWrapper(BaseDatabaseWrapper): vendor = 'ldap' # NOTE: These are copied from the mysql DatabaseWrapper operators = { 'exact': '= %s', 'iexact': 'LIKE %s', 'contains': 'LIKE BINARY %s', 'icontains': 'LIKE %s', 'regex': 'REGEXP BINARY %s', 'iregex': 'REGEXP %s', 'gt': '> %s', 'gte': '>= %s', 'lt': '< %s', 'lte': '<= %s', 'startswith': 'LIKE BINARY %s', 'endswith': 'LIKE BINARY %s', 'istartswith': 'LIKE %s', 'iendswith': 'LIKE %s', } def __init__(self, *args, **kwargs): super(DatabaseWrapper, self).__init__(*args, **kwargs) self.charset = "utf-8" self.creation = DatabaseCreation(self) self.features = DatabaseFeatures(self) if django.VERSION > (1, 4): self.ops = DatabaseOperations(self) else: self.ops = DatabaseOperations() self.settings_dict['SUPPORTS_TRANSACTIONS'] = True self.autocommit = True def close(self): if hasattr(self, 'validate_thread_sharing'): # django >= 1.4 self.validate_thread_sharing() if self.connection is not None: self.connection.unbind_s() self.connection = None def ensure_connection(self): if self.connection is None: #self.connection = ldap.initialize(self.settings_dict['NAME']) self.connection = ReconnectLDAPObject(self.settings_dict['NAME']) options = self.settings_dict.get('CONNECTION_OPTIONS', {}) for opt, value in options.items(): self.connection.set_option(opt, value) if self.settings_dict.get('TLS', False): self.connection.start_tls_s() self.connection.simple_bind_s( self.settings_dict['USER'], self.settings_dict['PASSWORD']) def _commit(self): pass def _cursor(self): self.ensure_connection() return DatabaseCursor(self.connection) def _rollback(self): pass def _set_autocommit(self, autocommit): pass def add_s(self, dn, modlist): cursor = self._cursor() return cursor.connection.add_s(dn.encode(self.charset), modlist) def delete_s(self, dn): cursor = self._cursor() return cursor.connection.delete_s(dn.encode(self.charset)) def modify_s(self, dn, modlist): cursor = self._cursor() return cursor.connection.modify_s(dn.encode(self.charset), modlist) def rename_s(self, dn, newrdn): cursor = self._cursor() return cursor.connection.rename_s(dn.encode(self.charset), newrdn.encode(self.charset)) def search_s(self, base, scope, filterstr='(objectClass=*)', attrlist=None): cursor = self._cursor() results = cursor.connection.search_s(base, scope, filterstr.encode(self.charset), attrlist) output = [] for dn, attrs in results: # skip referrals if dn is not None: output.append((dn.decode(self.charset), attrs)) return output
class LDAPLookup: """ Wraps ldap library query Args: ldap_url: LDAP server in the form of 'ldap://ldaphost' ldap_base: LDAP base for search ('ou=users,dc=department,dc=org') ldap_retry_max: LDAP number of reconnect attempts ldap_retry_delay: LDAP seconds between reconnect attempts """ DEFAULT_QUERY_FIELDS: List[str] = ['uid'] DEFAULT_RETURN_FIELDS: List[str] = ['uid', 'cn', 'mail'] def __init__(self, ldap_url: str, ldap_base: str, ldap_retry_max: int = 3, ldap_retry_delay: float = 5.0): self.ldap_url = ldap_url self.ldap_base = ldap_base self.ldap_retry_max = ldap_retry_max self.ldap_retry_delay = ldap_retry_delay self._ldap_client = None # lazy client init @property def ldap_client(self): if not self._ldap_client: self._ldap_client = ReconnectLDAPObject( self.ldap_url, retry_max=self.ldap_retry_max, retry_delay=self.ldap_retry_delay) self._ldap_client.set_option(ldap.OPT_PROTOCOL_VERSION, 3) self._ldap_client.set_option(ldap.OPT_REFERRALS, 0) return self._ldap_client def query( self, query: str, query_fields: List[str] = None, return_fields: List[str] = None, raise_exception: bool = False, ) -> List[dict]: """ Perform LDAP query Args: query: String to search query_fields: Which LDAP fields to search in return_fields: What LDAP fields to return raise_exception: If True - raise exception if no results Returns: List if dicts with LDAP results Example: [ {'uid': 'us1', 'cn': 'user 1', 'mail': '*****@*****.**'}, {'uid': 'us2', 'cn': 'user 2', 'mail': '*****@*****.**'} ] Raises: LDAPQueryNotFoundError: No result while raise_exception True """ query = query.rstrip('*') if not query_fields: query_fields = self.DEFAULT_QUERY_FIELDS if not return_fields: return_fields = self.DEFAULT_RETURN_FIELDS if len(query_fields) == 1: query_string = f'{query_fields[0]}={query}' else: # Example: (|(cn=query*)(sn=query*)(mail=query*)) field_queries = [ f'({field}={query}*)' for field in query_fields if field ] query_string = '(|%s)' % ''.join(field_queries) res = self.ldap_client.search_s(self.ldap_base, ldap.SCOPE_SUBTREE, query_string, return_fields) if raise_exception and not res: raise LDAPQueryNotFoundError(f'Query not found in LDAP: {query}') # Extract first values, convert from bytes return [{k: v[0].decode('utf-8') for k, v in record[1].items()} for record in res]