Пример #1
0
def get_ldap_bind_from_sk(bind_credential_name):
    """
    Retrieve the ldap bind secret from SK for a specific ldap id.
    ldap_response: the ldap object description containing the bind_credential attribute
    :return:
    """
    logger.debug(
        f'top of get_ldap_bind_from_sk; bind_credential_name: {bind_credential_name}'
    )
    if not bind_credential_name:
        msg = f"Error --get_ldap_bind_from_sk did not get a bind_credential_name."
        logger.error(msg)
        raise errors.BaseTapisError(msg)
    try:
        ldap_bind_secret = t.sk.readSecret(secretType='user',
                                           secretName=bind_credential_name,
                                           tenant=conf.service_tenant_id,
                                           user=conf.service_name)
    except Exception as e:
        msg = f"Got exception trying to retrieve ldap bind secret from SK; exception: {e}."
        logger.error(msg)
        raise errors.BaseTapisError(msg)
    # the SK stores secrets in the secretMap attribute, which is a mapping of user-provided string attributes
    # to string values. for the ldap bind secrets, the convention is that the secretMap should contain one
    # key, password, containing the actual password
    try:
        bind_credential = ldap_bind_secret.secretMap.password
    except Exception as e:
        msg = f"got exception trying to retrieve the ldap_bind_password from the SK secret; e: {e}"
        logger.error(msg)
        raise errors.BaseTapisError(msg)
    return bind_credential
Пример #2
0
def get_tenant_config(tenant_id):
    """
    Return the config for a specific tenant_id from the tenants config.
    :param tenant_id:
    :return:
    """
    for tenant in tenants:
        if tenant['tenant_id'] == tenant_id:
            return tenant
    raise errors.BaseTapisError("invalid tenant id.")
Пример #3
0
 def get_base_url_for_tenant_primary_site(self, tenant_id):
     """
     Compute the base_url at the primary site for a tenant owned by an associate site.
     """
     try:
         base_url_template = self.primary_site.tenant_base_url_template
     except AttributeError:
         raise errors.BaseTapisError(
             f"Could not compute the base_url for tenant {tenant_id} at the primary site."
             f"The primary site was missing the tenant_base_url_template attribute."
         )
     return base_url_template.replace('${tenant_id}', tenant_id)
Пример #4
0
 def get_tenants_for_tenants_api(self):
     """
     This method computes the tenants and sites for the tenants service only. Note that the tenants service is a
     special case because it must retrieve the sites and tenants from its own DB, not from
     """
     logger.debug(
         "this is the tenants service, pulling sites and tenants from db..."
     )
     # NOTE: only in the case of the tenants service will we be able to import this function; so this import needs to
     # stay guarded in this method.
     if not conf.service_name == 'tenants':
         raise errors.BaseTapisError(
             "get_tenants_for_tenants_api called by a service other than tenants."
         )
     from service.models import get_tenants as tenants_api_get_tenants
     from service.models import get_sites as tenants_api_get_sites
     # in the case where the tenants api migrations are running, this call will fail with a sqlalchemy.exc.ProgrammingError
     # because the tenants table will not exist yet.
     tenants = []
     result = []
     logger.info("calling the tenants api's get_sites() function...")
     try:
         sites = tenants_api_get_sites()
     except Exception as e:
         logger.info(
             "WARNING - got an exception trying to compute the sites.. "
             "this better be the tenants migration container.")
         return tenants
     logger.info("calling the tenants api's get_tenants() function...")
     try:
         tenants = tenants_api_get_tenants()
     except Exception as e:
         logger.info(
             "WARNING - got an exception trying to compute the tenants.. "
             "this better be the tenants migration container.")
         return tenants
     # for each tenant, look up its corresponding site record and save it on the tenant record--
     for t in tenants:
         # Remove datetime objects --
         t.pop('create_time')
         t.pop('last_update_time')
         # convert the tenants to TapisResult objects, and then append the sites object.
         tn = TapisResult(**t)
         for s in sites:
             if 'primary' in s.keys() and s['primary']:
                 self.primary_site = TapisResult(**s)
             if s['site_id'] == tn.site_id:
                 tn.site = TapisResult(**s)
                 result.append(tn)
                 break
     return result
