示例#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 get_tapis_ldap_server_info():
    """
    Returns dictionary of Tapis LDAP server connection information.
    :return: (dict)
    """
    if conf.use_tenants:
        if not conf.dev_ldap_tenant_id:
            msg = "No dev_ldap_tenant_id config provided. Don't know which config to use for the tapis ldap server. Giving up..."
            logger.error(msg)
            raise BaseTapisError(msg)
        dev_tenant = tenants.get_tenant_config(
            tenant_id=conf.dev_ldap_tenant_id)
        # check to see if we have basic LDAP attributes; it is possible we do not in which case we need to
        # exit out immediately.
        if not dev_tenant.get('ldap_url'):
            msg = f"Could not get the dev LDAP config for tenant dev.. It is probably because this authenticator doesn't serve the dev tenant..."
            logger.error(msg)
            raise BaseTapisError(msg)
        return {
            "server": dev_tenant.get('ldap_url'),
            "port": dev_tenant.get('ldap_port'),
            "bind_dn": dev_tenant.get('ldap_bind_dn'),
            "bind_password": dev_tenant.get('ldap_bind_credential'),
            "base_dn": dev_tenant.get('dev_ldap_tenants_base_dn'),
            "use_ssl": dev_tenant.get('ldap_use_ssl')
        }
    else:
        return {
            "server": conf.dev_ldap_url,
            "port": conf.dev_ldap_port,
            "bind_dn": conf.dev_ldap_bind_dn,
            "bind_password": conf.dev_ldap_bind_credential,
            "base_dn": conf.dev_ldap_tenants_base_dn,
            "use_ssl": conf.dev_ldap_use_ssl
        }
示例#3
0
 def get_derived_values(cls, data):
     result = data
     result['jti'] = str(uuid.uuid4())
     refresh_token_ttl = result.pop('refresh_token_ttl', None)
     if not refresh_token_ttl or refresh_token_ttl <= 0:
         tenant = tenants.get_tenant_config(result['tenant_id'])
         refresh_token_ttl = tenant.refresh_token_ttl
     result['ttl'] = refresh_token_ttl
     result['exp'] = TapisToken.compute_exp(refresh_token_ttl)
     return result
示例#4
0
 def sign_token(self):
     """
     Sign the token using the private key associated with the tenant.
     :return:
     """
     tenant = tenants.get_tenant_config(self.tenant_id)
     private_key = tenant.private_key
     self.jwt = jwt.encode(self.claims_to_dict(),
                           private_key,
                           algorithm=self.alg)
     return self.jwt
示例#5
0
def get_tokens_tapis_client():
    """
    Instantiates and returns a tapis client for the Tokens service by generating the service tokens
    using the private key associated with the admin tenant.
    """
    # start with a service client using the convenience function from the common package.
    # we put a 'dummy' jwt here to tell it to skip token generation, since we need to
    # generate our own tokens:
    t = get_service_tapis_client(jwt="dummy",
                                 tenant_id=conf.service_tenant_id,
                                 tenants=tenants)
    # generate our own service tokens ---
    # minimal data needed to create an access token:
    base_token_data = AccessTokenData(jti=uuid.uuid4(),
                                      token_tenant_id=conf.service_tenant_id,
                                      token_username=conf.service_name,
                                      account_type='service')
    # override some defaults --
    base_token_data.access_token_ttl = SERVICE_TOKEN_TTL
    # set up the service tokens object: dictionary mapping of tenant_id to token data for all
    # tenants the Tokens API will need to interact with.
    service_tokens = {
        t: {}
        for t in tenants.get_site_admin_tenants_for_service()
    }
    for tenant_id in service_tokens.keys():
        try:
            target_site_id = tenants.get_tenant_config(
                tenant_id=tenant_id).site_id
        except Exception as e:
            raise common_errors.BaseTapyException(
                f"Got exception computing target site id; e:{e}")
        base_token_data.target_site_id = target_site_id
        token_data = TapisAccessToken.get_derived_values(base_token_data)
        access_token = TapisAccessToken(**token_data)
        access_token.sign_token()
        # create the "access_token" attribute pointing to the raw JWT just as tapipy does
        # in its get_tokens() methods
        access_token.access_token = access_token.jwt
        service_tokens[tenant_id]['access_token'] = access_token

    # attach our service_tokens to the client and return --
    t.service_tokens = service_tokens
    return t
