import { createLogger } from '@sap-cloud-sdk/util' ; import { JwtPayload } from '../jsonwebtoken-type' ; import { decodeJwt , isUserToken , JwtPair , verifyJwt } from '../jwt' ; import { IsolationStrategy } from '../cache' ; import { jwtBearerToken , serviceToken } from '../token-accessor' ; import { addProxyConfigurationOnPrem } from '../connectivity-service' ; import { getDestinationService , getDestinationServiceCredentialsList } from '../environment-accessor' ; import { isIdenticalTenant } from '../tenant' ; import { DestinationServiceCredentials } from '../environment-accessor-types' ; import { exchangeToken , isTokenExchangeEnabled } from '../identity-service' ; import { getSubdomainAndZoneId } from '../xsuaa-service' ; import { Destination } from './destination-service-types' ; import { alwaysProvider , alwaysSubscriber , subscriberFirst } from './destination-selection-strategies' ; import { DestinationFetchOptions , DestinationOptions , DestinationsByType } from './destination-accessor-types' ; import { AuthAndExchangeTokens , fetchDestination , fetchInstanceDestinations , fetchSubaccountDestinations } from './destination-service' ; import { destinationCache } from './destination-cache' ; import { addProxyConfigurationInternet , ProxyStrategy , proxyStrategy } from './proxy-util' ; type DestinationOrigin = 'subscriber' | 'provider' ; type RequiredProperties < T , P extends keyof T > = Required < Pick < T , P > > & Omit < T , P > ; const logger = createLogger ( { package : 'connectivity' , messageContext : 'destination-accessor-service' } ) ; interface DestinationSearchResult { destination : Destination ; fromCache : boolean ; origin : DestinationOrigin ; } interface SubscriberTokens { userJwt ? : JwtPair ; serviceJwt : JwtPair ; } const emptyDestinationByType = { instance : [ ] , subaccount : [ ] } ; param options returns export async function getDestinationFromDestinationService ( options ) { logger . info ( 'Attempting to retrieve destination from destination service.' ) ; return DestinationFromServiceRetriever . getDestinationFromDestinationService ( options ) ; } internal class DestinationFromServiceRetriever { public static async getDestinationFromDestinationService ( options ) { if ( isTokenExchangeEnabled ( options ) ) { options . jwt = await exchangeToken ( options ) ; } const subscriberToken = await DestinationFromServiceRetriever . getSubscriberToken ( options ) ; const providerToken = await DestinationFromServiceRetriever . getProviderServiceToken ( options ) ; const da = new DestinationFromServiceRetriever ( options . destinationName , options , subscriberToken , providerToken ) ; const destinationResult = await da . searchDestinationWithSelectionStrategyAndCache ( ) ; if ( ! destinationResult ) { return null ; } if ( destinationResult . fromCache ) { return destinationResult . destination ; } let { destination } = destinationResult ; if ( destination . authentication === 'OAuth2UserTokenExchange' || destination . authentication === 'OAuth2JWTBearer' || ( destination . authentication === 'OAuth2SAMLBearerAssertion' && ! da . usesSystemUser ( destination ) ) ) { destination = await da . fetchDestinationWithUserExchangeFlows ( destinationResult ) ; } if ( destination . authentication === 'OAuth2Password' || destination . authentication === 'ClientCertificateAuthentication' || destination . authentication === 'OAuth2ClientCredentials' || da . usesSystemUser ( destination ) ) { destination = await da . fetchDestinationWithNonUserExchangeFlows ( destinationResult ) ; } const withProxySetting = await da . addProxyConfiguration ( destination ) ; da . updateDestinationCache ( withProxySetting , destinationResult . origin ) ; return withProxySetting ; } private static async getSubscriberToken ( options ) { if ( options . jwt ) { if ( options . iss ) { logger . warn ( 'You have provided the `userJwt` and `iss` options to fetch the destination. This is most likely unintentional. Ignoring `iss`.' ) ; } const encoded = await serviceToken ( 'destination' , { ... options } ) ; return { userJwt : { decoded : await verifyJwt ( options . jwt , options ) , encoded : options . jwt } , serviceJwt : { encoded , decoded : decodeJwt ( encoded ) } } ; } if ( options . iss ) { logger . info ( 'Using `iss` option to fetch a destination instead of a full JWT. No validation is performed.' ) ; const payload = { iss : options . iss } ; const encoded = await serviceToken ( 'destination' , { ... options , jwt : payload } ) ; const clientCertJwt = { encoded , decoded : decodeJwt ( encoded ) } ; return { serviceJwt : clientCertJwt } ; } } private static async getProviderServiceToken ( options ) { const { jwt , ... optionsWithoutJwt } = options ; const encoded = await serviceToken ( 'destination' , { ... optionsWithoutJwt } ) ; return { encoded , decoded : decodeJwt ( encoded ) } ; } private options : RequiredProperties < Omit < DestinationOptions , 'userJwt' | 'iss' > , 'isolationStrategy' | 'selectionStrategy' | 'useCache' > ; private constructor ( readonly name , options , readonly subscriberToken , readonly providerServiceToken ) { const defaultOptions = { isolationStrategy : IsolationStrategy . Tenant_User , selectionStrategy : subscriberFirst , useCache : false , ... options } ; this . options = { ... defaultOptions , ... options } ; } private async searchDestinationWithSelectionStrategyAndCache ( ) { let destinationSearchResult ; if ( this . isSubscriberNeeded ( ) ) { destinationSearchResult = await this . searchSubscriberAccountForDestination ( ) ; } if ( this . isProviderNeeded ( destinationSearchResult ) ) { destinationSearchResult = await this . searchProviderAccountForDestination ( ) ; } if ( destinationSearchResult && ! destinationSearchResult . fromCache ) { logger . debug ( 'Successfully retrieved destination from destination service.' ) ; } if ( destinationSearchResult && destinationSearchResult . fromCache ) { logger . debug ( `Successfully retrieved destination from destination service cache for ${ destinationSearchResult . origin } destinations.` ) ; } if ( ! destinationSearchResult ) { logger . info ( 'Could not retrieve destination from destination service.' ) ; } return destinationSearchResult ; } private async getInstanceAndSubaccountDestinations ( accessToken ) { const [ instance , subaccount ] = await Promise . all ( [ fetchInstanceDestinations ( this . destinationServiceCredentials . uri , accessToken , this . options ) , fetchSubaccountDestinations ( this . destinationServiceCredentials . uri , accessToken , this . options ) ] ) ; return { instance , subaccount } ; } private get destinationServiceCredentials ( ) : DestinationServiceCredentials { const credentials = getDestinationServiceCredentialsList ( ) ; if ( ! credentials || credentials . length === 0 ) { throw Error ( 'No binding to a destination service instance found. Please bind a destination service instance to your application.' ) ; } if ( credentials . length > 1 ) { logger . info ( 'Found more than one destination service instance. Using the first one.' ) ; } return credentials [ 0 ] ; } private async fetchDestinationByToken ( jwt ) { return fetchDestination ( this . destinationServiceCredentials . uri , jwt , this . name , this . options ) ; } private getExchangeTenant ( destination ) { if ( destination . authentication !== 'OAuth2ClientCredentials' ) { return undefined ; } if ( destination . originalProperties ! [ 'tokenServiceURLType' ] !== 'Common' ) { return undefined ; } const subdomainSubscriber = getSubdomainAndZoneId ( this . subscriberToken ?. userJwt ?. encoded ) . subdomain ; const subdomainProvider = getSubdomainAndZoneId ( this . providerServiceToken ?. encoded ) . subdomain ; return subdomainSubscriber || subdomainProvider || undefined ; } private async getAuthTokenForOAuth2ClientCrendentials ( destinationResult ) { const { destination , origin } = destinationResult ; const exchangeTenant = this . getExchangeTenant ( destination ) ; const clientGrant = await serviceToken ( 'destination' , { jwt : origin === 'subscriber' ? this . subscriberToken ! . serviceJwt . decoded : this . providerServiceToken . decoded } ) ; return { authHeaderJwt : clientGrant , exchangeTenant } ; } private usesSystemUser ( destination ) { if ( destination . systemUser && destination . authentication === 'OAuth2SAMLBearerAssertion' ) { logger . debug ( `System user found on destination: "${ destination . name }". 
The property SystemUser has been deprecated. 
It is highly recommended that you stop using it.
Possible alternatives for such technical user authentication are BasicAuthentication, OAuth2ClientCredentials, or ClientCertificateAuthentication` ) ; return true ; } return false ; } private async getAuthTokenForOAuth2UserBasedTokenExchanges ( destinationResult ) { const { destination , origin } = destinationResult ; if ( ! this . subscriberToken || ! isUserToken ( this . subscriberToken . userJwt ) ) { throw Error ( `No user token (JWT) has been provided. This is strictly necessary for '${ destination . authentication }'.` ) ; } if ( this . isProviderAndSubscriberSameTenant ( ) ) { logger . debug ( `UserExchange flow started without user exchange token for destination ${ this . name } of the provider account.` ) ; return { authHeaderJwt : await jwtBearerToken ( this . subscriberToken . userJwt . encoded , getDestinationService ( ) , this . options ) } ; } const serviceJwt = origin === 'subscriber' ? this . subscriberToken . serviceJwt : this . providerServiceToken ; logger . debug ( `UserExchange flow started for destination ${ this . name } of the ${ origin } account.` ) ; return { authHeaderJwt : serviceJwt . encoded , exchangeHeaderJwt : this . subscriberToken . userJwt . encoded } ; throw new Error ( `Not possible to build tokens for ${ destination . authentication } flow for destination ${ destination . name }.` ) ; } internal param destinationResult returns private async fetchDestinationWithNonUserExchangeFlows ( destinationResult ) { const token = await this . getAuthTokenForOAuth2ClientCrendentials ( destinationResult ) ; return this . fetchDestinationByToken ( token ) ; } private async fetchDestinationWithUserExchangeFlows ( destinationResult ) { const token = await this . getAuthTokenForOAuth2UserBasedTokenExchanges ( destinationResult ) ; return this . fetchDestinationByToken ( token ) ; } private async addProxyConfiguration ( destination ) { switch ( proxyStrategy ( destination ) ) { case ProxyStrategy . ON_PREMISE_PROXY : return addProxyConfigurationOnPrem ( destination , this . subscriberToken ?. userJwt ) ; case ProxyStrategy . INTERNET_PROXY : return addProxyConfigurationInternet ( destination ) ; case ProxyStrategy . NO_PROXY : return destination ; default : throw new Error ( 'Illegal argument: No valid proxy configuration found in the destination input to be aded.' ) ; } } private selectSubscriberJwt ( ) { if ( ! this . subscriberToken ) { throw new Error ( 'Try to get subscriber token but value is undefined.' ) ; } return ( this . subscriberToken . userJwt ?. decoded || this . subscriberToken . serviceJwt . decoded ) ; } private updateDestinationCache ( destination , destinationOrigin ) { if ( ! this . options . useCache ) { return destination ; } destinationCache . cacheRetrievedDestination ( destinationOrigin === 'subscriber' ? this . selectSubscriberJwt ( ) : this . providerServiceToken . decoded , destination , this . options . isolationStrategy ) ; } private async getProviderDestinationService ( ) { const provider = await this . getInstanceAndSubaccountDestinations ( this . providerServiceToken . encoded ) ; const destination = this . options . selectionStrategy ( { subscriber : emptyDestinationByType , provider } , this . name ) ; if ( destination ) { return { destination , fromCache : false , origin : 'provider' } ; } } private getProviderDestinationCache ( ) { const destination = destinationCache . retrieveDestinationFromCache ( this . providerServiceToken . decoded , this . name , this . options . isolationStrategy ) ; if ( destination ) { return { destination , fromCache : true , origin : 'subscriber' } ; } } private async getSubscriberDestinationService ( ) { if ( ! this . subscriberToken ) { throw new Error ( 'Try to get destinations from subscriber account but user JWT was not set.' ) ; } const subscriber = await this . getInstanceAndSubaccountDestinations ( this . subscriberToken . serviceJwt . encoded ) ; const destination = this . options . selectionStrategy ( { subscriber , provider : emptyDestinationByType } , this . name ) ; if ( destination ) { return { destination , fromCache : false , origin : 'subscriber' } ; } } private getSubscriberDestinationCache ( ) { const destination = destinationCache . retrieveDestinationFromCache ( this . selectSubscriberJwt ( ) , this . name , this . options . isolationStrategy ) ; if ( destination ) { return { destination , fromCache : true , origin : 'subscriber' } ; } } private isProviderAndSubscriberSameTenant ( ) { return ( this . subscriberToken && isIdenticalTenant ( this . subscriberToken . serviceJwt . decoded , this . providerServiceToken . decoded ) ) ; } private isProviderNeeded ( resultFromSubscriber ) { if ( this . options . selectionStrategy === alwaysSubscriber ) { return false ; } if ( this . options . selectionStrategy === subscriberFirst && resultFromSubscriber ) { return false ; } return true ; } private isSubscriberNeeded ( ) { if ( ! this . subscriberToken ) { return false ; } if ( this . options . selectionStrategy === alwaysProvider ) { return false ; } return true ; } private async searchProviderAccountForDestination ( ) { return ( ( this . options . useCache && this . getProviderDestinationCache ( ) ) || this . getProviderDestinationService ( ) ) ; } private async searchSubscriberAccountForDestination ( ) { return ( ( this . options . useCache && this . getSubscriberDestinationCache ( ) ) || this . getSubscriberDestinationService ( ) ) ; } }