Пример #5
0
def service_token_checks(claims, tenants):
    """
    This function does additional checks when a service token is used to make a Tapis request.

    """
    logger.debug(f"top of service_token_checks; claims: {claims}")
    # first check that the target_site claim in the token matches this service's site_id --
    target_site_id = claims.get('tapis/target_site')
    try:
        service_site_id = conf.service_site_id
    except AttributeError:
        msg = "service configured without a site_id. Aborting."
        logger.error(msg)
        raise errors.BaseTapisError(msg)
    if not target_site_id == service_site_id:
        msg = f"token's target_site ({target_site_id}) does not match service's site_id ({service_site_id}."
        logger.info(msg)
        raise errors.AuthenticationError(
            "Invalid service token; "
            "target_site claim does not match this service's site_id.")
    # check that this service should be fulfilling this request based on its site_id config --
    # the X-Tapis-* (OBO) headers are required for service requests; if it is not set, raise an error.
    if not g.x_tapis_tenant:
        raise errors.AuthenticationError(
            "Invalid service request; X-Tapis-Tenant header missing.")
    if not g.x_tapis_user:
        raise errors.AuthenticationError(
            "Invalid service request; X-Tapis-User header missing.")
    request_tenant = tenants.get_tenant_config(tenant_id=g.x_tapis_tenant)
    site_id_for_request = request_tenant.site_id
    # if the service's site_id is the same as the site for the request, the request is always allowed:
    if service_site_id == site_id_for_request:
        logger.debug(
            "request is for the same site as the service; allowing request.")
        return True
    # otherwise, we only allow the primary site to handle requests for other sites, and only if the service is NOT
    # on the site's list of services that it runs.
    if not tenants.service_running_at_primary_site:
        raise errors.AuthenticationError(
            "Cross-site service requests are only allowed to the primary site."
        )
    logger.debug("this service is running at the primary site.")
    # make sure this service is not on the list of services deployed at the associate site --
    if conf.service_name in request_tenant.site.services:
        raise errors.AuthenticationError(
            f"The primary site does not handle requests to service {conf.service}"
        )
    logger.debug(
        "this service is NOT in the JWT tenant's owning site's set of services. allowing the request."
    )
Пример #6
0
 def get_tenants(self):
     """
     Retrieve the set of tenants and associated data that this service instance is serving requests for.
     :return:
     """
     logger.debug("top of get_tenants()")
     # if this is the first time we are calling get_tenants, set the service_running_at_primary_site attribute.
     if not hasattr(self, "service_running_at_primary_site"):
         self.service_running_at_primary_site = False
     # the tenants service is a special case, as it must be a) configured to serve all tenants and b) actually
     # maintains the list of tenants in its own DB. in this case, we call a special method to use the tenants service
     # code that makes direct db access to get necessary data.
     if conf.service_name == 'tenants':
         self.service_running_at_primary_site = True
         return self.get_tenants_for_tenants_api()
     else:
         logger.debug(
             "this is not the tenants service; calling tenants API to get sites and tenants..."
         )
         # if this case, this is not the tenants service, so we will try to get
         # the list of tenants by making API calls to the tenants service.
         # NOTE: we intentionally create a new Tapis client with *no authentication* so that we can call the Tenants
         # API even _before_ the SK is started up. If we pass a JWT, the Tenants will try to validate it as part of
         # handling our request, and this validation will fail if SK is not available.
         t = Tapis(
             base_url=conf.primary_site_admin_tenant_base_url,
             resource_set='local')  # TODO -- remove resource_set='local'
         try:
             tenants = t.tenants.list_tenants()
             sites = t.tenants.list_sites()
         except Exception as e:
             msg = f"Got an exception trying to get the list of sites and tenants. Exception: {e}"
             logger.error(msg)
             raise errors.BaseTapisError(
                 "Unable to retrieve sites and tenants from the Tenants API."
             )
         for t in tenants:
             self.extend_tenant(t)
             for s in sites:
                 if hasattr(s, "primary") and s.primary:
                     self.primary_site = s
                     if s.site_id == conf.service_site_id:
                         logger.debug(
                             f"this service is running at the primary site: {s.site_id}"
                         )
                         self.service_running_at_primary_site = True
                 if s.site_id == t.site_id:
                     t.site = s
         return tenants