示例#6
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}'
示例#7
0
def get_tenant_ldap_connection(tenant_id, bind_dn=None, bind_password=None):
    """
    Convenience wrapper function to get an ldap connection to the ldap server corresponding to the tenant_id.
    :param tenant_id: (str) The id of the tenant.
    :param bind_dn: (str) Optional dn to use to bind. Pass this to check validity of a username/password.
    :param bind_password (str) Optional password to use to bind. Pass this to check validity of a username/password.
    :return: 
    """
    tenant = tenants.get_tenant_config(tenant_id)
    logger.debug(f"getting ldap connection for tenant {tenant_id}")
    # if we are passed specific bind credentials, use those:
    if not bind_dn is None:
        return get_ldap_connection(ldap_server=tenant.ldap_url,
                                   ldap_port=tenant.ldap_port,
                                   bind_dn=bind_dn,
                                   bind_password=bind_password,
                                   use_ssl=tenant.ldap_use_ssl)
    # otherwise, return the connection associated with the tenant's bind credentials -
    return get_ldap_connection(ldap_server=tenant.ldap_url,
                               ldap_port=tenant.ldap_port,
                               bind_dn=tenant.ldap_bind_dn,
                               bind_password=tenant.ldap_bind_credential,
                               use_ssl=tenant.ldap_use_ssl)
示例#8
0
def get_tapis_ldap_server_info():
    """
    Returns dictionary of Tapis LDAP server connection information.
    :return: (dict)
    """
    if conf.use_tenants:
        dev_tenant = tenants.get_tenant_config(tenant_id='dev')
        return {
            "server": dev_tenant.get('ldap_url'),
            "port": dev_tenant.get('ldap_port'),
            "bind_dn": dev_tenant.get('ldap_bind_dn'),
            "bind_password": dev_tenant.get('ldap_bind_credential'),
            "base_dn": dev_tenant.get('dev_ldap_tenants_base_dn'),
            "use_ssl": dev_tenant.get('ldap_use_ssl')
        }
    else:
        return {
            "server": conf.dev_ldap_url,
            "port": conf.dev_ldap_port,
            "bind_dn": conf.dev_ldap_bind_dn,
            "bind_password": conf.dev_ldap_bind_credential,
            "base_dn": conf.dev_ldap_tenants_base_dn,
            "use_ssl": conf.dev_ldap_use_ssl
        }
示例#9
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
示例#10
0
def check_authz_private_keypair(tenant_id):
    """
    Makes the following set of additional authorization checks:
      1). the tenant_id must be owned by the site where this Tokens API is running.
    and one of the following are true:
      2). the token's tenant_id claim matches the tenant_id being updated. OR
      3). the token's tenant_id claim is for the admin tenant for the site owning the tenant_id being updated.
    """
    # first check if the tenant_id is a tenant that this Tokens API handles
    logger.debug(f"top of check_authz_private_keypair for: {tenant_id}")
    # note that the tenant_id here could be for a tenant in status DRAFT or INACTIVE and therefore will not
    # be in the tenant cache. we have to go directly to the tenants API for to get the description for this tenant.
    request_tenant = t.tenants.get_tenant(tenant_id=tenant_id)
    site_id_for_request = request_tenant.site_id
    logger.debug(
        f"request_tenant: {request_tenant}; site_id_for_request: {site_id_for_request}"
    )
    if not conf.service_site_id == site_id_for_request:
        logger.info(
            f"the request was for a site {site_id_for_request} that does not match the site for this Tokens"
            f"API ({conf.service_site_id}. the request is not authorized.")
        raise common_errors.AuthenticationError(
            msg=
            f'Invalid tenant_id ({tenant_id}) provided. This tenant belongs to'
            f'site {site_id_for_request} but this Tokens API serves site'
            f'{conf.service_site_id}.')
    # if the tenant_id of the access token matched the tenant_id the request is trying to update, the request is
    # authorized
    if g.tenant_id == tenant_id:
        logger.debug(
            f"token's tenant {g.tenant_id} matched. request authorized.")
        return True
    # the rest of the checks are only for service tokens; if token was a user token, the request is not authorized:
    if not g.account_type == 'service':
        logger.info(
            f"the request was for a different tenant {tenant_id} than the token's tenant_id ({g.tenant_id}) and"
            f"the token was not s service token. the request is not authorized."
        )
        raise common_errors.AuthenticationError(
            msg=f'Invalid tenant_id ({tenant_id}) provided. The token provided '
            f'belongs to the {g.tenant_id} tenant but the request is trying to'
            f'update the {tenant_id} tenant. Only service accounts can update'
            f'other tenants.')
    # if the token tenant_id did not match the tenant_id in the request, the only way the request will be authorized is
    # if the token tenant_id is for the admin tenant of the owning site (which is the site of this Tokens API).
    # to check this, get the site associated with the token:
    token_tenant = tenants.get_tenant_config(tenant_id=g.tenant_id)
    site_id_for_token = token_tenant.site_id
    logger.debug(f"site_id_for_token: {site_id_for_token}")
    if site_id_for_request == site_id_for_token:
        logger.debug(
            f"token's site {site_id_for_token} matched tenant's site. request authorized."
        )
        return True
    logger.info(
        f"token site {site_id_for_token} did NOT match tenant's site ({site_id_for_request})"
    )
    raise common_errors.AuthenticationError(
        msg=f'Invalid tenant_id ({tenant_id}) provided. This tenant belongs to'
        f'site {site_id_for_request} but the Tapis token passed in the'
        f'X-Tapis-Token header is for site {site_id_for_token}. Services'
        f'can only update tenants at their site.')
示例#11
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
示例#12
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)