diff --git a/docs/SuccessFactors-batchsource.md b/docs/SuccessFactors-batchsource.md index 05df6e6..8769bc3 100644 --- a/docs/SuccessFactors-batchsource.md +++ b/docs/SuccessFactors-batchsource.md @@ -19,8 +19,21 @@ annotating metadata, etc. **Use Connection:** Whether to use a connection. If a connection is used, you do not need to provide the credentials. **Connection:** Name of the connection to use. Entity Names information will be provided by the connection. You also can use the macro function ${conn(connection-name)}. +**Authentication Type:** Authentication type used to submit request. Supported types are Basic & OAuth 2.0. Default is Basic Authentication. +* **Basic Authentication** **SAP SuccessFactors Logon Username (M)**: SAP SuccessFactors Logon Username for user authentication. **SAP SuccessFactors Logon Password (M)**: SAP SuccessFactors Logon password for user authentication. +* **OAuth 2.0** +**Client Id:** Client Id required to generate the token. +**Company Id:** Company Id required to generate the token. +**Assertion Token Type:** Assertion token can be entered or can be created using the required parameters. +* **Enter Token** +**Assertion Token:** Assertion token used to generate the access token. +* **Create Token** +**Token URL:** Token URL to generate the assertion token. +**Private Key:** Private key required to generate the token. +**User Id:** User Id required to generate the token. + **SAP SuccessFactors Base URL (M)**: SAP SuccessFactors Base URL. diff --git a/docs/SuccessFactors-connector.md b/docs/SuccessFactors-connector.md index 3430674..ab14dd5 100644 --- a/docs/SuccessFactors-connector.md +++ b/docs/SuccessFactors-connector.md @@ -10,9 +10,20 @@ Properties **Description:** Description of the connection. -**SAP SuccessFactors Logon Username (M)**: SAP SuccessFactors Logon Username for user authentication. - -**SAP SuccessFactors Logon Password (M)**: SAP SuccessFactors Logon password for user authentication. +**Authentication Type:** Authentication type used to submit request. Supported types are Basic & OAuth 2.0. Default is Basic Authentication. +* **Basic Authentication** + **SAP SuccessFactors Logon Username (M)**: SAP SuccessFactors Logon Username for user authentication. + **SAP SuccessFactors Logon Password (M)**: SAP SuccessFactors Logon password for user authentication. +* **OAuth 2.0** + **Client Id:** Client Id required to generate the token. + **Company Id:** Company Id required to generate the token. + **Assertion Token Type:** Assertion token can be entered or can be created using the required parameters. +* **Enter Token** + **Assertion Token:** Assertion token used to generate the access token. +* **Create Token** + **Token URL:** Token URL to generate the assertion token. + **Private Key:** Private key required to generate the token. + **User Id:** User Id required to generate the token. **SAP SuccessFactors Base URL (M)**: SAP SuccessFactors Base URL. diff --git a/pom.xml b/pom.xml index 339fd94..b46b690 100644 --- a/pom.xml +++ b/pom.xml @@ -41,6 +41,7 @@ 4.12 2.0.0 4.9.1 + 4.5.13 2.0.0 2.27.2 2.7.0 @@ -334,7 +335,57 @@ okhttp ${okhttp3.version} - + + org.apache.httpcomponents + httpclient + ${httpclient.version} + + + org.opensaml + xmltooling + 1.4.4 + + + org.opensaml + openws + 1.5.4 + + + org.opensaml + opensaml + 2.6.4 + + + commons-codec + commons-codec + 1.10 + + + xml-security + xmlsec + 1.3.0 + + + commons-collections + commons-collections + 3.2.2 + + + commons-lang + commons-lang + 2.1 + + + commons-logging + commons-logging + 1.1 + + + org.apache.velocity + velocity + 1.5 + pom + com.fasterxml.jackson.core jackson-databind diff --git a/src/main/java/io/cdap/plugin/successfactors/common/util/SuccessFactorsAccessToken.java b/src/main/java/io/cdap/plugin/successfactors/common/util/SuccessFactorsAccessToken.java new file mode 100644 index 0000000..64d83c7 --- /dev/null +++ b/src/main/java/io/cdap/plugin/successfactors/common/util/SuccessFactorsAccessToken.java @@ -0,0 +1,391 @@ +/* + * 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. + */ + +package io.cdap.plugin.successfactors.common.util; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; +import io.cdap.plugin.successfactors.connector.SuccessFactorsConnectorConfig; + +import org.apache.commons.codec.binary.Base64; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.HttpClientBuilder; +import org.joda.time.DateTime; +import org.opensaml.Configuration; +import org.opensaml.DefaultBootstrap; +import org.opensaml.common.SAMLVersion; +import org.opensaml.saml1.core.NameIdentifier; +import org.opensaml.saml2.core.Assertion; +import org.opensaml.saml2.core.Attribute; +import org.opensaml.saml2.core.AttributeStatement; +import org.opensaml.saml2.core.AttributeValue; +import org.opensaml.saml2.core.Audience; +import org.opensaml.saml2.core.AudienceRestriction; +import org.opensaml.saml2.core.AuthnContext; +import org.opensaml.saml2.core.AuthnContextClassRef; +import org.opensaml.saml2.core.AuthnStatement; +import org.opensaml.saml2.core.Conditions; +import org.opensaml.saml2.core.Issuer; +import org.opensaml.saml2.core.NameID; +import org.opensaml.saml2.core.Subject; +import org.opensaml.saml2.core.SubjectConfirmation; +import org.opensaml.saml2.core.SubjectConfirmationData; +import org.opensaml.saml2.core.impl.AssertionMarshaller; +import org.opensaml.xml.ConfigurationException; +import org.opensaml.xml.Namespace; +import org.opensaml.xml.XMLObjectBuilder; +import org.opensaml.xml.io.MarshallingException; +import org.opensaml.xml.schema.XSString; +import org.opensaml.xml.schema.impl.XSStringBuilder; +import org.opensaml.xml.security.SecurityConfiguration; +import org.opensaml.xml.security.SecurityException; +import org.opensaml.xml.security.SecurityHelper; +import org.opensaml.xml.security.x509.BasicX509Credential; +import org.opensaml.xml.signature.Signature; +import org.opensaml.xml.signature.SignatureConstants; +import org.opensaml.xml.signature.SignatureException; +import org.opensaml.xml.signature.Signer; +import org.opensaml.xml.util.XMLHelper; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.w3c.dom.Element; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.List; +import java.util.UUID; + +import javax.xml.namespace.QName; + + +/** + * AccessToken class + */ +public class SuccessFactorsAccessToken { + private static final Logger LOG = LoggerFactory.getLogger(SuccessFactorsAccessToken.class); + private final SuccessFactorsConnectorConfig config; + private final Gson gson = new Gson(); + + + public SuccessFactorsAccessToken(SuccessFactorsConnectorConfig config) { + this.config = config; + } + + /** + * Generates a signed SAML assertion for authentication purposes. + * + * @param clientId The client ID associated with the application. + * @param username The username of the user for whom the assertion is generated. + * @param tokenUrl The URL for obtaining the authentication token. + * @param privateKeyString The private key used for signing the assertion. + * @param expireInMinutes The validity period of the assertion in minutes. + * @param userUserNameAsUserId A boolean indicating whether to use the username as the User ID in the assertion. + * @return The signed SAML assertion as a string. + * @throws Exception If an error occurs during the generation or signing of the SAML assertion. + */ + public static String generateSignedSAMLAssertion(String clientId, String username, String tokenUrl, + String privateKeyString, int expireInMinutes, + boolean userUserNameAsUserId) { + + Assertion unsignedAssertion = buildDefaultAssertion(clientId, username, tokenUrl, expireInMinutes, + userUserNameAsUserId); + PrivateKey privateKey = generatePrivateKey(privateKeyString); + Assertion assertion = sign(unsignedAssertion, privateKey); + String signedAssertion = getSAMLAssertionString(assertion); + + return signedAssertion; + } + + /** + * Builds a default SAML assertion with specified parameters for authentication purposes. + * + * @param clientId The client ID associated with the application. + * @param userId The user ID for whom the assertion is generated. + * @param tokenUrl The URL for obtaining the authentication token. + * @param expireInMinutes The validity period of the assertion in minutes. + * @param userUserNameAsUserId A boolean indicating whether to use the username as the User ID in the assertion. + * @return The constructed SAML assertion. + * @throws RuntimeException if an error occurs during the construction of the SAML assertion. + */ + private static Assertion buildDefaultAssertion(String clientId, String userId, String tokenUrl, int expireInMinutes, + boolean userUserNameAsUserId) { + try { + DateTime currentTime = new DateTime(); + DefaultBootstrap.bootstrap(); + + // Create the assertion and set Id, namespace etc. + Assertion assertion = create(Assertion.class, Assertion.DEFAULT_ELEMENT_NAME); + assertion.setIssueInstant(currentTime); + assertion.setID(UUID.randomUUID().toString()); + assertion.setVersion(SAMLVersion.VERSION_20); + Namespace xsNS = new Namespace("http://www.w3.org/2001/XMLSchema", "xs"); + assertion.addNamespace(xsNS); + Namespace xsiNS = new Namespace("http://www.w3.org/2001/XMLSchema-instance", "xsi"); + assertion.addNamespace(xsiNS); + + Issuer issuer = create(Issuer.class, Issuer.DEFAULT_ELEMENT_NAME); + issuer.setValue("www.successfactors.com"); + assertion.setIssuer(issuer); + + // Create the subject and add it to assertion + Subject subject = create(Subject.class, Subject.DEFAULT_ELEMENT_NAME); + NameID nameID = create(NameID.class, NameID.DEFAULT_ELEMENT_NAME); + nameID.setValue(userId); + nameID.setFormat(NameIdentifier.UNSPECIFIED); + subject.setNameID(nameID); + SubjectConfirmation subjectConfirmation = create(SubjectConfirmation.class, + SubjectConfirmation.DEFAULT_ELEMENT_NAME); + subjectConfirmation.setMethod("urn:oasis:names:tc:SAML:2.0:cm:bearer"); + SubjectConfirmationData sconfData = create(SubjectConfirmationData.class, + SubjectConfirmationData.DEFAULT_ELEMENT_NAME); + sconfData.setNotOnOrAfter(currentTime.plusMinutes(expireInMinutes)); + sconfData.setRecipient(tokenUrl); + subjectConfirmation.setSubjectConfirmationData(sconfData); + subject.getSubjectConfirmations().add(subjectConfirmation); + assertion.setSubject(subject); + + // Create the Conditions + Conditions conditions = buildConditions(currentTime, expireInMinutes); + + AudienceRestriction audienceRestriction = create(AudienceRestriction.class, + AudienceRestriction.DEFAULT_ELEMENT_NAME); + Audience audience = create(Audience.class, Audience.DEFAULT_ELEMENT_NAME); + audience.setAudienceURI("www.successfactors.com"); + List audienceList = audienceRestriction.getAudiences(); + audienceList.add(audience); + List audienceRestrictions = conditions.getAudienceRestrictions(); + audienceRestrictions.add(audienceRestriction); + assertion.setConditions(conditions); + + // Create the AuthnStatement and add it to assertion + AuthnStatement authnStatement = create(AuthnStatement.class, AuthnStatement.DEFAULT_ELEMENT_NAME); + authnStatement.setAuthnInstant(currentTime); + authnStatement.setSessionIndex(UUID.randomUUID().toString()); + AuthnContext authContext = create(AuthnContext.class, AuthnContext.DEFAULT_ELEMENT_NAME); + AuthnContextClassRef authnContextClassRef = create(AuthnContextClassRef.class, + AuthnContextClassRef.DEFAULT_ELEMENT_NAME); + authnContextClassRef.setAuthnContextClassRef(AuthnContext.PPT_AUTHN_CTX); + authContext.setAuthnContextClassRef(authnContextClassRef); + authnStatement.setAuthnContext(authContext); + assertion.getAuthnStatements().add(authnStatement); + + // Create the attribute statement + AttributeStatement attributeStatement = create(AttributeStatement.class, + AttributeStatement.DEFAULT_ELEMENT_NAME); + Attribute apiKeyAttribute = createAttribute("api_key", clientId); + attributeStatement.getAttributes().add(apiKeyAttribute); + assertion.getAttributeStatements().add(attributeStatement); + + // Set user_username as true while using username as userId + if (userUserNameAsUserId) { + AttributeStatement useUserNameAsUserIdStatement = create(AttributeStatement.class, + AttributeStatement.DEFAULT_ELEMENT_NAME); + Attribute useUserNameKeyAttribute = createAttribute("use_username", "true"); + useUserNameAsUserIdStatement.getAttributes().add(useUserNameKeyAttribute); + assertion.getAttributeStatements().add(useUserNameAsUserIdStatement); + } + + return assertion; + } catch (ConfigurationException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + /** + * helper method to create open saml objects. + * @param cls class type + * @param qname qualified name + * @param class type + * @return the saml object + */ + @SuppressWarnings("unchecked") + public static T create(Class cls, QName qname) { + return (T) ((XMLObjectBuilder) Configuration.getBuilderFactory().getBuilder(qname)).buildObject(qname); + } + + private static Attribute createAttribute(String name, String value) { + Attribute result = create(Attribute.class, Attribute.DEFAULT_ELEMENT_NAME); + result.setName(name); + XSStringBuilder stringBuilder = (XSStringBuilder) Configuration.getBuilderFactory() + .getBuilder(XSString.TYPE_NAME); + XSString stringValue = stringBuilder.buildObject(AttributeValue.DEFAULT_ELEMENT_NAME, XSString.TYPE_NAME); + stringValue.setValue(value); + result.getAttributeValues().add(stringValue); + return result; + } + + private static Conditions buildConditions(DateTime currentTime, int expireInMinutes) { + Conditions conditions = create(Conditions.class, Conditions.DEFAULT_ELEMENT_NAME); + conditions.setNotBefore(currentTime.minusMinutes(10)); + conditions.setNotOnOrAfter(currentTime.plusMinutes(expireInMinutes)); + return conditions; + } + + private static String getSAMLAssertionString(Assertion assertion) { + AssertionMarshaller marshaller = new AssertionMarshaller(); + Element element = null; + try { + element = marshaller.marshall(assertion); + } catch (MarshallingException e) { + throw new RuntimeException(e.getMessage(), e); + } + String unencodedSAMLAssertion = XMLHelper.nodeToString(element); + + Base64 base64 = new Base64(); + try { + return base64.encodeToString(unencodedSAMLAssertion.getBytes("UTF-8")); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + /** + * Signs a SAML assertion using the provided private key. + * + * @param assertion The unsigned SAML assertion to be signed. + * @param privateKey The private key used for signing the assertion. + * @return The signed SAML assertion. + * @throws Exception If an error occurs during the signing process. + * - If the SAML assertion is already signed. + * - If an invalid X.509 private key is provided. + * - If there is a failure in signing the SAML2 assertion. + */ + private static Assertion sign(Assertion assertion, PrivateKey privateKey) { + BasicX509Credential credential = new BasicX509Credential(); + credential.setPrivateKey(privateKey); + + if (assertion.getSignature() != null) { + throw new RuntimeException("SAML assertion is already signed"); + } + + if (privateKey == null) { + throw new RuntimeException("Invalid X.509 private key"); + } + + try { + Signature signature = (Signature) Configuration.getBuilderFactory() + .getBuilder(Signature.DEFAULT_ELEMENT_NAME).buildObject(Signature.DEFAULT_ELEMENT_NAME); + signature.setSigningCredential(credential); + SecurityConfiguration secConfig = Configuration.getGlobalSecurityConfiguration(); + String keyInfoGeneratorProfile = null; // "XMLSignature"; + SecurityHelper.prepareSignatureParams(signature, credential, secConfig, keyInfoGeneratorProfile); + + // Support sha256 signing algorithm for external oauth saml assertion + signature.setSignatureAlgorithm(SignatureConstants.ALGO_ID_SIGNATURE_RSA_SHA256); + + assertion.setSignature(signature); + Configuration.getMarshallerFactory().getMarshaller(assertion).marshall(assertion); + Signer.signObject(signature); + } catch (MarshallingException | SecurityException | SignatureException e) { + throw new RuntimeException("Failure in signing the SAML2 assertion", e); + } + return assertion; + } + + private static PrivateKey generatePrivateKey(String privateKeyString) { + try { + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + + // Decode the base64-encoded private key string + String pk2 = new String(Base64.decodeBase64(privateKeyString), "UTF-8"); + + // Extract the actual private key string if it is in a format like "privateKey###additionalInfo" + String[] strs = pk2.split("###"); + if (strs.length == 2) { + privateKeyString = strs[0]; + } + + // Generate the private key from the decoded key string + PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyString)); + return keyFactory.generatePrivate(privateKeySpec); + } catch (NoSuchAlgorithmException | UnsupportedEncodingException | InvalidKeySpecException e) { + // Throw a runtime exception if an error occurs during the private key generation process + throw new RuntimeException("Error generating private key", e); + } + } + + public String getAssertionToken() { + + /** + * Below code is to produce signed assertion via code directly using provided + * input + */ + String tokenUrl = config.getTokenURL(), clientId = config.getClientId(), + privateKey = config.getPrivateKey(), userId = config.getUserId(); + boolean useUserNameAsUserId = false; + int expireInMinutes = 10; + + if (tokenUrl != null && clientId != null && privateKey != null && userId != null) { + LOG.info("All properties are set, generating the SAML Assertion..."); + + String signedSAMLAssertion = generateSignedSAMLAssertion(clientId, userId, tokenUrl, privateKey, + expireInMinutes, useUserNameAsUserId); + LOG.info("Signed SAML Assertion is generated"); + return signedSAMLAssertion; + } + return null; + } + + public String getAccessToken(String assertionToken) throws IOException { + HttpClient client = HttpClientBuilder.create().build(); + + // Build POST request + HttpPost request = new HttpPost(URI.create("https://apisalesdemo2.successfactors.eu/oauth/token")); + + // Set headers + request.setHeader("Authorization", "none"); + request.setHeader("Content-Type", "application/x-www-form-urlencoded"); + + // Build request body + StringBuilder body = new StringBuilder(); + body.append("client_id=").append(config.getClientId()); + body.append("&company_id=").append(config.getCompanyId()); + body.append("&grant_type=").append("urn:ietf:params:oauth:grant-type:saml2-bearer"); + body.append("&assertion=").append(assertionToken); + + // Set request entity + request.setEntity(new StringEntity(body.toString())); + + // Execute request and get response + HttpResponse response = client.execute(request); + String accessToken = null; + JsonObject jsonObject = null; + + // Read response body + if (response.getEntity() != null) { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent()))) { + jsonObject = gson.fromJson(reader, JsonObject.class); + + // Check if "access_token" is present in the JSON response + if (jsonObject != null && jsonObject.has("access_token")) { + accessToken = jsonObject.get("access_token").getAsString(); + } + } + } + return accessToken; + } +} diff --git a/src/main/java/io/cdap/plugin/successfactors/common/util/SuccessFactorsUtil.java b/src/main/java/io/cdap/plugin/successfactors/common/util/SuccessFactorsUtil.java index e646586..da2298f 100644 --- a/src/main/java/io/cdap/plugin/successfactors/common/util/SuccessFactorsUtil.java +++ b/src/main/java/io/cdap/plugin/successfactors/common/util/SuccessFactorsUtil.java @@ -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; } diff --git a/src/main/java/io/cdap/plugin/successfactors/connector/SuccessFactorsConnector.java b/src/main/java/io/cdap/plugin/successfactors/connector/SuccessFactorsConnector.java index 6f4e723..da10995 100644 --- a/src/main/java/io/cdap/plugin/successfactors/connector/SuccessFactorsConnector.java +++ b/src/main/java/io/cdap/plugin/successfactors/connector/SuccessFactorsConnector.java @@ -149,8 +149,7 @@ public ConnectorSpec generateSpec(ConnectorContext connectorContext, ConnectorSp List 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()) { @@ -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); @@ -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(); diff --git a/src/main/java/io/cdap/plugin/successfactors/connector/SuccessFactorsConnectorConfig.java b/src/main/java/io/cdap/plugin/successfactors/connector/SuccessFactorsConnectorConfig.java index 50f96c5..fb76d20 100644 --- a/src/main/java/io/cdap/plugin/successfactors/connector/SuccessFactorsConnectorConfig.java +++ b/src/main/java/io/cdap/plugin/successfactors/connector/SuccessFactorsConnectorConfig.java @@ -20,7 +20,6 @@ import io.cdap.cdap.api.annotation.Name; import io.cdap.cdap.api.plugin.PluginConfig; import io.cdap.cdap.etl.api.FailureCollector; -import io.cdap.plugin.successfactors.common.exception.SuccessFactorsServiceException; import io.cdap.plugin.successfactors.common.exception.TransportException; import io.cdap.plugin.successfactors.common.util.ResourceConstants; import io.cdap.plugin.successfactors.common.util.SuccessFactorsUtil; @@ -33,16 +32,29 @@ import java.net.HttpURLConnection; import java.net.URL; + +import javax.annotation.Nullable; import javax.ws.rs.core.MediaType; /** * SuccessFactorsConnectorConfig Class */ public class SuccessFactorsConnectorConfig extends PluginConfig { - + public static final String PROPERTY_AUTH_TYPE = "authType"; + public static final String ASSERTION_TOKEN_TYPE = "assertionTokenType"; + public static final String BASIC_AUTH = "basicAuth"; + public static final String OAUTH2 = "oAuth2"; + public static final String ENTER_TOKEN = "enterToken"; + public static final String CREATE_TOKEN = "createToken"; public static final String BASE_URL = "baseURL"; public static final String UNAME = "username"; public static final String PASSWORD = "password"; + public static final String TOKEN_URL = "tokenURL"; + public static final String CLIENT_ID = "clientId"; + public static final String PRIVATE_KEY = "privateKey"; + public static final String USER_ID = "userId"; + public static final String ASSERTION_TOKEN = "assertionToken"; + public static final String COMPANY_ID = "companyId"; 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"; @@ -50,25 +62,84 @@ public class SuccessFactorsConnectorConfig extends PluginConfig { private static final String SAP_SUCCESSFACTORS_BASE_URL = "SAP SuccessFactors Base URL"; private static final Logger LOG = LoggerFactory.getLogger(SuccessFactorsConnectorConfig.class); + @Name(PROPERTY_AUTH_TYPE) + @Description("Type of authentication used to submit request. OAuth 2.0, Basic Authentication types are available.") + protected String authType; + + @Nullable + @Name(ASSERTION_TOKEN_TYPE) + @Description("Assertion token can be entered or can be created using the required parameters.") + protected String assertionTokenType; + + @Nullable @Name(UNAME) @Macro @Description("SAP SuccessFactors Username for user authentication.") private final String username; + @Nullable @Name(PASSWORD) @Macro @Description("SAP SuccessFactors password for user authentication.") private final String password; + @Nullable + @Name(TOKEN_URL) + @Macro + @Description("Token URL to generate the assertion token.") + private final String tokenURL; + + @Nullable + @Name(CLIENT_ID) + @Macro + @Description("Client Id to generate the token.") + private final String clientId; + + @Nullable + @Name(PRIVATE_KEY) + @Macro + @Description("Private key to generate the token.") + private final String privateKey; + + @Nullable + @Name(USER_ID) + @Macro + @Description("User Id to generate the token.") + private final String userId; + + @Nullable + @Name(ASSERTION_TOKEN) + @Macro + @Description("Assertion token used to generate the access token.") + private final String assertionToken; + + @Nullable + @Name(COMPANY_ID) + @Macro + @Description("Company Id to generate the token.") + private final String companyId; + @Macro @Name(BASE_URL) @Description("SuccessFactors Base URL.") private final String baseURL; - public SuccessFactorsConnectorConfig(String username, String password, String baseURL) { + public SuccessFactorsConnectorConfig(@Nullable String username, @Nullable String password, @Nullable String tokenURL, + @Nullable String clientId, @Nullable String privateKey, @Nullable String userId, + @Nullable String companyId, String baseURL, String authType, + String assertionTokenType, + @Nullable String samlUsername, @Nullable String assertionToken) { this.username = username; this.password = password; + this.tokenURL = tokenURL; + this.clientId = clientId; + this.privateKey = privateKey; + this.userId = userId; + this.companyId = companyId; this.baseURL = baseURL; + this.authType = authType; + this.assertionTokenType = assertionTokenType; + this.assertionToken = assertionToken; } public String getUsername() { @@ -79,20 +150,100 @@ public String getPassword() { return password; } + public String getAuthType() { + return authType; + } + + @Nullable + public String getTokenURL() { + return tokenURL; + } + + @Nullable + public String getCompanyId() { + return companyId; + } + + @Nullable + public String getAssertionTokenType() { + return assertionTokenType; + } + + @Nullable + public String getClientId() { + return clientId; + } + + @Nullable + public String getAssertionToken() { + return assertionToken; + } + + @Nullable + public String getPrivateKey() { + return privateKey; + } + + @Nullable + public String getUserId() { + return userId; + } + public String getBaseURL() { return baseURL; } public void validateBasicCredentials(FailureCollector failureCollector) { - if (SuccessFactorsUtil.isNullOrEmpty(getUsername()) && !containsMacro(UNAME)) { - String errMsg = ResourceConstants.ERR_MISSING_PARAM_PREFIX.getMsgForKey(SAP_SUCCESSFACTORS_USERNAME); - failureCollector.addFailure(errMsg, COMMON_ACTION).withConfigProperty(UNAME); + if (SuccessFactorsUtil.isNullOrEmpty(getAuthType())) { + return; + } + + if (authType.equals(BASIC_AUTH)) { + if (SuccessFactorsUtil.isNullOrEmpty(getUsername()) && !containsMacro(UNAME)) { + String errMsg = ResourceConstants.ERR_MISSING_PARAM_PREFIX.getMsgForKey(SAP_SUCCESSFACTORS_USERNAME); + failureCollector.addFailure(errMsg, COMMON_ACTION).withConfigProperty(UNAME); + } + if (SuccessFactorsUtil.isNullOrEmpty(getPassword()) && !containsMacro(PASSWORD)) { + String errMsg = ResourceConstants.ERR_MISSING_PARAM_PREFIX.getMsgForKey(SAP_SUCCESSFACTORS_PASSWORD); + failureCollector.addFailure(errMsg, COMMON_ACTION).withConfigProperty(PASSWORD); + } + } - if (SuccessFactorsUtil.isNullOrEmpty(getPassword()) && !containsMacro(PASSWORD)) { - String errMsg = ResourceConstants.ERR_MISSING_PARAM_PREFIX.getMsgForKey(SAP_SUCCESSFACTORS_PASSWORD); - failureCollector.addFailure(errMsg, COMMON_ACTION).withConfigProperty(PASSWORD); + if (authType.equals(OAUTH2)) { + if (SuccessFactorsUtil.isNullOrEmpty(getClientId()) && !containsMacro(CLIENT_ID)) { + String errMsg = ResourceConstants.ERR_MISSING_PARAM_PREFIX.getMsgForKey(CLIENT_ID); + failureCollector.addFailure(errMsg, COMMON_ACTION).withConfigProperty(CLIENT_ID); + } + if (SuccessFactorsUtil.isNullOrEmpty(getCompanyId()) && !containsMacro(COMPANY_ID)) { + String errMsg = ResourceConstants.ERR_MISSING_PARAM_PREFIX.getMsgForKey(COMPANY_ID); + failureCollector.addFailure(errMsg, COMMON_ACTION).withConfigProperty(COMPANY_ID); + } + + if (assertionTokenType.equals(ENTER_TOKEN)) { + if (SuccessFactorsUtil.isNullOrEmpty(getAssertionToken()) && !containsMacro(ASSERTION_TOKEN)) { + String errMsg = ResourceConstants.ERR_MISSING_PARAM_PREFIX.getMsgForKey(ASSERTION_TOKEN); + failureCollector.addFailure(errMsg, COMMON_ACTION).withConfigProperty(ASSERTION_TOKEN); + } + } + + if (assertionTokenType.equals(CREATE_TOKEN)) { + if (SuccessFactorsUtil.isNullOrEmpty(getTokenURL()) && !containsMacro(TOKEN_URL)) { + String errMsg = ResourceConstants.ERR_MISSING_PARAM_PREFIX.getMsgForKey(TOKEN_URL); + failureCollector.addFailure(errMsg, COMMON_ACTION).withConfigProperty(TOKEN_URL); + } + if (SuccessFactorsUtil.isNullOrEmpty(getPrivateKey()) && !containsMacro(PRIVATE_KEY)) { + String errMsg = ResourceConstants.ERR_MISSING_PARAM_PREFIX.getMsgForKey(PRIVATE_KEY); + failureCollector.addFailure(errMsg, COMMON_ACTION).withConfigProperty(PRIVATE_KEY); + } + if ((SuccessFactorsUtil.isNullOrEmpty(getUserId()) && !containsMacro(USER_ID))) { + String errMsg = ResourceConstants.ERR_MISSING_PARAM_PREFIX.getMsgForKey(USER_ID); + failureCollector.addFailure(errMsg, COMMON_ACTION).withConfigProperty(USER_ID); + } + } + } + if (SuccessFactorsUtil.isNullOrEmpty(getBaseURL()) && !containsMacro(BASE_URL)) { String errMsg = ResourceConstants.ERR_MISSING_PARAM_PREFIX.getMsgForKey(SAP_SUCCESSFACTORS_BASE_URL); failureCollector.addFailure(errMsg, COMMON_ACTION).withConfigProperty(BASE_URL); @@ -109,7 +260,7 @@ 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 { @@ -118,6 +269,7 @@ public void validateConnection(FailureCollector collector) { } catch (TransportException e) { LOG.error("Unable to fetch the response", e); collector.addFailure("Unable to call SuccessFatorsEntity", "Please check the values"); + return; } if (responseContainer.getHttpStatusCode() == HttpURLConnection.HTTP_UNAUTHORIZED) { String errMsg = ResourceConstants.ERR_INVALID_CREDENTIAL.getMsgForKey(); diff --git a/src/main/java/io/cdap/plugin/successfactors/source/SuccessFactorsSource.java b/src/main/java/io/cdap/plugin/successfactors/source/SuccessFactorsSource.java index ec8087e..bd56c04 100644 --- a/src/main/java/io/cdap/plugin/successfactors/source/SuccessFactorsSource.java +++ b/src/main/java/io/cdap/plugin/successfactors/source/SuccessFactorsSource.java @@ -39,12 +39,14 @@ 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; import io.cdap.plugin.successfactors.source.input.SuccessFactorsPartitionBuilder; import io.cdap.plugin.successfactors.source.service.SuccessFactorsService; import io.cdap.plugin.successfactors.source.transport.SuccessFactorsTransporter; + import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.io.LongWritable; import org.apache.hadoop.mapreduce.Job; @@ -56,6 +58,7 @@ import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; + import javax.annotation.Nullable; /** @@ -126,8 +129,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. @@ -157,8 +159,24 @@ private void attachFieldWithError(SuccessFactorsServiceException ose, FailureCol errMsg = ResourceConstants.ERR_ODATA_ENTITY_FAILURE.getMsgForKeyWithCode(errMsg); switch (ose.getErrorCode()) { case HttpURLConnection.HTTP_UNAUTHORIZED: - failureCollector.addFailure(errMsg, null).withConfigProperty(SuccessFactorsPluginConfig.UNAME); - failureCollector.addFailure(errMsg, null).withConfigProperty(SuccessFactorsPluginConfig.PASSWORD); + if (config.getConnection().getAuthType().equals(SuccessFactorsConnectorConfig.BASIC_AUTH)) { + failureCollector.addFailure(errMsg, null).withConfigProperty(SuccessFactorsPluginConfig.UNAME); + failureCollector.addFailure(errMsg, null).withConfigProperty(SuccessFactorsPluginConfig.PASSWORD); + } else { + failureCollector.addFailure(errMsg, null).withConfigProperty(SuccessFactorsConnectorConfig.COMPANY_ID); + failureCollector.addFailure(errMsg, null).withConfigProperty(SuccessFactorsConnectorConfig.CLIENT_ID); + if (config.getConnection().getAssertionTokenType().equals(SuccessFactorsConnectorConfig.ENTER_TOKEN)) { + failureCollector.addFailure(errMsg, null). + withConfigProperty(SuccessFactorsConnectorConfig.ASSERTION_TOKEN); + } else { + failureCollector.addFailure(errMsg, null). + withConfigProperty(SuccessFactorsConnectorConfig.PRIVATE_KEY); + failureCollector.addFailure(errMsg, null). + withConfigProperty(SuccessFactorsConnectorConfig.USER_ID); + failureCollector.addFailure(errMsg, null). + withConfigProperty(SuccessFactorsConnectorConfig.TOKEN_URL); + } + } break; case HttpURLConnection.HTTP_FORBIDDEN: case ExceptionParser.NO_VERSION_FOUND: diff --git a/src/main/java/io/cdap/plugin/successfactors/source/config/SuccessFactorsPluginConfig.java b/src/main/java/io/cdap/plugin/successfactors/source/config/SuccessFactorsPluginConfig.java index c03bcf9..24268d4 100644 --- a/src/main/java/io/cdap/plugin/successfactors/source/config/SuccessFactorsPluginConfig.java +++ b/src/main/java/io/cdap/plugin/successfactors/source/config/SuccessFactorsPluginConfig.java @@ -135,12 +135,22 @@ public SuccessFactorsPluginConfig(String referenceName, String associateEntityName, @Nullable String username, @Nullable String password, + @Nullable String tokenURL, + @Nullable String clientId, + @Nullable String privateKey, + @Nullable String userId, + @Nullable String samlUsername, + @Nullable String assertionToken, + @Nullable String companyId, + String authType, + String assertionTokenType, @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, tokenURL, clientId, privateKey, userId, + companyId, baseURL, authType, assertionTokenType, samlUsername, assertionToken); this.referenceName = referenceName; this.entityName = entityName; this.associateEntityName = associateEntityName; @@ -150,6 +160,7 @@ public SuccessFactorsPluginConfig(String referenceName, this.paginationType = paginationType; this.additionalQueryParameters = additionalQueryParameters; } + @Nullable public SuccessFactorsConnectorConfig getConnection() { return connection; @@ -300,6 +311,15 @@ public static class Builder { private String expandOption; private String paginationType; private String additionalQueryParameters; + private String tokenURL; + private String clientId; + private String privateKey; + private String userId; + private String samlUsername; + private String assertionToken; + private String companyId; + private String authType; + private String assertionTokenType; public Builder referenceName(String referenceName) { this.referenceName = referenceName; @@ -346,11 +366,46 @@ public Builder expandOption(@Nullable String expandOption) { return this; } + public Builder authType(@Nullable String authType) { + this.authType = authType; + return this; + } + + public Builder setTokenURL(@Nullable String tokenURL) { + this.tokenURL = tokenURL; + return this; + } + + public Builder setClientId(@Nullable String clientId) { + this.clientId = clientId; + return this; + } + + public Builder setPrivateKey(@Nullable String privateKey) { + this.privateKey = privateKey; + return this; + } + + public Builder setUserId(@Nullable String userId) { + this.userId = userId; + return this; + } + public Builder paginationType(@Nullable String paginationType) { this.paginationType = paginationType; return this; } + public Builder assertionTokenType(@Nullable String assertionTokenType) { + this.assertionTokenType = assertionTokenType; + return this; + } + + public Builder assertionToken(@Nullable String assertionToken) { + this.assertionToken = assertionToken; + return this; + } + public Builder additionalQueryParameters(@Nullable String additionalQueryParameters) { this.additionalQueryParameters = additionalQueryParameters; return this; @@ -358,8 +413,9 @@ public Builder additionalQueryParameters(@Nullable String additionalQueryParamet public SuccessFactorsPluginConfig build() { return new SuccessFactorsPluginConfig(referenceName, baseURL, entityName, associateEntityName, username, password, - filterOption, selectOption, expandOption, additionalQueryParameters, - paginationType); + tokenURL, clientId, privateKey, userId, samlUsername, assertionToken, + companyId, authType, assertionTokenType, filterOption, selectOption, + expandOption, additionalQueryParameters, paginationType); } } } diff --git a/src/main/java/io/cdap/plugin/successfactors/source/service/SuccessFactorsService.java b/src/main/java/io/cdap/plugin/successfactors/source/service/SuccessFactorsService.java index 0780dc6..d4b8595 100644 --- a/src/main/java/io/cdap/plugin/successfactors/source/service/SuccessFactorsService.java +++ b/src/main/java/io/cdap/plugin/successfactors/source/service/SuccessFactorsService.java @@ -275,7 +275,6 @@ public ODataFeed readServiceEntityData(Edm edm, Long skip, Long top) String nextLink = dataFeed.getFeedMetadata().getNextLink(); if (nextLink != null) { nextUrl = nextLink; - LOG.info("Next page url: {}", nextLink); } } return dataFeed; diff --git a/src/main/java/io/cdap/plugin/successfactors/source/transport/SuccessFactorsTransporter.java b/src/main/java/io/cdap/plugin/successfactors/source/transport/SuccessFactorsTransporter.java index ec30cd4..7be9d2b 100644 --- a/src/main/java/io/cdap/plugin/successfactors/source/transport/SuccessFactorsTransporter.java +++ b/src/main/java/io/cdap/plugin/successfactors/source/transport/SuccessFactorsTransporter.java @@ -24,9 +24,12 @@ 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.SuccessFactorsAccessToken; +import io.cdap.plugin.successfactors.connector.SuccessFactorsConnectorConfig; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -34,10 +37,13 @@ import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; +import java.rmi.ConnectException; import java.util.Base64; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + import javax.ws.rs.core.MediaType; /** @@ -50,15 +56,12 @@ public class SuccessFactorsTransporter { private static final long CONNECTION_TIMEOUT = 300; 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 static String accessToken; private Response response; + private final SuccessFactorsConnectorConfig config; - public SuccessFactorsTransporter(String username, String password) { - this.username = username; - this.password = password; - + public SuccessFactorsTransporter(SuccessFactorsConnectorConfig config) { + this.config = config; } /** @@ -118,8 +121,10 @@ public SuccessFactorsResponseContainer callSuccessFactorsWithRetry(URL endpoint) * @throws IOException if all retries fail */ public Response retrySapTransportCall(URL endpoint, String mediaType) throws IOException { + AtomicInteger attemptCounter = new AtomicInteger(0); Callable fetchRecords = () -> { response = transport(endpoint, mediaType); + LOG.info("Response code in retry: {}", response.code()); if (response != null && response.code() >= HttpURLConnection.HTTP_INTERNAL_ERROR) { throw new RetryableException(); } @@ -128,6 +133,8 @@ public Response retrySapTransportCall(URL endpoint, String mediaType) throws IOE Retryer retryer = RetryerBuilder.newBuilder() .retryIfExceptionOfType(RetryableException.class) + .retryIfExceptionOfType(ExecutionException.class) + .retryIfExceptionOfType(ConnectException.class) .withWaitStrategy(WaitStrategies.exponentialWait(WAIT_TIME, TimeUnit.SECONDS)) .withStopStrategy(StopStrategies.stopAfterAttempt((int) MAX_NUMBER_OF_RETRY_ATTEMPTS)) .build(); @@ -135,8 +142,8 @@ public Response retrySapTransportCall(URL endpoint, String mediaType) throws IOE try { retryer.call(fetchRecords); } catch (RetryException | ExecutionException e) { - LOG.error("Data Recovery failed for URL {}.", endpoint); - throw new IOException(); + LOG.error("Data Recovery failed for URL {}.", endpoint, e); + throw new IOException("Failed after retries", e.getCause()); } return response; @@ -153,11 +160,50 @@ public Response retrySapTransportCall(URL endpoint, String mediaType) throws IOE */ private Response transport(URL endpoint, String mediaType) throws IOException, TransportException { OkHttpClient enhancedOkHttpClient = getConfiguredClient().build(); - Request req = buildRequest(endpoint, mediaType); + Request req = null; + + if (config.getAuthType().equals(SuccessFactorsConnectorConfig.BASIC_AUTH)) { + req = buildRequest(endpoint, mediaType); + } else { + if (accessToken == null || accessToken.isEmpty()) { + accessToken = getAccessToken(); + } + req = buildRequestWithBearerToken(endpoint, mediaType, accessToken); + + try { + Response response = enhancedOkHttpClient.newCall(req).execute(); + + // If the response code is 403 (Forbidden), attempt to refresh access token + if (response.code() == HttpURLConnection.HTTP_FORBIDDEN) { + LOG.info("refreshing access token"); + accessToken = getAccessToken(); // Refresh access token + req = buildRequestWithBearerToken(endpoint, mediaType, accessToken); + response = enhancedOkHttpClient.newCall(req).execute(); + } + + return response; + } catch (IOException e) { + throw new IOException("Failed to execute the request", e); + } + } return enhancedOkHttpClient.newCall(req).execute(); } + private String getAccessToken() throws IOException { + SuccessFactorsAccessToken token = new SuccessFactorsAccessToken(config); + + try { + if (config.getAssertionToken() == null) { + return token.getAccessToken(token.getAssertionToken()); + } else { + return token.getAccessToken(config.getAssertionToken()); + } + } catch (IOException e) { + throw new IOException("Unable to fetch access token", e); + } + } + /** * Prepares the {@code SuccessFactorsResponseContainer} from the given {@code Response}. * @@ -218,13 +264,22 @@ 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) ); } + private Request buildRequestWithBearerToken(URL endpoint, String mediaType, String accessToken) { + return new Request.Builder() + .addHeader("Authorization", "Bearer " + accessToken) + .addHeader("Accept", mediaType) + .get() + .url(endpoint) + .build(); + } + /** * Calls the SuccessFactors entity for the given URL and returns the respective response. * Supported calls are: diff --git a/src/main/resources/i10n/SuccessFactorsBatchSourceBundle.properties b/src/main/resources/i10n/SuccessFactorsBatchSourceBundle.properties index 9e432c5..5f1cbb5 100644 --- a/src/main/resources/i10n/SuccessFactorsBatchSourceBundle.properties +++ b/src/main/resources/i10n/SuccessFactorsBatchSourceBundle.properties @@ -11,7 +11,7 @@ root.cause.log=Root Cause: ## SAP SuccessFactors specific messages err.feature.not.supported=Entity 'Key' property based extraction is not supported. err.invalid.base.url=Please verify the provided base url is correct. Please contact the SAP administrator. -err.invalid.credential=Please verify the connection parameters. Username or password is incorrect. +err.invalid.credential=Please verify the connection parameters. err.invalid.entityCall=Associated Entity must be used with the expand field. Please provide the Expand fields. ## SAP SuccessFactors - Design Time generic error diff --git a/src/test/java/io/cdap/plugin/successfactors/connector/SuccessFactorsConnectorTest.java b/src/test/java/io/cdap/plugin/successfactors/connector/SuccessFactorsConnectorTest.java index acfbfb8..2d6c999 100644 --- a/src/test/java/io/cdap/plugin/successfactors/connector/SuccessFactorsConnectorTest.java +++ b/src/test/java/io/cdap/plugin/successfactors/connector/SuccessFactorsConnectorTest.java @@ -78,6 +78,7 @@ public void testConfiguration() throws TransportException, SuccessFactorsService .baseURL("http://localhost") .entityName("entity-name") .username("username") + .authType("basicAuth") .password("password"); pluginConfig = pluginConfigBuilder.build(); @@ -85,8 +86,7 @@ public void testConfiguration() throws TransportException, SuccessFactorsService @Test public void testValidateSuccessfulConnection() throws TransportException, SuccessFactorsServiceException { - successFactorsTransporter = new SuccessFactorsTransporter(pluginConfig.getConnection().getUsername(), - pluginConfig.getConnection().getPassword()); + successFactorsTransporter = new SuccessFactorsTransporter(pluginConfig.getConnection()); new Expectations(SuccessFactorsUrlContainer.class, SuccessFactorsTransporter.class, SuccessFactorsSchemaGenerator.class) { { @@ -103,8 +103,7 @@ public void testValidateSuccessfulConnection() throws TransportException, Succes public void testValidateUnauthorisedConnection() throws TransportException, SuccessFactorsServiceException { MockFailureCollector collector = new MockFailureCollector(); ConnectorContext context = new MockConnectorContext(new MockConnectorConfigurer()); - successFactorsTransporter = new SuccessFactorsTransporter(pluginConfig.getConnection().getUsername(), - pluginConfig.getConnection().getPassword()); + successFactorsTransporter = new SuccessFactorsTransporter(pluginConfig.getConnection()); new Expectations(SuccessFactorsUrlContainer.class, SuccessFactorsTransporter.class, SuccessFactorsSchemaGenerator.class) { { @@ -120,8 +119,7 @@ public void testValidateUnauthorisedConnection() throws TransportException, Succ @Test public void testValidateNotFoundConnection() throws TransportException, SuccessFactorsServiceException { MockFailureCollector collector = new MockFailureCollector(); - successFactorsTransporter = new SuccessFactorsTransporter(pluginConfig.getConnection().getUsername(), - pluginConfig.getConnection().getPassword()); + successFactorsTransporter = new SuccessFactorsTransporter(pluginConfig.getConnection()); new Expectations(SuccessFactorsUrlContainer.class, SuccessFactorsTransporter.class, SuccessFactorsSchemaGenerator.class) { { @@ -154,8 +152,7 @@ private SuccessFactorsResponseContainer getNotFoundResponseContainer() { public void testGenerateSpec() throws TransportException, IOException { ConnectorContext context = new MockConnectorContext(new MockConnectorConfigurer()); MockFailureCollector collector = new MockFailureCollector(); - successFactorsTransporter = new SuccessFactorsTransporter(pluginConfig.getConnection().getUsername(), - pluginConfig.getConnection().getPassword()); + successFactorsTransporter = new SuccessFactorsTransporter(pluginConfig.getConnection()); new Expectations(SuccessFactorsTransporter.class) { { successFactorsTransporter.callSuccessFactorsEntity(null, anyString, anyString); @@ -190,8 +187,7 @@ public void testGenerateSpecWithSchema() throws TransportException, IOException, ConnectorContext context = new MockConnectorContext(new MockConnectorConfigurer()); MockFailureCollector collector = new MockFailureCollector(); successFactorsConnector = new SuccessFactorsConnector(pluginConfig.getConnection()); - successFactorsTransporter = new SuccessFactorsTransporter(pluginConfig.getConnection().getUsername(), - pluginConfig.getConnection().getPassword()); + successFactorsTransporter = new SuccessFactorsTransporter(pluginConfig.getConnection()); new Expectations(SuccessFactorsConnector.class, SuccessFactorsTransporter.class) { { @@ -232,8 +228,7 @@ public void testBrowse() throws IOException, TransportException { ConnectorContext context = new MockConnectorContext(new MockConnectorConfigurer()); List entities = new ArrayList<>(); entities.add("Achievement"); - successFactorsTransporter = new SuccessFactorsTransporter(pluginConfig.getConnection().getUsername(), - pluginConfig.getConnection().getPassword()); + successFactorsTransporter = new SuccessFactorsTransporter(pluginConfig.getConnection()); successFactorsConnector = new SuccessFactorsConnector(pluginConfig.getConnection()); new Expectations(SuccessFactorsTransporter.class, SuccessFactorsTransporter.class, SuccessFactorsConnector.class) { @@ -267,8 +262,7 @@ public void testBrowse() throws IOException, TransportException { @Test(expected = IOException.class) public void testSampleWithoutSampleData() throws IOException, TransportException { ConnectorContext context = new MockConnectorContext(new MockConnectorConfigurer()); - successFactorsTransporter = new SuccessFactorsTransporter(pluginConfig.getConnection().getUsername(), - pluginConfig.getConnection().getPassword()); + successFactorsTransporter = new SuccessFactorsTransporter(pluginConfig.getConnection()); new Expectations(SuccessFactorsTransporter.class, SuccessFactorsTransporter.class, SuccessFactorsConnector.class) { { successFactorsTransporter.callSuccessFactorsEntity(null, anyString, anyString); @@ -291,8 +285,7 @@ public void testSampleWithoutSampleData() throws IOException, TransportException @Test public void testSampleWithSampleData() throws IOException, TransportException, EntityProviderException, SuccessFactorsServiceException, EdmException { - successFactorsTransporter = new SuccessFactorsTransporter(pluginConfig.getConnection().getUsername(), - pluginConfig.getConnection().getPassword()); + successFactorsTransporter = new SuccessFactorsTransporter(pluginConfig.getConnection()); String entityName = "entity"; List records = new ArrayList<>(); StructuredRecord structuredRecord = Mockito.mock(StructuredRecord.class); diff --git a/src/test/java/io/cdap/plugin/successfactors/source/SuccessFactorsSourceTest.java b/src/test/java/io/cdap/plugin/successfactors/source/SuccessFactorsSourceTest.java index c496581..5ee2d96 100644 --- a/src/test/java/io/cdap/plugin/successfactors/source/SuccessFactorsSourceTest.java +++ b/src/test/java/io/cdap/plugin/successfactors/source/SuccessFactorsSourceTest.java @@ -80,19 +80,13 @@ public void setUp() { .entityName("entity name") .username("username") .password("password") - .selectOption("col1,col2, \n parent/col1,\r col3 "); + .authType("basicAuth"); } @Test public void testConfigurePipelineWithInvalidUrl() throws Exception { - pluginConfigBuilder = SuccessFactorsPluginConfig.builder() - .referenceName("unit-test-ref-name") - .baseURL("base_url") - .entityName("entity name") - .username("username") - .password("password") - .selectOption("col1,col2, \n parent/col1,\r col3 "); - SuccessFactorsPluginConfig pluginConfig = pluginConfigBuilder.build(); + SuccessFactorsPluginConfig pluginConfig = pluginConfigBuilder.baseURL("base_url") + .selectOption("col1,col2, \n parent/col1,\r col3 ").build(); successFactorsSource = new SuccessFactorsSource(pluginConfig); Map plugins = new HashMap<>(); MockPipelineConfigurer mockPipelineConfigurer = new MockPipelineConfigurer(null, plugins); @@ -107,15 +101,9 @@ public void testConfigurePipelineWithInvalidUrl() throws Exception { @Test public void testConfigurePipelineWithInvalidReferenceName() { - pluginConfigBuilder = SuccessFactorsPluginConfig.builder() - .referenceName("") - .baseURL("http://localhost") - .entityName("entity name") - .username("username") - .password("password") - .selectOption("col1,col2, \n parent/col1,\r col3 "); try { - pluginConfig = pluginConfigBuilder.build(); + pluginConfig = pluginConfigBuilder.referenceName("") + .selectOption("col1,col2, \n parent/col1,\r col3 ").build(); successFactorsSource = new SuccessFactorsSource(pluginConfig); Map plugins = new HashMap<>(); MockPipelineConfigurer mockPipelineConfigurer = new MockPipelineConfigurer(null, plugins); @@ -152,16 +140,8 @@ private Schema getPluginSchema() throws IOException { @Test public void testConfigurePipelineWSchemaNotNull() throws SuccessFactorsServiceException, TransportException, IOException { - pluginConfigBuilder = SuccessFactorsPluginConfig.builder() - .referenceName("unit-test-ref-name") - .baseURL("http://localhost") - .entityName("entity-name") - .username("username") - .password("password"); - pluginConfig = pluginConfigBuilder.build(); - successFactorsTransporter = new SuccessFactorsTransporter(pluginConfig.getConnection().getUsername(), - pluginConfig.getConnection().getPassword()); + successFactorsTransporter = new SuccessFactorsTransporter(pluginConfig.getConnection()); successFactorsUrlContainer = new SuccessFactorsUrlContainer(pluginConfig); successFactorsSchemaGenerator = new SuccessFactorsSchemaGenerator(new SuccessFactorsEntityProvider(edm)); @@ -265,20 +245,12 @@ private List getSplits() { @Test public void testPrepareRun() throws Exception { + pluginConfig = pluginConfigBuilder.paginationType("serverSide") + .selectOption("col1,col2, \n parent/col1,\r col3 ") + .filterOption("$topeq2").build(); successFactorsService = new SuccessFactorsService(pluginConfig, null); successFactorsPartitionBuilder = new SuccessFactorsPartitionBuilder(); - successFactorsTransporter = new SuccessFactorsTransporter("username", "password"); - pluginConfigBuilder = SuccessFactorsPluginConfig.builder() - .referenceName("unit-test-ref-name") - .baseURL("http://localhost") - .entityName("entity name") - .username("username") - .password("password") - .paginationType("serverSide") - .selectOption("col1,col2, \n parent/col1,\r col3 ") - .filterOption("$topeq2"); - - pluginConfig = pluginConfigBuilder.build(); + successFactorsTransporter = new SuccessFactorsTransporter(pluginConfig.getConnection()); new Expectations(SuccessFactorsService.class, SuccessFactorsTransporter.class) { { @@ -313,14 +285,8 @@ public void testPrepareRun() throws Exception { @Test public void testPrepareRunUnauthorizedError() throws Exception { successFactorsService = new SuccessFactorsService(pluginConfig, null); - pluginConfigBuilder = SuccessFactorsPluginConfig.builder() - .referenceName("") - .baseURL("") - .entityName("entity name") - .username("username") - .password("password"); - - pluginConfig = pluginConfigBuilder.build(); + pluginConfig = pluginConfigBuilder.referenceName("") + .baseURL("").build(); successFactorsSource = new SuccessFactorsSource(pluginConfig); new Expectations(SuccessFactorsService.class) { @@ -346,14 +312,8 @@ public void testPrepareRunUnauthorizedError() throws Exception { @Test public void testPrepareRunForbiddenError() throws Exception { successFactorsService = new SuccessFactorsService(pluginConfig, null); - pluginConfigBuilder = SuccessFactorsPluginConfig.builder() - .referenceName("") - .baseURL("") - .entityName("entity name") - .username("username") - .password("password"); - - pluginConfig = pluginConfigBuilder.build(); + pluginConfig = pluginConfigBuilder.referenceName("") + .baseURL("").build(); successFactorsSource = new SuccessFactorsSource(pluginConfig); new Expectations(SuccessFactorsService.class) { @@ -379,14 +339,8 @@ public void testPrepareRunForbiddenError() throws Exception { @Test public void testPrepareRunNotFoundError() throws Exception { successFactorsService = new SuccessFactorsService(pluginConfig, null); - pluginConfigBuilder = SuccessFactorsPluginConfig.builder() - .referenceName("") - .baseURL("") - .entityName("entity name") - .username("username") - .password("password"); - - pluginConfig = pluginConfigBuilder.build(); + pluginConfig = pluginConfigBuilder.referenceName("") + .baseURL("").build(); successFactorsSource = new SuccessFactorsSource(pluginConfig); new Expectations(SuccessFactorsService.class) { @@ -412,14 +366,8 @@ public void testPrepareRunNotFoundError() throws Exception { @Test public void testPrepareRunBadRequestError() throws Exception { successFactorsService = new SuccessFactorsService(pluginConfig, null); - pluginConfigBuilder = SuccessFactorsPluginConfig.builder() - .referenceName("") - .baseURL("") - .entityName("entity name") - .username("username") - .password("password"); - - pluginConfig = pluginConfigBuilder.build(); + pluginConfig = pluginConfigBuilder.referenceName("") + .baseURL("").build(); successFactorsSource = new SuccessFactorsSource(pluginConfig); new Expectations(SuccessFactorsService.class) { @@ -445,14 +393,8 @@ public void testPrepareRunBadRequestError() throws Exception { @Test public void testPrepareRunInvalidVersionError() throws Exception { successFactorsService = new SuccessFactorsService(pluginConfig, null); - pluginConfigBuilder = SuccessFactorsPluginConfig.builder() - .referenceName("") - .baseURL("") - .entityName("entity name") - .username("username") - .password("password"); - - pluginConfig = pluginConfigBuilder.build(); + pluginConfig = pluginConfigBuilder.referenceName("") + .baseURL("").build(); successFactorsSource = new SuccessFactorsSource(pluginConfig); new Expectations(SuccessFactorsService.class) { diff --git a/src/test/java/io/cdap/plugin/successfactors/source/config/SuccessFactorsPluginConfigTest.java b/src/test/java/io/cdap/plugin/successfactors/source/config/SuccessFactorsPluginConfigTest.java index b30c39f..efcb30f 100644 --- a/src/test/java/io/cdap/plugin/successfactors/source/config/SuccessFactorsPluginConfigTest.java +++ b/src/test/java/io/cdap/plugin/successfactors/source/config/SuccessFactorsPluginConfigTest.java @@ -19,6 +19,8 @@ import io.cdap.cdap.etl.api.validation.ValidationFailure; import io.cdap.cdap.etl.mock.validation.MockFailureCollector; import io.cdap.plugin.successfactors.common.util.ResourceConstants; +import io.cdap.plugin.successfactors.connector.SuccessFactorsConnectorConfig; + import org.junit.Assert; import org.junit.Before; import org.junit.Test; @@ -43,6 +45,7 @@ public void setUp() { .referenceName(REFERENCE_NAME) .baseURL(BASE_URL) .entityName(ENTITY_NAME) + .authType(SuccessFactorsConnectorConfig.BASIC_AUTH) .username(USER_NAME) .password(PASSWORD); } @@ -174,4 +177,49 @@ public void testRefactoredPluginPropertyValues() { Assert.assertEquals("Entity name not trimmed", "entity-name", pluginConfig.getEntityName()); Assert.assertEquals("Select option not trimmed", "col1,col2,parent/col1,col3", pluginConfig.getSelectOption()); } + + @Test + public void testWithEmptyOauthCreateTokenParameters() { + SuccessFactorsPluginConfig pluginConfig = pluginConfigBuilder + .entityName("entity") + .authType("oAuth2") + .assertionTokenType("createToken") + .username(null) + .password(PASSWORD) + .setPrivateKey(null) + .setUserId(null) + .setTokenURL(null) + .build(); + try { + pluginConfig.validatePluginParameters(failureCollector); + Assert.fail("Username is null"); + } catch (ValidationException ve) { + List failures = ve.getFailures(); + Assert.assertEquals(5, failures.size()); + Assert.assertEquals(ResourceConstants.ERR_MISSING_PARAM_PREFIX.getMsgForKey("clientId"), + failures.get(0).getMessage()); + } + } + + @Test + public void testWithEmptyOauthEnterTokenParameters() { + SuccessFactorsPluginConfig pluginConfig = pluginConfigBuilder + .entityName("entity") + .authType("oAuth2") + .assertionTokenType("enterToken") + .username(null) + .password(PASSWORD) + .assertionToken(null) + .setClientId(null) + .build(); + try { + pluginConfig.validatePluginParameters(failureCollector); + Assert.fail("Username is null"); + } catch (ValidationException ve) { + List failures = ve.getFailures(); + Assert.assertEquals(3, failures.size()); + Assert.assertEquals(ResourceConstants.ERR_MISSING_PARAM_PREFIX.getMsgForKey("clientId"), + failures.get(0).getMessage()); + } + } } diff --git a/src/test/java/io/cdap/plugin/successfactors/source/input/SuccessFactorsInputFormatTest.java b/src/test/java/io/cdap/plugin/successfactors/source/input/SuccessFactorsInputFormatTest.java index 5124e28..b6dd4ff 100644 --- a/src/test/java/io/cdap/plugin/successfactors/source/input/SuccessFactorsInputFormatTest.java +++ b/src/test/java/io/cdap/plugin/successfactors/source/input/SuccessFactorsInputFormatTest.java @@ -22,6 +22,7 @@ import io.cdap.plugin.successfactors.source.metadata.TestSuccessFactorsUtil; import io.cdap.plugin.successfactors.source.service.SuccessFactorsService; import io.cdap.plugin.successfactors.source.transport.SuccessFactorsTransporter; + import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.mapreduce.TaskAttemptContext; import org.apache.olingo.odata2.api.edm.Edm; @@ -46,15 +47,13 @@ public class SuccessFactorsInputFormatTest { @Before public void initializeTests() { - pluginConfig = Mockito.spy(new SuccessFactorsPluginConfig("referenceName", - "baseURL", - "entityName", - null, - "username", - "password", - "filterOption", - "selectOption", - "expandOption", + pluginConfig = Mockito.spy(new SuccessFactorsPluginConfig("referenceName", "baseURL", + "entityName", null, + "username", "password", "", + "", "", "", "", + "", "", "", + "", "filterOption", + "selectOption", "expandOption", "additionalQueryParameters", null)); } diff --git a/src/test/java/io/cdap/plugin/successfactors/source/metadata/SuccessFactorsSchemaGeneratorTest.java b/src/test/java/io/cdap/plugin/successfactors/source/metadata/SuccessFactorsSchemaGeneratorTest.java index b95ee5e..c4a5ad2 100644 --- a/src/test/java/io/cdap/plugin/successfactors/source/metadata/SuccessFactorsSchemaGeneratorTest.java +++ b/src/test/java/io/cdap/plugin/successfactors/source/metadata/SuccessFactorsSchemaGeneratorTest.java @@ -80,8 +80,10 @@ public void testSelectWithExpandNames() throws SuccessFactorsServiceException { public void testBuildExpandOutputSchema() throws SuccessFactorsServiceException { SuccessFactorsPluginConfig pluginConfig = new SuccessFactorsPluginConfig("referenceName", "baseUR", "entityName", "associateEntityName", "username", - "password", "filterOption", "selectOption", "expandOption", - "additionalQueryParameters", "paginationType"); + "password", "", + "", "", "", "", "", "", "", "", + "filterOption", "selectOption", "expandOption", "additionalQueryParameters", + "paginationType"); Schema outputSchema = generator.buildExpandOutputSchema("Benefit", "eligibleBenefits", "associatedEntity", pluginConfig); int lastIndex = outputSchema.getFields().size() - 1; @@ -154,7 +156,8 @@ public void testInvalidEntityName() throws SuccessFactorsServiceException { public void testInvalidExpandName() throws SuccessFactorsServiceException { SuccessFactorsPluginConfig pluginConfig = new SuccessFactorsPluginConfig("referenceName", "baseUR", "entityName", "associateEntityName", "username", - "password", "filterOption", "selectOption", "expandOption", + "password", "", + "", "", "", "", "", "", "", "", "filterOption", "selectOption", "expandOption", "additionalQueryParameters", "paginationType"); exception.expectMessage("'assEntity' not found in the 'Benefit' entity."); generator.buildExpandOutputSchema("Benefit", "INVALID-NAVIGATION-NAME", diff --git a/src/test/java/io/cdap/plugin/successfactors/source/transport/RuntimeFunctionalForAssociatedEntityTest.java b/src/test/java/io/cdap/plugin/successfactors/source/transport/RuntimeFunctionalForAssociatedEntityTest.java index 928c7a1..3257186 100644 --- a/src/test/java/io/cdap/plugin/successfactors/source/transport/RuntimeFunctionalForAssociatedEntityTest.java +++ b/src/test/java/io/cdap/plugin/successfactors/source/transport/RuntimeFunctionalForAssociatedEntityTest.java @@ -73,6 +73,7 @@ public void setUp() throws Exception { .filterOption("picklistId eq 'hrRanking'") .username("test") .password("secret") + .authType("basicAuth") .paginationType("serverSide"); String metadataString = TestSuccessFactorsUtil.convertInputStreamToString(TestSuccessFactorsUtil.readResource @@ -88,8 +89,7 @@ public void testRecordReaderForAssociatedEntity() throws Exception { long availableRowCount = 1; List partitionList = new SuccessFactorsPartitionBuilder().buildSplits(availableRowCount); - transporter = new SuccessFactorsTransporter(pluginConfig.getConnection().getUsername(), pluginConfig. - getConnection().getPassword()); + transporter = new SuccessFactorsTransporter(pluginConfig.getConnection()); successFactorsService = new SuccessFactorsService(pluginConfig, transporter); prepareStubForMetadata(); edmData = successFactorsService.getSuccessFactorsServiceEdm(encodedMetadataString); diff --git a/src/test/java/io/cdap/plugin/successfactors/source/transport/RuntimeFunctionalTest.java b/src/test/java/io/cdap/plugin/successfactors/source/transport/RuntimeFunctionalTest.java index 847f94a..c861043 100644 --- a/src/test/java/io/cdap/plugin/successfactors/source/transport/RuntimeFunctionalTest.java +++ b/src/test/java/io/cdap/plugin/successfactors/source/transport/RuntimeFunctionalTest.java @@ -89,6 +89,7 @@ public void setUp() throws Exception { .entityName("Background_SpecialAssign") .username("test") .password("secret") + .authType("basicAuth") .paginationType("serverSide"); String metadataString = TestSuccessFactorsUtil.convertInputStreamToString(TestSuccessFactorsUtil.readResource @@ -104,8 +105,7 @@ public void runPipelineWithDefaultValues() throws Exception { long availableRowCount = 3; List partitionList = new SuccessFactorsPartitionBuilder().buildSplits(availableRowCount); - transporter = new SuccessFactorsTransporter(pluginConfig.getConnection().getUsername(), pluginConfig. - getConnection().getPassword()); + transporter = new SuccessFactorsTransporter(pluginConfig.getConnection()); successFactorsService = new SuccessFactorsService(pluginConfig, transporter); prepareStubForMetadata(pluginConfig); edmData = successFactorsService.getSuccessFactorsServiceEdm(encodedMetadataString); @@ -141,8 +141,7 @@ public void verifyFailToDecodeMetadataString() throws SuccessFactorsServiceExcep exceptionRule.expect(SuccessFactorsServiceException.class); exceptionRule .expectMessage(ResourceConstants.ERR_METADATA_DECODE.getMsgForKeyWithCode(pluginConfig.getEntityName())); - transporter = new SuccessFactorsTransporter(pluginConfig.getConnection().getUsername(), pluginConfig. - getConnection().getPassword()); + transporter = new SuccessFactorsTransporter(pluginConfig.getConnection()); successFactorsService = new SuccessFactorsService(pluginConfig, transporter); successFactorsService.getSuccessFactorsServiceEdm("encodedMetadataString"); } @@ -154,8 +153,7 @@ public void verifyDataCorrectness() prepareStubForMetadata(pluginConfig); long availableRowCount = 3; List partitionList = new SuccessFactorsPartitionBuilder().buildSplits(availableRowCount); - transporter = new SuccessFactorsTransporter(pluginConfig.getConnection().getUsername(), - pluginConfig.getConnection().getPassword()); + transporter = new SuccessFactorsTransporter(pluginConfig.getConnection()); successFactorsService = new SuccessFactorsService(pluginConfig, transporter); edmData = successFactorsService.getSuccessFactorsServiceEdm(encodedMetadataString); for (SuccessFactorsInputSplit inputSplit : partitionList) { diff --git a/src/test/java/io/cdap/plugin/successfactors/source/transport/SuccessFactorsTransporterTest.java b/src/test/java/io/cdap/plugin/successfactors/source/transport/SuccessFactorsTransporterTest.java index 8cf5dcc..5a8b961 100644 --- a/src/test/java/io/cdap/plugin/successfactors/source/transport/SuccessFactorsTransporterTest.java +++ b/src/test/java/io/cdap/plugin/successfactors/source/transport/SuccessFactorsTransporterTest.java @@ -122,11 +122,11 @@ public void setUp() { .entityName("Entity") .username("test") .password("secret") + .authType("basicAuth") .expandOption("Products/Supplier"); pluginConfig = pluginConfigBuilder.build(); successFactorsURL = new SuccessFactorsUrlContainer(pluginConfig); - transporter = new SuccessFactorsTransporter(pluginConfig.getConnection().getUsername(), - pluginConfig.getConnection().getPassword()); + transporter = new SuccessFactorsTransporter(pluginConfig.getConnection()); } @Test @@ -141,16 +141,48 @@ public void testCallSuccessFactors() throws TransportException { SuccessFactorsResponseContainer response = transporter .callSuccessFactors(successFactorsURL.getTesterURL(), MediaType.APPLICATION_JSON, SuccessFactorsService.TEST); - Assert.assertEquals("SuccessFactors Service data version is same.", + Assert.assertEquals("SuccessFactors Service data version is not same.", "2.0", response.getDataServiceVersion()); - Assert.assertEquals("HTTP status code is same.", + Assert.assertEquals("HTTP status code is not same.", HttpURLConnection.HTTP_OK, response.getHttpStatusCode()); - Assert.assertEquals("HTTP response body is same.", + Assert.assertEquals("HTTP response body is not same.", expectedBody, TestSuccessFactorsUtil.convertInputStreamToString(response.getResponseStream())); - Assert.assertEquals("HTTP status is same", "OK", response.getHttpStatusMsg()); + Assert.assertEquals("HTTP status is not same", "OK", response.getHttpStatusMsg()); + } + + @Test + public void testCallSuccessFactorsWithOauth2() throws TransportException { + pluginConfigBuilder = SuccessFactorsPluginConfig.builder() + .baseURL("https://localhost:" + wireMockRule.httpsPort()) + .entityName("Entity") + .username("test") + .password("secret") + .authType("oAuth2") + .expandOption("Products/Supplier"); + pluginConfig = pluginConfigBuilder.build(); + String expectedBody = "{\"d\": [{\"ID\": 0,\"Name\": \"Bread\"}}]}"; + WireMock.stubFor(WireMock.get("/Entity?%24expand=Products%2FSupplier&%24top=1") + .withBasicAuth(pluginConfig.getConnection().getUsername(), + pluginConfig.getConnection().getPassword()) + .willReturn(WireMock.ok() + .withHeader(SuccessFactorsTransporter.SERVICE_VERSION, "2.0") + .withBody(expectedBody))); + SuccessFactorsResponseContainer response = transporter + .callSuccessFactors(successFactorsURL.getTesterURL(), MediaType.APPLICATION_JSON, SuccessFactorsService.TEST); + + Assert.assertEquals("SuccessFactors Service data version is not same.", + "2.0", + response.getDataServiceVersion()); + Assert.assertEquals("HTTP status code is not same.", + HttpURLConnection.HTTP_OK, + response.getHttpStatusCode()); + Assert.assertEquals("HTTP response body is not same.", + expectedBody, + TestSuccessFactorsUtil.convertInputStreamToString(response.getResponseStream())); + Assert.assertEquals("HTTP status is not same", "OK", response.getHttpStatusMsg()); } @Test @@ -161,7 +193,7 @@ public void testUnAuthorized() throws TransportException { .callSuccessFactors(successFactorsURL.getMetadataURL(), MediaType.APPLICATION_XML, SuccessFactorsService.METADATA); WireMock.verify(1, WireMock.getRequestedFor(WireMock.urlEqualTo("/Entity/$metadata"))); - Assert.assertEquals("HTTP status code is matching.", + Assert.assertEquals("HTTP status code is not matching.", HttpURLConnection.HTTP_UNAUTHORIZED, response.getHttpStatusCode()); } diff --git a/src/test/java/io/cdap/plugin/successfactors/source/transport/SuccessFactorsUrlContainerTest.java b/src/test/java/io/cdap/plugin/successfactors/source/transport/SuccessFactorsUrlContainerTest.java index 57f58e9..76fdd7f 100644 --- a/src/test/java/io/cdap/plugin/successfactors/source/transport/SuccessFactorsUrlContainerTest.java +++ b/src/test/java/io/cdap/plugin/successfactors/source/transport/SuccessFactorsUrlContainerTest.java @@ -33,6 +33,9 @@ public void initializeTests() { "associatedEntity", "username", "password", + "", "", "", "", + "", "", "", + "", "", "filterOption", "selectOption", "expandOption", @@ -72,6 +75,9 @@ public void testGetURLWithAdditionalQueryParameters() { "associatedEntity", "username", "password", + "", "", "", "", + "", "", "", + "", "", "", "", "", diff --git a/widgets/SuccessFactors-batchsource.json b/widgets/SuccessFactors-batchsource.json index cd4523b..39bfce7 100644 --- a/widgets/SuccessFactors-batchsource.json +++ b/widgets/SuccessFactors-batchsource.json @@ -32,21 +32,83 @@ } }, { - "widget-type": "textbox", - "label": "SAP SuccessFactors Logon Username", - "name": "username", + "widget-type": "radio-group", + "label": "Authentication Type", + "name": "authType", "widget-attributes": { - "placeholder": "" + "layout": "inline", + "default": "basicAuth", + "options": [ + { + "id": "basicAuth", + "label": "Basic Authentication" + }, + { + "id": "oAuth2", + "label": "OAuth 2.0" + } + ] } }, { - "widget-type": "password", - "label": "SAP SuccessFactors Logon Password", - "name": "password", + "name": "assertionTokenType", + "label": "Assertion Token Type", + "widget-type": "radio-group", "widget-attributes": { - "placeholder": "" + "layout": "inline", + "default": "enterToken", + "options": [ + { + "id": "enterToken", + "label": "Enter Token" + }, + { + "id": "createToken", + "label": "Create Token" + } + ] } }, + { + "widget-type": "textbox", + "label": "Token URL", + "name": "tokenURL" + }, + { + "widget-type": "textbox", + "label": "Client Id", + "name": "clientId" + }, + { + "widget-type": "textbox", + "label": "Private Key", + "name": "privateKey" + }, + { + "widget-type": "textbox", + "label": "User Id", + "name": "userId" + }, + { + "widget-type": "textbox", + "label": "Company Id", + "name": "companyId" + }, + { + "widget-type": "textbox", + "label": "SAP SuccessFactors Logon Username", + "name": "username" + }, + { + "widget-type": "password", + "label": "SAP SuccessFactors Logon Password", + "name": "password" + }, + { + "widget-type": "textbox", + "label": "Assertion Token", + "name": "assertionToken" + }, { "widget-type": "textbox", "label": "SAP SuccessFactors Base URL", @@ -176,6 +238,38 @@ { "type": "property", "name": "baseURL" + }, + { + "type": "property", + "name": "authType" + }, + { + "type": "property", + "name": "tokenURL" + }, + { + "type": "property", + "name": "clientId" + }, + { + "type": "property", + "name": "privateKey" + }, + { + "type": "property", + "name": "userId" + }, + { + "type": "property", + "name": "companyId" + }, + { + "type": "property", + "name": "assertionTokenType" + }, + { + "type": "property", + "name": "assertionToken" } ] }, @@ -190,6 +284,78 @@ "name": "connection" } ] + }, + { + "name": "basicAuth", + "condition": { + "property": "authType", + "operator": "equal to", + "value": "basicAuth" + }, + "show": [ + { + "name": "username", + "type": "property" + }, + { + "name": "password", + "type": "property" + } + ] + }, + { + "name": "oAuth2", + "condition": { + "property": "authType", + "operator": "equal to", + "value": "oAuth2" + }, + "show": [ + { + "name": "assertionTokenType", + "type": "property" + }, + { + "name": "clientId", + "type": "property" + }, + { + "type": "property", + "name": "companyId" + } + ] + }, + { + "name": "enterAssertionToken", + "condition": { + "expression": "authType == 'oAuth2' && assertionTokenType == 'enterToken'" + }, + "show": [ + { + "type": "property", + "name": "assertionToken" + } + ] + }, + { + "name": "createAssertionToken", + "condition": { + "expression": "authType == 'oAuth2' && assertionTokenType == 'createToken'" + }, + "show": [ + { + "name": "tokenURL", + "type": "property" + }, + { + "name": "privateKey", + "type": "property" + }, + { + "name": "userId", + "type": "property" + } + ] } ], "outputs": [ diff --git a/widgets/SuccessFactors-connector.json b/widgets/SuccessFactors-connector.json index 7ab0324..c2721bc 100644 --- a/widgets/SuccessFactors-connector.json +++ b/widgets/SuccessFactors-connector.json @@ -8,21 +8,83 @@ "label": "Credentials", "properties": [ { - "widget-type": "textbox", - "label": "SAP SuccessFactors Logon Username", - "name": "username", + "widget-type": "radio-group", + "label": "Authentication Type", + "name": "authType", "widget-attributes": { - "placeholder": "" + "layout": "inline", + "default": "basicAuth", + "options": [ + { + "id": "basicAuth", + "label": "Basic Authentication" + }, + { + "id": "oAuth2", + "label": "OAuth 2.0" + } + ] } }, { - "widget-type": "password", - "label": "SAP SuccessFactors Logon Password", - "name": "password", + "name": "assertionTokenType", + "label": "Assertion Token Type", + "widget-type": "radio-group", "widget-attributes": { - "placeholder": "" + "layout": "inline", + "default": "enterToken", + "options": [ + { + "id": "enterToken", + "label": "Enter Token" + }, + { + "id": "createToken", + "label": "Create Token" + } + ] } }, + { + "widget-type": "textbox", + "label": "Token URL", + "name": "tokenURL" + }, + { + "widget-type": "textbox", + "label": "Client Id", + "name": "clientId" + }, + { + "widget-type": "textbox", + "label": "Private Key", + "name": "privateKey" + }, + { + "widget-type": "textbox", + "label": "User Id", + "name": "userId" + }, + { + "widget-type": "textbox", + "label": "Assertion Token", + "name": "assertionToken" + }, + { + "widget-type": "textbox", + "label": "Company Id", + "name": "companyId" + }, + { + "widget-type": "textbox", + "label": "SAP SuccessFactors Logon Username", + "name": "username" + }, + { + "widget-type": "password", + "label": "SAP SuccessFactors Logon Password", + "name": "password" + }, { "widget-type": "textbox", "label": "SAP SuccessFactors Base URL", @@ -34,5 +96,79 @@ ] } ], + "filters":[ + { + "name": "basicAuth", + "condition": { + "property": "authType", + "operator": "equal to", + "value": "basicAuth" + }, + "show": [ + { + "name": "username", + "type": "property" + }, + { + "name": "password", + "type": "property" + } + ] + }, + { + "name": "Authenticate with oAuth2", + "condition": { + "property": "authType", + "operator": "equal to", + "value": "oAuth2" + }, + "show": [ + { + "name": "assertionTokenType", + "type": "property" + }, + { + "name": "clientId", + "type": "property" + }, + { + "type": "property", + "name": "companyId" + } + ] + }, + { + "name": "enterAssertionToken", + "condition": { + "expression": "authType == 'oAuth2' && assertionTokenType== 'enterToken'" + }, + "show": [ + { + "type": "property", + "name": "assertionToken" + } + ] + }, + { + "name": "createAssertionToken", + "condition": { + "expression": "authType == 'oAuth2' && assertionTokenType == 'createToken'" + }, + "show": [ + { + "name": "tokenURL", + "type": "property" + }, + { + "name": "privateKey", + "type": "property" + }, + { + "name": "userId", + "type": "property" + } + ] + } + ], "outputs": [] } \ No newline at end of file