Пример #7
0
def get_service_tapis_client(
        tenant_id=None,
        base_url=None,
        jwt=None,
        resource_set='tapipy',  #todo -- change back to resource_set='tapipy'
        custom_spec_dict=None,
        download_latest_specs=False,
        tenants=None):
    """
    Returns a Tapis client for the service using the service's configuration. If tenant_id is not passed, uses the first
    tenant in the service's tenants configuration.
    :param tenant_id: (str) The tenant_id associated with the tenant to configure the client with.
    :param base_url: (str) The base URL for the tenant to configure the client with.
    :return: (tapipy.tapis.Tapis) A Tapipy client object.
    """
    # if there is no base_url the primary_site_admin_tenant_base_url configured for the service:
    if not base_url:
        base_url = conf.primary_site_admin_tenant_base_url
    if not tenant_id:
        tenant_id = conf.service_tenant_id
    if not tenants:
        # the following would work to reference this module's tenants object, but we'll choose to raise
        # an error instead; it could be that
        # tenants = sys.modules[__name__].tenants
        raise errors.BaseTapisError(
            "As a Tapis service, passing in the appropriate tenants manager object"
            "is required.")
    t = Tapis(base_url=base_url,
              tenant_id=tenant_id,
              username=conf.service_name,
              account_type='service',
              service_password=conf.service_password,
              jwt=jwt,
              resource_set=resource_set,
              custom_spec_dict=custom_spec_dict,
              download_latest_specs=download_latest_specs,
              tenants=tenants,
              is_tapis_service=True)
    if not jwt:
        t.get_tokens()
    return t
Пример #8
0
 def get_tenants(self):
     """
     Retrieve the set of tenants and associated data that this service instance is serving requests for.
     :return:
     """
     logger.debug("top of get_tenants()")
     # these are the tenant_id strings configured for the service -
     tenants_strings = conf.tenants
     result = []
     # in dev mode, services can be configured to not use the security kernel, in which case we must get
     # configuration for a "dev" tenant directly from the service configs:
     if not conf.use_tenants:
         logger.debug("use_tenants was False")
         for tenant in tenants_strings:
             t = {
                 'tenant_id': tenant,
                 'iss': conf.dev_iss,
                 'public_key': conf.dev_jwt_public_key,
                 'token_service': conf.dev_token_service,
                 'base_url': conf.dev_base_url,
                 'authenticator': conf.dev_authenticator,
                 'security_kernel': conf.dev_security_kernel,
                 'is_owned_by_associate_site':
                 conf.dev_is_owned_by_associate_site,
                 'allowable_x_tenant_ids': conf.dev_allowable_x_tenant_ids,
             }
             self.extend_tenant(t)
             result.append(t)
         return result
     # the tenants service is a special case, as it must be a) configured to serve all tenants and b) actually maintains
     # the list of tenants in its own DB. in this case, we return the empty list since the tenants service will use direct
     # db access to get necessary data.
     elif conf.service_name == 'tenants' and tenants_strings[0] == '*':
         logger.debug(
             "this is the tenants service, pulling tenants from db...")
         # NOTE: only in the case of the tenants service will we be able to import this function; so this import needs to
         # stay guarded by the above IF statement.
         from service.models import get_tenants as tenants_api_get_tenants
         # in the case where the tenants api migrations are running, this call will fail with a sqlalchemy.exc.ProgrammingError
         # because the tenants table will not exist yet.
         logger.info("calling the tenants api's get_tenants() function...")
         try:
             result = tenants_api_get_tenants()
             logger.info(f"Got {result} from the tenants API")
             return result
         except Exception as e:
             logger.info(
                 "WARNING - got an exception trying to compute the tenants.. this better be the tenants migration container."
             )
             return result
     else:
         logger.debug(
             "this is not the tenants service; calling tenants API to get tenants..."
         )
         # if we are here, this is not the tenants service and it is configured to use the tenants API, so we will try to get
         # the list of tenants directly from the tenants service.
         # NOTE: we intentionally create a new Tapis client with *no authentication* so that we can call the Tenants
         # API even _before_ the SK is started up. If we pass a JWT, Tenants will try to
         t = Tapis(base_url=conf.service_tenant_base_url)
         try:
             tenant_list = t.tenants.list_tenants()
         except Exception as e:
             msg = f"Got an exception trying to get the list of tenants. Exception: {e}"
             print(msg)
             logger.error(msg)
             raise errors.BaseTapisError(
                 "Unable to retrieve tenants from the Tenants API.")
         if not type(tenant_list) == list:
             logger.error(
                 f"Did not get a list object from list_tenants(); got: {tenant_list}"
             )
         for tn in tenant_list:
             t = {
                 'tenant_id': tn.tenant_id,
                 'iss': tn.token_service,
                 'public_key': tn.public_key,
                 'token_service': tn.token_service,
                 'base_url': tn.base_url,
                 'authenticator': tn.authenticator,
                 'security_kernel': tn.security_kernel,
                 'is_owned_by_associate_site':
                 tn.is_owned_by_associate_site,
                 'allowable_x_tenant_ids': tn.allowable_x_tenant_ids,
             }
             self.extend_tenant(t)
             logger.debug(f"adding tenant: {t}")
             result.append(t)
     return result
