Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Added
- Added connection property `OAuthWebServerTimeout` to configure the OAuth browser authentication timeout for U2M (user-to-machine) flows, and also updated hardcoded 1-hour timeout to default 120 seconds timeout.
- Added connection property `EnableOAuthSecretFromPwd` to allow reading the OAuth client secret from the `PWD`/`password` property instead of `OAuth2Secret`. This enables BI tools to mask the secret using their built-in password field handling. When enabled, it will only be read from `PWD`/`password`.

### Updated

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -340,9 +340,25 @@ public List<String> getOAuthScopesForU2M() throws DatabricksParsingException {

@Override
public String getClientSecret() {
if (isOAuthSecretFromPwdEnabled()) {
String pwdSecret =
getParameter(DatabricksJdbcUrlParams.PWD, getParameter(DatabricksJdbcUrlParams.PASSWORD));
if (pwdSecret == null) {
throw new DatabricksDriverException(
"EnableOAuthSecretFromPwd is enabled but no PWD/password property was provided."
+ " Set the OAuth client secret via the PWD or password connection property.",
DatabricksDriverErrorCode.INPUT_VALIDATION_ERROR);
}
return pwdSecret;
}
return getParameter(DatabricksJdbcUrlParams.CLIENT_SECRET);
}

@Override
public boolean isOAuthSecretFromPwdEnabled() {
return getParameter(DatabricksJdbcUrlParams.ENABLE_OAUTH_SECRET_FROM_PWD).equals("1");
}

@Override
public String getGoogleServiceAccount() {
return getParameter(DatabricksJdbcUrlParams.GOOGLE_SERVICE_ACCOUNT);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -468,4 +468,7 @@ public interface IDatabricksConnectionContext {
* @return the link prefetch window size (default: 128)
*/
int getLinkPrefetchWindow();

/** Returns whether the driver should read OAuth secret from PWD/password property. */
boolean isOAuthSecretFromPwdEnabled();
}
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,9 @@ public enum DatabricksJdbcUrlParams {
NON_ROWCOUNT_QUERY_PREFIXES(
"NonRowcountQueryPrefixes",
"Comma-separated list of query prefixes (like INSERT,UPDATE,DELETE) that should return result sets instead of row counts",
"");
""),
ENABLE_OAUTH_SECRET_FROM_PWD(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this gated though, can we add this to telemetry if you still feel it should be gated (in order to monitor and deprecate this later)

"EnableOAuthSecretFromPwd", "Read OAuth secret/token from PWD/password property", "0");

private final String paramName;
private final String defaultValue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,11 @@ public static List<DriverPropertyInfo> buildMissingPropertiesList(
case TOKEN_PASSTHROUGH:
if (connectionContext.getOAuthRefreshToken() != null) {
addMissingProperty(missingPropertyInfos, connectionContext, CLIENT_ID, true);
addMissingProperty(missingPropertyInfos, connectionContext, CLIENT_SECRET, true);
if (!(connectionContext.isOAuthSecretFromPwdEnabled()
&& (connectionContext.isPropertyPresent(PWD)
|| connectionContext.isPropertyPresent(PASSWORD)))) {
addMissingProperty(missingPropertyInfos, connectionContext, CLIENT_SECRET, true);
}
handleTokenEndpointAndDiscoveryMode(missingPropertyInfos, connectionContext);
} else {
addMissingProperty(
Expand All @@ -149,7 +153,11 @@ public static List<DriverPropertyInfo> buildMissingPropertiesList(
} else if (connectionContext.getCloud() == Cloud.AZURE) {
addMissingProperty(missingPropertyInfos, connectionContext, AZURE_TENANT_ID, false);
}
addMissingProperty(missingPropertyInfos, connectionContext, CLIENT_SECRET, true);
if (!(connectionContext.isOAuthSecretFromPwdEnabled()
&& (connectionContext.isPropertyPresent(PWD)
|| connectionContext.isPropertyPresent(PASSWORD)))) {
addMissingProperty(missingPropertyInfos, connectionContext, CLIENT_SECRET, true);
}
addMissingProperty(missingPropertyInfos, connectionContext, CLIENT_ID, true);

if (connectionContext.isPropertyPresent(USE_JWT_ASSERTION)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1357,4 +1357,173 @@ public void testOAuthWebServerTimeoutCustom() throws DatabricksSQLException {
TestConstants.VALID_URL_1 + ";OAuthWebServerTimeout=300", properties);
assertEquals(300, connectionContext.getOAuthWebServerTimeout());
}

// ===== OAuth Secret from PWD Tests =====

private static final String OAUTH_M2M_BASE_URL =
"jdbc:databricks://sample-host.cloud.databricks.com:9999/default;AuthMech=11;Auth_Flow=1;"
+ "httpPath=/sql/1.0/warehouses/9999999999999999";

@Test
public void testGetClientSecret_WithOAuthSecretFromPwd_ReadsFromPassword()
throws DatabricksSQLException {
String url = OAUTH_M2M_BASE_URL + ";EnableOAuthSecretFromPwd=1";
Properties props = new Properties();
props.setProperty("password", "my-oauth-secret");

DatabricksConnectionContext ctx =
(DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props);
assertEquals("my-oauth-secret", ctx.getClientSecret());
assertTrue(ctx.isOAuthSecretFromPwdEnabled());
}

@Test
public void testGetClientSecret_WithOAuthSecretFromPwd_PwdWinsOverExplicitSecret()
throws DatabricksSQLException {
// When feature is enabled, PWD/password always takes precedence over OAuth2Secret
String url = OAUTH_M2M_BASE_URL + ";EnableOAuthSecretFromPwd=1";
Properties props = new Properties();
props.setProperty("password", "password-value");
props.setProperty("OAuth2Secret", "explicit-secret");

DatabricksConnectionContext ctx =
(DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props);
assertEquals("password-value", ctx.getClientSecret());
}

@Test
public void testGetClientSecret_WithOAuthSecretFromPwd_PwdParamWinsOverExplicitSecret()
throws DatabricksSQLException {
// Same as above but with pwd param instead of password
String url = OAUTH_M2M_BASE_URL + ";EnableOAuthSecretFromPwd=1";
Properties props = new Properties();
props.setProperty("pwd", "pwd-value");
props.setProperty("OAuth2Secret", "explicit-secret");

DatabricksConnectionContext ctx =
(DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props);
assertEquals("pwd-value", ctx.getClientSecret());
}

@Test
public void testGetClientSecret_WithoutFeatureFlag_DoesNotReadPwd()
throws DatabricksSQLException {
Properties props = new Properties();
props.setProperty("password", "my-oauth-secret");

DatabricksConnectionContext ctx =
(DatabricksConnectionContext) DatabricksConnectionContext.parse(OAUTH_M2M_BASE_URL, props);
assertNull(ctx.getClientSecret());
assertFalse(ctx.isOAuthSecretFromPwdEnabled());
}

@Test
public void testGetClientSecret_WithoutFeatureFlag_ReadsExplicitSecret()
throws DatabricksSQLException {
// When feature is disabled, OAuth2Secret is used as normal
Properties props = new Properties();
props.setProperty("password", "password-value");
props.setProperty("OAuth2Secret", "explicit-secret");

DatabricksConnectionContext ctx =
(DatabricksConnectionContext) DatabricksConnectionContext.parse(OAUTH_M2M_BASE_URL, props);
assertEquals("explicit-secret", ctx.getClientSecret());
assertFalse(ctx.isOAuthSecretFromPwdEnabled());
}

@Test
public void testGetClientSecret_WithOAuthSecretFromPwd_ReadsFromPwdParam()
throws DatabricksSQLException {
String url = OAUTH_M2M_BASE_URL + ";EnableOAuthSecretFromPwd=1";
Properties props = new Properties();
props.setProperty("pwd", "pwd-value");

DatabricksConnectionContext ctx =
(DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props);
assertEquals("pwd-value", ctx.getClientSecret());
}

@Test
public void testGetClientSecret_WithOAuthSecretFromPwd_NoPwdProvided_ThrowsError()
throws DatabricksSQLException {
// When feature is enabled but no PWD/password is provided, should throw error
String url = OAUTH_M2M_BASE_URL + ";EnableOAuthSecretFromPwd=1";
Properties props = new Properties();

DatabricksConnectionContext ctx =
(DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props);
DatabricksDriverException ex =
assertThrows(DatabricksDriverException.class, ctx::getClientSecret);
assertTrue(ex.getMessage().contains("EnableOAuthSecretFromPwd is enabled"));
assertTrue(ex.getMessage().contains("PWD or password"));
}

@Test
public void testGetClientSecret_WithOAuthSecretFromPwd_ExplicitSecretOnly_NoPwd_ThrowsError()
throws DatabricksSQLException {
// When feature is enabled, OAuth2Secret provided but no PWD — should throw error
// because the feature mandates reading from PWD
String url = OAUTH_M2M_BASE_URL + ";EnableOAuthSecretFromPwd=1";
Properties props = new Properties();
props.setProperty("OAuth2Secret", "explicit-secret");

DatabricksConnectionContext ctx =
(DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props);
DatabricksDriverException ex =
assertThrows(DatabricksDriverException.class, ctx::getClientSecret);
assertTrue(ex.getMessage().contains("EnableOAuthSecretFromPwd is enabled"));
}

@Test
public void testGetClientSecret_FeatureDisabledExplicitly_DoesNotReadPwd()
throws DatabricksSQLException {
// Explicitly set EnableOAuthSecretFromPwd=0
String url = OAUTH_M2M_BASE_URL + ";EnableOAuthSecretFromPwd=0";
Properties props = new Properties();
props.setProperty("password", "my-oauth-secret");

DatabricksConnectionContext ctx =
(DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props);
assertNull(ctx.getClientSecret());
assertFalse(ctx.isOAuthSecretFromPwdEnabled());
}

@Test
public void testGetClientSecret_FeatureEnabled_PasswordInUrl() throws DatabricksSQLException {
// Password provided in the JDBC URL itself (not via Properties)
String url = OAUTH_M2M_BASE_URL + ";EnableOAuthSecretFromPwd=1;pwd=url-secret";
Properties props = new Properties();

DatabricksConnectionContext ctx =
(DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props);
assertEquals("url-secret", ctx.getClientSecret());
}

@Test
public void testGetClientSecret_FeatureEnabled_BrowserBasedAuth() throws DatabricksSQLException {
// Browser-based auth (Auth_Flow=2) with EnableOAuthSecretFromPwd
String url =
"jdbc:databricks://sample-host.cloud.databricks.com:9999/default;AuthMech=11;Auth_Flow=2;"
+ "httpPath=/sql/1.0/warehouses/9999999999999999;EnableOAuthSecretFromPwd=1";
Properties props = new Properties();
props.setProperty("password", "browser-secret");

DatabricksConnectionContext ctx =
(DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props);
assertEquals("browser-secret", ctx.getClientSecret());
}

@Test
public void testGetClientSecret_FeatureEnabled_RefreshTokenFlow() throws DatabricksSQLException {
// Refresh token flow (Auth_Flow=0) with EnableOAuthSecretFromPwd
String url =
"jdbc:databricks://sample-host.cloud.databricks.com:9999/default;AuthMech=11;Auth_Flow=0;"
+ "httpPath=/sql/1.0/warehouses/9999999999999999;EnableOAuthSecretFromPwd=1";
Properties props = new Properties();
props.setProperty("password", "refresh-secret");

DatabricksConnectionContext ctx =
(DatabricksConnectionContext) DatabricksConnectionContext.parse(url, props);
assertEquals("refresh-secret", ctx.getClientSecret());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,14 @@ public static Connection getValidM2MConnection() throws SQLException {
return DriverManager.getConnection(getJdbcM2MUrl(), createM2MConnectionProperties());
}

public static Connection getValidM2MConnectionWithSecretFromPwd() throws SQLException {
String url = getJdbcM2MUrl() + ";EnableOAuthSecretFromPwd=1";
Properties connProps = new Properties();
connProps.put("OAuth2ClientId", System.getenv("DATABRICKS_JDBC_M2M_CLIENT_ID"));
connProps.put("password", System.getenv("DATABRICKS_JDBC_M2M_CLIENT_SECRET"));
return DriverManager.getConnection(url, connProps);
}

public static Connection getValidSPTokenFedConnection() throws SQLException {
return DriverManager.getConnection(getSPTokenFedUrl(), createSPTokenFedConnectionProperties());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ void testM2M() throws SQLException {
assertDoesNotThrow(() -> connection.createStatement().execute("select 1"));
}

@Test
void testM2MWithSecretFromPwd() throws SQLException {
Connection connection = getValidM2MConnectionWithSecretFromPwd();
assertDoesNotThrow(() -> connection.createStatement().execute("select 1"));
}

@Test
void testPAT() throws SQLException {
Properties connectionProperties = new Properties();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,64 @@ void testIncorrectCredentialsForM2M() {
.contains("Connection failure while using the OSS Databricks JDBC driver.");
}

@Test
void testSuccessfulM2MConnectionWithSecretFromPwd() throws SQLException {
String url = getFakeServiceM2MUrl() + "EnableOAuthSecretFromPwd=1;";
Properties connProps = new Properties();
connProps.put("OAuth2ClientId", TEST_CLIENT_ID);
connProps.put("password", TEST_CLIENT_SECRET);
connProps.put(
DatabricksJdbcUrlParams.CONN_CATALOG.getParamName(),
FakeServiceConfigLoader.getProperty(DatabricksJdbcUrlParams.CONN_CATALOG.getParamName()));
connProps.put(
DatabricksJdbcUrlParams.CONN_SCHEMA.getParamName(),
FakeServiceConfigLoader.getProperty(DatabricksJdbcUrlParams.CONN_SCHEMA.getParamName()));

Connection conn = DriverManager.getConnection(url, connProps);
assertNotNull(conn);
assertFalse(conn.isClosed());
conn.close();
}

@Test
void testM2MWithSecretFromPwd_PwdWinsOverExplicitSecret() throws SQLException {
// When EnableOAuthSecretFromPwd=1, PWD/password takes precedence over OAuth2Secret.
// Providing the correct secret in password and an invalid one in OAuth2Secret should succeed.
String url = getFakeServiceM2MUrl() + "EnableOAuthSecretFromPwd=1;";
Properties connProps = new Properties();
connProps.put("OAuth2ClientId", TEST_CLIENT_ID);
connProps.put("password", TEST_CLIENT_SECRET);
connProps.put("OAuth2Secret", "invalid-should-be-ignored");
connProps.put(
DatabricksJdbcUrlParams.CONN_CATALOG.getParamName(),
FakeServiceConfigLoader.getProperty(DatabricksJdbcUrlParams.CONN_CATALOG.getParamName()));
connProps.put(
DatabricksJdbcUrlParams.CONN_SCHEMA.getParamName(),
FakeServiceConfigLoader.getProperty(DatabricksJdbcUrlParams.CONN_SCHEMA.getParamName()));

Connection conn = DriverManager.getConnection(url, connProps);
assertNotNull(conn);
assertFalse(conn.isClosed());
conn.close();
}

@Test
void testM2MWithSecretFromPwd_NoPwdProvided_ThrowsError() {
// EnableOAuthSecretFromPwd=1 but no PWD/password — should fail with validation error
String url = getFakeServiceM2MUrl() + "EnableOAuthSecretFromPwd=1;";
Properties connProps = new Properties();
connProps.put("OAuth2ClientId", TEST_CLIENT_ID);
connProps.put(
DatabricksJdbcUrlParams.CONN_CATALOG.getParamName(),
FakeServiceConfigLoader.getProperty(DatabricksJdbcUrlParams.CONN_CATALOG.getParamName()));
connProps.put(
DatabricksJdbcUrlParams.CONN_SCHEMA.getParamName(),
FakeServiceConfigLoader.getProperty(DatabricksJdbcUrlParams.CONN_SCHEMA.getParamName()));

Exception e = assertThrows(Exception.class, () -> DriverManager.getConnection(url, connProps));
assertTrue(e.getMessage().contains("EnableOAuthSecretFromPwd is enabled"));
}

private Connection getValidM2MConnection() throws SQLException {
return DriverManager.getConnection(
getFakeServiceM2MUrl(), createFakeServiceM2MConnectionProperties(TEST_CLIENT_SECRET));
Expand Down
Loading