예제 #1
0
 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)
예제 #2
0
 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
예제 #3
0
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)
예제 #4
0
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
예제 #5
0
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}'
예제 #6
0
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
예제 #7
0
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
예제 #8
0
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)