Skip to content

Commit

Permalink
feat: Fetch token for destinations from service bindings (#2633)
Browse files Browse the repository at this point in the history
  • Loading branch information
FrankEssenberger authored Jul 7, 2022
1 parent fdca6f1 commit 93d4128
Show file tree
Hide file tree
Showing 11 changed files with 467 additions and 63 deletions.
5 changes: 5 additions & 0 deletions .changeset/tidy-planets-collect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@sap-cloud-sdk/connectivity': minor
---

[New Functionality] Fetch client credential token for destinations created by service bindings.
1 change: 1 addition & 0 deletions packages/connectivity/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export {
serviceToken,
Destination,
DestinationFetchOptions,
ServiceBindingTransformFunction,
DestinationAccessorOptions,
DestinationSelectionStrategies,
JwtPayload,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`vcap-service-destination throws an error if no service binding can be found for the given name 1`] = `
"Unable to find a service binding for given name \\"non-existent-service\\"! Found the following bindings: my-business-logging, my-custom-service, S4_SYSTEM.
"Unable to find a service binding for given name \\"non-existent-service\\"! Found the following bindings: my-xsuaa, my-business-logging, my-workflow, my-destination-service, my-custom-service, my-s4-hana-cloud, my-saas-registry, my-service-manager.
"
`;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
providerServiceToken,
subscriberServiceToken,
subscriberUserJwt,
unmockDestinationsEnv
unmockDestinationsEnv,
xsuaaBindingMock
} from '../../../../../test-resources/test/test-util';
import {
DestinationWithName,
Expand Down Expand Up @@ -67,7 +68,9 @@ describe('register-destination', () => {
await expect(
registerDestinationCache
.getCacheInstance()
.hasKey('provider::RegisteredDestination')
.hasKey(
`${xsuaaBindingMock.credentials.subaccountid}::RegisteredDestination`
)
).resolves.toBe(true);
});

Expand Down Expand Up @@ -125,7 +128,10 @@ describe('register-destination', () => {

it('adds the auth token if forwardAuthToken is enabled', async () => {
registerDestination(destinationWithForwarding);
const jwtPayload: JwtPayload = { exp: 1234, zid: 'provider' };
const jwtPayload: JwtPayload = {
exp: 1234,
zid: xsuaaBindingMock.credentials.subaccountid
};
const jwtHeader: JwtHeader = { alg: 'HS256' };

const payloadEncoded = base64url(JSON.stringify(jwtPayload));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { createLogger } from '@sap-cloud-sdk/util';
import { decodeJwt } from '../jwt';
import { getXsuaaServiceCredentials } from '../environment-accessor';
import { parseSubdomain } from '../subdomain-replacer';
import { Destination, DestinationAuthToken } from './destination-service-types';
import { DestinationFetchOptions } from './destination-accessor-types';
import {
Expand Down Expand Up @@ -129,17 +128,18 @@ function isolationStrategy(
* This is then passed on to build the cache key.
* @param options - Options passed to register the destination containing the jwt.
* @returns The decoded JWT or a dummy JWT containing the tenant identifier (zid).
* @internal
*/
function decodedJwtOrZid(
export function decodedJwtOrZid(
options?: RegisterDestinationOptions
): Record<string, any> {
if (options?.jwt) {
return decodeJwt(options.jwt);
}

const providerTenantId = parseSubdomain(
getXsuaaServiceCredentials(options?.jwt).url
);
const providerTenantId = getXsuaaServiceCredentials(
options?.jwt
).subaccountid;

return { zid: providerTenantId };
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,38 +1,140 @@
import {
mockServiceToken,
providerUserPayload,
xsuaaBindingMock
} from '../../../../../test-resources/test/test-util';
import * as tokenAccessor from '../token-accessor';
import { getDestination } from './destination-accessor';
import {
destinationForServiceBinding,
ServiceBinding
} from './destination-from-vcap';
import { destinationCache } from './destination-cache';
import SpyInstance = jest.SpyInstance;

describe('vcap-service-destination', () => {
const spy = jest.spyOn(tokenAccessor, 'serviceToken');

beforeAll(() => {
mockServiceToken();
});

beforeEach(() => {
mockServiceBindings();
});

afterEach(() => {
delete process.env.VCAP_SERVICES;
jest.clearAllMocks();
});

function getActualClientId(spyInstance: SpyInstance): string {
return spyInstance.mock.calls[0][0]['credentials']['clientid'];
}

it('creates a destination for the business logging service', async () => {
await expect(
destinationForServiceBinding('my-business-logging')
destinationForServiceBinding('my-business-logging', {
jwt: providerUserPayload
})
).resolves.toEqual({
url: 'https://business-logging.my.example.com',
authentication: 'OAuth2ClientCredentials',
username: 'CLIENT_!_|_!_ID',
password: 'PASSWORD'
name: 'my-business-logging',
authTokens: [expect.objectContaining({ value: expect.any(String) })]
});

expect(getActualClientId(spy)).toBe('clientIdBusinessLogging');
});

it('creates a destination for the service manager service', async () => {
await expect(
destinationForServiceBinding('my-service-manager', {
jwt: providerUserPayload
})
).resolves.toEqual({
url: 'https://service-manager.cfapps.sap.hana.ondemand.com',
authentication: 'OAuth2ClientCredentials',
name: 'my-service-manager',
authTokens: [expect.objectContaining({ value: expect.any(String) })]
});

expect(getActualClientId(spy)).toBe('clientIdServiceManager');
});

it('creates a destination for the destination service', async () => {
await expect(
destinationForServiceBinding('my-destination-service', {
jwt: providerUserPayload
})
).resolves.toEqual({
url: 'https://destination-configuration.cfapps.sap.hana.ondemand.com',
authentication: 'OAuth2ClientCredentials',
name: 'my-destination-service',
authTokens: [expect.objectContaining({ value: expect.any(String) })]
});
expect(getActualClientId(spy)).toBe('clientIdDestination');
});

it('creates a destination for the saas registry', async () => {
await expect(
destinationForServiceBinding('my-saas-registry', {
jwt: providerUserPayload
})
).resolves.toEqual({
url: 'https://saas-manager.mesh.cf.sap.hana.ondemand.com',
authentication: 'OAuth2ClientCredentials',
name: 'my-saas-registry',
authTokens: [expect.objectContaining({ value: expect.any(String) })]
});
expect(getActualClientId(spy)).toBe('clientIdSaasRegistry');
});

it('creates a destination for the workflow', async () => {
await expect(
destinationForServiceBinding('my-workflow', {
jwt: providerUserPayload
})
).resolves.toEqual({
url: 'https://api.workflow-sap.cfapps.sap.hana.ondemand.com/workflow-service/odata',
authentication: 'OAuth2ClientCredentials',
name: 'my-workflow',
authTokens: [expect.objectContaining({ value: expect.any(String) })]
});
expect(getActualClientId(spy)).toBe('clientIdWorkFlow');
});

it('creates a destination for the XF s4 hana cloud service', async () => {
await expect(destinationForServiceBinding('S4_SYSTEM')).resolves.toEqual({
await expect(
destinationForServiceBinding('my-s4-hana-cloud')
).resolves.toEqual({
url: 'https://my.example.com',
authentication: 'BasicAuthentication',
username: 'USER_NAME',
password: 'PASSWORD'
});
});

it('uses the cache if enabled', async () => {
await destinationCache.clear();

await destinationForServiceBinding('my-destination-service', {
useCache: true,
jwt: providerUserPayload
});
await destinationForServiceBinding('my-destination-service', {
useCache: true,
jwt: providerUserPayload
});

expect(spy).toBeCalledTimes(1);
expect(
destinationCache
.getCacheInstance()
.get(`${providerUserPayload.zid}::my-destination`)
).toBeDefined();
});

it('creates a destination using a custom transformation function', async () => {
const serviceBindingTransformFn = jest.fn(
async (serviceBinding: ServiceBinding) => ({
Expand Down Expand Up @@ -93,18 +195,56 @@ function mockServiceBindings() {
}

const serviceBindings = {
xsuaa: [xsuaaBindingMock],
'business-logging': [
{
name: 'my-business-logging',
label: 'business-logging',
tags: [
'business-logging',
'logging',
'com.sap.appbasic.businesslogs',
'comsapappbasicbusinesslogs'
],
credentials: {
writeUrl: 'https://business-logging.my.example.com',
uaa: {
clientid: 'CLIENT_!_|_!_ID',
clientid: 'clientIdBusinessLogging',
clientsecret: 'PASSWORD'
}
}
}
],
workflow: [
{
name: 'my-workflow',
label: 'workflow',
tags: [],
credentials: {
endpoints: {
workflow_odata_url:
'https://api.workflow-sap.cfapps.sap.hana.ondemand.com/workflow-service/odata',
workflow_rest_url:
'https://api.workflow-sap.cfapps.sap.hana.ondemand.com/workflow-service/rest'
},
uaa: {
clientid: 'clientIdWorkFlow',
clientsecret: 'PASSWORD'
}
}
}
],
destination: [
{
name: 'my-destination-service',
label: 'destination',
credentials: {
clientid: 'clientIdDestination',
clientsecret: 'PASSWORD',
uri: 'https://destination-configuration.cfapps.sap.hana.ondemand.com'
}
}
],
'custom-service': [
{
credentials: {
Expand All @@ -115,12 +255,35 @@ const serviceBindings = {
],
's4-hana-cloud': [
{
name: 'S4_SYSTEM',
name: 'my-s4-hana-cloud',
label: 's4-hana-cloud',
credentials: {
Password: 'PASSWORD',
URL: 'https://my.example.com',
User: 'USER_NAME'
}
}
],
'saas-registry': [
{
label: 'saas-registry',
name: 'my-saas-registry',
credentials: {
clientid: 'clientIdSaasRegistry',
clientsecret: 'PASSWORD',
saas_registry_url: 'https://saas-manager.mesh.cf.sap.hana.ondemand.com'
}
}
],
'service-manager': [
{
label: 'service-manager',
name: 'my-service-manager',
credentials: {
sm_url: 'https://service-manager.cfapps.sap.hana.ondemand.com',
clientid: 'clientIdServiceManager',
clientsecret: 'PASSWORD'
}
}
]
};
Loading

0 comments on commit 93d4128

Please sign in to comment.