Пример #9
0
    def get_tenant_config(self, tenant_id=None, url=None):
        """
        Return the config for a specific tenant_id from the tenants config based on either a tenant_id or a URL.
        One or the other (but not both) must be passed.
        :param tenant_id: (str) The tenant_id to match.
        :param url: (str) The URL to use to match.
        :return:
        """
        def find_tenant_from_id():
            for tenant in self.tenants:
                if tenant['tenant_id'] == tenant_id:
                    return tenant
            return None

        def find_tenant_from_url():
            for tenant in self.tenants:
                if tenant['base_url'] in url:
                    return tenant
                # todo - also check the tenant's primary_site_url once that is added to the tenant registry and model...
            return None

        logger.debug(
            f"top of get_tenant_config; called with tenant_id: {tenant_id}; url: {url}"
        )
        # allow for local development by checking for localhost:500 in the url; note: using 500, NOT 5000 since services
        # might be running on different 500x ports locally, e.g., 5000, 5001, 5002, etc..
        if url and 'http://localhost:500' in url:
            logger.debug(
                "http://localhost:500 in url; resolving tenant id to dev.")
            tenant_id = 'dev'
        if tenant_id:
            logger.debug(f"looking for tenant with tenant_id: {tenant_id}")
            t = find_tenant_from_id()
        elif url:
            logger.debug(f"looking for tenant with url {url}")
            # convert URL from http:// to https://
            if url.startswith('http://'):
                logger.debug(
                    "url started with http://; stripping and replacing with https"
                )
                url = url[len('http://'):]
                url = 'https://{}'.format(url)
            logger.debug(f"looking for tenant with URL: {url}")
            t = find_tenant_from_url()
        else:
            raise errors.BaseTapisError(
                "Invalid call to get_tenant_config; either tenant_id or url must be passed."
            )
        if t:
            return t
        # try one reload and then give up -
        logger.debug(
            f"did not find tenant; going to reload tenants. Tenants list was: {tenants.tenants}"
        )
        tenants.reload_tenants()
        logger.debug(
            f"tenants reloaded. Tenants list is now: {tenants.tenants}")
        if tenant_id:
            t = find_tenant_from_id()
        elif url:
            t = find_tenant_from_url()
        if t:
            return t
        raise errors.BaseTapisError("invalid tenant id.")
