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)
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 }
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
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
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
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}'
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)
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 }
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
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.')
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
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)