def from_ldap3_entry(cls, tenant_id, entry): """ Create an LdapUser object from an ldap3 cn obect. {:param tenant_id: (str) The tenant_id associated with this entry. :param entry: :return: LdapUser """ # the attributes of the LdapUser object attrs = {} try: cn = entry['cn'][0] except Exception as e: logger.error(f"Got exception trying to get cn from entry; entry: {entry}") raise DAOError("Unable to parse LDAP user objects.") # the cn is the uid/username attrs['uid'] = cn # compute the DN from the CN tenant = tenants.get_tenant_config(tenant_id) ldap_user_dn = tenant.ldap_user_dn attrs['dn'] = f'cn={cn},{ldap_user_dn}' # the remaining params are computed directly in the same way -- as the first entry in an array of bytes params = ['givenName', 'sn', 'mail', 'telephoneNumber', 'mobile', 'createTimestamp', 'uidNumber', 'userPassword'] for param in params: if param in entry and entry[param][0]: # some parans are returned as bytes and others as strings: val = entry[param][0] if hasattr(val, 'decode'): attrs[param] = val.decode('utf-8') else: attrs[param] = val # now, construct and return a LdapUser object return LdapUser(**attrs)
def save(self, conn): """ Save an LdapUser object in an LDAP server with connection, conn. :param conn (ldap3.core.connection.Connection) A connection to the ldap server. :return: """ # first, get the ldap representation of this object and remove any fields not allowed to be passed to # ldap on save: repr = self.serialize_to_ldap repr.pop('create_time', None) repr.pop('dn') try: result = conn.add(self.dn, self.object_class, repr) except Exception as e: msg = f'Got exception trying to add a user to LDAP; exception: {e}' logger.error(msg) raise DAOError("Unable to communicate with LDAP database when trying to save user account.") if not result: msg = f'Got False result trying to add a user with dn {self.dn} to LDAP; error data: {conn.result}' logger.error(msg) raise DAOError("Unable to save user account in LDAP database; " "Required fields could be missing or improperly formatted.") # the object was saved successfully so we can now return it: return True
def get_tapis_ldap_connection(): """ Convenience wrapper function to get an ldap connection to the Tapis dev ldap server. :return: """ try: return get_ldap_connection(ldap_server=tapis_ldap['server'], ldap_port=tapis_ldap['port'], bind_dn=tapis_ldap['bind_dn'], bind_password=tapis_ldap['bind_password'], use_ssl=tapis_ldap['use_ssl']) except LDAPBindError as e: logger.debug(f'Invalid Tapis bind credential: {e}') raise InvalidPasswordError("Invalid username/password combination.") except Exception as e: msg = f"Got exception trying to create connection object to Tapis LDAP. e: {e}" logger.error(msg) raise DAOError(msg)
def add_tapis_ou(ou): """ Add an LDAP record representing an Organizational Unit (ou) to the Tapis LDAP. :param ou: (LdapOU) The OU object to add. :return: """ conn = get_tapis_ldap_connection() try: result = conn.add(ou.dn, ou.object_class) except Exception as e: msg = f'got an error trying to add an ou. Exception: {e}; ou.dn: {ou.dn}; ou.object_class: {ou.object_class}' logger.error(msg) if not result: msg = f'Got False result trying to add OU to LDAP; error data: {conn.result}' logger.error(msg) raise DAOError( "Unable to add OU to LDAP database; " "Required fields could be missing or improperly formatted.") return True
def get_dn(tenant_id, username): """ Get the DN for a specific username within a tenant. :param tenant_id: :param username: :return: """ tenant = tenants.get_tenant_config(tenant_id) ldap_user_dn = tenant.ldap_user_dn if '${username},' in tenant.ldap_user_dn: parts = tenant.ldap_user_dn.split('${username},') if not len(parts) == 2: raise DAOError("Unable to calculate search DN.") ldap_user_dn = parts[1] return f'{parts[0]}{username},{ldap_user_dn}' # needed for test ldap: if tenant.ldap_bind_dn.startswith('cn'): return f'cn={username},{ldap_user_dn}' # needed for tacc: else: return f'uid={username},{ldap_user_dn}'
def list_tapis_ous(): """ List the OUs associated with the Tapis LDAP server. :return: """ conn = get_tapis_ldap_connection() try: # search for all cn's under the tapis tenants base_dn and pull back all attributes result = conn.search(conf.dev_ldap_tenants_base_dn, '(ou=*)', attributes=['*']) except Exception as e: msg = f'Got an exception trying to list Tapis OUs. Exception: {e}' logger.error(msg) raise DAOError(msg) if not result: msg = f'Got an error trying to list Tapis OUs. message: {conn.result}' logger.error(msg) # return the results - result = [] for ent in conn.entries: result.append(ent.entry_attributes_as_dict) return result
def get_tenant_user(tenant_id, username): """ Get the profile of a specific user in a tenant. :param tenant_id: :param username: :return: """ logger.debug( f"top of get_tenant_user; tenant_id: {tenant_id}; username: {username}" ) tenant = tenants.get_tenant_config(tenant_id) if hasattr(tenant, 'ldap_bind_dn') and hasattr(tenant, 'ldap_bind_credential'): logger.debug(f"tenant {tenant} had ldap bind credentials; using those") conn = get_tenant_ldap_connection( tenant_id, bind_dn=tenant.ldap_bind_dn, bind_password=tenant.ldap_bind_credential) else: logger.debug(f"tenant {tenant} did NOT have ldap bind credentials...") conn = get_tenant_ldap_connection(tenant_id) tenant_base_dn = tenant.ldap_user_dn logger.debug( f"ldap_user_dn on tenant record: {tenant_base_dn}. Checking if we need to replace the " f"$username token...") # check if the ldap_user_dn on the tenant record has a ${username} token in it -- if so, this is providing # the default user filter prefix and we need to remove it to form the tenant_base_dn. default_user_filter_prefix = '(cn=*)' if '${username},' in tenant.ldap_user_dn: parts = tenant.ldap_user_dn.split('${username},') if not len(parts) == 2: raise DAOError("Unable to calculate search DN.") tenant_base_dn = parts[1] default_user_filter_prefix = f'({parts[0]}=*)' logger.debug(f"default_user_filter_prefix: {default_user_filter_prefix}") # this gets the custom authenticator config for the ldap -- custom_ldap_config = get_custom_ldap_config(tenant_id) user_search_filter = custom_ldap_config.get('user_search_filter') logger.debug( f"user_search_filter from custom ldap config: {user_search_filter}") # if user_search_filter is not specified, look for a user_search_prefix and/or user_search_supplemental_filter if not user_search_filter: # if user_search_prefix is not set, we default to using '(cn=*)' user_search_prefix = custom_ldap_config.get( 'user_search_prefix', default_user_filter_prefix) logger.debug( f"user_search_prefix from custom ldap config: {user_search_prefix}" ) user_search_supplemental_filter = custom_ldap_config.get( 'user_search_supplemental_filter') logger.debug( f"user_search_supplemental_filter from custom ldap config: {user_search_supplemental_filter}" ) if user_search_supplemental_filter: user_search_filter = f'(&{user_search_prefix}{user_search_supplemental_filter})' else: user_search_filter = user_search_prefix # the user_search_filter is formatted with a wildcard ( star (*) character) for retrieving all profiles, but # here we only want to retrieve a single profile, so we need to replace it with the username: user_search_filter = user_search_filter.replace('*', username) logger.debug(f"final custom user_search_filter: {user_search_filter}") logger.debug( f'searching with params: {tenant_base_dn}; user_filter: {user_search_filter}' ) result = conn.search(f'{tenant_base_dn}', user_search_filter, attributes=['*']) if not result: # it is possible to get a "success" result when there are no users in the OU - if hasattr(conn.result, 'description') and conn.result.description == 'success': return [], None msg = f'Error retrieving user; debug information: {conn.result}' logger.error(msg) raise DAOError(msg) result = [] logger.debug(f'conn.entries: {conn.entries}') user = LdapUser.from_ldap3_entry(tenant_id, conn.entries[0].entry_attributes_as_dict) return user
def list_tenant_users(tenant_id, limit=None, offset=0): """ List users in a tenant :param tenant_id: (str) the tenant id to use. :param limit (int): The maximum number of users to return. :param offset (int): A position to start the paged search. :return: """ logger.debug( f'top of list_tenant_users; tenant_id: {tenant_id}; limit: {limit}; offset: {offset}' ) # this gets the tenant object from the Tenants API cache -- tenant = tenants.get_tenant_config(tenant_id) if hasattr(tenant, 'ldap_bind_db') and hasattr(tenant, 'ldap_bind_credential'): logger.debug(f"tenant {tenant} had ldap bind credentials; using those") conn = get_tenant_ldap_connection( tenant_id, bind_dn=tenant.ldap_bind_dn, bind_password=tenant.ldap_bind_credential) else: conn = get_tenant_ldap_connection(tenant_id) # this gets the custom authenticator config for the ldap -- custom_ldap_config = get_custom_ldap_config(tenant_id) if not limit: limit = custom_ldap_config.get('default_page_limit') if not limit: limit = conf.default_page_limit cookie = None # there are multiple ways to modify the ldap search using the custom_ldap_config. If user_search_filter is provided, # that one is always used. user_search_filter = custom_ldap_config.get('user_search_filter') logger.debug( f"user_search_filter from custom ldap config: {user_search_filter}") # if user_search_filter is not specified, look for a user_search_prefix and/or user_search_supplemental_filter if not user_search_filter: # if user_search_prefix is not set, we default to using '(cn=*)' user_search_prefix = custom_ldap_config.get('user_search_prefix', '(cn=*)') logger.debug( f"user_search_prefix from custom ldap config: {user_search_prefix}" ) user_search_supplemental_filter = custom_ldap_config.get( 'user_search_supplemental_filter') logger.debug( f"user_search_supplemental_filter from custom ldap config: {user_search_supplemental_filter}" ) if user_search_supplemental_filter: user_search_filter = f'(&{user_search_prefix}{user_search_supplemental_filter})' else: user_search_filter = user_search_prefix logger.debug(f"final custom user_search_filter: {user_search_filter}") # the user_dn is always stored on the Tenants API's LDAP record. however, there are two possible user_dn # types: one that includes the user_search_prefix and one that does not. to include the user_search_prefix, the # user_dn will have the form <user_search_prefix>=${username},... user_dn = tenant.ldap_user_dn # if the tenant's user_dn config includes the template variable ${username}, we need to strip it out here and # pull out the user search prefix. if '${username},' in tenant.ldap_user_dn: parts = tenant.ldap_user_dn.split('${username},') if not len(parts) == 2: raise DAOError("Unable to compute LDAP user search DN.") # parts will be split into 'uid=' and 'ou=foo, o=bar, ..." # the user search prefix should therefore be of the form: '(<parts[0])*)' # we only use this for the user_search_filter if the user_search_filter was NOT set above (i.e., if it is still # just the default, (cn=*): if user_search_filter == '(cn=*)': user_search_filter = f'({parts[0]}*)' # regardless of the user_search_filter though, we need to strip out the ${username}, from the user_dn, so # override that now: user_dn = parts[1] logger.debug( f'using user_dn: {user_dn} and user_search_filter: {user_search_filter}' ) # As per RFC2696, the page cookie for paging can only be used by the same connection; we take the following # approach: # if the offset is not 0, we first pull the first <offset> entries to get the cookie, then we get use the returned # cookie to get the actual page of results that we want. if offset > 0: # we only need really need the cookie so we just get the cn attribute result = conn.search(user_dn, user_search_filter, attributes=['cn'], paged_size=offset) if not result: # it is possible to get a "success" result when there are no users in the OU - if hasattr(conn.result, 'get') and conn.result.get('description') == 'success': return [], None msg = f'Error retrieving users; debug information: {conn.result}' logger.error(msg) raise DAOError(msg) cookie = conn.result['controls']['1.2.840.113556.1.4.319']['value'][ 'cookie'] result = conn.search(user_dn, user_search_filter, attributes=['*'], paged_size=limit, paged_cookie=cookie) if not result: # it is possible to get a "success" result when there are no users in the OU - if hasattr(conn.result, 'get') and conn.result.get('description') == 'success': return [], None msg = f'Error retrieving users; debug information: {conn.result}' logger.error(msg) raise DAOError(msg) result = [] for ent in conn.entries: # create LdapUser objects for each entry: user = LdapUser.from_ldap3_entry(tenant_id, ent.entry_attributes_as_dict) result.append(user) return result, offset + len(result)