UNPKG

@sap-cloud-sdk/connectivity

Version:

SAP Cloud SDK for JavaScript connectivity

341 lines • 19.1 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) { // TODO: This is currently always skipped for tokens issued by XSUAA // in the XSUAA case no exchange takes place if ((0, identity_service_1.shouldExchangeToken)(options) && options.jwt) { // Exchange the IAS token to a XSUAA token using the destination service credentials options.jwt = await (0, token_accessor_1.jwtBearerToken)(options.jwt, 'destination'); } const subscriberToken = await (0, get_subscriber_token_1.getSubscriberToken)(options); const providerToken = await (0, get_provider_token_1.getProviderServiceToken)(options); const da = new DestinationFromServiceRetriever(options, subscriberToken, providerToken); const destinationResult = await da.searchDestinationWithSelectionStrategyAndCache(); if (!destinationResult) { return null; } let { destination } = destinationResult; (0, forward_auth_token_1.setForwardedAuthTokenIfNeeded)(destination, options.jwt); if (destinationResult.fromCache) { return da.addProxyConfiguration(destination); } if (!destination.forwardAuthToken) { if (destination.authentication === 'OAuth2UserTokenExchange' || destination.authentication === 'OAuth2JWTBearer' || destination.authentication === 'SAMLAssertion' || (destination.authentication === 'OAuth2SAMLBearerAssertion' && !da.usesSystemUser(destination))) { destination = await da.fetchDestinationWithUserExchangeFlows(destinationResult); } if (destination.authentication === 'PrincipalPropagation') { if (!this.isUserJwt(da.subscriberToken)) { DestinationFromServiceRetriever.throwUserTokenMissing(destination); } } if (destination.authentication === 'OAuth2Password' || destination.authentication === 'ClientCertificateAuthentication' || destination.authentication === 'OAuth2ClientCredentials' || da.usesSystemUser(destination)) { destination = await da.fetchDestinationWithNonUserExchangeFlows(destinationResult); } if (destination.authentication === 'OAuth2RefreshToken') { destination = await da.fetchDestinationWithRefreshTokenFlow(destinationResult); } } const withTrustStore = await da.addTrustStoreConfiguration(destination, destinationResult.origin); await da.updateDestinationCache(withTrustStore, destinationResult.origin); return da.addProxyConfiguration(withTrustStore); } 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(destinationResult) { const { destination, origin } = destinationResult; // 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(destinationResult) { const { destination, origin } = destinationResult; 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(destinationResult) { const { destination, origin } = destinationResult; 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 destinationResult - Result of the getDestinations call for which the exchange flow is triggered. * @returns Destination containing the auth token. */ async fetchDestinationWithNonUserExchangeFlows(destinationResult) { const token = await this.getAuthTokenForOAuth2ClientCredentials(destinationResult); return (0, destination_service_1.fetchDestinationWithTokenRetrieval)((0, environment_accessor_1.getDestinationServiceCredentials)().uri, token, this.options); } async fetchDestinationWithUserExchangeFlows(destinationResult) { const token = await this.getAuthTokenForOAuth2UserBasedTokenExchanges(destinationResult); return (0, destination_service_1.fetchDestinationWithTokenRetrieval)((0, environment_accessor_1.getDestinationServiceCredentials)().uri, token, this.options); } async fetchDestinationWithRefreshTokenFlow(destinationResult) { const token = await this.getAuthTokenForOAuth2RefreshToken(destinationResult); return (0, destination_service_1.fetchDestinationWithTokenRetrieval)((0, environment_accessor_1.getDestinationServiceCredentials)().uri, token, this.options); } 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 updateDestinationCache(destination, destinationOrigin) { if (!this.options.useCache) { return destination; } 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 addTrustStoreConfiguration(destination, origin) { if (destination.originalProperties?.TrustStoreLocation) { const trustStoreCertificate = await (0, destination_service_1.fetchCertificate)((0, environment_accessor_1.getDestinationServiceCredentials)().uri, origin === 'provider' ? this.providerServiceToken.encoded : this.subscriberToken.serviceJwt.encoded, destination.originalProperties.TrustStoreLocation); destination.trustStoreCertificate = 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