Пример #10
0
def authentication():
    """
    Entry point for checking authentication for all requests to the authenticator.
    :return:
    """
    # The authenticator uses different authentication methods for different endpoints. For example, the service
    # APIs such as clients and profiles use pure JWT authentication, while the OAuth endpoints use Basic Authentication
    # with OAuth client credentials.
    logger.debug(f"base_url: {request.base_url}; url_rule: {request.url_rule}")
    if not hasattr(request, 'url_rule') or not hasattr(
            request.url_rule, 'rule') or not request.url_rule.rule:
        raise common_errors.ResourceError(
            "The endpoint and HTTP method combination "
            "are not available from this service.")

    # the metadata endpoint is publicly available
    if '/v3/oauth2/.well-known/' in request.url_rule.rule:
        logger.debug(
            ".well-known endpoint; request is allowed to be made unauthenticated."
        )
        auth.resolve_tenant_id_for_request()
        return True
    # only the authenticator's own service token and tenant admins for the tenant can retrieve or modify the tenant
    # config
    if '/v3/oauth2/admin' in request.url_rule.rule:
        logger.debug(
            "admin endpoint; checking for authentictor service token or tenant admin role..."
        )
        # admin endpoints always require tapis token auth
        auth.authentication()
        # we'll need to use the request's tenant_id, so make sure it is resolved now
        auth.resolve_tenant_id_for_request()
        # first, make sure this request is for a tenant served by this authenticator
        if g.request_tenant_id not in conf.tenants:
            raise common_errors.PermissionsError(
                f"The request is for a tenant ({g.request_tenant_id}) that is not "
                f"served by this authenticator.")
        # we only want to honor tokens from THIS authenticator; i.e., not some other authenticator. therefore, we need
        # to check that the tenant_id associated with the token (g.tenant_id) is the same as THIS authenticator's tenant
        # id;
        if g.username == conf.service_name and g.tenant_id == conf.service_tenant_id:
            logger.info(
                f"allowing admin request because username was {conf.service_name} "
                f"and tenant was {conf.service_tenant_id}")
            return True
        logger.debug(
            f"request token does not represent THIS authenticator: token username: {g.username};"
            f" request tenant: {g.tenant_id}. Now checking for tenant admin..."
        )
        # all other service accounts are not allowed to update authenticator
        if g.account_type == 'service':
            raise common_errors.PermissionsError(
                "Not authorized -- service accounts are not allowed to access the"
                "authenticator admin endpoints.")
        # sanity check -- the request tenant id should be the same as the token tenant id in the remaining cases because
        # they are all user tokens
        if not g.request_tenant_id == g.tenant_id:
            logger.error(
                f"program error -- g.request_tenant_id: {g.request_tenant_id} not equal to "
                f"g.tenant_id: {g.tenant_id} even though account type was user!"
            )
            raise common_errors.ServiceConfigError(
                f"Unexpected program error checking permissions. The tenant id of"
                f"the request ({g.request_tenant_id})  did not match the tenant id "
                f"of the access token ({g.tenant_id}). Please contact server "
                f"administrators.")
        # check SK for tenant admin --
        try:
            rsp = t.sk.isAdmin(tenant=g.tenant_id, user=g.username)
        except Exception as e:
            logger.error(
                f"Got exception trying to check tenant admin role for tenant: {g.tenant_id} "
                f"and user: {g.username}; exception: {e}")
            raise common_errors.PermissionsError(
                "Could not check tenant admin role with SK; this role is required for "
                "accessing the authenticator admin endpoints.")
        try:
            if rsp.isAuthorized:
                logger.info(
                    f"user {g.username} had tenant admin role for tenant {g.tenant_id}; allowing request."
                )
                return True
            else:
                logger.info(
                    f"user {g.username} DID NOT have tenant admin role for tenant {g.tenant_id}; "
                    f"NOT allowing request.")
                raise common_errors.PermissionsError(
                    "Permission denied -- Tenant admin role required for accessing "
                    "the authenticator admin endpoints.")
        except Exception as e:
            logger.error(
                f"got exception trying to check isAuthorized property from isAdmin() call to SK."
                f"username: {g.username}; tenant: {g.tenant_id}; rsp: {rsp}; e: {e}"
            )
            logger.info(
                f"user {g.username} DID NOT have tenant admin role for tenant {g.tenant_id}; "
                f"NOT allowing request.")
            raise common_errors.PermissionsError(
                "Permission denied -- Tenant admin role required for accessing the "
                "authenticator admin endpoints.")

    # no credentials required on the authorize, login and oa2 extenion pages
    if '/v3/oauth2/authorize' in request.url_rule.rule or '/v3/oauth2/login' in request.url_rule.rule \
            or '/oauth2/extensions' in request.url_rule.rule:
        # always resolve the request tenant id based on the URL:
        logger.debug(
            "authorize, login or oa2 extension page. Resolving tenant_id")
        auth.resolve_tenant_id_for_request()
        try:
            logger.debug(f"request_tenant_id: {g.request_tenant_id}")
        except AttributeError:
            raise common_errors.BaseTapisError(
                "Unable to resolve tenant_id for request.")
        return True

    # the profiles endpoints always use standard Tapis Token auth -
    if '/v3/oauth2/profiles' in request.url_rule.rule or \
            '/v3/oauth2/userinfo' in request.url_rule.rule:
        auth.authentication()
        # always resolve the request tenant id based on the URL:
        auth.resolve_tenant_id_for_request()
        return True

    # the clients endpoints need to accept both standard Tapis Token auth and basic auth,
    if '/v3/oauth2/clients' in request.url_rule.rule:
        # first check for basic auth header:
        parts = get_basic_auth_parts()
        if parts:
            logger.debug("oauth2 clients page, with basic auth header.")
            # do basic auth against the ldap
            # always resolve the request tenant id based on the URL:
            auth.resolve_tenant_id_for_request()
            try:
                logger.debug(f"request_tenant_id: {g.request_tenant_id}")
            except AttributeError:
                raise common_errors.BaseTapisError(
                    "Unable to resolve tenant_id for request.")
            check_username_password(parts['tenant_id'], parts['username'],
                                    parts['password'])
            return True
        else:
            logger.debug("oauth2 clients page, no basic auth header.")
            # check for a Tapis token
            auth.authentication()
            # always resolve the request tenant id based on the URL:
            auth.resolve_tenant_id_for_request()
            try:
                logger.debug(f"request_tenant_id: {g.request_tenant_id}")
            except AttributeError:
                raise common_errors.BaseTapisError(
                    "Unable to resolve tenant_id for request.")
            return True

    if '/v3/oauth2/tokens' in request.url_rule.rule:
        logger.debug("oauth2 tokens URL")
        # the tokens endpoint uses basic auth with the client; logic handled in the controller. # however, it does
        # require the request tenant id:

        # first, check if an X-Tapis-Token header appears in the request. We do not honor JWT authentication for
        # generating new tokens, but we also don't want to fail for an expired token. So, we remove the token header
        # if it
        if 'X-Tapis-Token' in request.headers:
            logger.debug("Got an X-Tapis-Token header.")
            try:
                auth.add_headers()
                auth.validate_request_token()
            except:
                # we need to set the token claims because the resolve_tenant_id_for_request method depends on it:
                g.token_claims = {}
        # now, resolve the tenant_id
        try:
            auth.resolve_tenant_id_for_request()
        except:
            # we need to catch and swallow permissions errors having to do with an invalid JWT; if the JWT is invalid,
            # its claims (including its tenant claim) will be ignored, but then resolve_tenant_id_for_request() will
            # throw an error because the None tenant_id claim will not match the tenant_id of the URL.
            pass
        try:
            logger.debug(f"request_tenant_id: {g.request_tenant_id}")
        except AttributeError:
            raise common_errors.BaseTapisError(
                "Unable to resolve tenant_id for request.")
        return True

    if '/v3/oauth2/logout' in request.url_rule.rule \
        or '/v3/oauth2/login' in request.url_rule.rule \
        or '/v3/oauth2/tenant' in request.url_rule.rule \
        or '/v3/oauth2/webapp' in request.url_rule.rule \
        or '/v3/oauth2/portal-login' in request.url_rule.rule:
        # or '/v3/oauth2/webapp/callback' in request.url_rule.rule \
        # or '/v3/oauth2/webapp/token-display' in request.url_rule.rule \
        logger.debug("call is for some token webapp page.")
        auth.resolve_tenant_id_for_request()
        try:
            logger.debug(f"request_tenant_id: {g.request_tenant_id}")
        except AttributeError:
            raise common_errors.BaseTapisError(
                "Unable to resolve tenant_id for request.")
        #  make sure this tenant allows the token web app
        config = tenant_configs_cache.get_config(g.request_tenant_id)
        logger.debug(f"got tenant config: {config.serialize}")
        if not config.use_token_webapp:
            logger.info(
                f"tenant {g.request_tenant_id} not configured for the token webapp. Raising error"
            )
            raise common_errors.PermissionsError(
                "This tenant is not configured to use the Token Webapp.")

        return True
