UNPKG

@sap-cloud-sdk/connectivity

Version:

SAP Cloud SDK for JavaScript connectivity

362 lines • 19.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.DestinationFromServiceRetriever = void 0; exports.getDestinationFromDestinationService = getDestinationFromDestinationService; const util_1 = require("@sap-cloud-sdk/util"); const connectivity_service_1 = require("../connectivity-service"); const environment_accessor_1 = require("../environment-accessor"); const identity_service_1 = require("../identity-service"); const jwt_1 = require("../jwt"); const tenant_1 = require("../tenant"); const token_accessor_1 = require("../token-accessor"); const destination_cache_1 = require("./destination-cache"); const destination_selection_strategies_1 = require("./destination-selection-strategies"); const destination_service_1 = require("./destination-service"); const destination_service_types_1 = require("./destination-service-types"); const get_provider_token_1 = require("./get-provider-token"); const get_subscriber_token_1 = require("./get-subscriber-token"); const http_proxy_util_1 = require("./http-proxy-util"); const forward_auth_token_1 = require("./forward-auth-token"); const logger = (0, util_1.createLogger)({ package: 'connectivity', messageContext: 'destination-accessor-service' }); const emptyDestinationByType = { instance: [], subaccount: [] }; /** * Retrieves a destination with the given name from the Cloud Foundry destination service. * Returns `null`, if no destination can be found. * Requires the following service bindings: destination, XSUAA * By default, selects subscriber over provider and instance over subaccount destinations. * @param options - Configuration for how to retrieve destinations from the destination service. * @returns A promise returning the requested destination on success. */ async function getDestinationFromDestinationService(options) { logger.debug('Attempting to retrieve destination from destination service.'); return DestinationFromServiceRetriever.getDestinationFromDestinationService(options); } /** * @internal */ class DestinationFromServiceRetriever { static async getDestinationFromDestinationService(options) { // Exchange the IAS token to a XSUAA token using the destination service credentials if ((0, identity_service_1.shouldExchangeToken)(options)) { options.jwt = await (0, token_accessor_1.jwtBearerToken)(options.jwt, 'destination'); } // Create retriever with subscriber and provider tokens const retriever = new DestinationFromServiceRetriever(options, await (0, get_subscriber_token_1.getSubscriberToken)(options), await (0, get_provider_token_1.getProviderServiceToken)(options)); // Search destination with selection strategy and cache const destinationSearchResult = await retriever.searchDestinationWithSelectionStrategyAndCache(); // Immediately return null if no destination found if (!destinationSearchResult) { return null; } const { origin, fromCache } = destinationSearchResult; let { destination } = destinationSearchResult; if (!fromCache) { /* Destination NOT from cache */ // Fetch and add auth token if needed. // Needed means `forwardAuthToken` is `false` // AND authentication is one of the supported types destination = await retriever.fetchAndAddAuthTokenIfNeeded(destination, origin); // Add trust store configuration if needed. // Needed means `TrustStoreLocation` is defined destination = await retriever.addTrustStoreConfigurationIfNeeded(destination, origin); // Cache the destination await retriever.cacheDestination(destination, origin); } // Add auth token based on the given `options.jwt` if needed // Needed means `forwardAuthToken` is `true` destination = (0, forward_auth_token_1.addForwardedAuthTokenIfNeeded)(destination, options.jwt); // Add proxy configuration based on the proxy strategy destination = await retriever.addProxyConfiguration(destination); return destination; } static throwUserTokenMissing(destination) { throw Error(`No user token (JWT) has been provided. This is strictly necessary for '${destination.authentication}'.`); } static checkDestinationForCustomJwt(destination) { if (!destination.jwks && !destination.jwksUri) { throw new Error('Failed to verify the JWT with no JKU! Destination must have `x_user_token.jwks` or `x_user_token.jwks_uri` property.'); } } static isUserJwt(token) { return !!token?.userJwt; } constructor(options, subscriberToken, providerServiceToken) { this.subscriberToken = subscriberToken; this.providerServiceToken = providerServiceToken; const defaultOptions = { isolationStrategy: (0, destination_cache_1.getDefaultIsolationStrategy)(subscriberToken?.userJwt?.decoded), selectionStrategy: destination_selection_strategies_1.subscriberFirst, useCache: true }; this.options = { ...defaultOptions, ...options }; } async searchDestinationWithSelectionStrategyAndCache() { let destinationSearchResult; if (this.isSubscriberNeeded()) { destinationSearchResult = await this.searchSubscriberAccountForDestination(); } if (this.isProviderNeeded(destinationSearchResult)) { destinationSearchResult = await this.searchProviderAccountForDestination(); } if (destinationSearchResult) { if (destinationSearchResult.fromCache) { logger.debug(`Successfully retrieved destination from destination service cache for ${destinationSearchResult.origin} destinations.`); } else { logger.debug('Successfully retrieved destination from destination service.'); } } else { logger.debug('Could not retrieve destination from destination service.'); } return destinationSearchResult; } getExchangeTenant(destination) { if (destination.authentication !== 'OAuth2ClientCredentials') { return undefined; } if (destination.originalProperties?.['tokenServiceURLType'] !== 'Common') { return undefined; } const subdomainSubscriber = (0, jwt_1.getSubdomain)(this.subscriberToken?.serviceJwt?.decoded) || (0, jwt_1.getSubdomain)(this.subscriberToken?.userJwt?.decoded); const subdomainProvider = (0, jwt_1.getSubdomain)(this.providerServiceToken?.decoded); return subdomainSubscriber || subdomainProvider || undefined; } async getAuthTokenForOAuth2ClientCredentials(destination, origin) { // This covers the x-tenant case https://api.sap.com/api/SAP_CP_CF_Connectivity_Destination/resource const exchangeTenant = this.getExchangeTenant(destination); const authHeaderJwt = origin === 'provider' ? this.providerServiceToken.encoded : this.subscriberToken?.serviceJwt?.encoded; if (!authHeaderJwt) { throw Error('Could not retrieve service token for the destination service.'); } return { authHeaderJwt, exchangeTenant }; } // This covers the two technical user propagation https://help.sap.com/viewer/cca91383641e40ffbe03bdc78f00f681/Cloud/en-US/3cb7b81115c44cf594e0e3631291af94.html usesSystemUser(destination) { // put this in the non user dependent block 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; } async getAuthTokenForOAuth2UserBasedTokenExchanges(destination, origin) { const { destinationName } = this.options; if (!DestinationFromServiceRetriever.isUserJwt(this.subscriberToken)) { throw DestinationFromServiceRetriever.throwUserTokenMissing(destination); } // This covers OAuth to user-dependent auth flows https://help.sap.com/viewer/cca91383641e40ffbe03bdc78f00f681/Cloud/en-US/39d42654093e4f8db20398a06f7eab2b.html and https://api.sap.com/api/SAP_CP_CF_Connectivity_Destination/resource // Which is the same for: OAuth2UserTokenExchange, OAuth2JWTBearer and OAuth2SAMLBearerAssertion const isXsuaaUserJwt = (0, jwt_1.isXsuaaToken)(this.subscriberToken.userJwt.decoded); // If subscriber user token was not issued by XSUAA enforce the JWKS properties are there - destination service would do that as well. https://help.sap.com/docs/CP_CONNECTIVITY/cca91383641e40ffbe03bdc78f00f681/d81e1683bd434823abf3ceefc4ff157f.html if (!isXsuaaUserJwt) { DestinationFromServiceRetriever.checkDestinationForCustomJwt(destination); } // Case 1: subscriber account is the provider account, user JWT is from XSUAA // x-user-token header not needed if (isXsuaaUserJwt && (0, tenant_1.isIdenticalTenant)(this.subscriberToken.userJwt.decoded, this.providerServiceToken.decoded)) { logger.debug(`UserExchange flow started without user exchange token for destination ${destinationName} of the provider account.`); return { authHeaderJwt: await (0, token_accessor_1.jwtBearerToken)(this.subscriberToken.userJwt.encoded, getDestinationService()) }; } // Case 2a: subscriber and provider account not the same // Case 2b: user token is not an XSUAA token // x-user-token needed const serviceJwt = origin === 'provider' ? this.providerServiceToken : // on type level this could be undefined, but logically if the origin is subscriber, it must be defined. this.subscriberToken.serviceJwt; logger.debug(`UserExchange flow started for destination ${destinationName} of the ${origin} account.`); return { authHeaderJwt: serviceJwt.encoded, // token to get destination from service exchangeHeaderJwt: this.subscriberToken.userJwt.encoded // token considered for user and tenant }; } async getAuthTokenForOAuth2RefreshToken(destination, origin) { const { refreshToken } = this.options; if (!refreshToken) { throw Error(`No refresh token has been provided. This is strictly necessary for '${destination.authentication}'.`); } const clientGrant = origin === 'provider' ? this.providerServiceToken.encoded : this.subscriberToken.serviceJwt.encoded; return { authHeaderJwt: clientGrant, refreshToken }; } /** * @internal * This method calls the 'find destination by name' endpoint of the destination service using a client credentials grant. * For the find by name endpoint, the destination service will take care of OAuth flows and include the token in the destination. * @param destination - The destination for which the token should be fetched. * @param origin - The origin of the destination, either 'subscriber' or 'provider'. * @returns Destination containing the auth token. */ async fetchDestinationWithNonUserExchangeFlows(destination, origin) { const token = await this.getAuthTokenForOAuth2ClientCredentials(destination, origin); return (0, destination_service_1.fetchDestinationWithTokenRetrieval)((0, environment_accessor_1.getDestinationServiceCredentials)().uri, token, this.options); } async fetchDestinationWithUserExchangeFlows(destination, origin) { const token = await this.getAuthTokenForOAuth2UserBasedTokenExchanges(destination, origin); return (0, destination_service_1.fetchDestinationWithTokenRetrieval)((0, environment_accessor_1.getDestinationServiceCredentials)().uri, token, this.options); } async fetchDestinationWithRefreshTokenFlow(destination, origin) { const token = await this.getAuthTokenForOAuth2RefreshToken(destination, origin); return (0, destination_service_1.fetchDestinationWithTokenRetrieval)((0, environment_accessor_1.getDestinationServiceCredentials)().uri, token, this.options); } async fetchAndAddAuthTokenIfNeeded(destination, origin) { const { forwardAuthToken, authentication } = destination; if (forwardAuthToken) { return destination; } if (authentication === 'OAuth2UserTokenExchange' || authentication === 'OAuth2JWTBearer' || authentication === 'SAMLAssertion' || (authentication === 'OAuth2SAMLBearerAssertion' && !this.usesSystemUser(destination))) { return this.fetchDestinationWithUserExchangeFlows(destination, origin); } if (authentication === 'OAuth2Password' || authentication === 'ClientCertificateAuthentication' || authentication === 'OAuth2ClientCredentials' || this.usesSystemUser(destination)) { return this.fetchDestinationWithNonUserExchangeFlows(destination, origin); } if (authentication === 'OAuth2RefreshToken') { return this.fetchDestinationWithRefreshTokenFlow(destination, origin); } if (authentication === 'PrincipalPropagation') { if (!DestinationFromServiceRetriever.isUserJwt(this.subscriberToken)) { DestinationFromServiceRetriever.throwUserTokenMissing(destination); } } return destination; } async addProxyConfiguration(destination) { switch ((0, http_proxy_util_1.proxyStrategy)(destination)) { case 'on-premise': return (0, connectivity_service_1.addProxyConfigurationOnPrem)(destination, (0, get_subscriber_token_1.hasTokens)(this.subscriberToken) ? (0, get_subscriber_token_1.getRequiredSubscriberToken)(this.subscriberToken) : undefined); case 'internet': case 'private-link': (0, destination_service_types_1.assertHttpDestination)(destination); return (0, http_proxy_util_1.addProxyConfigurationInternet)(destination); case 'no-proxy': return destination; default: throw new Error('Illegal argument: No valid proxy configuration found in the destination input to be added.'); } } async cacheDestination(destination, destinationOrigin) { if (!this.options.useCache) { return; } if (destination.authentication === 'SAMLAssertion') { logger.debug('Destination with authentication type SAMLAssertion will not be cached.'); return; } await destination_cache_1.destinationCache.cacheRetrievedDestination(destinationOrigin === 'subscriber' ? (0, get_subscriber_token_1.getRequiredSubscriberToken)(this.subscriberToken) : this.providerServiceToken.decoded, destination, this.options.isolationStrategy); } async getProviderDestinationService() { const providerDestination = await (0, destination_service_1.fetchDestinationWithoutTokenRetrieval)(this.options.destinationName, (0, environment_accessor_1.getDestinationServiceCredentials)().uri, this.providerServiceToken.encoded); const destination = this.options.selectionStrategy({ subscriber: emptyDestinationByType, provider: providerDestination }, this.options.destinationName); if (destination) { return { destination, fromCache: false, origin: 'provider' }; } } async getProviderDestinationCache() { const destination = await destination_cache_1.destinationCache.retrieveDestinationFromCache(this.providerServiceToken.decoded, this.options.destinationName, this.options.isolationStrategy); if (destination) { return { destination, fromCache: true, origin: 'provider' }; } } async getSubscriberDestinationService() { if (!this.subscriberToken?.serviceJwt) { throw new Error('Try to get destinations from subscriber account but service JWT was not set.'); } const subscriberDestination = await (0, destination_service_1.fetchDestinationWithoutTokenRetrieval)(this.options.destinationName, (0, environment_accessor_1.getDestinationServiceCredentials)().uri, this.subscriberToken.serviceJwt.encoded); const destination = this.options.selectionStrategy({ subscriber: subscriberDestination, provider: emptyDestinationByType }, this.options.destinationName); if (destination) { return { destination, fromCache: false, origin: 'subscriber' }; } } async getSubscriberDestinationCache() { const destination = await destination_cache_1.destinationCache.retrieveDestinationFromCache((0, get_subscriber_token_1.getRequiredSubscriberToken)(this.subscriberToken), this.options.destinationName, this.options.isolationStrategy); if (destination) { return { destination, fromCache: true, origin: 'subscriber' }; } } isProviderNeeded(resultFromSubscriber) { if (this.options.selectionStrategy.toString() === destination_selection_strategies_1.alwaysSubscriber.toString()) { return false; } if (this.options.selectionStrategy.toString() === destination_selection_strategies_1.subscriberFirst.toString() && resultFromSubscriber) { return false; } return true; } isSubscriberNeeded() { if (!this.subscriberToken?.serviceJwt) { return false; } return (this.options.selectionStrategy.toString() !== destination_selection_strategies_1.alwaysProvider.toString()); } async searchProviderAccountForDestination() { return ((this.options.useCache && (await this.getProviderDestinationCache())) || this.getProviderDestinationService()); } async searchSubscriberAccountForDestination() { return ((this.options.useCache && (await this.getSubscriberDestinationCache())) || this.getSubscriberDestinationService()); } async addTrustStoreConfigurationIfNeeded(destination, origin) { const { originalProperties } = destination; const trustStoreLocation = originalProperties?.TrustStoreLocation || originalProperties?.destinationConfiguration?.TrustStoreLocation; if (trustStoreLocation) { const trustStoreCertificate = await (0, destination_service_1.fetchCertificate)((0, environment_accessor_1.getDestinationServiceCredentials)().uri, origin === 'provider' ? this.providerServiceToken.encoded : this.subscriberToken.serviceJwt.encoded, trustStoreLocation); return { ...destination, trustStoreCertificate }; } return destination; } } exports.DestinationFromServiceRetriever = DestinationFromServiceRetriever; function getDestinationService() { const destinationService = (0, environment_accessor_1.getServiceBinding)('destination'); if (!destinationService) { throw Error('No binding to a destination service found.'); } return destinationService; } //# sourceMappingURL=destination-from-service.js.map