Skip to content
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
The format is based on [Keep a Changelog](http://keepachangelog.com/).

## Version 1.4.1

### Fixed

- An issue related to IAS token fetch. Now plugin works with IAS flow as well.

## Version 1.4.0

### Added
Expand Down
31 changes: 30 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,36 @@ Follow these steps if you want to integrate the SDM CAP Plugin with your own CAP

## Support for Multitenancy

This plugin provides APIs for onboarding and offboarding of repositories for multitenant CAP SaaS applications. Refer the below example where onboarding and offboarding APIs are used on tenant subscription and tenant unsubscription events of SaaS application.

This plugin provides APIs for onboarding and offboarding of repositories for multitenant CAP SaaS applications.

GetDependencies,subscribe and unsubscribe are the mandatory steps to be performed to support multitenancy.

Refer the below example to pass the SDM Service dependencies to SaaSRegistry so that SDM credentials are passed to subscribing tenant.
```java
//Set the SDM xsappname to SaaS Registry Dependency.
@On(event = DeploymentService.EVENT_DEPENDENCIES)
public void onGetDependencies(DependenciesEventContext context) {

List<SaasRegistryDependency> dependencies = new ArrayList<>();
Map<String, Object> uaa = (Map<String, Object>) getSDMCredentials().get("uaa");
dependencies.add(SaasRegistryDependency.create(uaa.get("xsappname").toString()));
context.setResult(dependencies);
}
//Fetch the SDM service credentials
private Map<String, Object> getSDMCredentials() {
List<ServiceBinding> allServiceBindings =
DefaultServiceBindingAccessor.getInstance().getServiceBindings();
ServiceBinding sdmBinding =
allServiceBindings.stream()
.filter(binding -> "sdm".equalsIgnoreCase(binding.getServiceName().orElse(null)))
.findFirst()
.get();
return sdmBinding.getCredentials();

}
```
Refer the below example where onboarding and offboarding APIs are used on tenant subscription and tenant unsubscription events of SaaS application.

```java
@After(event = DeploymentService.EVENT_SUBSCRIBE)
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
</developers>

<properties>
<revision>1.4.1-SNAPSHOT</revision>
<revision>1.4.1</revision>
<java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
Expand Down
246 changes: 1 addition & 245 deletions sdm/src/main/java/com/sap/cds/sdm/handler/TokenHandler.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,9 @@
package com.sap.cds.sdm.handler;

import static com.sap.cds.sdm.constants.SDMConstants.NAMED_USER_FLOW;
import static com.sap.cds.sdm.constants.SDMConstants.TECHNICAL_USER_FLOW;
import static java.util.Objects.requireNonNull;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.sap.cds.sdm.caching.CacheConfig;
import com.sap.cds.sdm.caching.CacheKey;
import com.sap.cds.sdm.caching.TokenCacheKey;
import com.sap.cds.sdm.constants.SDMConstants;
import com.sap.cds.sdm.model.SDMCredentials;
import com.sap.cds.services.environment.CdsProperties;
Expand All @@ -22,31 +14,14 @@
import com.sap.cloud.sdk.cloudplatform.connectivity.OAuth2DestinationBuilder;
import com.sap.cloud.sdk.cloudplatform.connectivity.OnBehalfOf;
import com.sap.cloud.security.config.ClientCredentials;
import com.sap.cloud.security.xsuaa.client.OAuth2ServiceException;
import com.sap.cloud.security.xsuaa.http.HttpHeaders;
import com.sap.cloud.security.xsuaa.http.MediaType;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand Down Expand Up @@ -96,165 +71,6 @@ public static Map<String, Object> getUaaCredentials() {
return sdmBinding.getCredentials();
}

public static String getUserTokenFromAuthorities(
String email, String subdomain, SDMCredentials sdmCredentials) throws IOException {
// Fetch the token from Cache if present use it else generate and store
String cachedToken = null;
String userCredentials = sdmCredentials.getClientId() + ":" + sdmCredentials.getClientSecret();
String authHeaderValue = "Basic " + Base64.encodeBase64String(toBytes(userCredentials));
// Define the authorities (JSON) and URL encode it
String authoritiesJson =
"{\"az_attr\":{\"X-EcmUserEnc\":" + email + ",\"X-EcmAddPrincipals\":" + email + "}}";
String encodedAuthorities =
URLEncoder.encode(authoritiesJson, StandardCharsets.UTF_8.toString());

// Create body parameters including the grant type and authorities
String bodyParams = "grant_type=client_credentials&authorities=" + encodedAuthorities;
byte[] postData = bodyParams.getBytes(StandardCharsets.UTF_8);
String baseTokenUrl = sdmCredentials.getBaseTokenUrl();
if (subdomain != null && !subdomain.equals("")) {
String providersubdomain =
baseTokenUrl.substring(baseTokenUrl.indexOf("/") + 2, baseTokenUrl.indexOf("."));
baseTokenUrl = baseTokenUrl.replace(providersubdomain, subdomain);
}
// Create the URL for the token endpoint
String authUrl = baseTokenUrl + "/oauth/token";
URL url = new URL(authUrl);

// Open the connection and set the properties
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestProperty("Authorization", authHeaderValue);
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
conn.setRequestProperty("charset", "utf-8");
conn.setRequestProperty("Content-Length", String.valueOf(postData.length));
conn.setUseCaches(false);
conn.setDoInput(true);
conn.setDoOutput(true);

// Write the POST data to the output stream
try (DataOutputStream os = new DataOutputStream(conn.getOutputStream())) {
os.write(postData);
}
String resp;
try (DataInputStream is = new DataInputStream(conn.getInputStream());
BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
resp = br.lines().collect(Collectors.joining("\n"));
}
conn.disconnect();
cachedToken = mapper.readValue(resp, JsonNode.class).get("access_token").asText();
TokenCacheKey cacheKey = new TokenCacheKey();
cacheKey.setKey(email + "_" + subdomain);
CacheConfig.getUserAuthoritiesTokenCache().put(cacheKey, cachedToken);
return cachedToken;
}

public static String getDITokenUsingAuthorities(
SDMCredentials sdmCredentials, String email, String subdomain) throws IOException {
TokenCacheKey cacheKey = new TokenCacheKey();
cacheKey.setKey(email + "_" + subdomain);
String cachedToken = CacheConfig.getUserAuthoritiesTokenCache().get(cacheKey);
if (cachedToken == null) {
cachedToken = getUserTokenFromAuthorities(email, subdomain, sdmCredentials);
}
return cachedToken;
}

public static String getDIToken(String token, SDMCredentials sdmCredentials) throws IOException {
JsonObject payloadObj = getTokenFields(token);
String email = payloadObj.get("email").getAsString();
JsonObject tenantDetails = payloadObj.get("ext_attr").getAsJsonObject();
String subdomain = tenantDetails.get("zdn").getAsString();
String tokenexpiry = payloadObj.get("exp").getAsString();
CacheKey cacheKey = new CacheKey();
cacheKey.setKey(email + "_" + subdomain);
cacheKey.setExpiration(tokenexpiry);
String cachedToken = CacheConfig.getUserTokenCache().get(cacheKey);
if (cachedToken == null) {
cachedToken = generateDITokenFromTokenExchange(token, sdmCredentials, payloadObj);
}
return cachedToken;
}

public static Map<String, String> fillTokenExchangeBody(String token, SDMCredentials sdmEnv) {
Map<String, String> parameters = new HashMap<>();
parameters.put("assertion", token);
return parameters;
}

public static String generateDITokenFromTokenExchange(
String token, SDMCredentials sdmCredentials, JsonObject payloadObj)
throws OAuth2ServiceException {
String cachedToken = null;
CloseableHttpClient httpClient = null;
try {
httpClient = HttpClients.createDefault();
if (sdmCredentials.getClientId() == null) {
throw new IOException(SDMConstants.NO_SDM_BINDING);
}
Map<String, String> parameters = fillTokenExchangeBody(token, sdmCredentials);
HttpPost httpPost =
new HttpPost(sdmCredentials.getBaseTokenUrl() + SDMConstants.DI_TOKEN_EXCHANGE_PARAMS);
httpPost.setHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON.value());
httpPost.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED.value());
httpPost.setHeader("X-zid", getTokenFields(token).get("zid").getAsString());

String encoded =
java.util.Base64.getEncoder()
.encodeToString(
(sdmCredentials.getClientId() + ":" + sdmCredentials.getClientSecret())
.getBytes());
httpPost.setHeader("Authorization", "Basic " + encoded);

List<BasicNameValuePair> basicNameValuePairs =
parameters.entrySet().stream()
.map(entry -> new BasicNameValuePair(entry.getKey(), entry.getValue()))
.collect(Collectors.toList());
httpPost.setEntity(new UrlEncodedFormEntity(basicNameValuePairs));

HttpResponse response = httpClient.execute(httpPost);
String responseBody = extractResponseBodyAsString(response);
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
logger.error("Error fetching token with JWT bearer : " + responseBody);
throw new OAuth2ServiceException(
String.format(SDMConstants.DI_TOKEN_EXCHANGE_ERROR, responseBody));
}
Map<String, Object> accessTokenMap = new JSONObject(responseBody).toMap();
cachedToken = String.valueOf(accessTokenMap.get("access_token"));
String expiryTime = payloadObj.get("exp").getAsString();
CacheKey cacheKey = new CacheKey();
JsonObject tenantDetails = payloadObj.get("ext_attr").getAsJsonObject();
String subdomain = tenantDetails.get("zdn").getAsString();
cacheKey.setKey(payloadObj.get("email").getAsString() + "_" + subdomain);
cacheKey.setExpiration(expiryTime);
CacheConfig.getUserTokenCache().put(cacheKey, cachedToken);
} catch (UnsupportedEncodingException e) {
throw new OAuth2ServiceException("Unexpected error parsing URI: " + e.getMessage());
} catch (ClientProtocolException e) {
throw new OAuth2ServiceException(
"Unexpected error while fetching client protocol: " + e.getMessage());
} catch (IOException e) {
logger.error(
"Error in POST request while fetching token with JWT bearer \n"
+ Arrays.toString(e.getStackTrace()));
throw new OAuth2ServiceException(
"Error in POST request while fetching token with JWT bearer: " + e.getMessage());
} finally {
safeClose(httpClient);
}
return cachedToken;
}

private static void safeClose(CloseableHttpClient httpClient) {
if (httpClient != null) {
try {
httpClient.close();
} catch (IOException ex) {
logger.error("Failed to close httpclient \n" + Arrays.toString(ex.getStackTrace()));
}
}
}

public static String extractResponseBodyAsString(HttpResponse response) throws IOException {
// Ensure that InputStream and BufferedReader are automatically closed
try (InputStream inputStream = response.getEntity().getContent();
Expand All @@ -263,14 +79,6 @@ public static String extractResponseBodyAsString(HttpResponse response) throws I
}
}

public static JsonObject getTokenFields(String token) {
String[] chunks = token.split("\\.");
java.util.Base64.Decoder decoder = java.util.Base64.getUrlDecoder();
String payload = new String(decoder.decode(chunks[1]));
JsonElement jelement = new JsonParser().parse(payload);
return jelement.getAsJsonObject();
}

public static HttpClient getHttpClient(
ServiceBinding binding,
CdsProperties.ConnectionPool connectionPoolConfig,
Expand All @@ -293,7 +101,7 @@ public static HttpClient getHttpClient(
}

DefaultHttpDestination destination;
if (type.equals("TOKEN_EXCHANGE")) {
if (type.equals(NAMED_USER_FLOW)) {
destination =
OAuth2DestinationBuilder.forTargetUrl(uaaCredentials.get(SDM_URL).toString())
.withTokenEndpoint(baseTokenUrl)
Expand Down Expand Up @@ -328,56 +136,4 @@ public static HttpClient getHttpClient(

return factory.createHttpClient(destination);
}

public static String getSubdomainFromToken(String token) {
JsonObject payloadObj = TokenHandler.getTokenFields(token);
JsonObject tenantDetails = payloadObj.get("ext_attr").getAsJsonObject();
return tenantDetails.get("zdn").getAsString();
}

public static String getGrantType(String token) {
JsonObject payloadObj = TokenHandler.getTokenFields(token);
String grantType = payloadObj.get("grant_type").getAsString();
if (grantType.equalsIgnoreCase("client_credentials")) {
grantType = TECHNICAL_USER_FLOW;
} else {
grantType = NAMED_USER_FLOW;
}
return grantType;
}

public static String getTechnicalUserAccessToken(String subdomain, SDMCredentials sdmCredentials)
throws IOException {
String baseTokenUrl = sdmCredentials.getBaseTokenUrl();
if (subdomain != null && !subdomain.isEmpty()) {
String providersubdomain =
baseTokenUrl.substring(baseTokenUrl.indexOf("/") + 2, baseTokenUrl.indexOf("."));
baseTokenUrl = baseTokenUrl.replace(providersubdomain, subdomain);
}
String userCredentials = sdmCredentials.getClientId() + ":" + sdmCredentials.getClientSecret();
String authHeaderValue = "Basic " + Base64.encodeBase64String(toBytes(userCredentials));
String bodyParams = "grant_type=client_credentials";
byte[] postData = toBytes(bodyParams);
String authurl = baseTokenUrl + "/oauth/token";
URL url = new URL(authurl);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestProperty("Authorization", authHeaderValue);
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
conn.setRequestProperty("charset", "utf-8");
conn.setRequestProperty("Content-Length", "" + postData.length);
conn.setUseCaches(false);
conn.setDoInput(true);
conn.setDoOutput(true);
try (DataOutputStream os = new DataOutputStream(conn.getOutputStream())) {
os.write(postData);
}
String resp;
try (DataInputStream is = new DataInputStream(conn.getInputStream());
BufferedReader br = new BufferedReader(new InputStreamReader(is))) {
resp = br.lines().collect(Collectors.joining("\n"));
}
conn.disconnect();
return mapper.readValue(resp, JsonNode.class).get("access_token").asText();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
import com.sap.cds.sdm.service.SDMService;
import com.sap.cds.sdm.utilities.SDMUtils;
import com.sap.cds.services.ServiceException;
import com.sap.cds.services.authentication.AuthenticationInfo;
import com.sap.cds.services.authentication.JwtTokenAuthenticationInfo;
import com.sap.cds.services.cds.ApplicationService;
import com.sap.cds.services.cds.CdsCreateEventContext;
import com.sap.cds.services.handler.EventHandler;
Expand Down Expand Up @@ -172,15 +170,14 @@ private void processAttachment(
String filenameInRequest =
(String) attachment.get("fileName"); // Fetching the name of the file from request
String objectId = (String) attachment.get("objectId");
AuthenticationInfo authInfo = context.getAuthenticationInfo();
JwtTokenAuthenticationInfo jwtTokenInfo = authInfo.as(JwtTokenAuthenticationInfo.class);
String jwtToken = jwtTokenInfo.getToken();
SDMCredentials sdmCredentials = TokenHandler.getSDMCredentials();
String fileNameInSDM =
sdmService.getObject(
jwtToken,
objectId,
sdmCredentials); // Fetch original filename from SDM since it's null in attachments
sdmCredentials,
context
.getUserInfo()
.isSystemUser()); // Fetch original filename from SDM since it's null in attachments
// table until save; needed to revert UI-modified names on error.

Map<String, String> secondaryTypeProperties =
Expand Down Expand Up @@ -236,11 +233,11 @@ private void processAttachment(
try {
int responseCode =
sdmService.updateAttachments(
jwtToken,
sdmCredentials,
cmisDocument,
updatedSecondaryProperties,
secondaryPropertiesWithInvalidDefinitions);
secondaryPropertiesWithInvalidDefinitions,
context.getUserInfo().isSystemUser());
switch (responseCode) {
case 403:
// SDM Roles for user are missing
Expand Down
Loading
Loading