@sap-cloud-sdk/connectivity
Version:
SAP Cloud SDK for JavaScript connectivity
362 lines • 19.9 kB
JavaScript
"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