diff --git a/docs/SuccessFactors-batchsource.md b/docs/SuccessFactors-batchsource.md index be73309..e0fade4 100644 --- a/docs/SuccessFactors-batchsource.md +++ b/docs/SuccessFactors-batchsource.md @@ -19,8 +19,22 @@ 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 (API Key) required to generate the token. + **Company ID:** Company ID required to generate the token. + **Token URL:** Token URL to generate the assertion 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** + **Private Key:** Private key required to generate the token. + **Expire Assertion Token In (Minutes):** Assertion Token will not be valid after the specified time. Default 1440 minutes (24 hours). + **User ID:** User ID required to generate the token. + **SAP SuccessFactors Base URL (M)**: SAP SuccessFactors Base URL. ## Proxy Configuration diff --git a/docs/SuccessFactors-connector.md b/docs/SuccessFactors-connector.md index 85739a9..c0e2b88 100644 --- a/docs/SuccessFactors-connector.md +++ b/docs/SuccessFactors-connector.md @@ -10,9 +10,21 @@ 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 (API Key) required to generate the token. + **Company ID:** Company ID required to generate the token. + **Token URL:** Token URL to generate the assertion 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** + **Private Key:** Private key required to generate the token. + **Expire Assertion Token In (Minutes):** Assertion Token will not be valid after the specified time. Default 1440 minutes (24 hours). + **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 b993994..3407080 100644 --- a/pom.xml +++ b/pom.xml @@ -41,6 +41,7 @@ 4.12 2.0.0 4.9.1 + 4.5.14 2.0.0 2.27.2 2.7.0 @@ -339,7 +340,16 @@ okhttp ${okhttp3.version} - + + org.apache.httpcomponents + httpclient + ${httpclient.version} + + + org.opensaml + opensaml + 2.6.4 + com.fasterxml.jackson.core jackson-databind diff --git a/src/main/java/io/cdap/plugin/successfactors/common/util/ResourceConstants.java b/src/main/java/io/cdap/plugin/successfactors/common/util/ResourceConstants.java index debbe80..3177138 100644 --- a/src/main/java/io/cdap/plugin/successfactors/common/util/ResourceConstants.java +++ b/src/main/java/io/cdap/plugin/successfactors/common/util/ResourceConstants.java @@ -23,6 +23,7 @@ public enum ResourceConstants { ERR_MISSING_PARAM_PREFIX(null, "err.missing.param.prefix"), + ERR_INVALID_EXPIRE_TIME(null, "err.invalid.expire.time.prefix"), ERR_MISSING_PARAM_OR_MACRO_ACTION(null, "err.missing.param.or.macro.action"), ERR_INVALID_BASE_URL(null, "err.invalid.base.url"), ERR_FEATURE_NOT_SUPPORTED("CDF_SAP_ODATA_01500", "err.feature.not.supported"), 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..79cb681 --- /dev/null +++ b/src/main/java/io/cdap/plugin/successfactors/common/util/SuccessFactorsAccessToken.java @@ -0,0 +1,389 @@ +/* + * Copyright © 2025 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; + 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, + config.getExpireInMinutes(), 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(config.getTokenURL())); + + // 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/connector/SuccessFactorsConnector.java b/src/main/java/io/cdap/plugin/successfactors/connector/SuccessFactorsConnector.java index c377536..d759aca 100644 --- a/src/main/java/io/cdap/plugin/successfactors/connector/SuccessFactorsConnector.java +++ b/src/main/java/io/cdap/plugin/successfactors/connector/SuccessFactorsConnector.java @@ -101,7 +101,7 @@ public SuccessFactorsConnector(SuccessFactorsConnectorConfig config) { @Override public void test(ConnectorContext connectorContext) throws ValidationException { FailureCollector collector = connectorContext.getFailureCollector(); - config.validateBasicCredentials(collector); + config.validateAuthCredentials(collector); config.validateConnection(collector); } 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 57377f3..9d35b46 100644 --- a/src/main/java/io/cdap/plugin/successfactors/connector/SuccessFactorsConnectorConfig.java +++ b/src/main/java/io/cdap/plugin/successfactors/connector/SuccessFactorsConnectorConfig.java @@ -15,6 +15,7 @@ */ package io.cdap.plugin.successfactors.connector; +import com.google.common.base.Strings; import io.cdap.cdap.api.annotation.Description; import io.cdap.cdap.api.annotation.Macro; import io.cdap.cdap.api.annotation.Name; @@ -40,30 +41,96 @@ * 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 EXPIRE_IN_MINUTES = "expireInMinutes"; + public static final Integer DEFAULT_EXPIRE_IN_MINUTES = 24 * 60; + 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 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"; private static final String SAP_SUCCESSFACTORS_PASSWORD = "SAP SuccessFactors Password"; private static final String SAP_SUCCESSFACTORS_BASE_URL = "SAP SuccessFactors Base URL"; private static final Logger LOG = LoggerFactory.getLogger(SuccessFactorsConnectorConfig.class); + @Nullable + @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(EXPIRE_IN_MINUTES) + @Macro + @Description("Assertion Token will not be valid after the specified time. Default 1440 minutes (24 hours).") + private final Integer expireInMinutes; + + @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.") @@ -86,11 +153,27 @@ public class SuccessFactorsConnectorConfig extends PluginConfig { @Description("Proxy password.") @Macro private String proxyPassword; - - public SuccessFactorsConnectorConfig(String username, String password, String baseURL, String proxyUrl, - String proxyUsername, String proxyPassword) { + + public SuccessFactorsConnectorConfig(@Nullable String username, @Nullable String password, + String tokenURL, + @Nullable String clientId, @Nullable String privateKey, + @Nullable Integer expireInMinutes, @Nullable String userId, + @Nullable String companyId, String baseURL, String authType, + String assertionTokenType, @Nullable String samlUsername, + @Nullable String assertionToken, + @Nullable String proxyUrl, + @Nullable String proxyUsername, @Nullable String proxyPassword) { this.username = username; this.password = password; + this.tokenURL = tokenURL; + this.clientId = clientId; + this.privateKey = privateKey; + this.expireInMinutes = expireInMinutes; + this.userId = userId; + this.companyId = companyId; + this.authType = authType; + this.assertionTokenType = assertionTokenType; + this.assertionToken = assertionToken; this.baseURL = baseURL; this.proxyUrl = proxyUrl; this.proxyUsername = proxyUsername; @@ -117,25 +200,112 @@ public String getPassword() { return password; } + public String getAuthType() { + // Default to basic auth if authType is not set for backward compatibility + return Strings.isNullOrEmpty(authType) ? SuccessFactorsConnectorConfig.BASIC_AUTH : 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; + } + + public Integer getExpireInMinutes() { + return expireInMinutes == null ? DEFAULT_EXPIRE_IN_MINUTES : expireInMinutes; + } + + @Nullable + public String getUserId() { + return userId; + } + public String getBaseURL() { return baseURL; } - public void validateBasicCredentials(FailureCollector failureCollector) { + public void validateAuthCredentials(FailureCollector failureCollector) { + + if (BASIC_AUTH.equals(getAuthType())) { + if (!containsMacro(UNAME) && Strings.isNullOrEmpty(getUsername())) { + String errMsg = ResourceConstants.ERR_MISSING_PARAM_PREFIX.getMsgForKey(SAP_SUCCESSFACTORS_USERNAME); + failureCollector.addFailure(errMsg, COMMON_ACTION).withConfigProperty(UNAME); + } + if (!containsMacro(PASSWORD) && Strings.isNullOrEmpty(getPassword())) { + String errMsg = ResourceConstants.ERR_MISSING_PARAM_PREFIX.getMsgForKey(SAP_SUCCESSFACTORS_PASSWORD); + failureCollector.addFailure(errMsg, COMMON_ACTION).withConfigProperty(PASSWORD); + } + } else { + if (!containsMacro(CLIENT_ID) && Strings.isNullOrEmpty(getClientId())) { + String errMsg = ResourceConstants.ERR_MISSING_PARAM_PREFIX.getMsgForKey(CLIENT_ID); + failureCollector.addFailure(errMsg, COMMON_ACTION).withConfigProperty(CLIENT_ID); + } + if (!containsMacro(COMPANY_ID) && Strings.isNullOrEmpty(getCompanyId())) { + String errMsg = ResourceConstants.ERR_MISSING_PARAM_PREFIX.getMsgForKey(COMPANY_ID); + failureCollector.addFailure(errMsg, COMMON_ACTION).withConfigProperty(COMPANY_ID); + } + if (!containsMacro(TOKEN_URL) && Strings.isNullOrEmpty(getTokenURL())) { + String errMsg = ResourceConstants.ERR_MISSING_PARAM_PREFIX.getMsgForKey(TOKEN_URL); + failureCollector.addFailure(errMsg, COMMON_ACTION).withConfigProperty(TOKEN_URL); + } + if (!containsMacro(TOKEN_URL) && !Strings.isNullOrEmpty(getTokenURL()) && HttpUrl.parse(getTokenURL()) == null) { + String errMsg = ResourceConstants.ERR_INVALID_BASE_URL.getMsgForKey(TOKEN_URL); + failureCollector.addFailure(errMsg, COMMON_ACTION).withConfigProperty(TOKEN_URL); + } + + if (ENTER_TOKEN.equals(assertionTokenType)) { + if (!containsMacro(ASSERTION_TOKEN) && Strings.isNullOrEmpty(getAssertionToken())) { + String errMsg = ResourceConstants.ERR_MISSING_PARAM_PREFIX.getMsgForKey(ASSERTION_TOKEN); + failureCollector.addFailure(errMsg, COMMON_ACTION).withConfigProperty(ASSERTION_TOKEN); + } + } + + if (CREATE_TOKEN.equals(assertionTokenType)) { + if (!containsMacro(PRIVATE_KEY) && Strings.isNullOrEmpty(getPrivateKey())) { + String errMsg = ResourceConstants.ERR_MISSING_PARAM_PREFIX.getMsgForKey(PRIVATE_KEY); + failureCollector.addFailure(errMsg, COMMON_ACTION).withConfigProperty(PRIVATE_KEY); + } + if (!containsMacro(EXPIRE_IN_MINUTES) && getExpireInMinutes() <= 0) { + String errMsg = ResourceConstants.ERR_INVALID_EXPIRE_TIME.getMsgForKey(EXPIRE_IN_MINUTES); + failureCollector.addFailure(errMsg, COMMON_ACTION).withConfigProperty(EXPIRE_IN_MINUTES); + } + if (!containsMacro(USER_ID) && Strings.isNullOrEmpty(getUserId())) { + String errMsg = ResourceConstants.ERR_MISSING_PARAM_PREFIX.getMsgForKey(USER_ID); + failureCollector.addFailure(errMsg, COMMON_ACTION).withConfigProperty(USER_ID); + } + } - 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(getBaseURL()) && !containsMacro(BASE_URL)) { + + if (!containsMacro(BASE_URL) && Strings.isNullOrEmpty(getBaseURL())) { String errMsg = ResourceConstants.ERR_MISSING_PARAM_PREFIX.getMsgForKey(SAP_SUCCESSFACTORS_BASE_URL); failureCollector.addFailure(errMsg, COMMON_ACTION).withConfigProperty(BASE_URL); } - if (SuccessFactorsUtil.isNotNullOrEmpty(getBaseURL()) && !containsMacro(BASE_URL)) { + if (!containsMacro(BASE_URL) && SuccessFactorsUtil.isNotNullOrEmpty(getBaseURL())) { if (HttpUrl.parse(getBaseURL()) == null) { String errMsg = ResourceConstants.ERR_INVALID_BASE_URL.getMsgForKey(SAP_SUCCESSFACTORS_BASE_URL); failureCollector.addFailure(errMsg, COMMON_ACTION).withConfigProperty(BASE_URL); @@ -156,7 +326,7 @@ public void validateConnection(FailureCollector collector) { } catch (TransportException e) { LOG.error("Unable to fetch the response", e); collector.addFailure("Unable to call SuccessFactorsEntity", - "Please check the values for basic and proxy parameters if proxy exists."); + "Please check the values for connection and proxy parameters if proxy exists."); return; } if (responseContainer.getHttpStatusCode() == HttpURLConnection.HTTP_UNAUTHORIZED) { 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 2e5a49c..6a736ee 100644 --- a/src/main/java/io/cdap/plugin/successfactors/source/SuccessFactorsSource.java +++ b/src/main/java/io/cdap/plugin/successfactors/source/SuccessFactorsSource.java @@ -15,6 +15,7 @@ */ package io.cdap.plugin.successfactors.source; +import com.google.common.base.Strings; import com.google.gson.Gson; import io.cdap.cdap.api.annotation.Description; import io.cdap.cdap.api.annotation.Metadata; @@ -138,6 +139,9 @@ private Schema getOutputSchema(FailureCollector failureCollector) { errorMsg = ResourceConstants.ERR_ODATA_SERVICE_CALL.getMsgForKeyWithCode(errorMsg); if (SuccessFactorsUtil.isNullOrEmpty(config.getConnection().getProxyUrl())) { failureCollector.addFailure(errorMsg, null).withConfigProperty(SuccessFactorsPluginConfig.BASE_URL); + if (!Strings.isNullOrEmpty(config.getConnection().getTokenURL())) { + failureCollector.addFailure(errorMsg, null).withConfigProperty(SuccessFactorsPluginConfig.TOKEN_URL); + } } else { failureCollector.addFailure(errorMsg, "Unable to connect to successFactors. Please check the basic and proxy connection parameters") @@ -164,8 +168,21 @@ 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 (SuccessFactorsConnectorConfig.BASIC_AUTH.equals(config.getConnection().getAuthType())) { + 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 (SuccessFactorsConnectorConfig.ENTER_TOKEN.equals(config.getConnection().getAssertionTokenType())) { + 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 3594323..1772fee 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 @@ -40,6 +40,7 @@ */ public class SuccessFactorsPluginConfig extends PluginConfig { public static final String BASE_URL = "baseURL"; + public static final String TOKEN_URL = "tokenURL"; public static final String ENTITY_NAME = "entityName"; public static final String UNAME = "username"; public static final String PASSWORD = "password"; @@ -170,6 +171,16 @@ public SuccessFactorsPluginConfig(String referenceName, @Nullable String proxyUrl, @Nullable String proxyPassword, @Nullable String proxyUsername, + @Nullable String tokenURL, + @Nullable String clientId, + @Nullable String privateKey, + @Nullable Integer expireInMinutes, + @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, @@ -179,8 +190,9 @@ public SuccessFactorsPluginConfig(String referenceName, @Nullable Integer maxRetryDuration, @Nullable Integer retryMultiplier, @Nullable Integer maxRetryCount) { - this.connection = new SuccessFactorsConnectorConfig(username, password, baseURL, proxyUrl, proxyPassword, - proxyUsername); + this.connection = new SuccessFactorsConnectorConfig(username, password, tokenURL, clientId, privateKey, + expireInMinutes, userId, companyId, baseURL, authType, assertionTokenType, samlUsername, assertionToken, + proxyUrl, proxyUsername, proxyPassword); this.referenceName = referenceName; this.entityName = entityName; this.associateEntityName = associateEntityName; @@ -194,6 +206,7 @@ public SuccessFactorsPluginConfig(String referenceName, this.retryMultiplier = retryMultiplier; this.maxRetryCount = maxRetryCount; } + @Nullable public SuccessFactorsConnectorConfig getConnection() { return connection; @@ -277,7 +290,14 @@ public boolean isSchemaBuildRequired() { 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)); + || containsMacro(SuccessFactorsConnectorConfig.PROPERTY_PROXY_PASSWORD) + || containsMacro(SuccessFactorsConnectorConfig.TOKEN_URL) + || containsMacro(SuccessFactorsConnectorConfig.CLIENT_ID) + || containsMacro(SuccessFactorsConnectorConfig.PRIVATE_KEY) + || containsMacro(SuccessFactorsConnectorConfig.EXPIRE_IN_MINUTES) + || containsMacro(SuccessFactorsConnectorConfig.USER_ID) + || containsMacro(SuccessFactorsConnectorConfig.COMPANY_ID) + || containsMacro(SuccessFactorsConnectorConfig.ASSERTION_TOKEN)); } /** @@ -311,7 +331,7 @@ public void validatePluginParameters(FailureCollector failureCollector) { private void validateMandatoryParameters(FailureCollector failureCollector) { IdUtils.validateReferenceName(getReferenceName(), failureCollector); - if (SuccessFactorsUtil.isNullOrEmpty(getEntityName()) && !containsMacro(ENTITY_NAME)) { + if (!containsMacro(ENTITY_NAME) && SuccessFactorsUtil.isNullOrEmpty(getEntityName())) { String errMsg = ResourceConstants.ERR_MISSING_PARAM_PREFIX.getMsgForKey(SAP_SUCCESSFACTORS_ENTITY_NAME); failureCollector.addFailure(errMsg, COMMON_ACTION).withConfigProperty(ENTITY_NAME); } @@ -324,7 +344,7 @@ private void validateMandatoryParameters(FailureCollector failureCollector) { */ private void validateBasicCredentials(FailureCollector failureCollector) { if (connection != null) { - connection.validateBasicCredentials(failureCollector); + connection.validateAuthCredentials(failureCollector); } } @@ -335,14 +355,14 @@ private void validateBasicCredentials(FailureCollector failureCollector) { * @param failureCollector {@code FailureCollector} */ private void validateEntityParameter(FailureCollector failureCollector) { - if (SuccessFactorsUtil.isNotNullOrEmpty(getEntityName()) && !containsMacro(getEntityName())) { + if (!containsMacro(getEntityName()) && SuccessFactorsUtil.isNotNullOrEmpty(getEntityName())) { if (PATTERN.matcher(getEntityName()).find()) { failureCollector.addFailure(ResourceConstants.ERR_FEATURE_NOT_SUPPORTED.getMsgForKey(), null) .withConfigProperty(ENTITY_NAME); } } if (SuccessFactorsUtil.isNotNullOrEmpty(associateEntityName)) { - if (SuccessFactorsUtil.isNullOrEmpty(getExpandOption()) && !containsMacro(EXPAND_OPTION)) { + if (!containsMacro(EXPAND_OPTION) && SuccessFactorsUtil.isNullOrEmpty(getExpandOption())) { failureCollector.addFailure(ResourceConstants.ERR_INVALID_ENTITY_CALL.getMsgForKey(), null) .withConfigProperty(ASSOCIATED_ENTITY_NAME); } @@ -401,6 +421,16 @@ public static class Builder { private String expandOption; private String paginationType; private String additionalQueryParameters; + private String tokenURL; + private String clientId; + private String privateKey; + private Integer expireInMinutes; + private String userId; + private String samlUsername; + private String assertionToken; + private String companyId; + private String authType; + private String assertionTokenType; private String proxyUrl; private String proxyUsername; private String proxyPassword; @@ -467,11 +497,51 @@ public Builder expandOption(@Nullable String expandOption) { return this; } + public Builder authType(@Nullable String authType) { + this.authType = Strings.isNullOrEmpty(authType) ? SuccessFactorsConnectorConfig.BASIC_AUTH : 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 setExpireInMinutes(@Nullable Integer expireInMinutes) { + this.expireInMinutes = expireInMinutes; + 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; @@ -495,11 +565,12 @@ public Builder setMaxRetryCount(Integer maxRetryCount) { } public SuccessFactorsPluginConfig build() { - return new SuccessFactorsPluginConfig(referenceName, baseURL, entityName, associateEntityName, username, - password, proxyUrl, proxyUsername, proxyPassword, - filterOption, selectOption, expandOption, additionalQueryParameters, - paginationType, initialRetryDuration, maxRetryDuration, - retryMultiplier, maxRetryCount); + return new SuccessFactorsPluginConfig(referenceName, baseURL, entityName, associateEntityName, username, password, + proxyUrl, proxyUsername, proxyPassword, + tokenURL, clientId, privateKey, expireInMinutes, userId, samlUsername, assertionToken, + companyId, authType, assertionTokenType, filterOption, selectOption, + expandOption, additionalQueryParameters, paginationType, + initialRetryDuration, maxRetryDuration, retryMultiplier, maxRetryCount); } } } 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 0e21ed6..9964fef 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 @@ -277,7 +277,7 @@ 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); + LOG.trace("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 9b85b0c..dc6d24a 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 @@ -16,12 +16,14 @@ package io.cdap.plugin.successfactors.source.transport; +import com.google.common.base.Strings; import dev.failsafe.Failsafe; import dev.failsafe.FailsafeException; import dev.failsafe.RetryPolicy; 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.common.util.SuccessFactorsUtil; import io.cdap.plugin.successfactors.connector.SuccessFactorsConnectorConfig; import okhttp3.Authenticator; @@ -53,12 +55,14 @@ public class SuccessFactorsTransporter { static final String SERVICE_VERSION = "dataserviceversion"; private static final Logger LOG = LoggerFactory.getLogger(SuccessFactorsTransporter.class); private static final long CONNECTION_TIMEOUT = 300; - - private SuccessFactorsConnectorConfig config; + private static final long WAIT_TIME = 5; + private static final long MAX_NUMBER_OF_RETRY_ATTEMPTS = 5; + private static String accessToken; private Response response; + private final SuccessFactorsConnectorConfig config; - public SuccessFactorsTransporter(SuccessFactorsConnectorConfig pluginConfig) { - this.config = pluginConfig; + public SuccessFactorsTransporter(SuccessFactorsConnectorConfig config) { + this.config = config; } /** @@ -173,11 +177,46 @@ private RetryPolicy getRetryPolicy(int initialRetryDuration, int maxRetr private Response transport(URL endpoint, String mediaType) throws IOException, TransportException { OkHttpClient enhancedOkHttpClient = buildConfiguredClient(config.getProxyUrl(), config.getProxyUsername(), config.getProxyPassword()); - Request req = buildRequest(endpoint, mediaType); + Request req; + if (SuccessFactorsConnectorConfig.BASIC_AUTH.equals(config.getAuthType())) { + req = buildRequest(endpoint, mediaType); + } else { + if (Strings.isNullOrEmpty(accessToken)) { + 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}. * @@ -278,6 +317,15 @@ private String getAuthenticationKey() { ); } + 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..0763da7 100644 --- a/src/main/resources/i10n/SuccessFactorsBatchSourceBundle.properties +++ b/src/main/resources/i10n/SuccessFactorsBatchSourceBundle.properties @@ -1,6 +1,9 @@ ## Mandatory UI parameter validation error messages err.missing.param.prefix=Required property ''{0}'' is blank. +## Negative value validation error messages +err.invalid.expire.time.prefix=Invalid value for property ''{0}'', it should be a non-negative number. + ## Connection Parameter validation error messages err.missing.param.or.macro.action=An actual value or a macro variable is expected. 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 10d43db..03eb798 100644 --- a/src/test/java/io/cdap/plugin/successfactors/connector/SuccessFactorsConnectorTest.java +++ b/src/test/java/io/cdap/plugin/successfactors/connector/SuccessFactorsConnectorTest.java @@ -17,6 +17,8 @@ import io.cdap.cdap.api.data.format.StructuredRecord; import io.cdap.cdap.api.data.schema.Schema; +import io.cdap.cdap.api.plugin.PluginConfig; +import io.cdap.cdap.etl.api.FailureCollector; import io.cdap.cdap.etl.api.batch.BatchSource; import io.cdap.cdap.etl.api.batch.BatchSourceContext; import io.cdap.cdap.etl.api.connector.BrowseDetail; @@ -78,6 +80,7 @@ public void testConfiguration() throws TransportException, SuccessFactorsService .baseURL("http://localhost") .entityName("entity-name") .username("username") + .authType("basicAuth") .password("password"); pluginConfig = pluginConfigBuilder.build(); @@ -86,6 +89,7 @@ public void testConfiguration() throws TransportException, SuccessFactorsService @Test public void testValidateSuccessfulConnection() throws TransportException, SuccessFactorsServiceException { + successFactorsTransporter = new SuccessFactorsTransporter(pluginConfig.getConnection()); new Expectations(SuccessFactorsUrlContainer.class, SuccessFactorsTransporter.class, SuccessFactorsSchemaGenerator.class) { { @@ -102,6 +106,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()); new Expectations(SuccessFactorsUrlContainer.class, SuccessFactorsTransporter.class, SuccessFactorsSchemaGenerator.class) { { @@ -117,6 +122,7 @@ public void testValidateUnauthorisedConnection() throws TransportException, Succ @Test public void testValidateNotFoundConnection() throws TransportException, SuccessFactorsServiceException { MockFailureCollector collector = new MockFailureCollector(); + successFactorsTransporter = new SuccessFactorsTransporter(pluginConfig.getConnection()); new Expectations(SuccessFactorsUrlContainer.class, SuccessFactorsTransporter.class, SuccessFactorsSchemaGenerator.class) { { @@ -149,6 +155,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()); new Expectations(SuccessFactorsTransporter.class) { { successFactorsTransporter.callSuccessFactorsEntity(null, anyString); @@ -183,6 +190,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()); new Expectations(SuccessFactorsConnector.class, SuccessFactorsTransporter.class) { { @@ -223,6 +231,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()); successFactorsConnector = new SuccessFactorsConnector(pluginConfig.getConnection()); new Expectations(SuccessFactorsTransporter.class, SuccessFactorsTransporter.class, SuccessFactorsConnector.class) { @@ -256,6 +265,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()); new Expectations(SuccessFactorsTransporter.class, SuccessFactorsTransporter.class, SuccessFactorsConnector.class) { { successFactorsTransporter.callSuccessFactorsEntity(null, anyString); @@ -275,9 +285,72 @@ public void testSampleWithoutSampleData() throws IOException, TransportException Assert.assertNull(sample); } + @Test + public void testOAuthEnterTokenMissingFields() { + SuccessFactorsPluginConfig pluginConfig = new SuccessFactorsPluginConfig.Builder() + .referenceName("unit-test-ref-name") + .baseURL("http://localhost") + .entityName("entity-name") + .authType(SuccessFactorsConnectorConfig.OAUTH2) + .assertionTokenType(SuccessFactorsConnectorConfig.ENTER_TOKEN) + .setTokenURL("http://localhost/token") + .build(); + ConnectorContext context = new MockConnectorContext(new MockConnectorConfigurer()); + FailureCollector collector = context.getFailureCollector(); + SuccessFactorsConnector connector = new SuccessFactorsConnector(pluginConfig.getConnection()); + connector.test(context); + Assert.assertEquals(4, collector.getValidationFailures().size()); + Assert.assertEquals("Required property 'clientId' is blank.", + collector.getValidationFailures().get(0).getMessage()); + Assert.assertEquals("Required property 'companyId' is blank.", + collector.getValidationFailures().get(1).getMessage()); + Assert.assertEquals("Required property 'assertionToken' is blank.", + collector.getValidationFailures().get(2).getMessage()); + Assert.assertEquals("Unable to call SuccessFactorsEntity", collector.getValidationFailures().get(3).getMessage()); + } + + @Test + public void testOAuthCreateTokenMissingFields() { + SuccessFactorsPluginConfig pluginConfig = new SuccessFactorsPluginConfig.Builder() + .referenceName("unit-test-ref-name") + .baseURL("http://localhost") + .entityName("entity-name") + .authType(SuccessFactorsConnectorConfig.OAUTH2) + .assertionTokenType(SuccessFactorsConnectorConfig.CREATE_TOKEN) + .setTokenURL("http://localhost/token") + .build(); + ConnectorContext context = new MockConnectorContext(new MockConnectorConfigurer()); + FailureCollector collector = context.getFailureCollector(); + SuccessFactorsConnector connector = new SuccessFactorsConnector(pluginConfig.getConnection()); + connector.test(context); + Assert.assertEquals(5, collector.getValidationFailures().size()); + Assert.assertEquals("Required property 'clientId' is blank.", + collector.getValidationFailures().get(0).getMessage()); + Assert.assertEquals("Required property 'companyId' is blank.", + collector.getValidationFailures().get(1).getMessage()); + Assert.assertEquals("Required property 'privateKey' is blank.", + collector.getValidationFailures().get(2).getMessage()); + Assert.assertEquals("Required property 'userId' is blank.", collector.getValidationFailures().get(3).getMessage()); + Assert.assertEquals("Unable to call SuccessFactorsEntity", collector.getValidationFailures().get(4).getMessage()); + } + + @Test + public void testDefaultValueForExpireTime() { + SuccessFactorsPluginConfig pluginConfig = new SuccessFactorsPluginConfig.Builder() + .referenceName("unit-test-ref-name") + .baseURL("http://localhost") + .entityName("entity-name") + .authType(SuccessFactorsConnectorConfig.OAUTH2) + .assertionTokenType(SuccessFactorsConnectorConfig.CREATE_TOKEN) + .setExpireInMinutes(null) + .build(); + Assert.assertEquals(1440, pluginConfig.getConnection().getExpireInMinutes().intValue()); + } + @Test public void testSampleWithSampleData() throws IOException, TransportException, EntityProviderException, SuccessFactorsServiceException, EdmException { + 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 af53dc3..833c1fc 100644 --- a/src/test/java/io/cdap/plugin/successfactors/source/SuccessFactorsSourceTest.java +++ b/src/test/java/io/cdap/plugin/successfactors/source/SuccessFactorsSourceTest.java @@ -81,19 +81,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); @@ -108,15 +102,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); @@ -153,13 +141,6 @@ 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()); successFactorsUrlContainer = new SuccessFactorsUrlContainer(pluginConfig); @@ -270,6 +251,9 @@ 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(); pluginConfigBuilder = SuccessFactorsPluginConfig.builder() @@ -317,14 +301,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) { @@ -350,14 +328,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) { @@ -383,14 +355,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) { @@ -416,14 +382,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) { @@ -449,14 +409,7 @@ 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 4af5aaf..5740abe 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); } 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 bd62534..ad9541b 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 @@ -21,7 +21,6 @@ import io.cdap.plugin.successfactors.source.config.SuccessFactorsPluginConfig; 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,19 +45,15 @@ public class SuccessFactorsInputFormatTest { @Before public void initializeTests() { - pluginConfig = Mockito.spy(new SuccessFactorsPluginConfig("referenceName", - "baseURL", - "entityName", - null, - "username", - "password", null, null, - null, - "filterOption", - "selectOption", - "expandOption", - "additionalQueryParameters", - null, null, - null, null, null)); + pluginConfig = Mockito.spy(SuccessFactorsPluginConfig.builder().referenceName("referenceName") + .baseURL("baseUrl") + .entityName("entityName") + .username("username") + .password("password") + .filterOption("filterOption") + .selectOption("selectOption") + .expandOption("expandOption") + .additionalQueryParameters("additionalQueryParameters").build()); } @Test 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 afa344b..c804ce8 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 @@ -46,11 +46,17 @@ public void setup() throws EntityProviderException { edm = EntityProvider.readMetadata(TestSuccessFactorsUtil.readResource("successfactors-metadata.xml"), false); serviceHelper = new SuccessFactorsEntityProvider(edm); generator = new SuccessFactorsSchemaGenerator(serviceHelper); - pluginConfig = new SuccessFactorsPluginConfig("referenceName", "baseUR", - "entityName", "associateEntityName", "username", "password", - null, null, null, "filterOption", "selectOption", - "expandOption", "additionalQueryParameters", "paginationType", - null, null, null, null); + pluginConfig = SuccessFactorsPluginConfig.builder().referenceName("referenceName") + .baseURL("baseUrl") + .entityName("entityName") + .associateEntityName("associateEntityName") + .username("username") + .password("password") + .filterOption("filterOption") + .selectOption("selectOption") + .expandOption("expandOption") + .additionalQueryParameters("additionalQueryParameters") + .paginationType("paginationType").build(); } @Test @@ -84,7 +90,18 @@ public void testSelectWithExpandNames() throws SuccessFactorsServiceException { @Test public void testBuildExpandOutputSchema() throws SuccessFactorsServiceException { - Schema outputSchema = generator.buildExpandOutputSchema("Benefit", + pluginConfig = SuccessFactorsPluginConfig.builder().referenceName("referenceName") + .baseURL("https://baseUrl") + .entityName("entityName") + .associateEntityName("associateEntityName") + .username("username") + .password("password") + .filterOption("filterOption") + .selectOption("selectOption") + .expandOption("expandOption") + .additionalQueryParameters("additionalQueryParameters") + .paginationType("paginationType").build(); + Schema outputSchema = generator.buildExpandOutputSchema("Benefit", "eligibleBenefits", "associatedEntity", pluginConfig); int lastIndex = outputSchema.getFields().size() - 1; Assert.assertEquals("Schema field size is same.", @@ -154,6 +171,18 @@ public void testInvalidEntityName() throws SuccessFactorsServiceException { @Test public void testInvalidExpandName() throws SuccessFactorsServiceException { + SuccessFactorsPluginConfig pluginConfig = SuccessFactorsPluginConfig.builder().referenceName("referenceName") + .baseURL("https://baseUrl") + .entityName("entityName") + .associateEntityName("associateEntityName") + .username("username") + .password("password") + .filterOption("filterOption") + .selectOption("selectOption") + .expandOption("expandOption") + .additionalQueryParameters("additionalQueryParameters") + .paginationType("paginationType") + .build(); exception.expectMessage("'assEntity' not found in the 'Benefit' entity."); generator.buildExpandOutputSchema("Benefit", "INVALID-NAVIGATION-NAME", "assEntity", pluginConfig); 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 31cf59c..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 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 1de10ca..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 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 538045b..faac153 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 @@ -127,6 +127,7 @@ public void setUp() { .entityName("Entity") .username("test") .password("secret") + .authType("basicAuth") .expandOption("Products/Supplier"); pluginConfig = pluginConfigBuilder.build(); successFactorsURL = new SuccessFactorsUrlContainer(pluginConfig); @@ -157,6 +158,38 @@ public void testCallSuccessFactors() throws TransportException { 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 public void testCallSuccessFactorsWithProxy() throws TransportException { pluginConfigBuilder = SuccessFactorsPluginConfig.builder() 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 9f64c2c..dcbad07 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 @@ -27,20 +27,18 @@ public class SuccessFactorsUrlContainerTest { public SuccessFactorsPluginConfig pluginConfig; @Before public void initializeTests() { - pluginConfig = Mockito.spy(new SuccessFactorsPluginConfig("referenceName", - "https://baseUrl", - "entityName", - "associatedEntity", - "username", - "password", null, null, - null, - "filterOption", - "selectOption", - "expandOption", - "", - null, null, - null, null, null)); + pluginConfig = Mockito.spy(SuccessFactorsPluginConfig.builder().referenceName("referenceName") + .baseURL("https://baseUrl") + .entityName("entityName") + .associateEntityName("associatedEntity") + .username("username") + .password("password") + .filterOption("filterOption") + .selectOption("selectOption") + .expandOption("expandOption") + .build()); } + @Test public void testGetTesterURL() { SuccessFactorsUrlContainer urlContainer = new SuccessFactorsUrlContainer(pluginConfig); @@ -68,19 +66,15 @@ public void testGetTotalRecordCountURL() { @Test public void testGetURLWithAdditionalQueryParameters() { - pluginConfig = Mockito.spy(new SuccessFactorsPluginConfig("referenceName", - "https://successfactors.com", - "EmpJob", - "associatedEntity", - "username", - "password", null, null, - null, - "", - "", - "", - "startDate=2023-01-01&endDate=2023-02-02", - null, null, - null, null, null)); + pluginConfig = Mockito.spy(SuccessFactorsPluginConfig.builder() + .referenceName("referenceName") + .baseURL("https://successfactors.com") + .entityName("EmpJob") + .associateEntityName("associatedEntity") + .username("username") + .password("password") + .additionalQueryParameters("startDate=2023-01-01&endDate=2023-02-02") + .build()); SuccessFactorsUrlContainer urlContainer = new SuccessFactorsUrlContainer(pluginConfig); String expectedUrl = "https://successfactors.com/EmpJob?startDate=2023-01-01&endDate=2023-02-02&%24top=1"; URL actualUrl = urlContainer.getTesterURL(); diff --git a/widgets/SuccessFactors-batchsource.json b/widgets/SuccessFactors-batchsource.json index 65e1f99..f4e0adb 100644 --- a/widgets/SuccessFactors-batchsource.json +++ b/widgets/SuccessFactors-batchsource.json @@ -31,22 +31,97 @@ "connectionType": "SuccessFactors" } }, + { + "widget-type": "radio-group", + "label": "Authentication Type", + "name": "authType", + "widget-attributes": { + "layout": "inline", + "default": "basicAuth", + "options": [ + { + "id": "basicAuth", + "label": "Basic Authentication" + }, + { + "id": "oAuth2", + "label": "OAuth 2.0" + } + ] + } + }, + { + "name": "assertionTokenType", + "label": "Assertion Token Type", + "widget-type": "radio-group", + "widget-attributes": { + "layout": "inline", + "default": "enterToken", + "options": [ + { + "id": "enterToken", + "label": "Enter Token" + }, + { + "id": "createToken", + "label": "Create Token" + } + ] + } + }, { "widget-type": "textbox", - "label": "SAP SuccessFactors Logon Username", - "name": "username", + "label": "Token URL", + "name": "tokenURL", "widget-attributes": { - "placeholder": "" + "placeholder": "SAP SuccessFactors token url, for example, https:///oauth/token" } }, { - "widget-type": "password", - "label": "SAP SuccessFactors Logon Password", - "name": "password", + "widget-type": "textbox", + "label": "Client ID", + "name": "clientId" + }, + { + "widget-type": "textbox", + "label": "Private Key", + "name": "privateKey" + }, + { + "widget-type": "number", + "name": "expireInMinutes", + "label": "Expire Assertion Token In (Minutes)", "widget-attributes": { - "placeholder": "" + "min": 1, + "step": 1, + "default": 1440 } }, + { + "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", @@ -233,6 +308,42 @@ "type": "property", "name": "baseURL" }, + { + "type": "property", + "name": "authType" + }, + { + "type": "property", + "name": "tokenURL" + }, + { + "type": "property", + "name": "clientId" + }, + { + "type": "property", + "name": "privateKey" + }, + { + "name": "expireInMinutes", + "type": "property" + }, + { + "type": "property", + "name": "userId" + }, + { + "type": "property", + "name": "companyId" + }, + { + "type": "property", + "name": "assertionTokenType" + }, + { + "type": "property", + "name": "assertionToken" + }, { "type": "property", "name": "proxyUrl" @@ -259,6 +370,82 @@ } ] }, + { + "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": "tokenURL", + "type": "property" + } + ] + }, + { + "name": "enterAssertionToken", + "condition": { + "expression": "authType == 'oAuth2' && assertionTokenType == 'enterToken'" + }, + "show": [ + { + "type": "property", + "name": "assertionToken" + } + ] + }, + { + "name": "createAssertionToken", + "condition": { + "expression": "authType == 'oAuth2' && assertionTokenType == 'createToken'" + }, + "show": [ + { + "name": "privateKey", + "type": "property" + }, + { + "name": "expireInMinutes", + "type": "property" + }, + { + "name": "userId", + "type": "property" + } + ] + }, { "name": "Proxy authentication", "condition": { diff --git a/widgets/SuccessFactors-connector.json b/widgets/SuccessFactors-connector.json index 7ffbc4a..7dd56a8 100644 --- a/widgets/SuccessFactors-connector.json +++ b/widgets/SuccessFactors-connector.json @@ -7,22 +7,97 @@ { "label": "Credentials", "properties": [ + { + "widget-type": "radio-group", + "label": "Authentication Type", + "name": "authType", + "widget-attributes": { + "layout": "inline", + "default": "basicAuth", + "options": [ + { + "id": "basicAuth", + "label": "Basic Authentication" + }, + { + "id": "oAuth2", + "label": "OAuth 2.0" + } + ] + } + }, + { + "name": "assertionTokenType", + "label": "Assertion Token Type", + "widget-type": "radio-group", + "widget-attributes": { + "layout": "inline", + "default": "enterToken", + "options": [ + { + "id": "enterToken", + "label": "Enter Token" + }, + { + "id": "createToken", + "label": "Create Token" + } + ] + } + }, { "widget-type": "textbox", - "label": "SAP SuccessFactors Logon Username", - "name": "username", + "label": "Token URL", + "name": "tokenURL", "widget-attributes": { - "placeholder": "" + "placeholder": "SAP SuccessFactors token url, for example, https:///oauth/token" } }, { - "widget-type": "password", - "label": "SAP SuccessFactors Logon Password", - "name": "password", + "widget-type": "textbox", + "label": "Client ID", + "name": "clientId" + }, + { + "widget-type": "textbox", + "label": "Private Key", + "name": "privateKey" + }, + { + "widget-type": "number", + "name": "expireInMinutes", + "label": "Expire In (Minutes)", "widget-attributes": { - "placeholder": "" + "min": 1, + "step": 1, + "default": 1440 } }, + { + "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", @@ -71,6 +146,80 @@ "type": "property" } ] + }, + { + "name": "basicAuth", + "condition": { + "expression": "authType == 'basicAuth' || authType == 'null'" + }, + "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": "tokenURL", + "type": "property" + } + ] + }, + { + "name": "enterAssertionToken", + "condition": { + "expression": "authType == 'oAuth2' && assertionTokenType== 'enterToken'" + }, + "show": [ + { + "type": "property", + "name": "assertionToken" + } + ] + }, + { + "name": "createAssertionToken", + "condition": { + "expression": "authType == 'oAuth2' && assertionTokenType == 'createToken'" + }, + "show": [ + { + "name": "privateKey", + "type": "property" + }, + { + "name": "expireInMinutes", + "type": "property" + }, + { + "name": "userId", + "type": "property" + } + ] } ], "outputs": []