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
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.")
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)
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
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." )
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
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
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
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.")
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
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