Skip to content

Commit

Permalink
Merge pull request data-integrations#33 from cloudsufi/feature/proxyA…
Browse files Browse the repository at this point in the history
…ddition

[PLUGIN-1732] - Successfactors Proxy Implementation
  • Loading branch information
Shubhangi-cs authored Feb 19, 2024
2 parents d3c06f2 + cda12dc commit 3bc7a6e
Show file tree
Hide file tree
Showing 19 changed files with 326 additions and 75 deletions.
33 changes: 32 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,32 @@
# successfactors-plugins
# successfactors-plugins

## Overview

This repository contains resources and documentation for integrating and working with SAP SuccessFactors. SuccessFactors is a cloud-based human capital management (HCM) suite that helps organizations manage their workforce effectively.

## Prerequisites

Before you begin, ensure you have the following:

- **SuccessFactors Account:** Obtain access to SAP SuccessFactors and have the necessary permissions to configure integrations.

- **API Access:** Set up API access and obtain the required API credentials from your SuccessFactors instance.

# License and Trademarks

Copyright © 2022 Cask Data, Inc.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
in compliance with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the
License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
either express or implied. See the License for the specific language governing permissions
and limitations under the License.

Cask is a trademark of Cask Data, Inc. All rights reserved.

Apache, Apache HBase, and HBase are trademarks of The Apache Software Foundation. Used with
permission. No endorsement by The Apache Software Foundation is implied by the use of these marks.
4 changes: 4 additions & 0 deletions docs/SuccessFactors-batchsource.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ You also can use the macro function ${conn(connection-name)}.
**SAP SuccessFactors Logon Password (M)**: SAP SuccessFactors Logon password for user authentication.
**SAP SuccessFactors Base URL (M)**: SAP SuccessFactors Base URL.

## Proxy Configuration
**Proxy URL:** Proxy URL. Must contain a protocol, address and port.
**Username:** Proxy username.
**Password:** Proxy password.

## Advance Option:

Expand Down
5 changes: 5 additions & 0 deletions docs/SuccessFactors-connector.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ Properties

**SAP SuccessFactors Base URL (M)**: SAP SuccessFactors Base URL.

**Proxy URL:** Proxy URL. Must contain a protocol, address and port.

**Username:** Proxy username.

**Password:** Proxy password.

