Skip to main content

Destination Cache and Isolation

To reduce the number of calls to fetch destinations, the SAP Cloud SDK provides an option to cache destinations.

This guide explains how destinations are cached and shows options for destination cache configuration. The general destination lookup is described in this guide.

All options discussed in the following are part of the DestinationFetchOptions. You use those options when:

The following examples will use execute() for brevity, but could be replaced by all of the above.

Destination Cache

By default, destination caching is disabled. Enable it by setting the useCache option to true:

.execute({ destinationName: 'DESTINATION', jwt: 'JWT', useCache: true })

The destination cache holds destinations that potentially require HTTP requests, i.e.:

  • destinations that were retrieved from the destination service (requires HTTP request to the destination service)
  • destinations from service bindings (potentially requires HTTP request for authentication flow)

Registered destinations and destinations from environment variables are not held in the destination cache.

Cache Expiration

Destinations include authorization information and therefore should not be cached limitlessly. The cache expiration time depends on the authentication type of the destination:

  • If the destination includes an authorization token, the cache expires at the expiration time of the authorization token.
  • Otherwise the cache expires after 5 minutes (e.g. BasicAuthentication or ClientCertificateAuthentication).

Isolation Strategy

Cached destinations should only be available to authorized parties and therefore need to be isolated on user and/or tenant level. The SAP Cloud SDK provides two options for destination cache isolation:

  • tenant: Destinations are isolated on tenant level. Different users on the same tenant hit the same cache entries for the same destination name.
  • tenant-user: Destinations are cached separately for each user and tenant. Different users on the same tenant hit different cache entries for the same destination name. The same user hits different chache entries on different tenants.

The default cache isolation strategy depends on the provided JWT:

  • no JWT: Isolation is set to tenant. The value for the tenant is the provider account tenant.
  • JWT without user_id: Isolation is set to tenant. The value for the tenant is the zid property of the JWT.
  • JWT with user_id: Isolation is set to tenant-user. The and values are taken from zid and user_id.

The first two cases (no JWT and JWT wihout user_id) are typical for user-independent authentication types, like BasicAuthentication, ClientCertificateAuthentication or OAuth2ClientCredentials. This is a safe choice which prevents privilege escalation due to caching.

Changing Isolation Strategy

There are cases where the default isolation strategy is not optimal. Assume you have a multi-tenant scenario, where you pass a JWT to obtain the destination for the respective tenant.

The JWT contains a user_id but the authentication type of the destination is user-independent. The default isolation strategy for this case is tenant-user, although tenant would suffice.

For such cases, you can manually enforce weaker isolation by passing the isolationStrategy option:

.execute({
destinationName: 'DESTINATION',
jwt: 'JWT',
isolationStrategy: 'tenant'
})
caution

Be very careful with this option. You should know what you're doing when manually downgrading the isolation strategy from tenant-user level. An erroneous configuration can have severe consequences like privilege escalation between users.

Custom Destination Cache

When caching is enabled, destinations are cached in-memory. If you need more control over the destination cache, e.g. to use a persistent or distributed cache, you can create a custom destination cache. For this, implement the DestinationCacheInterface:

export interface DestinationCacheInterface {
/**
* This is called when an entry is added to the cache.
* @param key - The cache key to store the item under.
* @param item - The destination alongside an expiration time to store in the cache.
*/
set(key: string | undefined, item: CacheEntry<Destination>): Promise<void>;
/**
* This is called when an entry shall be retrieved from the cache.
* @param key - The cache key the item is stored under.
*/
get(key: string | undefined): Promise<Destination | undefined>;
/**
* This is called when checking if a given key occurs in the cache.
* @param key - The cache key the item should be stored under, if available.
*/
hasKey(key: string): Promise<boolean>;
/**
* This can be called to remove all existing entries from the cache.
*/
clear(): Promise<void>;
}

Note that each item in the cache is represented using the CacheEntry<T> type, which defines two properties:

  • entry - The item, i.e. destination, you want to cache.
  • expires - The expiration time of the entry in milliseconds. This is optional.

You are free to choose, how you implement the DestinationCacheInterface, e.g. as a class or an object:

// Custom cache implementation as an object
const customCache = {
set(
key: string | undefined,
{ entry, expires }: CacheEntry<Destination>
): Promise<void> {
// add entry to cache
// ensure entry expires after given time
},

get(key: string | undefined): Promise<Destination | undefined> {
// retrieve entry from cache
},

hasKey(key: string): Promise<boolean> {
// check cache based on key
},

clear(): Promise<void> {
// remove all entries from cache
}
};

To use your custom cache, pass your implementation to the global setDestinationCache() function before fetching destinations for the first time. All subsequent calls to fetch destinations will use the custom cache.

setDestinationCache(customCache);

// ...

.execute({ destination:'DESTINATION', jwt:'JWT', useCache: true })

Registered Destination Cache

When registering destinations using the registerDestination() function, destinations are stored in a separate cache. This cache is always enabled and behaves similarly to the destination cache discussed above. It uses the same JWT-dependent defaults for the isolation strategy as the destination cache. To change the isolation strategy pass the isolationStrategy option to the function.

registerDestination(destination, {
jwt: 'JWT',
isolationStrategy: 'tenant'
});

Be cautious when downgrading the isolation strategy to avoid privilege escalation (see change the isolation strategy).

import { registerDestination } from '@sap-cloud-sdk/connectivity';

const destination = {
name: 'DESTINATION',
url: 'https://example.com'
};

registerDestination(destination, { jwt: 'JWT' });

You cannot replace the registered destination cache with a custom cache.