diff --git a/docs/Salesforce-batchsink.md b/docs/Salesforce-batchsink.md index 777893c5..6cc935bd 100644 --- a/docs/Salesforce-batchsink.md +++ b/docs/Salesforce-batchsink.md @@ -16,12 +16,16 @@ You also can use the macro function ${conn(connection-name)}. **Reference Name:** Name used to uniquely identify this sink for lineage, annotating metadata, etc. -**Username:** Salesforce username. +**Grant Type:** Grant type to use for OAuth authentication. Supported values are 'password' and +'client_credentials'. When set to 'client_credentials', only Consumer Key, Consumer Secret, and Login URL +are required. Username, Password, and Security Token are not needed. Defaults to 'password' if not specified. -**Password:** Salesforce password. +**Username:** Salesforce username. Required for 'password' grant type. -**Security Token:** Salesforce security token. If the password does not contain the security token, the plugin -will append the token before authenticating with Salesforce. +**Password:** Salesforce password. Required for 'password' grant type. + +**Security Token:** Salesforce security token. If the password does not contain the security token, the plugin +will append the token before authenticating with Salesforce. Only applicable for 'password' grant type. **Consumer Key:** Application Consumer Key. This is also known as the OAuth client ID. A Salesforce connected application must be created in order to get a consumer key. @@ -29,7 +33,10 @@ A Salesforce connected application must be created in order to get a consumer ke **Consumer Secret:** Application Consumer Secret. This is also known as the OAuth client secret. A Salesforce connected application must be created in order to get a client secret. -**Login URL:** Salesforce OAuth2 login URL. +**Login URL:** Salesforce OAuth2 login URL. For the 'password' grant type, the default generic URL +`https://login.salesforce.com/services/oauth2/token` can be used. For the 'client_credentials' grant type, +you must provide your Salesforce instance-specific URL, for example +`https://.my.salesforce.com/services/oauth2/token`. **Connect Timeout:** Maximum time in milliseconds to wait for connection initialization before it times out. diff --git a/docs/Salesforce-batchsource.md b/docs/Salesforce-batchsource.md index 8513725d..c5d0c761 100644 --- a/docs/Salesforce-batchsource.md +++ b/docs/Salesforce-batchsource.md @@ -18,12 +18,16 @@ Configuration **Reference Name:** Name used to uniquely identify this source for lineage, annotating metadata, etc. -**Username:** Salesforce username. +**Grant Type:** Grant type to use for OAuth authentication. Supported values are 'password' and +'client_credentials'. When set to 'client_credentials', only Consumer Key, Consumer Secret, and Login URL +are required. Username, Password, and Security Token are not needed. Defaults to 'password' if not specified. -**Password:** Salesforce password. +**Username:** Salesforce username. Required for 'password' grant type. -**Security Token:** Salesforce security token. If the password does not contain the security token the plugin, -will append the token before authenticating with Salesforce. +**Password:** Salesforce password. Required for 'password' grant type. + +**Security Token:** Salesforce security token. If the password does not contain the security token the plugin, +will append the token before authenticating with Salesforce. Only applicable for 'password' grant type. **Consumer Key:** Application Consumer Key. This is also known as the OAuth client ID. A Salesforce connected application must be created in order to get a consumer key. @@ -31,7 +35,10 @@ A Salesforce connected application must be created in order to get a consumer ke **Consumer Secret:** Application Consumer Secret. This is also known as the OAuth client secret. A Salesforce connected application must be created in order to get a client secret. -**Login URL:** Salesforce OAuth2 login URL. +**Login URL:** Salesforce OAuth2 login URL. For the 'password' grant type, the default generic URL +`https://login.salesforce.com/services/oauth2/token` can be used. For the 'client_credentials' grant type, +you must provide your Salesforce instance-specific URL, for example +`https://.my.salesforce.com/services/oauth2/token`. **Connect Timeout:** Maximum time in milliseconds to wait for connection initialization before it times out. diff --git a/docs/Salesforce-connector.md b/docs/Salesforce-connector.md index 850abb81..bedf63d3 100644 --- a/docs/Salesforce-connector.md +++ b/docs/Salesforce-connector.md @@ -10,12 +10,16 @@ Properties **Description:** Description of the connection. -**Username:** Salesforce username. +**Grant Type:** Grant type to use for OAuth authentication. Supported values are 'password' and +'client_credentials'. When set to 'client_credentials', only Consumer Key, Consumer Secret, and Login URL +are required. Username, Password, and Security Token are not needed. Defaults to 'password' if not specified. -**Password:** Salesforce password. +**Username:** Salesforce username. Required for 'password' grant type. + +**Password:** Salesforce password. Required for 'password' grant type. **Security Token:** Salesforce security token. If the password does not contain the security token, the plugin -will append the token before authenticating with Salesforce. +will append the token before authenticating with Salesforce. Only applicable for 'password' grant type. **Consumer Key:** Application Consumer Key. This is also known as the OAuth client ID. A Salesforce connected application must be created in order to get a consumer key. @@ -23,7 +27,10 @@ A Salesforce connected application must be created in order to get a consumer ke **Consumer Secret:** Application Consumer Secret. This is also known as the OAuth client secret. A Salesforce connected application must be created in order to get a client secret. -**Login URL:** Salesforce OAuth2 login URL. +**Login URL:** Salesforce OAuth2 login URL. For the 'password' grant type, the default generic URL +`https://login.salesforce.com/services/oauth2/token` can be used. For the 'client_credentials' grant type, +you must provide your Salesforce instance-specific URL, for example +`https://.my.salesforce.com/services/oauth2/token`. **Connect Timeout:** Maximum time in milliseconds to wait for connection initialization before it times out. diff --git a/docs/Salesforce-streamingsource.md b/docs/Salesforce-streamingsource.md index 6da67cbc..aa23a87f 100644 --- a/docs/Salesforce-streamingsource.md +++ b/docs/Salesforce-streamingsource.md @@ -21,12 +21,16 @@ Configuration **Reference Name:** Name used to uniquely identify this source for lineage, annotating metadata, etc. -**Username:** Salesforce username. +**Grant Type:** Grant type to use for OAuth authentication. Supported values are 'password' and +'client_credentials'. When set to 'client_credentials', only Consumer Key, Consumer Secret, and Login URL +are required. Username, Password, and Security Token are not needed. Defaults to 'password' if not specified. -**Password:** Salesforce password. +**Username:** Salesforce username. Required for 'password' grant type. -**Security Token:** Salesforce security token. If the password does not contain the security token, the plugin -will append the token before authenticating with Salesforce. +**Password:** Salesforce password. Required for 'password' grant type. + +**Security Token:** Salesforce security token. If the password does not contain the security token, the plugin +will append the token before authenticating with Salesforce. Only applicable for 'password' grant type. **Consumer Key:** Application Consumer Key. This is also known as the OAuth client ID. A Salesforce connected application must be created in order to get a consumer key. @@ -34,7 +38,10 @@ A Salesforce connected application must be created in order to get a consumer ke **Consumer Secret:** Application Consumer Secret. This is also known as the OAuth client secret. A Salesforce connected application must be created in order to get a client secret. -**Login URL:** Salesforce OAuth2 login URL. +**Login URL:** Salesforce OAuth2 login URL. For the 'password' grant type, the default generic URL +`https://login.salesforce.com/services/oauth2/token` can be used. For the 'client_credentials' grant type, +you must provide your Salesforce instance-specific URL, for example +`https://.my.salesforce.com/services/oauth2/token`. **Connect Timeout:** Maximum time in milliseconds to wait for connection initialization before it times out. diff --git a/docs/SalesforceMultiObjects-batchsource.md b/docs/SalesforceMultiObjects-batchsource.md index 8966d0ab..687ff426 100644 --- a/docs/SalesforceMultiObjects-batchsource.md +++ b/docs/SalesforceMultiObjects-batchsource.md @@ -18,12 +18,16 @@ Configuration **Reference Name:** Name used to uniquely identify this source for lineage, annotating metadata, etc. -**Username:** Salesforce username. +**Grant Type:** Grant type to use for OAuth authentication. Supported values are 'password' and +'client_credentials'. When set to 'client_credentials', only Consumer Key, Consumer Secret, and Login URL +are required. Username, Password, and Security Token are not needed. Defaults to 'password' if not specified. -**Password:** Salesforce password. +**Username:** Salesforce username. Required for 'password' grant type. -**Security Token:** Salesforce security token. If the password does not contain the security token, the plugin -will append the token before authenticating with Salesforce. +**Password:** Salesforce password. Required for 'password' grant type. + +**Security Token:** Salesforce security token. If the password does not contain the security token, the plugin +will append the token before authenticating with Salesforce. Only applicable for 'password' grant type. **Consumer Key:** Application Consumer Key. This is also known as the OAuth client ID. A Salesforce connected application must be created in order to get a consumer key. @@ -31,7 +35,10 @@ A Salesforce connected application must be created in order to get a consumer ke **Consumer Secret:** Application Consumer Secret. This is also known as the OAuth client secret. A Salesforce connected application must be created in order to get a client secret. -**Login URL:** Salesforce OAuth2 login URL. +**Login URL:** Salesforce OAuth2 login URL. For the 'password' grant type, the default generic URL +`https://login.salesforce.com/services/oauth2/token` can be used. For the 'client_credentials' grant type, +you must provide your Salesforce instance-specific URL, for example +`https://.my.salesforce.com/services/oauth2/token`. **Connect Timeout:** Maximum time in milliseconds to wait for connection initialization before time out. diff --git a/src/main/java/io/cdap/plugin/salesforce/SalesforceConnectionUtil.java b/src/main/java/io/cdap/plugin/salesforce/SalesforceConnectionUtil.java index a5ea493f..fc6135b1 100644 --- a/src/main/java/io/cdap/plugin/salesforce/SalesforceConnectionUtil.java +++ b/src/main/java/io/cdap/plugin/salesforce/SalesforceConnectionUtil.java @@ -123,6 +123,7 @@ public static OAuthInfo getOAuthInfo(SalesforceConnectorInfo config, FailureColl if (!config.canAttemptToEstablishConnection()) { return null; } + config.validateAuthenticationFields(collector); OAuthInfo oAuthInfo = null; try { oAuthInfo = Authenticator.getOAuthInfo(config.getAuthenticatorCredentials()); diff --git a/src/main/java/io/cdap/plugin/salesforce/SalesforceConstants.java b/src/main/java/io/cdap/plugin/salesforce/SalesforceConstants.java index a6991cfc..d18282a9 100644 --- a/src/main/java/io/cdap/plugin/salesforce/SalesforceConstants.java +++ b/src/main/java/io/cdap/plugin/salesforce/SalesforceConstants.java @@ -16,6 +16,7 @@ package io.cdap.plugin.salesforce; import io.cdap.cdap.api.plugin.PluginConfig; +import io.cdap.plugin.salesforce.authenticator.AuthenticatorCredentials.GrantType; import java.util.Arrays; import java.util.Collections; @@ -65,11 +66,15 @@ public class SalesforceConstants { public static final String CONFIG_MAX_RETRY_DURATION = "mapred.salesforce.maxRetryDuration"; public static final String CONFIG_MAX_RETRY_COUNT = "mapred.salesforce.maxRetryCount"; public static final String CONFIG_RETRY_REQUIRED = "mapred.salesforce.retryOnBackendError"; + public static final String CONFIG_GRANT_TYPE = "mapred.salesforce.grantType"; public static final String PROPERTY_PROXY_URL = "proxyUrl"; public static final String CONFIG_PROXY_URL = "mapred.salesforce.proxyUrl"; public static final String REGEX_PROXY_URL = "^(?i)(https?)://.*$"; + public static final String PROPERTY_AUTHENTICATION_GRANT_TYPE = "authenticationGrantType"; + public static final GrantType DEFAULT_GRANT_TYPE = GrantType.PASSWORD; + public static final String PROPERTY_MAX_RETRY_TIME_IN_MINS = "cdap.streaming.maxRetryTimeInMins"; public static final long DEFAULT_MAX_RETRY_TIME_IN_MINS = 360L; diff --git a/src/main/java/io/cdap/plugin/salesforce/plugin/SalesforceConnectorBaseConfig.java b/src/main/java/io/cdap/plugin/salesforce/plugin/SalesforceConnectorBaseConfig.java index cdd3ac18..4b2ec32d 100644 --- a/src/main/java/io/cdap/plugin/salesforce/plugin/SalesforceConnectorBaseConfig.java +++ b/src/main/java/io/cdap/plugin/salesforce/plugin/SalesforceConnectorBaseConfig.java @@ -15,6 +15,7 @@ */ package io.cdap.plugin.salesforce.plugin; +import com.google.common.base.Strings; import com.sforce.ws.ConnectionException; import io.cdap.cdap.api.annotation.Description; import io.cdap.cdap.api.annotation.Macro; @@ -24,6 +25,7 @@ import io.cdap.plugin.salesforce.SalesforceConnectionUtil; import io.cdap.plugin.salesforce.SalesforceConstants; import io.cdap.plugin.salesforce.authenticator.AuthenticatorCredentials; +import io.cdap.plugin.salesforce.authenticator.AuthenticatorCredentials.GrantType; import javax.annotation.Nullable; @@ -32,6 +34,12 @@ */ public class SalesforceConnectorBaseConfig extends PluginConfig { + @Name(SalesforceConstants.PROPERTY_AUTHENTICATION_GRANT_TYPE) + @Description("Salesforce authentication grant type: basic or client credentials") + @Nullable + @Macro + protected String authenticationGrantType; + @Nullable @Name(SalesforceConstants.PROPERTY_PROXY_URL) @Description("Proxy URL. Must contain a protocol, address and port.") @@ -118,7 +126,8 @@ public SalesforceConnectorBaseConfig(@Nullable String consumerKey, @Nullable Long initialRetryDuration, @Nullable Long maxRetryDuration, @Nullable Integer maxRetryCount, - @Nullable Boolean retryOnBackendError) { + @Nullable Boolean retryOnBackendError, + @Nullable String authenticationGrantType) { this.consumerKey = consumerKey; this.consumerSecret = consumerSecret; this.username = username; @@ -132,6 +141,16 @@ public SalesforceConnectorBaseConfig(@Nullable String consumerKey, this.maxRetryDuration = maxRetryDuration; this.retryOnBackendError = retryOnBackendError; this.maxRetryCount = maxRetryCount; + this.authenticationGrantType = authenticationGrantType; + } + + public GrantType getAuthenticationGrantType() { + if (!Strings.isNullOrEmpty(authenticationGrantType) && + authenticationGrantType.equals(GrantType.CLIENT_CREDENTIALS.getType())) { + return GrantType.CLIENT_CREDENTIALS; + } + // Default auth, handles null case when upgrading pipeline + return SalesforceConstants.DEFAULT_GRANT_TYPE; } @Nullable @@ -232,4 +251,55 @@ public String getProxyUrl() { return proxyUrl; } + /** + * Validates that required authentication fields are present based on the selected OAuth grant type. + * For PASSWORD grant type: consumerKey, consumerSecret, username, password, and loginUrl are required. + * For CLIENT_CREDENTIALS grant type: consumerKey, consumerSecret, and loginUrl are required. + * + * @param collector the failure collector to report validation errors + */ + public void validateAuthenticationFields(FailureCollector collector) { + if (containsMacro(SalesforceConstants.PROPERTY_AUTHENTICATION_GRANT_TYPE)) { + return; + } + + GrantType grantType = getAuthenticationGrantType(); + + // Fields required for all grant types + if (!containsMacro(SalesforceConstants.PROPERTY_CONSUMER_KEY) && Strings.isNullOrEmpty(consumerKey)) { + collector.addFailure("Consumer Key is required for authentication.", + "Please provide the Consumer Key from your Salesforce connected app.") + .withConfigProperty(SalesforceConstants.PROPERTY_CONSUMER_KEY); + } + if (!containsMacro(SalesforceConstants.PROPERTY_CONSUMER_SECRET) && Strings.isNullOrEmpty(consumerSecret)) { + collector.addFailure("Consumer Secret is required for authentication.", + "Please provide the Consumer Secret from your Salesforce connected app.") + .withConfigProperty(SalesforceConstants.PROPERTY_CONSUMER_SECRET); + } + if (!containsMacro(SalesforceConstants.PROPERTY_LOGIN_URL) && Strings.isNullOrEmpty(loginUrl)) { + collector.addFailure("Login URL is required for authentication.", + "Please provide the Salesforce login URL.") + .withConfigProperty(SalesforceConstants.PROPERTY_LOGIN_URL); + } + + // Fields required only for PASSWORD grant type + if (grantType == GrantType.PASSWORD) { + if (!containsMacro(SalesforceConstants.PROPERTY_USERNAME) && Strings.isNullOrEmpty(username)) { + collector.addFailure("Username is required for password grant type authentication.", + "Please provide the Salesforce username.") + .withConfigProperty(SalesforceConstants.PROPERTY_USERNAME); + } + if (!containsMacro(SalesforceConstants.PROPERTY_PASSWORD) && Strings.isNullOrEmpty(password)) { + collector.addFailure("Password is required for password grant type authentication.", + "Please provide the Salesforce password.") + .withConfigProperty(SalesforceConstants.PROPERTY_PASSWORD); + } + if (!containsMacro(SalesforceConstants.PROPERTY_SECURITY_TOKEN) && Strings.isNullOrEmpty(securityToken)) { + collector.addFailure("Security Token is required for password grant type authentication.", + "Please provide the Salesforce security token.") + .withConfigProperty(SalesforceConstants.PROPERTY_SECURITY_TOKEN); + } + } + } + } diff --git a/src/main/java/io/cdap/plugin/salesforce/plugin/SalesforceConnectorInfo.java b/src/main/java/io/cdap/plugin/salesforce/plugin/SalesforceConnectorInfo.java index 3228d9d4..16fc814c 100644 --- a/src/main/java/io/cdap/plugin/salesforce/plugin/SalesforceConnectorInfo.java +++ b/src/main/java/io/cdap/plugin/salesforce/plugin/SalesforceConnectorInfo.java @@ -20,6 +20,7 @@ import io.cdap.plugin.salesforce.SalesforceConnectionUtil; import io.cdap.plugin.salesforce.SalesforceConstants; import io.cdap.plugin.salesforce.authenticator.AuthenticatorCredentials; +import io.cdap.plugin.salesforce.authenticator.AuthenticatorCredentials.GrantType; import io.cdap.plugin.salesforce.plugin.connector.SalesforceConnectorConfig; import javax.annotation.Nullable; @@ -103,6 +104,14 @@ public Boolean isRetryOnBackendError() { return config.isRetryOnBackendError(); } + public GrantType getAuthenticationGrantType() { + return config.getAuthenticationGrantType(); + } + + public void validateAuthenticationFields(FailureCollector collector) { + config.validateAuthenticationFields(collector); + } + public void validate(FailureCollector collector, @Nullable OAuthInfo oAuthInfo) { try { validateConnection(oAuthInfo); @@ -149,6 +158,7 @@ public boolean canAttemptToEstablishConnection() { return !(config.containsMacro(SalesforceConstants.PROPERTY_CONSUMER_KEY) || config.containsMacro(SalesforceConstants.PROPERTY_CONSUMER_SECRET) + || config.containsMacro(SalesforceConstants.PROPERTY_AUTHENTICATION_GRANT_TYPE) || config.containsMacro(SalesforceConstants.PROPERTY_USERNAME) || config.containsMacro(SalesforceConstants.PROPERTY_PASSWORD) || config.containsMacro(SalesforceConstants.PROPERTY_LOGIN_URL) diff --git a/src/main/java/io/cdap/plugin/salesforce/plugin/connector/SalesforceConnectorConfig.java b/src/main/java/io/cdap/plugin/salesforce/plugin/connector/SalesforceConnectorConfig.java index c7e30e7c..8ac03dbb 100644 --- a/src/main/java/io/cdap/plugin/salesforce/plugin/connector/SalesforceConnectorConfig.java +++ b/src/main/java/io/cdap/plugin/salesforce/plugin/connector/SalesforceConnectorConfig.java @@ -51,9 +51,10 @@ public SalesforceConnectorConfig(@Nullable String consumerKey, @Nullable Long initialRetryDuration, @Nullable Long maxRetryDuration, @Nullable Integer maxRetryCount, - @Nullable Boolean retryOnBackendError) { + @Nullable Boolean retryOnBackendError, + @Nullable String authenticationGrantType) { super(consumerKey, consumerSecret, username, password, loginUrl, securityToken, connectTimeout, readTimeout, - proxyUrl, initialRetryDuration, maxRetryDuration, maxRetryCount, retryOnBackendError); + proxyUrl, initialRetryDuration, maxRetryDuration, maxRetryCount, retryOnBackendError, authenticationGrantType); this.oAuthInfo = oAuthInfo; } diff --git a/src/main/java/io/cdap/plugin/salesforce/plugin/sink/batch/SalesforceOutputFormatProvider.java b/src/main/java/io/cdap/plugin/salesforce/plugin/sink/batch/SalesforceOutputFormatProvider.java index 37ab156c..4917ea1c 100644 --- a/src/main/java/io/cdap/plugin/salesforce/plugin/sink/batch/SalesforceOutputFormatProvider.java +++ b/src/main/java/io/cdap/plugin/salesforce/plugin/sink/batch/SalesforceOutputFormatProvider.java @@ -64,7 +64,8 @@ public SalesforceOutputFormatProvider(SalesforceSinkConfig config) { .put(SalesforceConstants.CONFIG_MAX_RETRY_COUNT, Integer.toString(config.getConnection().getMaxRetryCount())) .put(SalesforceConstants.CONFIG_RETRY_REQUIRED, Boolean.toString(config.getConnection() - .isRetryOnBackendError())); + .isRetryOnBackendError())) + .put(SalesforceConstants.CONFIG_GRANT_TYPE, config.getConnection().getAuthenticationGrantType().getType()); if (!Strings.isNullOrEmpty(config.getConnection().getProxyUrl())) { configBuilder.put(SalesforceConstants.CONFIG_PROXY_URL, config.getConnection().getProxyUrl()); @@ -75,13 +76,15 @@ public SalesforceOutputFormatProvider(SalesforceSinkConfig config) { .put(SalesforceConstants.CONFIG_OAUTH_TOKEN, oAuthInfo.getAccessToken()) .put(SalesforceConstants.CONFIG_OAUTH_INSTANCE_URL, oAuthInfo.getInstanceURL()); } else { - configBuilder - .put(SalesforceConstants.CONFIG_USERNAME, Objects.requireNonNull(config.getConnection().getUsername())) - .put(SalesforceConstants.CONFIG_PASSWORD, Objects.requireNonNull(config.getConnection().getPassword())) - .put(SalesforceConstants.CONFIG_CONSUMER_KEY, Objects.requireNonNull(config.getConnection().getConsumerKey())) - .put(SalesforceConstants.CONFIG_CONSUMER_SECRET, Objects.requireNonNull(config.getConnection(). - getConsumerSecret())) - .put(SalesforceConstants.CONFIG_LOGIN_URL, Objects.requireNonNull(config.getConnection().getLoginUrl())); + configBuilder.put(SalesforceConstants.CONFIG_CONSUMER_KEY, + Objects.requireNonNull(config.getConnection().getConsumerKey())) + .put(SalesforceConstants.CONFIG_CONSUMER_SECRET, + Objects.requireNonNull(config.getConnection().getConsumerSecret())) + .put(SalesforceConstants.CONFIG_LOGIN_URL, Objects.requireNonNull(config.getConnection().getLoginUrl())); + if (config.getConnection().getAuthenticationGrantType() == AuthenticatorCredentials.GrantType.PASSWORD) { + configBuilder.put(SalesforceConstants.CONFIG_USERNAME, config.getConnection().getUsername()) + .put(SalesforceConstants.CONFIG_PASSWORD, config.getConnection().getPassword()); + } } if (config.getExternalIdField() != null) { diff --git a/src/main/java/io/cdap/plugin/salesforce/plugin/sink/batch/SalesforceSinkConfig.java b/src/main/java/io/cdap/plugin/salesforce/plugin/sink/batch/SalesforceSinkConfig.java index cb8e7b68..b2f24043 100644 --- a/src/main/java/io/cdap/plugin/salesforce/plugin/sink/batch/SalesforceSinkConfig.java +++ b/src/main/java/io/cdap/plugin/salesforce/plugin/sink/batch/SalesforceSinkConfig.java @@ -170,12 +170,13 @@ public SalesforceSinkConfig(String referenceName, @Nullable Long initialRetryDuration, @Nullable Long maxRetryDuration, @Nullable Integer maxRetryCount, - @Nullable Boolean retryOnBackendError) { + @Nullable Boolean retryOnBackendError, + @Nullable String authenticationGrantType) { super(referenceName); connection = new SalesforceConnectorConfig(clientId, clientSecret, username, password, loginUrl, securityToken, connectTimeout, readTimeout, oAuthInfo, proxyUrl, initialRetryDuration, maxRetryDuration, maxRetryCount, - retryOnBackendError); + retryOnBackendError, authenticationGrantType); this.sObject = sObject; this.operation = operation; this.externalIdField = externalIdField; diff --git a/src/main/java/io/cdap/plugin/salesforce/plugin/source/batch/SalesforceBaseSourceConfig.java b/src/main/java/io/cdap/plugin/salesforce/plugin/source/batch/SalesforceBaseSourceConfig.java index 306186e1..bc2f690a 100644 --- a/src/main/java/io/cdap/plugin/salesforce/plugin/source/batch/SalesforceBaseSourceConfig.java +++ b/src/main/java/io/cdap/plugin/salesforce/plugin/source/batch/SalesforceBaseSourceConfig.java @@ -133,12 +133,13 @@ protected SalesforceBaseSourceConfig(String referenceName, @Nullable Long maxRetryDuration, @Nullable Integer maxRetryCount, @Nullable Boolean retryOnBackendError, - @Nullable String proxyUrl) { + @Nullable String proxyUrl, + @Nullable String authenticationGrantType) { super(referenceName); this.connection = new SalesforceConnectorConfig(consumerKey, consumerSecret, username, password, loginUrl, securityToken, connectTimeout, readTimeout, oAuthInfo, proxyUrl, initialRetryDuration, maxRetryDuration, maxRetryCount, - retryOnBackendError); + retryOnBackendError, authenticationGrantType); this.datetimeAfter = datetimeAfter; this.datetimeBefore = datetimeBefore; this.duration = duration; diff --git a/src/main/java/io/cdap/plugin/salesforce/plugin/source/batch/SalesforceInputFormatProvider.java b/src/main/java/io/cdap/plugin/salesforce/plugin/source/batch/SalesforceInputFormatProvider.java index ea4e9019..3bb0d187 100644 --- a/src/main/java/io/cdap/plugin/salesforce/plugin/source/batch/SalesforceInputFormatProvider.java +++ b/src/main/java/io/cdap/plugin/salesforce/plugin/source/batch/SalesforceInputFormatProvider.java @@ -21,6 +21,7 @@ import com.google.gson.Gson; import io.cdap.cdap.api.data.batch.InputFormatProvider; import io.cdap.plugin.salesforce.SalesforceConstants; +import io.cdap.plugin.salesforce.authenticator.AuthenticatorCredentials; import io.cdap.plugin.salesforce.plugin.OAuthInfo; import io.cdap.plugin.salesforce.plugin.source.batch.util.SalesforceSourceConstants; @@ -51,7 +52,8 @@ public SalesforceInputFormatProvider(SalesforceBaseSourceConfig config, config.getConnection().getInitialRetryDuration().toString()) .put(SalesforceConstants.CONFIG_MAX_RETRY_DURATION, config.getConnection().getMaxRetryDuration().toString()) .put(SalesforceConstants.CONFIG_MAX_RETRY_COUNT, config.getConnection().getMaxRetryCount().toString()) - .put(SalesforceConstants.CONFIG_RETRY_REQUIRED, config.getConnection().isRetryOnBackendError().toString()); + .put(SalesforceConstants.CONFIG_RETRY_REQUIRED, config.getConnection().isRetryOnBackendError().toString()) + .put(SalesforceConstants.CONFIG_GRANT_TYPE, config.getConnection().getAuthenticationGrantType().getType()); if (!Strings.isNullOrEmpty(config.getConnection().getProxyUrl())) { configBuilder.put(SalesforceConstants.CONFIG_PROXY_URL, config.getConnection().getProxyUrl()); @@ -62,13 +64,15 @@ public SalesforceInputFormatProvider(SalesforceBaseSourceConfig config, .put(SalesforceConstants.CONFIG_OAUTH_TOKEN, oAuthInfo.getAccessToken()) .put(SalesforceConstants.CONFIG_OAUTH_INSTANCE_URL, oAuthInfo.getInstanceURL()); } else { - configBuilder - .put(SalesforceConstants.CONFIG_USERNAME, Objects.requireNonNull(config.getConnection().getUsername())) - .put(SalesforceConstants.CONFIG_PASSWORD, Objects.requireNonNull(config.getConnection().getPassword())) - .put(SalesforceConstants.CONFIG_CONSUMER_KEY, Objects.requireNonNull(config.getConnection().getConsumerKey())) - .put(SalesforceConstants.CONFIG_CONSUMER_SECRET, Objects.requireNonNull(config.getConnection(). - getConsumerSecret())) - .put(SalesforceConstants.CONFIG_LOGIN_URL, Objects.requireNonNull(config.getConnection().getLoginUrl())); + configBuilder.put(SalesforceConstants.CONFIG_CONSUMER_KEY, + Objects.requireNonNull(config.getConnection().getConsumerKey())) + .put(SalesforceConstants.CONFIG_CONSUMER_SECRET, + Objects.requireNonNull(config.getConnection().getConsumerSecret())) + .put(SalesforceConstants.CONFIG_LOGIN_URL, Objects.requireNonNull(config.getConnection().getLoginUrl())); + if (config.getConnection().getAuthenticationGrantType() == AuthenticatorCredentials.GrantType.PASSWORD) { + configBuilder.put(SalesforceConstants.CONFIG_USERNAME, config.getConnection().getUsername()) + .put(SalesforceConstants.CONFIG_PASSWORD, config.getConnection().getPassword()); + } } if (sObjectNameField != null) { configBuilder.put(SalesforceSourceConstants.CONFIG_SOBJECT_NAME_FIELD, sObjectNameField); diff --git a/src/main/java/io/cdap/plugin/salesforce/plugin/source/batch/SalesforceMultiSourceConfig.java b/src/main/java/io/cdap/plugin/salesforce/plugin/source/batch/SalesforceMultiSourceConfig.java index 4d07a38d..56dd1b35 100644 --- a/src/main/java/io/cdap/plugin/salesforce/plugin/source/batch/SalesforceMultiSourceConfig.java +++ b/src/main/java/io/cdap/plugin/salesforce/plugin/source/batch/SalesforceMultiSourceConfig.java @@ -90,10 +90,11 @@ public SalesforceMultiSourceConfig(String referenceName, @Nullable Long maxRetryDuration, @Nullable Integer maxRetryCount, Boolean retryOnBackendError, - @Nullable String proxyUrl) { + @Nullable String proxyUrl, + @Nullable String authenticationGrantType) { super(referenceName, consumerKey, consumerSecret, username, password, loginUrl, connectTimeout, readTimeout, datetimeAfter, datetimeBefore, duration, offset, securityToken, oAuthInfo, operation, initialRetryDuration, - maxRetryDuration, maxRetryCount, retryOnBackendError, proxyUrl); + maxRetryDuration, maxRetryCount, retryOnBackendError, proxyUrl, authenticationGrantType); this.whiteList = whiteList; this.blackList = blackList; this.sObjectNameField = sObjectNameField; diff --git a/src/main/java/io/cdap/plugin/salesforce/plugin/source/batch/SalesforceMultiSourceConfigBuilder.java b/src/main/java/io/cdap/plugin/salesforce/plugin/source/batch/SalesforceMultiSourceConfigBuilder.java index 1b90b518..8908c89b 100644 --- a/src/main/java/io/cdap/plugin/salesforce/plugin/source/batch/SalesforceMultiSourceConfigBuilder.java +++ b/src/main/java/io/cdap/plugin/salesforce/plugin/source/batch/SalesforceMultiSourceConfigBuilder.java @@ -45,6 +45,7 @@ public class SalesforceMultiSourceConfigBuilder { private Integer readTimeout; private String proxyUrl; private Boolean retryOnBackendError; + private String authenticationGrantType; public SalesforceMultiSourceConfigBuilder setReferenceName(String referenceName) { this.referenceName = referenceName; @@ -156,11 +157,20 @@ public SalesforceMultiSourceConfigBuilder setProxyUrl(String proxyUrl) { return this; } + public String getAuthenticationGrantType() { + return authenticationGrantType; + } + + public SalesforceMultiSourceConfigBuilder setAuthenticationGrantType(String authenticationGrantType) { + this.authenticationGrantType = authenticationGrantType; + return this; + } + public SalesforceMultiSourceConfig build() { return new SalesforceMultiSourceConfig(referenceName, consumerKey, consumerSecret, username, password, loginUrl, connectTimeout, readTimeout, datetimeAfter, datetimeBefore, duration, offset, whiteList, blackList, sObjectNameField, securityToken, oAuthInfo, operation, initialRetryDuration, maxRetryDuration, maxRetryCount, - retryOnBackendError, proxyUrl); + retryOnBackendError, proxyUrl, authenticationGrantType); } } diff --git a/src/main/java/io/cdap/plugin/salesforce/plugin/source/batch/SalesforceSourceConfig.java b/src/main/java/io/cdap/plugin/salesforce/plugin/source/batch/SalesforceSourceConfig.java index a828a626..95081832 100644 --- a/src/main/java/io/cdap/plugin/salesforce/plugin/source/batch/SalesforceSourceConfig.java +++ b/src/main/java/io/cdap/plugin/salesforce/plugin/source/batch/SalesforceSourceConfig.java @@ -112,10 +112,11 @@ public SalesforceSourceConfig(String referenceName, @Nullable Boolean enablePKChunk, @Nullable Integer chunkSize, @Nullable String parent, - @Nullable String proxyUrl) { + @Nullable String proxyUrl, + @Nullable String authenticationGrantType) { super(referenceName, consumerKey, consumerSecret, username, password, loginUrl, connectTimeout, readTimeout, datetimeAfter, datetimeBefore, duration, offset, securityToken, oAuthInfo, operation, initialRetryDuration, - maxRetryDuration, maxRetryCount, retryOnBackendError, proxyUrl); + maxRetryDuration, maxRetryCount, retryOnBackendError, proxyUrl, authenticationGrantType); this.query = query; this.sObjectName = sObjectName; this.schema = schema; diff --git a/src/main/java/io/cdap/plugin/salesforce/plugin/source/batch/SalesforceSourceConfigBuilder.java b/src/main/java/io/cdap/plugin/salesforce/plugin/source/batch/SalesforceSourceConfigBuilder.java index 8013f0e3..f7e40a46 100644 --- a/src/main/java/io/cdap/plugin/salesforce/plugin/source/batch/SalesforceSourceConfigBuilder.java +++ b/src/main/java/io/cdap/plugin/salesforce/plugin/source/batch/SalesforceSourceConfigBuilder.java @@ -45,6 +45,7 @@ public class SalesforceSourceConfigBuilder { private Integer readTimeout; private String proxyUrl; private Boolean retryOnBackendError; + private String authenticationGrantType; public SalesforceSourceConfigBuilder setReferenceName(String referenceName) { this.referenceName = referenceName; @@ -166,11 +167,20 @@ public SalesforceSourceConfigBuilder setProxyUrl(String proxyUrl) { return this; } + public String getAuthenticationGrantType() { + return authenticationGrantType; + } + + public SalesforceSourceConfigBuilder setAuthenticationGrantType(String authenticationGrantType) { + this.authenticationGrantType = authenticationGrantType; + return this; + } + public SalesforceSourceConfig build() { return new SalesforceSourceConfig(referenceName, consumerKey, consumerSecret, username, password, loginUrl, connectTimeout, readTimeout, query, sObjectName, datetimeAfter, datetimeBefore, duration, offset, schema, securityToken, operation, initialRetryDuration, maxRetryDuration, maxRetryCount, retryOnBackendError, null, enablePKChunk, - chunkSize, parent, proxyUrl); + chunkSize, parent, proxyUrl, authenticationGrantType); } } diff --git a/src/main/java/io/cdap/plugin/salesforce/plugin/source/streaming/SalesforceStreamingSourceConfig.java b/src/main/java/io/cdap/plugin/salesforce/plugin/source/streaming/SalesforceStreamingSourceConfig.java index 15df4f7d..839c1721 100644 --- a/src/main/java/io/cdap/plugin/salesforce/plugin/source/streaming/SalesforceStreamingSourceConfig.java +++ b/src/main/java/io/cdap/plugin/salesforce/plugin/source/streaming/SalesforceStreamingSourceConfig.java @@ -153,14 +153,15 @@ public SalesforceStreamingSourceConfig(String referenceName, @Nullable Long initialRetryDuration, @Nullable Long maxRetryDuration, @Nullable Integer maxRetryCount, - @Nullable Boolean retryOnBackendError) { + @Nullable Boolean retryOnBackendError, + @Nullable String authenticationGrantType) { super(referenceName); this.pushTopicName = pushTopicName; this.sObjectName = sObjectName; this.connection = new SalesforceConnectorConfig(consumerKey, consumerSecret, username, password, loginUrl, securityToken, connectTimeout, readTimeout, oAuthInfo, proxyUrl, initialRetryDuration, maxRetryDuration, maxRetryCount, - retryOnBackendError); + retryOnBackendError, authenticationGrantType); this.schema = schema; } diff --git a/src/test/java/io/cdap/plugin/salesforce/etl/BaseSalesforceBatchSinkETLTest.java b/src/test/java/io/cdap/plugin/salesforce/etl/BaseSalesforceBatchSinkETLTest.java index 39760add..b9f1ba65 100644 --- a/src/test/java/io/cdap/plugin/salesforce/etl/BaseSalesforceBatchSinkETLTest.java +++ b/src/test/java/io/cdap/plugin/salesforce/etl/BaseSalesforceBatchSinkETLTest.java @@ -235,6 +235,6 @@ protected SalesforceSinkConfig getDefaultConfig(String sObject) { Long.parseLong(INITIAL_RETRY_DURATION), Long.parseLong(MAX_RETRY_DURATION), Integer.parseInt(MAX_RETRY_COUNT), - Boolean.parseBoolean(RETRY_ON_BACKEND_ERROR)); + Boolean.parseBoolean(RETRY_ON_BACKEND_ERROR), null); } } diff --git a/src/test/java/io/cdap/plugin/salesforce/etl/SalesforceBatchSinkSchemaValidation.java b/src/test/java/io/cdap/plugin/salesforce/etl/SalesforceBatchSinkSchemaValidation.java index 8678830e..6de9504e 100644 --- a/src/test/java/io/cdap/plugin/salesforce/etl/SalesforceBatchSinkSchemaValidation.java +++ b/src/test/java/io/cdap/plugin/salesforce/etl/SalesforceBatchSinkSchemaValidation.java @@ -68,7 +68,7 @@ public void testInputSchemaValidation() throws Exception { "1000000", "10000", "Fail on Error", BaseSalesforceETLTest.SECURITY_TOKEN, - null, null, true, 5L, 10L, 5, true)); + null, null, true, 5L, 10L, 5, true, null)); Schema schema = Schema.recordOf("output", Schema.Field.of("Name", Schema.of(Schema.Type.STRING)), Schema.Field.of("StageName", Schema.of(Schema.Type.STRING)), @@ -138,7 +138,7 @@ public void testInputSchemaValidationWithDifferentFields() throws Exception { "1000000", "10000", "Fail on Error", BaseSalesforceETLTest.SECURITY_TOKEN, - null, null, true, 5L, 10L, 5, true)); + null, null, true, 5L, 10L, 5, true, null)); Schema schema = Schema.recordOf("output", Schema.Field.of("Name", Schema.of(Schema.Type.STRING))); Field field = new Field(); diff --git a/src/test/java/io/cdap/plugin/salesforce/plugin/SalesforceConnectorBaseConfigTest.java b/src/test/java/io/cdap/plugin/salesforce/plugin/SalesforceConnectorBaseConfigTest.java new file mode 100644 index 00000000..9dda627e --- /dev/null +++ b/src/test/java/io/cdap/plugin/salesforce/plugin/SalesforceConnectorBaseConfigTest.java @@ -0,0 +1,250 @@ +/* + * Copyright © 2026 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.salesforce.plugin; + +import io.cdap.cdap.etl.api.validation.CauseAttributes; +import io.cdap.cdap.etl.api.validation.ValidationFailure; +import io.cdap.cdap.etl.mock.validation.MockFailureCollector; +import io.cdap.plugin.salesforce.SalesforceConstants; +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Tests for {@link SalesforceConnectorBaseConfig#validateAuthenticationFields}. + */ +public class SalesforceConnectorBaseConfigTest { + + private static final String VALID_CONSUMER_KEY = "testConsumerKey"; + private static final String VALID_CONSUMER_SECRET = "testConsumerSecret"; + private static final String VALID_USERNAME = "testUser"; + private static final String VALID_PASSWORD = "testPassword"; + private static final String VALID_SECURITY_TOKEN = "testToken"; + private static final String VALID_LOGIN_URL = "https://login.salesforce.com/services/oauth2/token"; + + // --- PASSWORD grant type tests --- + + @Test + public void testPasswordGrantAllFieldsValid() { + SalesforceConnectorBaseConfig config = new SalesforceConnectorBaseConfig( + VALID_CONSUMER_KEY, VALID_CONSUMER_SECRET, VALID_USERNAME, VALID_PASSWORD, + VALID_LOGIN_URL, VALID_SECURITY_TOKEN, null, null, null, null, null, null, null, "password"); + MockFailureCollector collector = new MockFailureCollector(); + config.validateAuthenticationFields(collector); + Assert.assertEquals(0, collector.getValidationFailures().size()); + } + + @Test + public void testPasswordGrantMissingConsumerKey() { + SalesforceConnectorBaseConfig config = new SalesforceConnectorBaseConfig( + null, VALID_CONSUMER_SECRET, VALID_USERNAME, VALID_PASSWORD, + VALID_LOGIN_URL, VALID_SECURITY_TOKEN, null, null, null, null, null, null, null, "password"); + MockFailureCollector collector = new MockFailureCollector(); + config.validateAuthenticationFields(collector); + assertSingleFailureOnField(collector, SalesforceConstants.PROPERTY_CONSUMER_KEY, + "Consumer Key is required for authentication."); + } + + @Test + public void testPasswordGrantEmptyConsumerKey() { + SalesforceConnectorBaseConfig config = new SalesforceConnectorBaseConfig( + "", VALID_CONSUMER_SECRET, VALID_USERNAME, VALID_PASSWORD, + VALID_LOGIN_URL, VALID_SECURITY_TOKEN, null, null, null, null, null, null, null, "password"); + MockFailureCollector collector = new MockFailureCollector(); + config.validateAuthenticationFields(collector); + assertSingleFailureOnField(collector, SalesforceConstants.PROPERTY_CONSUMER_KEY, + "Consumer Key is required for authentication."); + } + + @Test + public void testPasswordGrantMissingConsumerSecret() { + SalesforceConnectorBaseConfig config = new SalesforceConnectorBaseConfig( + VALID_CONSUMER_KEY, null, VALID_USERNAME, VALID_PASSWORD, + VALID_LOGIN_URL, VALID_SECURITY_TOKEN, null, null, null, null, null, null, null, "password"); + MockFailureCollector collector = new MockFailureCollector(); + config.validateAuthenticationFields(collector); + assertSingleFailureOnField(collector, SalesforceConstants.PROPERTY_CONSUMER_SECRET, + "Consumer Secret is required for authentication."); + } + + @Test + public void testPasswordGrantMissingLoginUrl() { + SalesforceConnectorBaseConfig config = new SalesforceConnectorBaseConfig( + VALID_CONSUMER_KEY, VALID_CONSUMER_SECRET, VALID_USERNAME, VALID_PASSWORD, + null, VALID_SECURITY_TOKEN, null, null, null, null, null, null, null, "password"); + MockFailureCollector collector = new MockFailureCollector(); + config.validateAuthenticationFields(collector); + assertSingleFailureOnField(collector, SalesforceConstants.PROPERTY_LOGIN_URL, + "Login URL is required for authentication."); + } + + @Test + public void testPasswordGrantMissingUsername() { + SalesforceConnectorBaseConfig config = new SalesforceConnectorBaseConfig( + VALID_CONSUMER_KEY, VALID_CONSUMER_SECRET, null, VALID_PASSWORD, + VALID_LOGIN_URL, VALID_SECURITY_TOKEN, null, null, null, null, null, null, null, "password"); + MockFailureCollector collector = new MockFailureCollector(); + config.validateAuthenticationFields(collector); + assertSingleFailureOnField(collector, SalesforceConstants.PROPERTY_USERNAME, + "Username is required for password grant type authentication."); + } + + @Test + public void testPasswordGrantMissingPassword() { + SalesforceConnectorBaseConfig config = new SalesforceConnectorBaseConfig( + VALID_CONSUMER_KEY, VALID_CONSUMER_SECRET, VALID_USERNAME, null, + VALID_LOGIN_URL, VALID_SECURITY_TOKEN, null, null, null, null, null, null, null, "password"); + MockFailureCollector collector = new MockFailureCollector(); + config.validateAuthenticationFields(collector); + assertSingleFailureOnField(collector, SalesforceConstants.PROPERTY_PASSWORD, + "Password is required for password grant type authentication."); + } + + @Test + public void testPasswordGrantMissingSecurityToken() { + SalesforceConnectorBaseConfig config = new SalesforceConnectorBaseConfig( + VALID_CONSUMER_KEY, VALID_CONSUMER_SECRET, VALID_USERNAME, VALID_PASSWORD, + VALID_LOGIN_URL, null, null, null, null, null, null, null, null, "password"); + MockFailureCollector collector = new MockFailureCollector(); + config.validateAuthenticationFields(collector); + assertSingleFailureOnField(collector, SalesforceConstants.PROPERTY_SECURITY_TOKEN, + "Security Token is required for password grant type authentication."); + } + + @Test + public void testPasswordGrantMissingMultipleFields() { + SalesforceConnectorBaseConfig config = new SalesforceConnectorBaseConfig( + null, null, null, null, + null, null, null, null, null, null, null, null, null, "password"); + MockFailureCollector collector = new MockFailureCollector(); + config.validateAuthenticationFields(collector); + List failedFields = collector.getValidationFailures().stream() + .map(f -> f.getCauses().get(0).getAttribute(CauseAttributes.STAGE_CONFIG)) + .collect(Collectors.toList()); + Assert.assertEquals(6, collector.getValidationFailures().size()); + Assert.assertTrue(failedFields.contains(SalesforceConstants.PROPERTY_CONSUMER_KEY)); + Assert.assertTrue(failedFields.contains(SalesforceConstants.PROPERTY_CONSUMER_SECRET)); + Assert.assertTrue(failedFields.contains(SalesforceConstants.PROPERTY_LOGIN_URL)); + Assert.assertTrue(failedFields.contains(SalesforceConstants.PROPERTY_USERNAME)); + Assert.assertTrue(failedFields.contains(SalesforceConstants.PROPERTY_PASSWORD)); + Assert.assertTrue(failedFields.contains(SalesforceConstants.PROPERTY_SECURITY_TOKEN)); + } + + // --- CLIENT_CREDENTIALS grant type tests --- + + @Test + public void testClientCredentialsGrantAllFieldsValid() { + SalesforceConnectorBaseConfig config = new SalesforceConnectorBaseConfig( + VALID_CONSUMER_KEY, VALID_CONSUMER_SECRET, null, null, + VALID_LOGIN_URL, null, null, null, null, null, null, null, null, "client_credentials"); + MockFailureCollector collector = new MockFailureCollector(); + config.validateAuthenticationFields(collector); + Assert.assertEquals(0, collector.getValidationFailures().size()); + } + + @Test + public void testClientCredentialsGrantMissingConsumerKey() { + SalesforceConnectorBaseConfig config = new SalesforceConnectorBaseConfig( + null, VALID_CONSUMER_SECRET, null, null, + VALID_LOGIN_URL, null, null, null, null, null, null, null, null, "client_credentials"); + MockFailureCollector collector = new MockFailureCollector(); + config.validateAuthenticationFields(collector); + assertSingleFailureOnField(collector, SalesforceConstants.PROPERTY_CONSUMER_KEY, + "Consumer Key is required for authentication."); + } + + @Test + public void testClientCredentialsGrantMissingConsumerSecret() { + SalesforceConnectorBaseConfig config = new SalesforceConnectorBaseConfig( + VALID_CONSUMER_KEY, null, null, null, + VALID_LOGIN_URL, null, null, null, null, null, null, null, null, "client_credentials"); + MockFailureCollector collector = new MockFailureCollector(); + config.validateAuthenticationFields(collector); + assertSingleFailureOnField(collector, SalesforceConstants.PROPERTY_CONSUMER_SECRET, + "Consumer Secret is required for authentication."); + } + + @Test + public void testClientCredentialsGrantMissingLoginUrl() { + SalesforceConnectorBaseConfig config = new SalesforceConnectorBaseConfig( + VALID_CONSUMER_KEY, VALID_CONSUMER_SECRET, null, null, + null, null, null, null, null, null, null, null, null, "client_credentials"); + MockFailureCollector collector = new MockFailureCollector(); + config.validateAuthenticationFields(collector); + assertSingleFailureOnField(collector, SalesforceConstants.PROPERTY_LOGIN_URL, + "Login URL is required for authentication."); + } + + @Test + public void testClientCredentialsGrantDoesNotRequireUsername() { + SalesforceConnectorBaseConfig config = new SalesforceConnectorBaseConfig( + VALID_CONSUMER_KEY, VALID_CONSUMER_SECRET, null, null, + VALID_LOGIN_URL, null, null, null, null, null, null, null, null, "client_credentials"); + MockFailureCollector collector = new MockFailureCollector(); + config.validateAuthenticationFields(collector); + List failedFields = collector.getValidationFailures().stream() + .map(f -> f.getCauses().get(0).getAttribute(CauseAttributes.STAGE_CONFIG)) + .collect(Collectors.toList()); + Assert.assertFalse(failedFields.contains(SalesforceConstants.PROPERTY_USERNAME)); + Assert.assertFalse(failedFields.contains(SalesforceConstants.PROPERTY_PASSWORD)); + Assert.assertFalse(failedFields.contains(SalesforceConstants.PROPERTY_SECURITY_TOKEN)); + } + + @Test + public void testClientCredentialsGrantMissingMultipleFields() { + SalesforceConnectorBaseConfig config = new SalesforceConnectorBaseConfig( + null, null, null, null, + null, null, null, null, null, null, null, null, null, "client_credentials"); + MockFailureCollector collector = new MockFailureCollector(); + config.validateAuthenticationFields(collector); + List failedFields = collector.getValidationFailures().stream() + .map(f -> f.getCauses().get(0).getAttribute(CauseAttributes.STAGE_CONFIG)) + .collect(Collectors.toList()); + Assert.assertEquals(3, collector.getValidationFailures().size()); + Assert.assertTrue(failedFields.contains(SalesforceConstants.PROPERTY_CONSUMER_KEY)); + Assert.assertTrue(failedFields.contains(SalesforceConstants.PROPERTY_CONSUMER_SECRET)); + Assert.assertTrue(failedFields.contains(SalesforceConstants.PROPERTY_LOGIN_URL)); + } + + // --- Default grant type (null defaults to PASSWORD) --- + + @Test + public void testDefaultGrantTypeIsPassword() { + SalesforceConnectorBaseConfig config = new SalesforceConnectorBaseConfig( + VALID_CONSUMER_KEY, VALID_CONSUMER_SECRET, null, null, + VALID_LOGIN_URL, null, null, null, null, null, null, null, null, null); + MockFailureCollector collector = new MockFailureCollector(); + config.validateAuthenticationFields(collector); + List failedFields = collector.getValidationFailures().stream() + .map(f -> f.getCauses().get(0).getAttribute(CauseAttributes.STAGE_CONFIG)) + .collect(Collectors.toList()); + // null grant type defaults to PASSWORD, so username/password/securityToken should be required + Assert.assertTrue(failedFields.contains(SalesforceConstants.PROPERTY_USERNAME)); + Assert.assertTrue(failedFields.contains(SalesforceConstants.PROPERTY_PASSWORD)); + Assert.assertTrue(failedFields.contains(SalesforceConstants.PROPERTY_SECURITY_TOKEN)); + } + + private void assertSingleFailureOnField(MockFailureCollector collector, String expectedField, + String expectedMessage) { + List failures = collector.getValidationFailures(); + Assert.assertEquals(1, failures.size()); + Assert.assertEquals(expectedField, + failures.get(0).getCauses().get(0).getAttribute(CauseAttributes.STAGE_CONFIG)); + Assert.assertEquals(expectedMessage, failures.get(0).getMessage()); + } +} diff --git a/src/test/java/io/cdap/plugin/salesforce/plugin/source/batch/SalesforceSourceConfigTest.java b/src/test/java/io/cdap/plugin/salesforce/plugin/source/batch/SalesforceSourceConfigTest.java index 49230fb4..1fd4b770 100644 --- a/src/test/java/io/cdap/plugin/salesforce/plugin/source/batch/SalesforceSourceConfigTest.java +++ b/src/test/java/io/cdap/plugin/salesforce/plugin/source/batch/SalesforceSourceConfigTest.java @@ -287,7 +287,7 @@ private void testPKChunkInvalidConfig(SalesforceSourceConfig config, String stag Mockito.any(), Mockito.any(), Mockito.any(), Mockito.anyString(), Mockito.anyLong(), Mockito.anyLong(), Mockito.anyInt(), - Mockito.anyBoolean()) + Mockito.anyBoolean(), Mockito.anyString()) .thenReturn(connectorConfig); SalesforceConnectorInfo salesforceConnectorInfo = new SalesforceConnectorInfo(null, connectorConfig, diff --git a/widgets/Salesforce-batchsink.json b/widgets/Salesforce-batchsink.json index 4c76f186..b03fca50 100644 --- a/widgets/Salesforce-batchsink.json +++ b/widgets/Salesforce-batchsink.json @@ -45,6 +45,25 @@ "label": "OAuth Information", "name": "oAuthInfo" }, + { + "name": "authenticationGrantType", + "label": "OAuth Grant Type", + "widget-type": "radio-group", + "widget-attributes": { + "layout": "inline", + "default": "password", + "options": [ + { + "id": "password", + "label": "Password" + }, + { + "id": "client_credentials", + "label": "Client Credentials" + } + ] + } + }, { "widget-type": "textbox", "label": "Username", @@ -266,6 +285,26 @@ } ], "filters": [ + { + "name": "GrantTypePassword", + "condition": { + "expression": "authenticationGrantType != 'client_credentials'" + }, + "show": [ + { + "type": "property", + "name": "username" + }, + { + "type": "property", + "name": "password" + }, + { + "type": "property", + "name": "securityToken" + } + ] + }, { "name": "showConnectionProperties ", "condition": { diff --git a/widgets/Salesforce-batchsource.json b/widgets/Salesforce-batchsource.json index 822729b6..b8305ff6 100644 --- a/widgets/Salesforce-batchsource.json +++ b/widgets/Salesforce-batchsource.json @@ -45,6 +45,25 @@ "label": "OAuth Information", "name": "oAuthInfo" }, + { + "name": "authenticationGrantType", + "label": "OAuth Grant Type", + "widget-type": "radio-group", + "widget-attributes": { + "layout": "inline", + "default": "password", + "options": [ + { + "id": "password", + "label": "Password" + }, + { + "id": "client_credentials", + "label": "Client Credentials" + } + ] + } + }, { "widget-type": "textbox", "label": "Username", @@ -307,6 +326,26 @@ } ], "filters":[ + { + "name": "GrantTypePassword", + "condition": { + "expression": "authenticationGrantType != 'client_credentials'" + }, + "show": [ + { + "type": "property", + "name": "username" + }, + { + "type": "property", + "name": "password" + }, + { + "type": "property", + "name": "securityToken" + } + ] + }, { "name": "enablePKChunk", "condition": { diff --git a/widgets/Salesforce-connector.json b/widgets/Salesforce-connector.json index ebe41ee0..def56bf8 100644 --- a/widgets/Salesforce-connector.json +++ b/widgets/Salesforce-connector.json @@ -12,6 +12,25 @@ "label": "OAuth Information", "name": "oAuthInfo" }, + { + "name": "authenticationGrantType", + "label": "OAuth Grant Type", + "widget-type": "radio-group", + "widget-attributes": { + "layout": "inline", + "default": "password", + "options": [ + { + "id": "password", + "label": "Password" + }, + { + "id": "client_credentials", + "label": "Client Credentials" + } + ] + } + }, { "widget-type": "textbox", "label": "User Name", @@ -138,5 +157,26 @@ } ], "outputs": [], - "filters": [] -} \ No newline at end of file + "filters":[ + { + "name": "GrantTypePassword", + "condition": { + "expression": "authenticationGrantType != 'client_credentials'" + }, + "show": [ + { + "type": "property", + "name": "username" + }, + { + "type": "property", + "name": "password" + }, + { + "type": "property", + "name": "securityToken" + } + ] + } + ] +} diff --git a/widgets/Salesforce-streamingsource.json b/widgets/Salesforce-streamingsource.json index 547dfa17..dd22df0a 100644 --- a/widgets/Salesforce-streamingsource.json +++ b/widgets/Salesforce-streamingsource.json @@ -45,6 +45,25 @@ "label": "OAuth Information", "name": "oAuthInfo" }, + { + "name": "authenticationGrantType", + "label": "OAuth Grant Type", + "widget-type": "radio-group", + "widget-attributes": { + "layout": "inline", + "default": "password", + "options": [ + { + "id": "password", + "label": "Password" + }, + { + "id": "client_credentials", + "label": "Client Credentials" + } + ] + } + }, { "widget-type": "textbox", "label": "Username", @@ -240,7 +259,28 @@ ] } ], - "filters": [], + "filters":[ + { + "name": "GrantTypePassword", + "condition": { + "expression": "authenticationGrantType != 'client_credentials'" + }, + "show": [ + { + "type": "property", + "name": "username" + }, + { + "type": "property", + "name": "password" + }, + { + "type": "property", + "name": "securityToken" + } + ] + } + ], "outputs": [ { "name": "schema", diff --git a/widgets/SalesforceMultiObjects-batchsource.json b/widgets/SalesforceMultiObjects-batchsource.json index d22ee64b..13dc9fbd 100644 --- a/widgets/SalesforceMultiObjects-batchsource.json +++ b/widgets/SalesforceMultiObjects-batchsource.json @@ -45,6 +45,25 @@ "label": "OAuth Information", "name": "oAuthInfo" }, + { + "name": "authenticationGrantType", + "label": "OAuth Grant Type", + "widget-type": "radio-group", + "widget-attributes": { + "layout": "inline", + "default": "password", + "options": [ + { + "id": "password", + "label": "Password" + }, + { + "id": "client_credentials", + "label": "Client Credentials" + } + ] + } + }, { "widget-type": "textbox", "label": "Username", @@ -270,6 +289,26 @@ } ], "filters": [ + { + "name": "GrantTypePassword", + "condition": { + "expression": "authenticationGrantType != 'client_credentials'" + }, + "show": [ + { + "type": "property", + "name": "username" + }, + { + "type": "property", + "name": "password" + }, + { + "type": "property", + "name": "securityToken" + } + ] + }, { "name": "showConnectionProperties ", "condition": {