Esempio n. 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)
Esempio n. 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
Esempio n. 3
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
Esempio n. 4
0
def get_tapis_ldap_connection():
    """
    Convenience wrapper function to get an ldap connection to the Tapis dev ldap server.
    :return:
    """
    logger.debug(
        f"get_tapis_ldap_connection for: {tapis_ldap['server']}:{tapis_ldap['port']}"
    )
    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)
Esempio n. 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}'
Esempio n. 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
Esempio n. 7
0
    def get_derived_values(cls, data):
        """
        Computes derived values for the access token from input and defaults.
        :param data:
        :return: dict (result)
        """
        # convert required fields to their data model attributes -
        try:
            result = {
                'tenant_id': data.token_tenant_id,
                'username': data.token_username,
                'account_type': data.account_type,
            }
        except KeyError as e:
            logger.error(f"Missing required token attribute; KeyError: {e}")
            raise DAOError("Missing required token attribute.")

        # service tokens must also have a target_site claim:
        if result['account_type'] == 'service':
            if hasattr(data, 'target_site_id'):
                result['target_site_id'] = data.target_site_id
            else:
                raise errors.InvalidTokenClaimsError(
                    "The target_site_id claim is required for 'service' tokens."
                )

        # generate a jti
        result['jti'] = str(uuid.uuid4())

        # compute the subject from the parts
        result['sub'] = TapisToken.compute_sub(result['tenant_id'],
                                               result['username'])
        tenant = tenants.get_tenant_config(result['tenant_id'])
        # derive the issuer from the associated config for the tenant.
        result['iss'] = tenant.token_service

        # compute optional fields -
        access_token_ttl = getattr(data, 'access_token_ttl', None)
        if not access_token_ttl or access_token_ttl <= 0:
            access_token_ttl = tenant.access_token_ttl
        result['ttl'] = access_token_ttl
        result['exp'] = TapisToken.compute_exp(access_token_ttl)

        delegation = getattr(data, 'delegation_token', False)
        result['delegation'] = delegation
        # when creating a delegation token, the components needed to create the delegation sub are required:
        if delegation:
            try:
                delegation_tenant_id = data.delegation_sub_tenant_id
                delegation_username = data.delegation_sub_username
            except (AttributeError, KeyError) as e:
                logger.error(
                    f"Missing required delegation token attribute; KeyError: {e}"
                )
                raise DAOError(
                    "Missing required delegation token attribute; both delegation_sub_tenant_id and "
                    "delegation_sub_username are required when generating a delegation token."
                )
            result['delegation_sub'] = TapisToken.compute_sub(
                delegation_tenant_id, delegation_username)
        if hasattr(data, 'claims'):
            # result.update(data.claims)
            result['extra_claims'] = data.claims
        return result
Esempio n. 8
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
Esempio n. 9
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)