diff --git a/CHANGELOG.md b/CHANGELOG.md
index 291c62a309..217f2d7a8b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/)
+## [9.1.1] Preview Release
+### Added
+- Added maxResultBuffer connection property [1431](https://github.com/microsoft/mssql-jdbc/pull/1431)
+- Added support for Service Principal Authentication [1456](https://github.com/microsoft/mssql-jdbc/pull/1456)
+- Added support for Azure Active Directory Interactive Authentication [1464](https://github.com/microsoft/mssql-jdbc/pull/1464)
+
+### Changed
+- Enabled useBulkCopyForBatchInsert against non Azure SQL Data Warehouse servers [#1465](https://github.com/microsoft/mssql-jdbc/pull/1465)
+
## [9.1.0] Preview Release
### Added
- Added support for already connected sockets when using custom socket factory [1420](https://github.com/microsoft/mssql-jdbc/pull/1420)
diff --git a/README.md b/README.md
index 89f08e1662..c08f6b5c7c 100644
--- a/README.md
+++ b/README.md
@@ -92,7 +92,7 @@ To get the latest preview version of the driver, add the following to your POM f
com.microsoft.sqlserver
mssql-jdbc
- 9.1.0.jre15-preview
+ 9.1.1.jre15-preview
```
@@ -127,7 +127,7 @@ Projects that require either of the two features need to explicitly declare the
com.microsoft.sqlserver
mssql-jdbc
- 9.1.0.jre15-preview
+ 9.1.1.jre15-preview
compile
@@ -145,7 +145,7 @@ Projects that require either of the two features need to explicitly declare the
com.microsoft.sqlserver
mssql-jdbc
- 9.1.0.jre15-preview
+ 9.1.1.jre15-preview
compile
@@ -172,7 +172,7 @@ When setting 'useFmtOnly' property to 'true' for establishing a connection or cr
com.microsoft.sqlserver
mssql-jdbc
- 9.1.0.jre15-preview
+ 9.1.1.jre15-preview
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index 429100f00f..977851c76d 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -38,7 +38,7 @@ jobs:
name: authDLL
displayName: 'Download mssql-jdbc_auth DLL'
inputs:
- secureFile: 'mssql-jdbc_auth-9.1.0.x64-preview.dll'
+ secureFile: 'mssql-jdbc_auth-9.1.1.x64-preview.dll'
- task: Maven@3
displayName: 'Maven build jre15'
inputs:
diff --git a/build.gradle b/build.gradle
index 8d317397e1..dc223da6ba 100644
--- a/build.gradle
+++ b/build.gradle
@@ -14,7 +14,7 @@
apply plugin: 'java'
-version = '9.1.0'
+version = '9.1.1'
def jreVersion = ""
def testOutputDir = file("build/classes/java/test")
def archivesBaseName = 'mssql-jdbc'
diff --git a/pom.xml b/pom.xml
index 02ab98c7c9..a93874f604 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
com.microsoft.sqlserver
mssql-jdbc
- 9.1.0
+ 9.1.1
jar
Microsoft JDBC Driver for SQL Server
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ICounter.java b/src/main/java/com/microsoft/sqlserver/jdbc/ICounter.java
new file mode 100644
index 0000000000..dcba3944ad
--- /dev/null
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/ICounter.java
@@ -0,0 +1,27 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+
+package com.microsoft.sqlserver.jdbc;
+
+/**
+ * Interface for MaxResultBufferCounter
+ */
+interface ICounter {
+
+ /**
+ * Increases the state of Counter
+ *
+ * @param bytes
+ * Number of bytes to increase state
+ * @throws SQLServerException
+ * Exception is thrown, when limit of Counter is exceeded
+ */
+ void increaseCounter(long bytes) throws SQLServerException;
+
+ /**
+ * Resets the state of Counter
+ */
+ void resetCounter();
+}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
index 1fcdf40e4b..4e0752aeb2 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java
@@ -51,6 +51,7 @@
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
+import java.util.Properties;
import java.util.Set;
import java.util.SimpleTimeZone;
import java.util.TimeZone;
@@ -109,6 +110,9 @@ final class TDS {
static final byte ADALWORKFLOW_ACTIVEDIRECTORYPASSWORD = 0x01;
static final byte ADALWORKFLOW_ACTIVEDIRECTORYINTEGRATED = 0x02;
static final byte ADALWORKFLOW_ACTIVEDIRECTORYMSI = 0x03;
+ static final byte ADALWORKFLOW_ACTIVEDIRECTORYINTERACTIVE = 0x03;
+ static final byte ADALWORKFLOW_ACTIVEDIRECTORYSERVICEPRINCIPAL = 0x01; // Using the Password byte as that is the
+ // closest we have.
static final byte FEDAUTH_INFO_ID_STSURL = 0x01; // FedAuthInfoData is token endpoint URL from which to acquire fed
// auth token
static final byte FEDAUTH_INFO_ID_SPN = 0x02; // FedAuthInfoData is the SPN to use for acquiring fed auth token
@@ -3155,6 +3159,15 @@ boolean isEOMSent() {
traceID = "TDSWriter@" + Integer.toHexString(hashCode()) + " (" + con.toString() + ")";
}
+ /**
+ * Checks If tdsMessageType is RPC or QUERY
+ *
+ * @return boolean
+ */
+ boolean checkIfTdsMessageTypeIsBatchOrRPC() {
+ return tdsMessageType == TDS.PKT_QUERY || tdsMessageType == TDS.PKT_RPC;
+ }
+
// TDS message start/end operations
void preparePacket() throws SQLServerException {
@@ -6545,6 +6558,11 @@ private boolean nextPacket() throws SQLServerException {
// This action must be synchronized against against another thread calling
// readAllPackets() to read in ALL of the remaining packets of the current response.
if (null == consumedPacket.next) {
+ // if the read comes from getNext() and responseBuffering is Adaptive (in this place is), then reset Counter
+ // State
+ if (null != command && command.getTDSWriter().checkIfTdsMessageTypeIsBatchOrRPC()) {
+ command.getCounter().resetCounter();
+ }
readPacket();
if (null == consumedPacket.next)
@@ -6640,6 +6658,11 @@ synchronized final boolean readPacket() throws SQLServerException {
System.arraycopy(newPacket.header, 0, logBuffer, 0, TDS.PACKET_HEADER_SIZE);
}
+ // if messageType is RPC or QUERY, then increment Counter's state
+ if (tdsChannel.getWriter().checkIfTdsMessageTypeIsBatchOrRPC()) {
+ command.getCounter().increaseCounter(packetLength);
+ }
+
// Now for the payload...
for (int payloadBytesRead = 0; payloadBytesRead < newPacket.payloadLength;) {
int bytesRead = tdsChannel.read(newPacket.payload, payloadBytesRead,
@@ -7344,6 +7367,23 @@ final boolean readingResponse() {
protected ArrayList enclaveCEKs;
+ // Counter reference, so maxResultBuffer property can by acknowledged
+ private ICounter counter;
+
+ ICounter getCounter() {
+ return counter;
+ }
+
+ void createCounter(ICounter previousCounter, Properties activeConnectionProperties) {
+ if (null == previousCounter) {
+ String maxResultBuffer = activeConnectionProperties
+ .getProperty(SQLServerDriverStringProperty.MAX_RESULT_BUFFER.toString());
+ counter = new MaxResultBufferCounter(Long.parseLong(maxResultBuffer));
+ } else {
+ counter = previousCounter;
+ }
+ }
+
/**
* Creates this command with an optional timeout.
*
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java
index 84bb424385..8715286287 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/ISQLServerDataSource.java
@@ -1021,4 +1021,49 @@ public interface ISQLServerDataSource extends javax.sql.CommonDataSource {
*/
void setSendTemporalDataTypesAsStringForBulkCopy(boolean sendTemporalDataTypesAsStringForBulkCopy);
+ /**
+ * Returns the value for the connection property 'AADSecurePrincipalId'.
+ *
+ * @return 'AADSecurePrincipalId' property value.
+ */
+ String getAADSecurePrincipalId();
+
+ /**
+ * Sets the 'AADSecurePrincipalId' connection property used for Active Directory Service Principal authentication.
+ *
+ * @param AADSecurePrincipalId
+ * Active Directory Service Principal Id.
+ */
+ void setAADSecurePrincipalId(String AADSecurePrincipalId);
+
+ /**
+ * Returns the value for the connection property 'AADSecurePrincipalSecret'.
+ *
+ * @return 'AADSecurePrincipalSecret' property value.
+ */
+ String getAADSecurePrincipalSecret();
+
+ /**
+ * Sets the 'AADSecurePrincipalSecret' connection property used for Active Directory Service Principal
+ * authentication.
+ *
+ * @param AADSecurePrincipalSecret
+ * Active Directory Service Principal secret.
+ */
+ void setAADSecurePrincipalSecret(String AADSecurePrincipalSecret);
+
+ /**
+ * Returns value of 'maxResultBuffer' from Connection String.
+ *
+ * @return 'maxResultBuffer' property.
+ */
+ String getMaxResultBuffer();
+
+ /**
+ * Specifies value for 'maxResultBuffer' property
+ *
+ * @param maxResultBuffer
+ * String value for 'maxResultBuffer'
+ */
+ void setMaxResultBuffer(String maxResultBuffer);
}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/MaxResultBufferCounter.java b/src/main/java/com/microsoft/sqlserver/jdbc/MaxResultBufferCounter.java
new file mode 100644
index 0000000000..82c56b71ef
--- /dev/null
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/MaxResultBufferCounter.java
@@ -0,0 +1,52 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+
+package com.microsoft.sqlserver.jdbc;
+
+import java.text.MessageFormat;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+
+/**
+ * Implementation of ICounter for 'maxResultBuffer' property.
+ */
+public class MaxResultBufferCounter implements ICounter {
+
+ private final Logger logger = Logger.getLogger("com.microsoft.sqlserver.jdbc.MaxResultBufferCounter");
+
+ private long counter = 0;
+ private final long maxResultBuffer;
+
+ public MaxResultBufferCounter(long maxResultBuffer) {
+ this.maxResultBuffer = maxResultBuffer;
+ }
+
+ public void increaseCounter(long bytes) throws SQLServerException {
+ if (maxResultBuffer > 0) {
+ counter += bytes;
+ checkForMaxResultBufferOverflow(counter);
+ }
+ }
+
+ public void resetCounter() {
+ counter = 0;
+ }
+
+ private void checkForMaxResultBufferOverflow(long number) throws SQLServerException {
+ if (number > maxResultBuffer) {
+ if (logger.isLoggable(Level.SEVERE)) {
+ logger.log(Level.SEVERE, SQLServerException.getErrString("R_maxResultBufferPropertyExceeded"),
+ new Object[] {number, maxResultBuffer});
+ }
+ throwExceededMaxResultBufferException(counter, maxResultBuffer);
+ }
+ }
+
+ private void throwExceededMaxResultBufferException(Object... arguments) throws SQLServerException {
+ MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_maxResultBufferPropertyExceeded"));
+ throw new SQLServerException(form.format(arguments), null);
+ }
+}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/MaxResultBufferParser.java b/src/main/java/com/microsoft/sqlserver/jdbc/MaxResultBufferParser.java
new file mode 100644
index 0000000000..88f83ec6a4
--- /dev/null
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/MaxResultBufferParser.java
@@ -0,0 +1,151 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+
+package com.microsoft.sqlserver.jdbc;
+
+import java.lang.management.ManagementFactory;
+import java.text.MessageFormat;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+
+/**
+ * Parser created to parse String value from Connection String to equivalent number of bytes for JDBC Driver to work on.
+ */
+public class MaxResultBufferParser {
+
+ private static final Logger logger = Logger.getLogger("com.microsoft.sqlserver.jdbc.MaxResultBufferParser");
+ private static final String[] PERCENT_PHRASES = {"percent", "pct", "p"};
+ private static final String ERROR_MESSAGE = "MaxResultBuffer property is badly formatted: {0}.";
+
+ private MaxResultBufferParser() {}
+
+ /**
+ *
+ * Returns number of bytes for maxResultBuffer property
+ *
+ * @param input
+ * String value for maxResultProperty provided in Connection String
+ * @return 'maxResultBuffer' property as number of bytes
+ * @throws SQLServerException
+ * Is Thrown when maxResultProperty's syntax is wrong
+ */
+ public static long validateMaxResultBuffer(String input) throws SQLServerException {
+ String numberString;
+ long number = -1;
+
+ // check for null values and empty String "", if so return -1 (default value)
+ if (StringUtils.isEmpty(input) || input.equals("-1")) {
+ return number;
+ }
+
+ // check if input is number
+ if (!StringUtils.isEmpty(input) && input.matches("-?\\d+(\\.\\d+)?")) {
+ try {
+ number = Long.parseLong(input);
+ } catch (NumberFormatException e) {
+ if (logger.isLoggable(Level.SEVERE)) {
+ logger.log(Level.SEVERE, ERROR_MESSAGE, new Object[] {input});
+ }
+ throwNewInvalidMaxResultBufferParameterException(e, input);
+ }
+ return adjustMemory(number, 1);
+ } else {
+ // check PERCENT_PHRASES
+ for (String percentPhrase : PERCENT_PHRASES) {
+ if (input.endsWith(percentPhrase)) {
+ numberString = input.substring(0, input.length() - percentPhrase.length());
+ try {
+ number = Long.parseLong(numberString);
+ } catch (NumberFormatException e) {
+ if (logger.isLoggable(Level.SEVERE)) {
+ logger.log(Level.SEVERE, ERROR_MESSAGE, new Object[] {input});
+ }
+ throwNewInvalidMaxResultBufferParameterException(e, numberString);
+ }
+ return adjustMemoryPercentage(number);
+ }
+ }
+
+ // check if prefix was supplied
+ long multiplier = getMultiplier(input);
+ numberString = input.substring(0, input.length() - 1);
+
+ try {
+ number = Long.parseLong(numberString);
+ } catch (NumberFormatException e) {
+ if (logger.isLoggable(Level.SEVERE)) {
+ logger.log(Level.SEVERE, ERROR_MESSAGE, new Object[] {input});
+ }
+ throwNewInvalidMaxResultBufferParameterException(e, numberString);
+ }
+ return adjustMemory(number, multiplier);
+ }
+ }
+
+ private static void checkForNegativeValue(long value) throws SQLServerException {
+ if (value <= 0) {
+ Object[] objectToThrow = new Object[] {value};
+ MessageFormat form = new MessageFormat(
+ SQLServerException.getErrString("R_maxResultBufferNegativeParameterValue"));
+ if (logger.isLoggable(Level.SEVERE)) {
+ logger.log(Level.SEVERE, SQLServerException.getErrString("R_maxResultBufferNegativeParameterValue"),
+ objectToThrow);
+ }
+ throw new SQLServerException(form.format(objectToThrow), new Throwable());
+ }
+ }
+
+ private static long getMultiplier(String input) throws SQLServerException {
+ long multiplier = 1;
+ switch (Character.toUpperCase(input.charAt(input.length() - 1))) {
+ case 'K':
+ multiplier = 1_000L;
+ break;
+ case 'M':
+ multiplier = 1_000_000L;
+ break;
+ case 'G':
+ multiplier = 1_000_000_000L;
+ break;
+ case 'T':
+ multiplier = 1_000_000_000_000L;
+ break;
+ default:
+ if (logger.isLoggable(Level.SEVERE)) {
+ logger.log(Level.SEVERE, ERROR_MESSAGE, new Object[] {input});
+ }
+ throwNewInvalidMaxResultBufferParameterException(null, input);
+ }
+ return multiplier;
+ }
+
+ private static long adjustMemoryPercentage(long percentage) throws SQLServerException {
+ checkForNegativeValue(percentage);
+ if (percentage > 90)
+ return (long) (0.9 * getMaxMemory());
+ else
+ return (long) ((percentage) / 100.0 * getMaxMemory());
+ }
+
+ private static long adjustMemory(long size, long multiplier) throws SQLServerException {
+ checkForNegativeValue(size);
+ if (size * multiplier > 0.9 * getMaxMemory())
+ return (long) (0.9 * getMaxMemory());
+ else
+ return size * multiplier;
+ }
+
+ private static long getMaxMemory() {
+ return ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getMax();
+ }
+
+ private static void throwNewInvalidMaxResultBufferParameterException(Throwable cause,
+ Object... arguments) throws SQLServerException {
+ MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_maxResultBufferInvalidSyntax"));
+ throw new SQLServerException(form.format(arguments), cause);
+ }
+
+}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/PersistentTokenCacheAccessAspect.java b/src/main/java/com/microsoft/sqlserver/jdbc/PersistentTokenCacheAccessAspect.java
new file mode 100644
index 0000000000..3f2cc2c98e
--- /dev/null
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/PersistentTokenCacheAccessAspect.java
@@ -0,0 +1,58 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+
+package com.microsoft.sqlserver.jdbc;
+
+import com.microsoft.aad.msal4j.ITokenCacheAccessAspect;
+import com.microsoft.aad.msal4j.ITokenCacheAccessContext;
+
+
+/**
+ * Access aspect for accessing the token cache.
+ *
+ * MSAL token cache does not persist beyond lifetime of the application. This class implements the
+ * ITokenCacheAccessAspect interface to persist the token cache between application instances so subsequent
+ * authentications can use silent authentication if the user account is in the token cache.
+ *
+ * @see https://aka.ms/msal4j-token-cache
+ */
+public class PersistentTokenCacheAccessAspect implements ITokenCacheAccessAspect {
+ private static PersistentTokenCacheAccessAspect instance = new PersistentTokenCacheAccessAspect();
+
+ private PersistentTokenCacheAccessAspect() {};
+
+ static PersistentTokenCacheAccessAspect getInstance() {
+ return instance;
+ }
+
+ /**
+ * Token cache in JSON format
+ */
+ private String cache = null;
+
+ @Override
+ public synchronized void beforeCacheAccess(ITokenCacheAccessContext iTokenCacheAccessContext) {
+ if (null != cache && null != iTokenCacheAccessContext && null != iTokenCacheAccessContext.tokenCache()) {
+ iTokenCacheAccessContext.tokenCache().deserialize(cache);
+ }
+ }
+
+ @Override
+ public synchronized void afterCacheAccess(ITokenCacheAccessContext iTokenCacheAccessContext) {
+ if (null != iTokenCacheAccessContext && iTokenCacheAccessContext.hasCacheChanged()
+ && null != iTokenCacheAccessContext.tokenCache())
+ cache = iTokenCacheAccessContext.tokenCache().serialize();
+ }
+
+ /**
+ * Clears User token cache. This will clear all account info so interactive login will be required on the next
+ * request to acquire an access token.
+ */
+ static void clearUserTokenCache() {
+ if (null != instance.cache && !instance.cache.isEmpty()) {
+ instance.cache = null;
+ }
+ }
+}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java
index 3a1a8c7c44..207f24bd92 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java
@@ -8,7 +8,7 @@
final class SQLJdbcVersion {
static final int major = 9;
static final int minor = 1;
- static final int patch = 0;
+ static final int patch = 1;
static final int build = 0;
/*
* Used to load mssql-jdbc_auth DLL.
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
index 1e3a5c0c78..0b65ff8a55 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java
@@ -137,6 +137,8 @@ public class SQLServerConnection implements ISQLServerConnection, java.io.Serial
private String clientCertificate = null;
private String clientKey = null;
private String clientKeyPassword = "";
+ private String aadPrincipalID = "";
+ private String aadPrincipalSecret = "";
private boolean sendTemporalDataTypesAsStringForBulkCopy = true;
@@ -404,6 +406,12 @@ class FederatedAuthenticationFeatureExtensionData implements Serializable {
case "ACTIVEDIRECTORYMSI":
this.authentication = SqlAuthentication.ActiveDirectoryMSI;
break;
+ case "ACTIVEDIRECTORYSERVICEPRINCIPAL":
+ this.authentication = SqlAuthentication.ActiveDirectoryServicePrincipal;
+ break;
+ case "ACTIVEDIRECTORYINTERACTIVE":
+ this.authentication = SqlAuthentication.ActiveDirectoryInteractive;
+ break;
default:
assert (false);
MessageFormat form = new MessageFormat(
@@ -944,6 +952,14 @@ static synchronized List getColumnEncryptionTrustedMasterKeyPaths(String
}
}
+ /**
+ * Clears User token cache. This will clear all account info so interactive login will be required on the next
+ * request to acquire an access token.
+ */
+ public static synchronized void clearUserTokenCache() {
+ PersistentTokenCacheAccessAspect.clearUserTokenCache();
+ }
+
Properties activeConnectionProperties; // the active set of connection properties
private boolean integratedSecurity = SQLServerDriverBooleanProperty.INTEGRATED_SECURITY.getDefaultValue();
private boolean ntlmAuthentication = false;
@@ -1760,6 +1776,22 @@ Connection connectInternal(Properties propsIn,
}
}
+ sPropKey = SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_ID.toString();
+ sPropValue = activeConnectionProperties.getProperty(sPropKey);
+ if (null == sPropValue) {
+ sPropValue = SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_ID.getDefaultValue();
+ activeConnectionProperties.setProperty(sPropKey, sPropValue);
+ }
+ aadPrincipalID = sPropValue;
+
+ sPropKey = SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_SECRET.toString();
+ sPropValue = activeConnectionProperties.getProperty(sPropKey);
+ if (null == sPropValue) {
+ sPropValue = SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_SECRET.getDefaultValue();
+ activeConnectionProperties.setProperty(sPropKey, sPropValue);
+ }
+ aadPrincipalSecret = sPropValue;
+
// Must be set after STATEMENT_POOLING_CACHE_SIZE
sPropKey = SQLServerDriverBooleanProperty.DISABLE_STATEMENT_POOLING.toString();
sPropValue = activeConnectionProperties.getProperty(sPropKey);
@@ -1866,6 +1898,20 @@ Connection connectInternal(Properties propsIn,
null);
}
+ if (authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryServicePrincipal.toString())
+ && ((activeConnectionProperties
+ .getProperty(SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_ID.toString()).isEmpty())
+ || (activeConnectionProperties
+ .getProperty(SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_SECRET.toString())
+ .isEmpty()))) {
+ if (connectionlogger.isLoggable(Level.SEVERE)) {
+ connectionlogger.severe(toString() + " "
+ + SQLServerException.getErrString("R_NoUserPasswordForActiveServicePrincipal"));
+ }
+ throw new SQLServerException(
+ SQLServerException.getErrString("R_NoUserPasswordForActiveServicePrincipal"), null);
+ }
+
if (authenticationString.equalsIgnoreCase(SqlAuthentication.SqlPassword.toString())
&& ((activeConnectionProperties.getProperty(SQLServerDriverStringProperty.USER.toString())
.isEmpty())
@@ -2161,6 +2207,11 @@ else if (0 == requestedPacketSize)
sendTemporalDataTypesAsStringForBulkCopy = isBooleanPropertyOn(sPropKey, sPropValue);
}
+ sPropKey = SQLServerDriverStringProperty.MAX_RESULT_BUFFER.toString();
+ sPropValue = activeConnectionProperties.getProperty(sPropKey);
+ activeConnectionProperties.setProperty(sPropKey,
+ String.valueOf(MaxResultBufferParser.validateMaxResultBuffer(sPropValue)));
+
sPropKey = SQLServerDriverBooleanProperty.DELAY_LOADING_LOBS.toString();
sPropValue = activeConnectionProperties.getProperty(sPropKey);
if (null == sPropValue) {
@@ -2715,6 +2766,8 @@ private InetSocketAddress connectHelper(ServerPortPlaceHolder serverInfo, int ti
// We have successfully connected, now do the login. logon takes seconds timeout
executeCommand(new LogonCommand());
+ aadPrincipalSecret = "";
+ activeConnectionProperties.remove(SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_SECRET.toString());
return inetSocketAddress;
}
@@ -3179,6 +3232,7 @@ final void terminate(int driverErrorCode, String message, Throwable throwable) t
*/
boolean executeCommand(TDSCommand newCommand) throws SQLServerException {
synchronized (schedulerLock) {
+ ICounter previousCounter = null;
/*
* Detach (buffer) the response from any previously executing command so that we can execute the new
* command. Note that detaching the response does not process it. Detaching just buffers the response off of
@@ -3186,6 +3240,12 @@ boolean executeCommand(TDSCommand newCommand) throws SQLServerException {
*/
if (null != currentCommand) {
try {
+
+ /**
+ * If currentCommand needs to be detached, reset Counter to acknowledge number of Bytes in remaining
+ * packets
+ */
+ currentCommand.getCounter().resetCounter();
currentCommand.detach();
} catch (SQLServerException e) {
/*
@@ -3197,10 +3257,14 @@ boolean executeCommand(TDSCommand newCommand) throws SQLServerException {
connectionlogger.fine("Failed to detach current command : " + e.getMessage());
}
} finally {
+ previousCounter = currentCommand.getCounter();
currentCommand = null;
}
}
-
+ /**
+ * Add Counter reference to newCommand
+ */
+ newCommand.createCounter(previousCounter, activeConnectionProperties);
/*
* The implementation of this scheduler is pretty simple... Since only one command at a time may use a
* connection (to avoid TDS protocol errors), just synchronize to serialize command execution.
@@ -3872,6 +3936,12 @@ int writeFedAuthFeatureRequest(boolean write, /* if false just calculates the le
case ActiveDirectoryMSI:
workflow = TDS.ADALWORKFLOW_ACTIVEDIRECTORYMSI;
break;
+ case ActiveDirectoryInteractive:
+ workflow = TDS.ADALWORKFLOW_ACTIVEDIRECTORYINTERACTIVE;
+ break;
+ case ActiveDirectoryServicePrincipal:
+ workflow = TDS.ADALWORKFLOW_ACTIVEDIRECTORYSERVICEPRINCIPAL;
+ break;
default:
assert (false); // Unrecognized Authentication type for fedauth ADAL request
break;
@@ -3966,15 +4036,19 @@ private void logon(LogonCommand command) throws SQLServerException {
ntlmPasswordHash, hostName);
}
}
-
- // If the workflow being used is Active Directory Password or Active Directory Integrated and server's prelogin
- // response
- // for FEDAUTHREQUIRED option indicates Federated Authentication is required, we have to insert FedAuth Feature
- // Extension
- // in Login7, indicating the intent to use Active Directory Authentication Library for SQL Server.
+ /*
+ * If the workflow being used is Active Directory Password or Active Directory Integrated and server's prelogin
+ * response for FEDAUTHREQUIRED option indicates Federated Authentication is required, we have to insert FedAuth
+ * Feature Extension in Login7, indicating the intent to use Active Directory Authentication Library for SQL
+ * Server.
+ */
if (authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryPassword.toString())
|| ((authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryIntegrated.toString())
- || authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryMSI.toString()))
+ || authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryMSI.toString())
+ || authenticationString
+ .equalsIgnoreCase(SqlAuthentication.ActiveDirectoryServicePrincipal.toString())
+ || authenticationString
+ .equalsIgnoreCase(SqlAuthentication.ActiveDirectoryInteractive.toString()))
&& fedAuthRequiredPreLoginResponse)) {
federatedAuthenticationInfoRequested = true;
fedAuthFeatureExtensionData = new FederatedAuthenticationFeatureExtensionData(TDS.TDS_FEDAUTH_LIBRARY_ADAL,
@@ -3984,16 +4058,18 @@ private void logon(LogonCommand command) throws SQLServerException {
if (null != accessTokenInByte) {
fedAuthFeatureExtensionData = new FederatedAuthenticationFeatureExtensionData(
TDS.TDS_FEDAUTH_LIBRARY_SECURITYTOKEN, fedAuthRequiredPreLoginResponse, accessTokenInByte);
- // No need any further info from the server for token based authentication. So set
- // _federatedAuthenticationRequested to true
+ /*
+ * No need any further info from the server for token based authentication. So set
+ * _federatedAuthenticationRequested to true
+ */
federatedAuthenticationRequested = true;
}
try {
sendLogon(command, authentication, fedAuthFeatureExtensionData);
-
- // If we got routed in the current attempt,
- // the server closes the connection. So, we should not
- // be sending anymore commands to the server in that case.
+ /*
+ * If we got routed in the current attempt, the server closes the connection. So, we should not be sending
+ * anymore commands to the server in that case.
+ */
if (!isRoutedInCurrentAttempt) {
originalCatalog = sCatalog;
String sqlStmt = sqlStatementToInitialize();
@@ -4411,6 +4487,8 @@ void onFedAuthInfo(SqlFedAuthInfo fedAuthInfo, TDSTokenHandler tdsTokenHandler)
&& null != activeConnectionProperties.getProperty(SQLServerDriverStringProperty.PASSWORD.toString()))
|| (authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryIntegrated.toString())
|| authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryMSI.toString())
+ || authenticationString
+ .equalsIgnoreCase(SqlAuthentication.ActiveDirectoryInteractive.toString())
&& fedAuthRequiredPreLoginResponse);
assert null != fedAuthInfo;
@@ -4455,8 +4533,14 @@ private SqlFedAuthToken getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLSe
// Break out of the retry loop in successful case.
break;
- } else if (authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryIntegrated.toString())) {
+ } else if (authenticationString
+ .equalsIgnoreCase(SqlAuthentication.ActiveDirectoryServicePrincipal.toString())) {
+ fedAuthToken = SQLServerMSAL4JUtils.getSqlFedAuthTokenPrincipal(fedAuthInfo, aadPrincipalID,
+ aadPrincipalSecret, authenticationString);
+ // Break out of the retry loop in successful case.
+ break;
+ } else if (authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryIntegrated.toString())) {
// If operating system is windows and mssql-jdbc_auth is loaded then choose the DLL authentication.
if (System.getProperty("os.name").toLowerCase(Locale.ENGLISH).startsWith("windows")
&& AuthenticationJNI.isDllLoaded()) {
@@ -4539,6 +4623,17 @@ private SqlFedAuthToken getFedAuthToken(SqlFedAuthInfo fedAuthInfo) throws SQLSe
}
// Break out of the retry loop in successful case.
break;
+ } else if (authenticationString.equalsIgnoreCase(SqlAuthentication.ActiveDirectoryInteractive.toString())) {
+ if (!msalContextExists()) {
+ MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_MSALMissing"));
+ throw new SQLServerException(form.format(new Object[] {authenticationString}), null, 0, null);
+ }
+ // interactive flow
+ fedAuthToken = SQLServerMSAL4JUtils.getSqlFedAuthTokenInteractive(fedAuthInfo, user,
+ authenticationString);
+
+ // Break out of the retry loop in successful case.
+ break;
}
}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java
index e7de985492..59776ceec8 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDataSource.java
@@ -1018,6 +1018,30 @@ public void setClientKeyPassword(String password) {
setStringProperty(connectionProps, SQLServerDriverStringProperty.CLIENT_KEY_PASSWORD.toString(), password);
}
+ @Override
+ public String getAADSecurePrincipalId() {
+ return getStringProperty(connectionProps, SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_ID.toString(),
+ SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_ID.getDefaultValue());
+ }
+
+ @Override
+ public void setAADSecurePrincipalId(String AADSecurePrincipalId) {
+ setStringProperty(connectionProps, SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_ID.toString(),
+ AADSecurePrincipalId);
+ }
+
+ @Override
+ public String getAADSecurePrincipalSecret() {
+ return getStringProperty(connectionProps, SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_SECRET.toString(),
+ SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_SECRET.getDefaultValue());
+ }
+
+ @Override
+ public void setAADSecurePrincipalSecret(String AADSecurePrincipalSecret) {
+ setStringProperty(connectionProps, SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_SECRET.toString(),
+ AADSecurePrincipalSecret);
+ }
+
@Override
public boolean getSendTemporalDataTypesAsStringForBulkCopy() {
return getBooleanProperty(connectionProps,
@@ -1032,6 +1056,17 @@ public void setSendTemporalDataTypesAsStringForBulkCopy(boolean sendTemporalData
sendTemporalDataTypesAsStringForBulkCopy);
}
+ @Override
+ public String getMaxResultBuffer() {
+ return getStringProperty(connectionProps, SQLServerDriverStringProperty.MAX_RESULT_BUFFER.toString(),
+ SQLServerDriverStringProperty.MAX_RESULT_BUFFER.getDefaultValue());
+ }
+
+ @Override
+ public void setMaxResultBuffer(String maxResultBuffer) {
+ setStringProperty(connectionProps, SQLServerDriverStringProperty.MAX_RESULT_BUFFER.toString(), maxResultBuffer);
+ }
+
/**
* Sets a property string value.
*
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
index d9f41510a5..89cf3c4096 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDriver.java
@@ -65,7 +65,9 @@ enum SqlAuthentication {
SqlPassword,
ActiveDirectoryPassword,
ActiveDirectoryIntegrated,
- ActiveDirectoryMSI;
+ ActiveDirectoryMSI,
+ ActiveDirectoryServicePrincipal,
+ ActiveDirectoryInteractive;
static SqlAuthentication valueOfString(String value) throws SQLServerException {
SqlAuthentication method = null;
@@ -82,6 +84,12 @@ static SqlAuthentication valueOfString(String value) throws SQLServerException {
method = SqlAuthentication.ActiveDirectoryIntegrated;
} else if (value.toLowerCase(Locale.US).equalsIgnoreCase(SqlAuthentication.ActiveDirectoryMSI.toString())) {
method = SqlAuthentication.ActiveDirectoryMSI;
+ } else if (value.toLowerCase(Locale.US)
+ .equalsIgnoreCase(SqlAuthentication.ActiveDirectoryServicePrincipal.toString())) {
+ method = SqlAuthentication.ActiveDirectoryServicePrincipal;
+ } else if (value.toLowerCase(Locale.US)
+ .equalsIgnoreCase(SqlAuthentication.ActiveDirectoryInteractive.toString())) {
+ method = SqlAuthentication.ActiveDirectoryInteractive;
} else {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidConnectionSetting"));
Object[] msgArgs = {"authentication", value};
@@ -366,7 +374,10 @@ enum SQLServerDriverStringProperty {
KEY_STORE_PRINCIPAL_ID("keyStorePrincipalId", ""),
CLIENT_CERTIFICATE("clientCertificate", ""),
CLIENT_KEY("clientKey", ""),
- CLIENT_KEY_PASSWORD("clientKeyPassword", "");
+ CLIENT_KEY_PASSWORD("clientKeyPassword", ""),
+ AAD_SECURE_PRINCIPAL_ID("AADSecurePrincipalId", ""),
+ AAD_SECURE_PRINCIPAL_SECRET("AADSecurePrincipalSecret", ""),
+ MAX_RESULT_BUFFER("maxResultBuffer", "-1");
private final String name;
private final String defaultValue;
@@ -582,7 +593,10 @@ public final class SQLServerDriver implements java.sql.Driver {
new String[] {SqlAuthentication.NotSpecified.toString(), SqlAuthentication.SqlPassword.toString(),
SqlAuthentication.ActiveDirectoryPassword.toString(),
SqlAuthentication.ActiveDirectoryIntegrated.toString(),
- SqlAuthentication.ActiveDirectoryMSI.toString()}),
+ SqlAuthentication.ActiveDirectoryMSI.toString(),
+ SqlAuthentication.ActiveDirectoryServicePrincipal.toString(),
+ SqlAuthentication.ActiveDirectoryInteractive.toString()}),
+
new SQLServerDriverPropertyInfo(SQLServerDriverIntProperty.SOCKET_TIMEOUT.toString(),
Integer.toString(SQLServerDriverIntProperty.SOCKET_TIMEOUT.getDefaultValue()), false, null),
new SQLServerDriverPropertyInfo(SQLServerDriverBooleanProperty.FIPS.toString(),
@@ -634,7 +648,13 @@ public final class SQLServerDriver implements java.sql.Driver {
SQLServerDriverBooleanProperty.SEND_TEMPORAL_DATATYPES_AS_STRING_FOR_BULK_COPY.toString(),
Boolean.toString(SQLServerDriverBooleanProperty.SEND_TEMPORAL_DATATYPES_AS_STRING_FOR_BULK_COPY
.getDefaultValue()),
- false, TRUE_FALSE),};
+ false, TRUE_FALSE),
+ new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_ID.toString(),
+ SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_ID.getDefaultValue(), false, null),
+ new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_SECRET.toString(),
+ SQLServerDriverStringProperty.AAD_SECURE_PRINCIPAL_SECRET.getDefaultValue(), false, null),
+ new SQLServerDriverPropertyInfo(SQLServerDriverStringProperty.MAX_RESULT_BUFFER.toString(),
+ SQLServerDriverStringProperty.MAX_RESULT_BUFFER.getDefaultValue(), false, null),};
/**
* Properties that can only be set by using Properties. Cannot set in connection string
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java
index 8e878c08c0..b1bd17ebbe 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java
@@ -7,68 +7,87 @@
import java.io.IOException;
import java.net.MalformedURLException;
+import java.net.URI;
+import java.net.URISyntaxException;
import java.text.MessageFormat;
import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.logging.Level;
-
import javax.security.auth.kerberos.KerberosPrincipal;
-
+import com.microsoft.aad.msal4j.IAccount;
+import com.microsoft.aad.msal4j.ClientCredentialFactory;
+import com.microsoft.aad.msal4j.ClientCredentialParameters;
+import com.microsoft.aad.msal4j.ConfidentialClientApplication;
import com.microsoft.aad.msal4j.IAuthenticationResult;
+import com.microsoft.aad.msal4j.IClientCredential;
import com.microsoft.aad.msal4j.IntegratedWindowsAuthenticationParameters;
+import com.microsoft.aad.msal4j.InteractiveRequestParameters;
+import com.microsoft.aad.msal4j.MsalInteractionRequiredException;
import com.microsoft.aad.msal4j.PublicClientApplication;
+import com.microsoft.aad.msal4j.SilentParameters;
+import com.microsoft.aad.msal4j.SystemBrowserOptions;
import com.microsoft.aad.msal4j.UserNamePasswordParameters;
import com.microsoft.sqlserver.jdbc.SQLServerConnection.ActiveDirectoryAuthentication;
+
import com.microsoft.sqlserver.jdbc.SQLServerConnection.SqlFedAuthInfo;
class SQLServerMSAL4JUtils {
+ static final String REDIRECTURI = "http://localhost";
+
static final private java.util.logging.Logger logger = java.util.logging.Logger
.getLogger("com.microsoft.sqlserver.jdbc.SQLServerMSAL4JUtils");
static SqlFedAuthToken getSqlFedAuthToken(SqlFedAuthInfo fedAuthInfo, String user, String password,
String authenticationString) throws SQLServerException {
- ExecutorService executorService = Executors.newFixedThreadPool(1);
+ ExecutorService executorService = Executors.newSingleThreadExecutor();
+
try {
- final PublicClientApplication clientApplication = PublicClientApplication
+ final PublicClientApplication pca = PublicClientApplication
.builder(ActiveDirectoryAuthentication.JDBC_FEDAUTH_CLIENT_ID).executorService(executorService)
.authority(fedAuthInfo.stsurl).build();
- final CompletableFuture future = clientApplication
- .acquireToken(UserNamePasswordParameters
- .builder(Collections.singleton(fedAuthInfo.spn + "/.default"), user, password.toCharArray())
- .build());
+ final CompletableFuture future = pca.acquireToken(UserNamePasswordParameters
+ .builder(Collections.singleton(fedAuthInfo.spn + "/.default"), user, password.toCharArray())
+ .build());
final IAuthenticationResult authenticationResult = future.get();
return new SqlFedAuthToken(authenticationResult.accessToken(), authenticationResult.expiresOnDate());
-
} catch (MalformedURLException | InterruptedException e) {
throw new SQLServerException(e.getMessage(), e);
} catch (ExecutionException e) {
- if (logger.isLoggable(Level.SEVERE)) {
- logger.fine(logger.toString() + " MSAL exception:" + e.getMessage());
- }
-
- MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_MSALExecution"));
- Object[] msgArgs = {user, authenticationString};
-
- /*
- * the cause error message uses \\n\\r which does not give correct format change it to \r\n to provide
- * correct format
- */
- String correctedErrorMessage = e.getCause().getMessage().replaceAll("\\\\r\\\\n", "\r\n");
- RuntimeException correctedAuthenticationException = new RuntimeException(correctedErrorMessage);
-
- /*
- * SQLServerException is caused by ExecutionException, which is caused by AuthenticationException to match
- * the exception tree before error message correction
- */
- ExecutionException correctedExecutionException = new ExecutionException(correctedAuthenticationException);
+ throw getCorrectedException(e, user, authenticationString);
+ } finally {
+ executorService.shutdown();
+ }
+ }
- throw new SQLServerException(form.format(msgArgs), null, 0, correctedExecutionException);
+ static SqlFedAuthToken getSqlFedAuthTokenPrincipal(SqlFedAuthInfo fedAuthInfo, String aadPrincipalID,
+ String aadPrincipalSecret, String authenticationString) throws SQLServerException {
+ ExecutorService executorService = Executors.newSingleThreadExecutor();
+ try {
+ String defaultScopeSuffix = "/.default";
+ String scope = fedAuthInfo.spn.endsWith(defaultScopeSuffix) ? fedAuthInfo.spn
+ : fedAuthInfo.spn + defaultScopeSuffix;
+ Set scopes = new HashSet();
+ scopes.add(scope);
+ IClientCredential credential = ClientCredentialFactory.createFromSecret(aadPrincipalSecret);
+ ConfidentialClientApplication clientApplication = ConfidentialClientApplication
+ .builder(aadPrincipalID, credential).executorService(executorService).authority(fedAuthInfo.stsurl)
+ .build();
+ final CompletableFuture future = clientApplication
+ .acquireToken(ClientCredentialParameters.builder(scopes).build());
+ final IAuthenticationResult authenticationResult = future.get();
+ return new SqlFedAuthToken(authenticationResult.accessToken(), authenticationResult.expiresOnDate());
+ } catch (MalformedURLException | InterruptedException e) {
+ throw new SQLServerException(e.getMessage(), e);
+ } catch (ExecutionException e) {
+ throw getCorrectedException(e, aadPrincipalID, authenticationString);
} finally {
executorService.shutdown();
}
@@ -76,7 +95,7 @@ static SqlFedAuthToken getSqlFedAuthToken(SqlFedAuthInfo fedAuthInfo, String use
static SqlFedAuthToken getSqlFedAuthTokenIntegrated(SqlFedAuthInfo fedAuthInfo,
String authenticationString) throws SQLServerException {
- ExecutorService executorService = Executors.newFixedThreadPool(1);
+ ExecutorService executorService = Executors.newSingleThreadExecutor();
try {
/*
@@ -90,10 +109,10 @@ static SqlFedAuthToken getSqlFedAuthTokenIntegrated(SqlFedAuthInfo fedAuthInfo,
logger.fine(logger.toString() + " realm name is:" + kerberosPrincipal.getRealm());
}
- final PublicClientApplication clientApplication = PublicClientApplication
+ final PublicClientApplication pca = PublicClientApplication
.builder(ActiveDirectoryAuthentication.JDBC_FEDAUTH_CLIENT_ID).executorService(executorService)
.authority(fedAuthInfo.stsurl).build();
- final CompletableFuture future = clientApplication
+ final CompletableFuture future = pca
.acquireToken(IntegratedWindowsAuthenticationParameters
.builder(Collections.singleton(fedAuthInfo.spn + "/.default"), user).build());
@@ -102,35 +121,109 @@ static SqlFedAuthToken getSqlFedAuthTokenIntegrated(SqlFedAuthInfo fedAuthInfo,
} catch (InterruptedException | IOException e) {
throw new SQLServerException(e.getMessage(), e);
} catch (ExecutionException e) {
- if (logger.isLoggable(Level.SEVERE)) {
- logger.fine(logger.toString() + " MSAL exception:" + e.getMessage());
- }
+ throw getCorrectedException(e, "", authenticationString);
+ } finally {
+ executorService.shutdown();
+ }
+ }
- MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_MSALExecution"));
- Object[] msgArgs = {"", authenticationString};
+ static SqlFedAuthToken getSqlFedAuthTokenInteractive(SqlFedAuthInfo fedAuthInfo, String user,
+ String authenticationString) throws SQLServerException {
+ ExecutorService executorService = Executors.newSingleThreadExecutor();
- if (null == e.getCause() || null == e.getCause().getMessage()) {
- // the case when Future's outcome has no AuthenticationResult but exception
- throw new SQLServerException(form.format(msgArgs), null);
+ try {
+ PublicClientApplication pca = PublicClientApplication
+ .builder(ActiveDirectoryAuthentication.JDBC_FEDAUTH_CLIENT_ID).executorService(executorService)
+ .setTokenCacheAccessAspect(PersistentTokenCacheAccessAspect.getInstance())
+ .authority(fedAuthInfo.stsurl).logPii((logger.isLoggable(Level.FINE)) ? true : false).build();
+
+ CompletableFuture future = null;
+ IAuthenticationResult authenticationResult = null;
+
+ // try to acquire token silently if user account found in cache
+ try {
+ Set accountsInCache = pca.getAccounts().join();
+ if (null != accountsInCache && !accountsInCache.isEmpty() && null != user && !user.isEmpty()) {
+ IAccount account = getAccountByUsername(accountsInCache, user);
+ if (null != account) {
+ if (logger.isLoggable(Level.FINE)) {
+ logger.fine(logger.toString() + "Silent authentication for user:" + user);
+ }
+ SilentParameters silentParameters = SilentParameters
+ .builder(Collections.singleton(fedAuthInfo.spn + "/.default"), account).build();
+
+ future = pca.acquireTokenSilently(silentParameters);
+ }
+ }
+ } catch (MsalInteractionRequiredException e) {
+ // not an error, need to get token interactively
+ }
+
+ if (null != future) {
+ authenticationResult = future.get();
} else {
- /*
- * the cause error message uses \\n\\r which does not give correct format change it to \r\n to provide
- * correct format
- */
- String correctedErrorMessage = e.getCause().getMessage().replaceAll("\\\\r\\\\n", "\r\n");
- RuntimeException correctedAuthenticationException = new RuntimeException(correctedErrorMessage);
-
- /*
- * SQLServerException is caused by ExecutionException, which is caused by AuthenticationException to
- * match the exception tree before error message correction
- */
- ExecutionException correctedExecutionException = new ExecutionException(
- correctedAuthenticationException);
-
- throw new SQLServerException(form.format(msgArgs), null, 0, correctedExecutionException);
+ // acquire token interactively with system browser
+ if (logger.isLoggable(Level.FINE)) {
+ logger.fine(logger.toString() + "Interactive authentication");
+ }
+ InteractiveRequestParameters parameters = InteractiveRequestParameters.builder(new URI(REDIRECTURI))
+ .systemBrowserOptions(SystemBrowserOptions.builder()
+ .htmlMessageSuccess(SQLServerResource.getResource("R_MSALAuthComplete")).build())
+ .loginHint(user).scopes(Collections.singleton(fedAuthInfo.spn + "/.default")).build();
+
+ future = pca.acquireToken(parameters);
+ authenticationResult = future.get();
}
+
+ return new SqlFedAuthToken(authenticationResult.accessToken(), authenticationResult.expiresOnDate());
+ } catch (MalformedURLException | InterruptedException | URISyntaxException e) {
+ throw new SQLServerException(e.getMessage(), e);
+ } catch (ExecutionException e) {
+ throw getCorrectedException(e, user, authenticationString);
} finally {
executorService.shutdown();
}
}
+
+ // Helper function to return account containing user name from set of accounts, or null if no match
+ private static IAccount getAccountByUsername(Set accounts, String username) {
+ if (!accounts.isEmpty()) {
+ for (IAccount account : accounts) {
+ if (account.username().equals(username)) {
+ return account;
+ }
+ }
+ }
+ return null;
+ }
+
+ private static SQLServerException getCorrectedException(ExecutionException e, String user,
+ String authenticationString) {
+ if (logger.isLoggable(Level.SEVERE)) {
+ logger.fine(logger.toString() + " MSAL exception:" + e.getMessage());
+ }
+
+ MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_MSALExecution"));
+ Object[] msgArgs = {user, authenticationString};
+
+ if (null == e.getCause() || null == e.getCause().getMessage()) {
+ // The case when Future's outcome has no AuthenticationResult but Exception.
+ return new SQLServerException(form.format(msgArgs), null);
+ } else {
+ /*
+ * the cause error message uses \\n\\r which does not give correct format change it to \r\n to provide
+ * correct format
+ */
+ String correctedErrorMessage = e.getCause().getMessage().replaceAll("\\\\r\\\\n", "\r\n");
+ RuntimeException correctedAuthenticationException = new RuntimeException(correctedErrorMessage);
+
+ /*
+ * SQLServerException is caused by ExecutionException, which is caused by AuthenticationException to match
+ * the exception tree before error message correction
+ */
+ ExecutionException correctedExecutionException = new ExecutionException(correctedAuthenticationException);
+
+ return new SQLServerException(form.format(msgArgs), null, 0, correctedExecutionException);
+ }
+ }
}
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java
index b95a6b5762..6848ea6dc1 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java
@@ -1951,7 +1951,7 @@ public int[] executeBatch() throws SQLServerException, BatchUpdateException, SQL
localUserSQL = userSQL;
try {
- if (this.useBulkCopyForBatchInsert && connection.isAzureDW() && isInsert(localUserSQL)) {
+ if (this.useBulkCopyForBatchInsert && isInsert(localUserSQL)) {
if (null == batchParamValues) {
updateCounts = new int[0];
loggerExternal.exiting(getClassNameLogging(), "executeBatch", updateCounts);
@@ -2108,7 +2108,7 @@ public long[] executeLargeBatch() throws SQLServerException, BatchUpdateExceptio
localUserSQL = userSQL;
try {
- if (this.useBulkCopyForBatchInsert && connection.isAzureDW() && isInsert(localUserSQL)) {
+ if (this.useBulkCopyForBatchInsert && isInsert(localUserSQL)) {
if (null == batchParamValues) {
updateCounts = new long[0];
loggerExternal.exiting(getClassNameLogging(), "executeLargeBatch", updateCounts);
diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
index 613a1fb2f8..33e27f313f 100644
--- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
+++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java
@@ -268,6 +268,10 @@ protected Object[][] getContents() {
"Flag to indicate whether the driver will send temporal datatypes as String value to the server for bulk copy."},
{"R_delayLoadingLobsPropertyDescription",
"Boolean flag which indicates whether the driver will load LOB datatypes into memory."},
+ {"R_AADSecurePrincipalIdPropertyDescription",
+ "The Application Id of a registered application which has been granted permission to the database connected."},
+ {"R_AADSecurePrincipalSecretPropertyDescription",
+ "A Secret defined for a registered application which has been granted permission to the database connected."},
{"R_noParserSupport", "An error occurred while instantiating the required parser. Error: \"{0}\""},
{"R_writeOnlyXML", "Cannot read from this SQLXML instance. This instance is for writing data only."},
{"R_dataHasBeenReadXML", "Cannot read from this SQLXML instance. The data has already been read."},
@@ -405,6 +409,7 @@ protected Object[][] getContents() {
{"R_FedAuthInfoDoesNotContainStsurlAndSpn",
"FEDAUTHINFO token stream does not contain both STSURL and SPN."},
{"R_MSALExecution", "Failed to authenticate the user {0} in Active Directory (Authentication={1})."},
+ {"R_MSALAuthComplete", "Authentication complete. You can close the browser and return to the application."},
{"R_UnrequestedFeatureAckReceived", "Unrequested feature acknowledge is received. Feature ID: {0}."},
{"R_FedAuthFeatureAckContainsExtraData",
"Federated authentication feature extension ack for ADAL and Security Token includes extra data."},
@@ -428,6 +433,8 @@ protected Object[][] getContents() {
"Cannot set the AccessToken property if \"Authentication\" has been specified in the connection string."},
{"R_NoUserPasswordForActivePassword",
"Both \"User\" (or \"UserName\") and \"Password\" connection string keywords must be specified, if \"Authentication=ActiveDirectoryPassword\"."},
+ {"R_NoUserPasswordForActiveServicePrincipal",
+ "Both \"AADSecurePrincipalId\" and \"AADSecurePrincipalSecret\" connection string keywords must be specified, if \"Authentication=ActiveDirectoryServicePrincipal\"."},
{"R_NoUserPasswordForSqlPassword",
"Both \"User\" (or \"UserName\") and \"Password\" connection string keywords must be specified, if \"Authentication=SqlPassword\"."},
{"R_ForceEncryptionTrue_HonorAEFalse",
@@ -649,5 +656,10 @@ protected Object[][] getContents() {
{"R_unassignableError", "The class specified by the {0} property must be assignable to {1}."},
{"R_InvalidCSVQuotes",
"Failed to parse the CSV file, verify that the fields are correctly enclosed in double quotes."},
- {"R_TokenRequireUrl", "Token credentials require a URL using the HTTPS protocol scheme."},};
-};
+ {"R_TokenRequireUrl", "Token credentials require a URL using the HTTPS protocol scheme."},
+ {"R_maxResultBufferPropertyDescription",
+ "Determines maximum amount of bytes that can be read during retrieval of result set"},
+ {"R_maxResultBufferInvalidSyntax", "Invalid syntax: {0} in maxResultBuffer parameter."},
+ {"R_maxResultBufferNegativeParameterValue", "MaxResultBuffer must have positive value: {0}."},
+ {"R_maxResultBufferPropertyExceeded", "MaxResultBuffer property exceeded: {0}. MaxResultBuffer was set to: {1}."},};
+}
diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/MaxResultBufferParserTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/MaxResultBufferParserTest.java
new file mode 100644
index 0000000000..ebfea27eb2
--- /dev/null
+++ b/src/test/java/com/microsoft/sqlserver/jdbc/MaxResultBufferParserTest.java
@@ -0,0 +1,108 @@
+/*
+ * Microsoft JDBC Driver for SQL Server Copyright(c) Microsoft Corporation All rights reserved. This program is made
+ * available under the terms of the MIT License. See the LICENSE file in the project root for more information.
+ */
+
+package com.microsoft.sqlserver.jdbc;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.lang.management.ManagementFactory;
+import java.util.Arrays;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.fail;
+
+
+/**
+ * A class for testing MaxResultBufferParser functionality.
+ */
+class MaxResultBufferParserTest {
+
+ /**
+ * Method with input data for testValidateMaxResultBuffer Tests
+ */
+ public static Iterable