Path of the connection
----------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,7 @@ public static String trim(String rawString) {
* @return SuccessFactorsService instance
*/
public static SuccessFactorsService getSuccessFactorsService(SuccessFactorsPluginConfig pluginConfig) {
SuccessFactorsTransporter transporter = new SuccessFactorsTransporter(pluginConfig.getConnection().getUsername(),
pluginConfig.getConnection().getPassword());
SuccessFactorsTransporter transporter = new SuccessFactorsTransporter(pluginConfig.getConnection());
SuccessFactorsService successFactorsService = new SuccessFactorsService(pluginConfig, transporter);
return successFactorsService;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,7 @@ public ConnectorSpec generateSpec(ConnectorContext connectorContext, ConnectorSp

List<String> listEntities() throws TransportException, IOException {
URL dataURL = HttpUrl.parse(config.getBaseURL()).newBuilder().build().url();
SuccessFactorsTransporter successFactorsHttpClient = new SuccessFactorsTransporter(config.getUsername(),
config.getPassword());
SuccessFactorsTransporter successFactorsHttpClient = new SuccessFactorsTransporter(config);
SuccessFactorsResponseContainer responseContainer = successFactorsHttpClient.callSuccessFactorsEntity
(dataURL, MediaType.APPLICATION_JSON, METADATA);
try (InputStream inputStream = responseContainer.getResponseStream()) {
Expand Down Expand Up @@ -225,8 +224,7 @@ private InputStream callEntityData(long top, String entityName)
URL dataURL = HttpUrl.parse(config.getBaseURL()).newBuilder().addPathSegment(entityName).
addQueryParameter(TOP_OPTION, String.valueOf(top)).addQueryParameter(SELECT_OPTION, selectFields.toString())
.build().url();
SuccessFactorsTransporter successFactorsHttpClient = new SuccessFactorsTransporter(config.getUsername(),
config.getPassword());
SuccessFactorsTransporter successFactorsHttpClient = new SuccessFactorsTransporter(config);
SuccessFactorsResponseContainer responseContainer = successFactorsHttpClient.callSuccessFactorsWithRetry(dataURL);

ExceptionParser.checkAndThrowException("", responseContainer);
Expand Down Expand Up @@ -255,8 +253,7 @@ SuccessFactorsEntityProvider fetchServiceMetadata(String entity) throws Transpor
private InputStream getMetaDataStream(String entity) throws TransportException, IOException {
URL metadataURL = HttpUrl.parse(config.getBaseURL()).newBuilder().addPathSegments(entity)
.addPathSegment(METADATACALL).build().url();
SuccessFactorsTransporter successFactorsHttpClient = new SuccessFactorsTransporter(config.getUsername(),
config.getPassword());
SuccessFactorsTransporter successFactorsHttpClient = new SuccessFactorsTransporter(config);
SuccessFactorsResponseContainer responseContainer = successFactorsHttpClient
.callSuccessFactorsEntity(metadataURL, MediaType.APPLICATION_XML, METADATA);
return responseContainer.getResponseStream();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@

import java.net.HttpURLConnection;
import java.net.URL;

import javax.annotation.Nullable;
import javax.ws.rs.core.MediaType;

/**
Expand All @@ -43,6 +45,9 @@ public class SuccessFactorsConnectorConfig extends PluginConfig {
public static final String BASE_URL = "baseURL";
public static final String UNAME = "username";
public static final String PASSWORD = "password";
public static final String PROPERTY_PROXY_URL = "proxyUrl";
public static final String PROPERTY_PROXY_USERNAME = "proxyUsername";
public static final String PROPERTY_PROXY_PASSWORD = "proxyPassword";
public static final String TEST = "TEST";
private static final String COMMON_ACTION = ResourceConstants.ERR_MISSING_PARAM_OR_MACRO_ACTION.getMsgForKey();
private static final String SAP_SUCCESSFACTORS_USERNAME = "SAP SuccessFactors Username";
Expand All @@ -65,10 +70,44 @@ public class SuccessFactorsConnectorConfig extends PluginConfig {
@Description("SuccessFactors Base URL.")
private final String baseURL;

public SuccessFactorsConnectorConfig(String username, String password, String baseURL) {
@Nullable
@Name(PROPERTY_PROXY_URL)
@Description("Proxy URL. Must contain a protocol, address and port.")
@Macro
private String proxyUrl;

@Nullable
@Name(PROPERTY_PROXY_USERNAME)
@Description("Proxy username.")
@Macro
private String proxyUsername;

@Nullable
@Name(PROPERTY_PROXY_PASSWORD)
@Description("Proxy password.")
@Macro
private String proxyPassword;

public SuccessFactorsConnectorConfig(String username, String password, String baseURL, String proxyUrl,
String proxyUsername, String proxyPassword) {
this.username = username;
this.password = password;
this.baseURL = baseURL;
this.proxyUrl = proxyUrl;
this.proxyUsername = proxyUsername;
this.proxyPassword = proxyPassword;
}

public String getProxyUrl() {
return proxyUrl;
}

public String getProxyUsername() {
return proxyUsername;
}

public String getProxyPassword() {
return proxyPassword;
}

public String getUsername() {
Expand Down Expand Up @@ -109,15 +148,17 @@ public void validateBasicCredentials(FailureCollector failureCollector) {
* Method to validate the credential fields.
*/
public void validateConnection(FailureCollector collector) {
SuccessFactorsTransporter successFactorsHttpClient = new SuccessFactorsTransporter(getUsername(), getPassword());
SuccessFactorsTransporter successFactorsHttpClient = new SuccessFactorsTransporter(this);
URL testerURL = HttpUrl.parse(getBaseURL()).newBuilder().build().url();
SuccessFactorsResponseContainer responseContainer = null;
try {
responseContainer =
successFactorsHttpClient.callSuccessFactorsEntity(testerURL, MediaType.APPLICATION_JSON, TEST);
} catch (TransportException e) {
LOG.error("Unable to fetch the response", e);
collector.addFailure("Unable to call SuccessFatorsEntity", "Please check the values");
collector.addFailure("Unable to call SuccessFactorsEntity",
"Please check the values for basic and proxy parameters if proxy exists.");
return;
}
if (responseContainer.getHttpStatusCode() == HttpURLConnection.HTTP_UNAUTHORIZED) {
String errMsg = ResourceConstants.ERR_INVALID_CREDENTIAL.getMsgForKey();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import io.cdap.plugin.successfactors.common.util.ResourceConstants;
import io.cdap.plugin.successfactors.common.util.SuccessFactorsUtil;
import io.cdap.plugin.successfactors.connector.SuccessFactorsConnector;
import io.cdap.plugin.successfactors.connector.SuccessFactorsConnectorConfig;
import io.cdap.plugin.successfactors.source.config.SuccessFactorsPluginConfig;
import io.cdap.plugin.successfactors.source.input.SuccessFactorsInputFormat;
import io.cdap.plugin.successfactors.source.input.SuccessFactorsInputSplit;
Expand Down Expand Up @@ -126,8 +127,7 @@ public void prepareRun(BatchSourceContext context) throws Exception {
@Nullable
private Schema getOutputSchema(FailureCollector failureCollector) {
if (config.getConnection() != null) {
SuccessFactorsTransporter transporter = new SuccessFactorsTransporter(config.getConnection().getUsername(),
config.getConnection().getPassword());
SuccessFactorsTransporter transporter = new SuccessFactorsTransporter(config.getConnection());
SuccessFactorsService successFactorsServices = new SuccessFactorsService(config, transporter);
try {
//validate if the given parameters form a valid SuccessFactors URL.
Expand All @@ -136,7 +136,14 @@ private Schema getOutputSchema(FailureCollector failureCollector) {
} catch (TransportException te) {
String errorMsg = ExceptionParser.buildTransportError(te);
errorMsg = ResourceConstants.ERR_ODATA_SERVICE_CALL.getMsgForKeyWithCode(errorMsg);
failureCollector.addFailure(errorMsg, null).withConfigProperty(SuccessFactorsPluginConfig.BASE_URL);
if (SuccessFactorsUtil.isNullOrEmpty(config.getConnection().getProxyUrl())) {
failureCollector.addFailure(errorMsg, null).withConfigProperty(SuccessFactorsPluginConfig.BASE_URL);
} else {
failureCollector.addFailure(errorMsg,
"Unable to connect to successFactors. Please check the basic and proxy connection parameters")
.withConfigProperty(SuccessFactorsPluginConfig.BASE_URL)
.withConfigProperty(SuccessFactorsConnectorConfig.PROPERTY_PROXY_URL);
}
} catch (SuccessFactorsServiceException ose) {
attachFieldWithError(ose, failureCollector);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,16 @@ public SuccessFactorsPluginConfig(String referenceName,
String associateEntityName,
@Nullable String username,
@Nullable String password,
@Nullable String proxyUrl,
@Nullable String proxyPassword,
@Nullable String proxyUsername,
@Nullable String filterOption,
@Nullable String selectOption,
@Nullable String expandOption,
@Nullable String additionalQueryParameters,
String paginationType) {
this.connection = new SuccessFactorsConnectorConfig(username, password, baseURL);
this.connection = new SuccessFactorsConnectorConfig(username, password, baseURL, proxyUrl, proxyPassword,
proxyUsername);
this.referenceName = referenceName;
this.entityName = entityName;
this.associateEntityName = associateEntityName;
Expand Down Expand Up @@ -214,7 +218,10 @@ public String getAdditionalQueryParameters() {
* @return boolean flag as per the check
*/
public boolean isSchemaBuildRequired() {
return !(containsMacro(UNAME) || containsMacro(PASSWORD) || containsMacro(BASE_URL) || containsMacro(ENTITY_NAME));
return !(containsMacro(UNAME) || containsMacro(PASSWORD) || containsMacro(BASE_URL) || containsMacro(ENTITY_NAME)
|| containsMacro(SuccessFactorsConnectorConfig.PROPERTY_PROXY_URL)
|| containsMacro(SuccessFactorsConnectorConfig.PROPERTY_PROXY_USERNAME)
|| containsMacro(SuccessFactorsConnectorConfig.PROPERTY_PROXY_PASSWORD));
}

/**
Expand Down Expand Up @@ -300,6 +307,9 @@ public static class Builder {
private String expandOption;
private String paginationType;
private String additionalQueryParameters;
private String proxyUrl;
private String proxyUsername;
private String proxyPassword;

public Builder referenceName(String referenceName) {
this.referenceName = referenceName;
Expand Down Expand Up @@ -331,6 +341,19 @@ public Builder password(@Nullable String password) {
return this;
}

public Builder proxyUrl(@Nullable String proxyUrl) {
this.proxyUrl = proxyUrl;
return this;
}
public Builder proxyUsername(@Nullable String proxyUsername) {
this.proxyUsername = proxyUsername;
return this;
}
public Builder proxyPassword(@Nullable String proxyPassword) {
this.proxyPassword = proxyPassword;
return this;
}

public Builder filterOption(@Nullable String filterOption) {
this.filterOption = filterOption;
return this;
Expand All @@ -357,7 +380,8 @@ public Builder additionalQueryParameters(@Nullable String additionalQueryParamet
}

public SuccessFactorsPluginConfig build() {
return new SuccessFactorsPluginConfig(referenceName, baseURL, entityName, associateEntityName, username, password,
return new SuccessFactorsPluginConfig(referenceName, baseURL, entityName, associateEntityName, username,
password, proxyUrl, proxyUsername, proxyPassword,
filterOption, selectOption, expandOption, additionalQueryParameters,
paginationType);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,20 +24,30 @@
import io.cdap.cdap.api.retry.RetryableException;
import io.cdap.plugin.successfactors.common.exception.TransportException;
import io.cdap.plugin.successfactors.common.util.ResourceConstants;
import io.cdap.plugin.successfactors.common.util.SuccessFactorsUtil;
import io.cdap.plugin.successfactors.connector.SuccessFactorsConnectorConfig;
import okhttp3.Authenticator;
import okhttp3.Credentials;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.Route;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.Proxy;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import javax.ws.rs.core.MediaType;

/**
Expand All @@ -51,14 +61,11 @@ public class SuccessFactorsTransporter {
private static final long WAIT_TIME = 5;
private static final long MAX_NUMBER_OF_RETRY_ATTEMPTS = 5;

private final String username;
private final String password;
private SuccessFactorsConnectorConfig config;
private Response response;

public SuccessFactorsTransporter(String username, String password) {
this.username = username;
this.password = password;

public SuccessFactorsTransporter(SuccessFactorsConnectorConfig pluginConfig) {
this.config = pluginConfig;
}

/**
Expand Down Expand Up @@ -152,7 +159,8 @@ public Response retrySapTransportCall(URL endpoint, String mediaType) throws IOE
* @throws TransportException any error while preparing the {@code OkHttpClient}
*/
private Response transport(URL endpoint, String mediaType) throws IOException, TransportException {
OkHttpClient enhancedOkHttpClient = getConfiguredClient().build();
OkHttpClient enhancedOkHttpClient =
buildConfiguredClient(config.getProxyUrl(), config.getProxyUsername(), config.getProxyPassword());
Request req = buildRequest(endpoint, mediaType);

return enhancedOkHttpClient.newCall(req).execute();
Expand Down Expand Up @@ -189,6 +197,39 @@ private Request buildRequest(URL endpoint, String mediaType) {
.build();
}

/**
* Builds and configures an OkHttpClient with the specified proxy settings and authentication credentials.
* @param proxyUrl The URL of the proxy server (e.g., "http://proxy.example.com:8080").
* Set to null or an empty string to bypass proxy configuration.
* @param proxyUsername The username for proxy authentication. Set to null or an empty string if not required.
* @param proxyPassword The password for proxy authentication. Set to null or an empty string if not required.
* @return An OkHttpClient configured with the specified proxy settings and authentication credentials.
*/
private OkHttpClient buildConfiguredClient(String proxyUrl, String proxyUsername, String proxyPassword)
throws MalformedURLException, TransportException {
OkHttpClient.Builder builder = getConfiguredClient();

if (SuccessFactorsUtil.isNotNullOrEmpty(proxyUrl)) {
URL url = new URL(proxyUrl);
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(url.getHost(), url.getPort()));
builder.proxy(proxy);

if (SuccessFactorsUtil.isNotNullOrEmpty(proxyUsername) && SuccessFactorsUtil.isNotNullOrEmpty(proxyPassword)) {
builder.proxyAuthenticator(new Authenticator() {
@Override
public Request authenticate(Route route, Response response) {
String credential = Credentials.basic(proxyUsername, proxyPassword);
return response.request().newBuilder()
.header("Proxy-Authorization", credential)
.build();
}
});
}
}

return builder.build();
}

/**
* Builds the {@code OkHttpClient.Builder} with following optimized configuration parameters as per the SAP Gateway
* recommendations.
Expand Down Expand Up @@ -218,9 +259,9 @@ private OkHttpClient.Builder getConfiguredClient() throws TransportException {
*/
private String getAuthenticationKey() {
return "Basic " + Base64.getEncoder()
.encodeToString(username
.encodeToString(config.getUsername()
.concat(":")
.concat(password)
.concat(config.getPassword())
.getBytes(StandardCharsets.UTF_8)
);
}
Expand Down
Loading

0 comments on commit 3bc7a6e

Please sign in to comment.