Пример #11
0
    def extend_tenant(self, tenant):
        """
        Add the LDAP metadata to the tenant description
        :param t: a tenant
        :return:
        """
        tenant_id = tenant.tenant_id
        # if this is not a tenant that this authenticator is supposed to serve, then just return immediately
        if not tenant_id in conf.tenants:
            logger.debug(
                f"skipping tenant_id: {tenant_id} as it is not in the list of tenants."
            )
            return tenant
        if not conf.use_tenants:
            if tenant_id == 'dev':
                tenant.ldap_url = conf.dev_ldap_url
                tenant.ldap_port = conf.dev_ldap_port
                tenant.ldap_use_ssl = conf.dev_ldap_use_ssl
                tenant.dev_ldap_tenants_base_dn = conf.dev_ldap_tenants_base_dn
                tenant.ldap_user_dn = conf.dev_ldap_user_dn
                tenant.ldap_bind_dn = conf.dev_ldap_bind_dn
            # we only support testing the "dev" tenant ldap under the scenario of use_tenants == false.
        else:
            # todo - the "dev_ldap_tenants_base_dn" property describes where to store the organizational units (OUs) for
            #  the tenants. this property is unique to the dev LDAP where the authenticator has write access and can
            #  create OUs for each tenant. thus, it is not stored in /returned by the tenants service, so we hard code
            #  it based on a service config for now,
            if tenant_id == 'dev':
                tenant.dev_ldap_tenants_base_dn = conf.dev_ldap_tenants_base_dn
            # look up ldap info from tenants service
            try:
                tenant_response = t.tenants.get_tenant(tenant_id=tenant_id)
            except Exception as e:
                logger.error(
                    f"Got exception trying to look up tenant info for tenant: {tenant_id}; e: {e}"
                )
                raise e
            # tenants with a custom IdP will not necessarily have a user_ldap_connection_id attribute...
            if hasattr(tenant_response, 'user_ldap_connection_id') and \
                    tenant_response.user_ldap_connection_id:
                logger.debug(
                    f'got a user_ldap_connection_id: {tenant_response.user_ldap_connection_id} for '
                    f'tenant: {tenant_id}. Now looking up LDAP data...')
                try:
                    ldap_response = t.tenants.get_ldap(
                        ldap_id=tenant_response.user_ldap_connection_id)
                except Exception as e:
                    logger.error(
                        f"Got exception trying to look up ldap info for "
                        f"ldap_id: {tenant_response.user_ldap_connection_id}; e: {e}"
                    )
                    raise e
                try:
                    tenant.ldap_url = ldap_response.url
                    tenant.ldap_port = ldap_response.port
                    tenant.ldap_use_ssl = ldap_response.use_ssl
                    tenant.ldap_user_dn = ldap_response.user_dn
                    tenant.ldap_bind_dn = ldap_response.bind_dn
                except AttributeError as e:
                    logger.error(
                        f"Got KeyError looking for an LDAP attr in the response; e: {e}"
                    )
                    raise e
            else:
                logger.debug(
                    f'did not get a user_ldap_connection_id for tenant: {tenant_id}.'
                )

        if not conf.use_sk:
            if tenant.tenant_id == 'dev':
                tenant.ldap_bind_credential = conf.dev_ldap_bind_credential
            elif tenant.tenant_id == 'tacc':
                tenant.ldap_bind_credential = conf.dev_tacc_ldap_bind_credential
        else:
            if hasattr(tenant_response, 'user_ldap_connection_id') and \
                    tenant_response.user_ldap_connection_id:
                if not getattr(ldap_response, 'bind_credential'):
                    msg = f"Error -- ldap object missing bind credential; description: {ldap_response}."
                    logger.error(msg)
                    raise errors.BaseTapisError(msg)
                tenant.ldap_bind_credential = get_ldap_bind_from_sk(
                    ldap_response.bind_credential)
        return tenant