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 data() { + return Arrays.asList(new Object[][] { + {"10p", (long) (0.1 * getMaxMemory())}, + {"010p", (long) (0.1 * getMaxMemory())}, + {"10pct", (long) (0.1 * getMaxMemory())}, + {"10percent", (long) (0.1 * getMaxMemory())}, + {"100", 100}, + {"0100", 100}, + {"100k", 100 * 1000}, + {"0100k", 100 * 1000}, + {"100K", 100 * 1000}, + {"0100K", 100 * 1000}, + {"100m", 100 * 1000 * 1000}, + {"100M", 100 * 1000 * 1000}, + // these values are too big (assuming heap size is 4GB) + {"200p", (long) (0.9 * getMaxMemory())}, + {"0200p", (long) (0.9 * getMaxMemory())}, + {"200pct", (long) (0.9 * getMaxMemory())}, + {"200percent", (long) (0.9 * getMaxMemory())}, + {"100g", (long) (0.9 * getMaxMemory())}, + {"100G", (long) (0.9 * getMaxMemory())}, + {"100t", (long) (0.9 * getMaxMemory())}, + {"100T", (long) (0.9 * getMaxMemory())}, + //when maxResultBuffer property is not supplied, assume -1 + {"", -1}, + {null, -1}, + {"-1", -1}, + }); + } + + /** + * Method with input data for testValidateMaxResultBufferException Tests + */ + public static Iterable exceptionData() { + return Arrays.asList(new Object[][] { + {"-123p"}, {"-423pct"}, {"-100m"}, {"-500K"}, {"-123"},// values are correctly formatted, but they're negative + {"123precd"}, {"456pc"}, // percent phrases are misspelled + {"32P"}, {"-456PCT"}, {"150PERCENT"}, // percent phrases are correct, but they're in upper Case also middle one is negative + {"0101D"}, {"100l"}, {"-100L"}, // incorrect prefixes, last value is also negative + {"1@D"}, // incorrect prefix and malformed value as well + {"0"}, {"0t"}, {"0T"}, {"0p"}, {"0pct"}, {"0percent"}, // 0 is not positive, maxResultBuffer must have positive value + {"0.5"}, {"0.5g"}, {"0.5G"}, {"0.5p"}, {"0.5pct"}, {"0.5percent"}, // maxResultBuffer must be whole number + {" "}, {"ASD"}, {"@!|?:'{}"}, {"a5D"} // malformed values + }); + } + + /** + * + * Tests for correctly formatted input String + * + * @param input + * MaxResultBuffer property + * @param expected + * Expected number of bytes + */ + @ParameterizedTest + @MethodSource("data") + void testValidateMaxResultBuffer(String input, long expected) { + try { + assertEquals(expected, MaxResultBufferParser.validateMaxResultBuffer(input)); + } catch (SQLServerException throwables) { + fail(); + } + } + + /** + * + * Tests for badly formatted maxResultProperty + * + * @param input + * Badly formatted MaxResultBuffer property + */ + @ParameterizedTest + @MethodSource("exceptionData") + void testValidateMaxResultBufferException(String input) { + Assertions.assertThrows(SQLServerException.class, () -> MaxResultBufferParser.validateMaxResultBuffer(input)); + } + + private static long getMaxMemory() { + return ManagementFactory.getMemoryMXBean().getHeapMemoryUsage().getMax(); + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/MaxResultBufferTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/MaxResultBufferTest.java new file mode 100644 index 0000000000..594e4f709d --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/MaxResultBufferTest.java @@ -0,0 +1,450 @@ +/* + * 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.sqlserver.testframework.AbstractSQLGenerator; +import com.microsoft.sqlserver.testframework.AbstractTest; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Arrays; +import java.util.Random; + +import static org.junit.Assert.fail; + + +/** + * Class for testing maxResultBuffer property, all tests were performed on default connection settings + * (defaultPacketLength = 8000), only changed were ResponsiveBuffering and MaxResultBuffer + */ +@DisplayName("maxResultBuffer Tests") +public class MaxResultBufferTest extends AbstractTest { + + @SuppressWarnings("SqlResolve") + private static final String TEST_TABLE_NAME = "maxResultBufferTestTable"; + private static String localConnectionString; + + /** + * This copies value of localConnectionString from connectionString for each test + */ + @BeforeEach + void prepareMaxResultBuffer() { + localConnectionString = new String(connectionString); + } + + /** + * Create TEST_TABLE with 1 column nchar(precision) with numberOfRows. Let's calculate payload on example: + * numberOfRows = 800, precision = 10 + * + * Payload (in Bytes) = 49 (Header plus column metadata) + numberOfRows * (precision * 2 + 1 + 2) (3 extra bytes are + * for column length and end of line character) + * + * So payload generated by this method = 49 + 800 * (10 * 2 + 2 + 1) = 49 + 800 * 23 = 18449 + * + * Default packetLength = 8000, so payload is sent in 3 packets + * + * @throws SQLException + * Signalizes error when creating TEST_TABLE + */ + @BeforeAll + static void createAndPopulateNCharTestTable() throws SQLException { + String insertSQL = "INSERT INTO " + AbstractSQLGenerator.escapeIdentifier(TEST_TABLE_NAME) + " VALUES (?)"; + int numberOfRows = 800; + int precision = 10; + + try (Connection connection = DriverManager.getConnection(connectionString); + Statement statement = connection.createStatement(); + PreparedStatement preparedStatement = connection.prepareStatement(insertSQL)) { + + // drop Table if exists and then create new one + TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(TEST_TABLE_NAME), statement); + statement.execute("CREATE TABLE " + AbstractSQLGenerator.escapeIdentifier(TEST_TABLE_NAME) + + " ( col1 nchar(" + precision + "))"); + + // insert into Table + for (int i = 0; i < numberOfRows; i++) { + preparedStatement.setString(1, generateRandomString(precision)); + preparedStatement.addBatch(); + } + preparedStatement.executeBatch(); + } + } + + @AfterAll + static void teardownTestTable() throws SQLException { + try (Statement statement = connection.createStatement()) { + TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(TEST_TABLE_NAME), statement); + } + } + + /** + * Test shows influence of MaxResultBuffer on all types of ResultSet (considering state of response buffering). In + * this test, maxResultBuffer is not affecting the work of driver in any way. + * + * @param maxResultBuffer + * value of MaxResultBuffer parameter provided by source method + * @param adaptiveBuffering + * value of responseBuffering provided by source method + * @param resultSetType + * type of ResultSet provided by source method + * @param concurrencyMode + * type of ResultSet's concurrency provided by source method + */ + @ParameterizedTest( + name = "[{index}] maxResultBuffer = {0}, responseBuffering = {1}, resultSetType = {2}, concurrencyMode = {3}") + @MethodSource("linearResultSetData") + void testResultSetLinear(String maxResultBuffer, boolean adaptiveBuffering, int resultSetType, + int concurrencyMode) { + setResponseBufferingAdaptive(adaptiveBuffering); + setMaxResultBuffer(maxResultBuffer); + try { + resultSet(resultSetType, concurrencyMode); + } catch (SQLException e) { + fail(); + } + } + + private static Iterable linearResultSetData() { + return Arrays.asList(new Object[][] { + // maxResultBuffer set to 5k + {"5k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE}, + {"5k", true, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"5k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"5k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE}, + {"5k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE}, + {"5k", false, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"5k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"5k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE}, + // maxResultBuffer set to 10k + {"10k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY}, + {"10k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE}, + {"10k", true, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"10k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"10k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE}, + {"10k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE}, + {"10k", false, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"10k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"10k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE}, + // max ResultBuffer set to 15k + {"15k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY}, + {"15k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE}, + {"15k", true, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"15k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"15k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE}, + {"15k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE}, + {"15k", false, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"15k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"15k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE}, + // maxResultBuffer set to 17k + {"17k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY}, + {"17k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE}, + {"17k", true, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"17k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"17k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE}, + {"17k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE}, + {"17k", false, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"17k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"17k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE}, + // maxResultBuffer set to 20k + {"20k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY}, + {"20k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE}, + {"20k", true, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"20k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"20k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE}, + {"20k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY}, + {"20k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE}, + {"20k", false, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"20k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"20k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE},}); + } + + /** + * Test shows influence of MaxResultBuffer on all types of ResultSet (considering state of response buffering). In + * this test, driver is throwing exception because MaxResultBuffer property was exceeded. + * + * @param maxResultBuffer + * value of MaxResultBuffer parameter provided by source method + * @param adaptiveBuffering + * value of responseBuffering connection property provided by source method + * @param resultSetType + * type of ResultSet provided by source method + * @param concurrencyMode + * type of ResultSet's concurrency provided by source method + */ + @ParameterizedTest( + name = "[{index}] maxResultBuffer = {0}, responseBuffering = {1}, resultSetType = {2}, concurrencyMode = {3}") + @MethodSource("linearResultSetDataThrowsSQLException") + void testResultSetLinearThrowsSQLException(String maxResultBuffer, boolean adaptiveBuffering, int resultSetType, + int concurrencyMode) { + setResponseBufferingAdaptive(adaptiveBuffering); + setMaxResultBuffer(maxResultBuffer); + + Assertions.assertThrows(SQLServerException.class, () -> resultSet(resultSetType, concurrencyMode)); + } + + private static Iterable linearResultSetDataThrowsSQLException() { + return Arrays.asList(new Object[][] { + // maxResultBuffer set to 3k + {"3k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY}, + {"3k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE}, + {"3k", true, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"3k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"3k", true, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE}, + {"3k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY}, + {"3k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE}, + {"3k", false, ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"3k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY}, + {"3k", false, ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_UPDATABLE}, + // maxResultBuffer set to 5k + {"5k", true, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY}, + {"5k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY}, + // maxResultBuffer set to 10k + {"10k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY}, + // maxResultBuffer set to 15k + {"15k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY}, + // maxResultBuffer set to 17k + {"17k", false, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY},}); + } + + /** + * Test shows influence of MaxResultBuffer when multiple statements are executed, which result in multiple + * ResultSets (considering state of response buffering). + * + * @param maxResultBuffer + * value of MaxResultBuffer parameter provided by source method + * @param adaptiveBuffering + * value of responseBuffering connection property provided by source method + */ + @ParameterizedTest(name = "[{index}] maxResultBuffer = {0}, responseBuffering = {1}") + @MethodSource("preparedStatementData") + void testPreparedStatementMultipleResultSets(String maxResultBuffer, boolean adaptiveBuffering) { + setResponseBufferingAdaptive(adaptiveBuffering); + setMaxResultBuffer(maxResultBuffer); + try { + preparedStatementWithMultipleResultSets(); + } catch (SQLException e) { + fail(); + } + } + + private static Iterable preparedStatementData() { + return Arrays.asList(new Object[][] { + {"20k", true}, + {"20k", false}, + }); + } + + /** + * Test shows influence of MaxResultBuffer when multiple statements are executed, which result in multiple + * ResultSets (considering state of response buffering). In this test, driver is throwing exception because + * MaxResultBuffer property was exceeded. + * + * @param maxResultBuffer + * value of MaxResultBuffer parameter provided by source method + * @param adaptiveBuffering + * value of responseBuffering connection property provided by source method + */ + @ParameterizedTest(name = "[{index}] maxResultBuffer = {0}, responseBuffering = {1}") + @MethodSource("preparedStatementDataThrowsSQLException") + void testPreparedStatementMultipleResultSetsThrowsSQLException(String maxResultBuffer, boolean adaptiveBuffering) { + setResponseBufferingAdaptive(adaptiveBuffering); + setMaxResultBuffer(maxResultBuffer); + + Assertions.assertThrows(SQLServerException.class, this::preparedStatementWithMultipleResultSets); + } + + private static Iterable preparedStatementDataThrowsSQLException() { + return Arrays.asList(new Object[][] { + // maxResultBuffer set to 3k + {"3k", true}, + {"3k", false}, + // maxResultBuffer set to 5k + {"5k", true}, + {"5k", false}, + // maxResultBuffer set to 10k + {"10k", true}, + {"10k", false}, + // maxResultBuffer set to 15k + {"15k", true}, + {"15k", false}, + // maxResultBuffer set to 17k + {"17k", true}, + {"17k", false}, + }); + } + + /** + * Test shows influence of MaxResultBuffer when multiple statements are executed in one call (considering state of + * response buffering). + * + * @param maxResultBuffer + * value of MaxResultBuffer parameter provided by source method + * @param adaptiveBuffering + * value of responseBuffering connection property provided by source method + */ + @ParameterizedTest(name = "[{index}] maxResultBuffer = {0}, responseBuffering = {1}") + @MethodSource("twoQueriesData") + void testTwoQueriesInOneStatement(String maxResultBuffer, boolean adaptiveBuffering) { + setResponseBufferingAdaptive(adaptiveBuffering); + setMaxResultBuffer(maxResultBuffer); + try { + twoQueriesInOneStatement(); + } catch (SQLException e) { + fail(); + } + } + + private static Iterable twoQueriesData() { + return Arrays.asList(new Object[][] { + {"10k", true}, + {"15k", true}, + {"17k", true}, + {"20k", true}, + }); + } + + /** + * Test shows influence of MaxResultBuffer when multiple statements are executed in one call (considering state of + * response buffering).In this test, driver is throwing exception because MaxResultBuffer property was exceeded. + * + * @param maxResultBuffer + * value of MaxResultBuffer parameter provided by source method + * @param adaptiveBuffering + * value of responseBuffering connection property provided by source method + */ + @ParameterizedTest(name = "[{index}] maxResultBuffer = {0}, responseBuffering = {1}") + @MethodSource("twoQueriesDataThrowsSQLException") + void testTwoQueriesInOneStatementThrowsSQLException(String maxResultBuffer, boolean adaptiveBuffering) { + setResponseBufferingAdaptive(adaptiveBuffering); + setMaxResultBuffer(maxResultBuffer); + + Assertions.assertThrows(SQLServerException.class, this::twoQueriesInOneStatement); + } + + private static Iterable twoQueriesDataThrowsSQLException() { + return Arrays.asList(new Object[][] { + // maxResultBuffer set to 3k + {"3k", true}, + {"3k", false}, + // maxResultBuffer set to 5k + {"5k", true}, + {"5k", false}, + // maxResultBuffer set to 10k + {"10k", false}, + // maxResultBuffer set to 15k + {"15k", false}, + // maxResultBuffer set to 17k + {"17k", false}, + // maxResultBuffer set to 20k + {"20k", false}, + }); + } + + /** + * This method tests if all packets from ResultSet are correctly retrieved + * + * @param resultSetType + * Result set type; one of ResultSet.TYPE_FORWARD_ONLY, + * ResultSet.TYPE_SCROLL_INSENSITIVE, or ResultSet.TYPE_SCROLL_SENSITIVE + * @param concurrencyMode + * Concurrency type; one of ResultSet.CONCUR_READ_ONLY or + * ResultSet.CONCUR_UPDATABLE + * + * @throws SQLException + * Exception is thrown when maxResultBuffer is exceeded + */ + private void resultSet(int resultSetType, int concurrencyMode) throws SQLException { + try (Connection connection = DriverManager.getConnection(localConnectionString); + Statement statement = connection.createStatement(resultSetType, concurrencyMode)) { + statement.execute("SELECT * FROM " + TEST_TABLE_NAME); + try (ResultSet resultSet = statement.getResultSet()) { + while (resultSet.next()) {} + } + } + } + + /** + * This method tests if Statements are detached properly, when first one hasn't been completely retrieved and second + * one have been executed. + * + * @throws SQLException + * Exception is thrown when maxResultBuffer is exceeded + */ + private void preparedStatementWithMultipleResultSets() throws SQLException { + String selectSQL = "SELECT * FROM " + TEST_TABLE_NAME; + + try (Connection connection = DriverManager.getConnection(localConnectionString); + PreparedStatement statement = connection.prepareStatement(selectSQL); + ResultSet resultSet = statement.executeQuery()) { + + try (PreparedStatement secondStatement = connection.prepareStatement(selectSQL); + ResultSet secondResultSet = secondStatement.executeQuery()) { + while (resultSet.next()) {} + + try (PreparedStatement thirdStatement = connection.prepareStatement(selectSQL); + ResultSet thirdResultSet = thirdStatement.executeQuery()) { + while (thirdResultSet.next()) {} + while (secondResultSet.next()) {} + } + } + } + } + + /** + * This method tests if ResultSet's are retrieved correctly, when more than one Query is executed inside single + * statement + * + * @throws SQLException + * Exception is thrown when maxResultBuffer is exceeded + */ + private void twoQueriesInOneStatement() throws SQLException { + try (Connection connection = DriverManager.getConnection(localConnectionString); + Statement statement = connection.createStatement()) { + statement.execute("SELECT * FROM " + TEST_TABLE_NAME + ";SELECT * FROM " + TEST_TABLE_NAME); + + try (ResultSet resultSet = statement.getResultSet()) { + while (resultSet.next()) {} + } + + if (statement.getMoreResults()) { + try (ResultSet totallyNewResultSet = statement.getResultSet()) { + while (totallyNewResultSet.next()) {} + } + } + } + } + + private static String generateRandomString(int precision) { + int leftLimit = 33; + int rightLimit = 126; + Random random = new Random(); + return random.ints(leftLimit, rightLimit).limit(precision) + .collect(StringBuilder::new, StringBuilder::appendCodePoint, StringBuilder::append).toString(); + } + + private static void setResponseBufferingAdaptive(boolean adaptive) { + String value = adaptive ? "adaptive" : "full"; + localConnectionString = TestUtils.addOrOverrideProperty(localConnectionString, "responseBuffering", value); + AbstractTest.updateDataSource(localConnectionString, ds); + } + + private static void setMaxResultBuffer(String maxResultBuffer) { + localConnectionString = TestUtils.addOrOverrideProperty(localConnectionString, "maxResultBuffer", + maxResultBuffer); + AbstractTest.updateDataSource(localConnectionString, ds); + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java index db98f825ab..c700a708e4 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java @@ -32,6 +32,8 @@ import org.junit.platform.runner.JUnitPlatform; import org.junit.runner.RunWith; +import com.microsoft.aad.msal4j.TokenCache; +import com.microsoft.aad.msal4j.TokenCacheAccessContext; import com.microsoft.sqlserver.testframework.AbstractSQLGenerator; import com.microsoft.sqlserver.testframework.AbstractTest; import com.microsoft.sqlserver.testframework.Constants; @@ -839,7 +841,8 @@ public void testConnectionPoolProxyWithLobs() throws SQLException, IOException { stringBuilder.append(buffer, 0, amountRead); } String received = stringBuilder.toString(); - assertTrue(data.equals(received), "Expected String: " + data + "\nReceived String: " + received); + assertTrue(data.equals(received), + "Expected String: " + data + "\nReceived String: " + received); } } } finally { @@ -861,4 +864,19 @@ public void testConnectionPoolProxyWithLobs() throws SQLException, IOException { assertTrue(data.equals(received), "Expected String: " + data + "\nReceived String: " + received); } } + + /* + * Test PersistentTokenCacheAccessAspect methods - this test just executes the methods in the class it does not test + * correct functionality as that requires manual interactive auth + */ + @Test + public void testPersistentTokenCacheAccessAspect() throws SQLException { + TokenCacheAccessContext tokenCacheAccessContext = TokenCacheAccessContext.builder().clientId(null) + .tokenCache(new TokenCache()).account(null).hasCacheChanged(true).build(); + + PersistentTokenCacheAccessAspect persistentTokenAspect = PersistentTokenCacheAccessAspect.getInstance(); + persistentTokenAspect.afterCacheAccess(tokenCacheAccessContext); + persistentTokenAspect.beforeCacheAccess(tokenCacheAccessContext); + PersistentTokenCacheAccessAspect.clearUserTokenCache(); + } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java index 9db64b9eb1..6d131ca2e9 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/TestResource.java @@ -193,5 +193,7 @@ protected Object[][] getContents() { {"R_signinTooManyTimes", "You've tried to sign in too many times with an incorrect user ID or password."}, {"R_toSigninAdd", "To sign into this application, the account must be added to"}, {"R_socketClosed", "Socket closed"}, {"R_aeStreamReadError", "The multi-part identifier"}, - {"R_dataClassificationNotSupported", "Data Classification is not supported on this server."}}; + {"R_dataClassificationNotSupported", "Data Classification is not supported on this server."}, + {"R_maxResultBufferExceeded", "MaxResultBuffer exceeded {0}."}, + {"R_noAuthorizationCode", "No Authorization code was returned from the server"}}; } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/ConnectionEncryptionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/ConnectionEncryptionTest.java index 8e87cf824f..b9b7b4ab79 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/ConnectionEncryptionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/ConnectionEncryptionTest.java @@ -64,8 +64,7 @@ public void testWrongCertificate() throws SQLException { MessageFormat form = new MessageFormat(TestUtils.R_BUNDLE.getString("R_sslFailed")); Object[] msgArgs = {e.getCause().getLocalizedMessage()}; - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), - e.getMessage().contains(form.format(msgArgs))); + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().contains(form.format(msgArgs))); } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/ConnectionSuspensionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/ConnectionSuspensionTest.java index 5cbda65da7..04c71fd45d 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/ConnectionSuspensionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/ConnectionSuspensionTest.java @@ -95,7 +95,7 @@ private void testAccessTokenExpiredThenCreateNewStatement(SqlAuthentication auth } } } catch (Exception e) { - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().contains(ERR_MSG_RESULTSET_IS_CLOSED)); } } @@ -161,7 +161,7 @@ private void testAccessTokenExpiredThenExecuteUsingSameStatement( } } } catch (Exception e) { - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().contains(ERR_MSG_RESULTSET_IS_CLOSED)); } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/ErrorMessageTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/ErrorMessageTest.java index 719ff88842..ce92d69164 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/ErrorMessageTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/ErrorMessageTest.java @@ -17,8 +17,10 @@ import org.junit.platform.runner.JUnitPlatform; import org.junit.runner.RunWith; +import com.microsoft.sqlserver.jdbc.SQLServerConnectionTest; import com.microsoft.sqlserver.jdbc.SQLServerDataSource; import com.microsoft.sqlserver.jdbc.SQLServerException; +import com.microsoft.sqlserver.jdbc.TestResource; import com.microsoft.sqlserver.testframework.Constants; @@ -41,7 +43,7 @@ public void testWrongAccessTokenWithConnectionStringUserName() throws SQLExcepti if (!(e instanceof SQLServerException)) { fail(e.getMessage()); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), e.getMessage().contains(ERR_MSG_LOGIN_FAILED)); + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().contains(ERR_MSG_LOGIN_FAILED)); } } @@ -59,7 +61,7 @@ public void testWrongAccessTokenWithDatasource() throws SQLException { if (!(e instanceof SQLServerException)) { fail(e.getMessage()); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), e.getMessage().contains(ERR_MSG_LOGIN_FAILED)); + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().contains(ERR_MSG_LOGIN_FAILED)); } } @@ -71,7 +73,7 @@ public void testCorrectAccessTokenPassedInConnectionString() { if (!(e instanceof SQLServerException)) { fail(EXPECTED_EXCEPTION_NOT_THROWN); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), e.getMessage().contains(ERR_MSG_LOGIN_FAILED)); + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().contains(ERR_MSG_LOGIN_FAILED)); } } @@ -86,7 +88,7 @@ public void testNotProvideWithConnectionStringUserName() throws SQLException { } String wrongUserName = azureUserName.split("@")[1]; - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith( ERR_MSG_CANNOT_OPEN_SERVER + " \"" + wrongUserName + "\" requested by the login.") || e.getMessage().startsWith(ERR_TCPIP_CONNECTION)); @@ -111,7 +113,7 @@ public void testNotProvideWithDatasource() throws SQLException { } String wrongUserName = azureUserName.split("@")[1]; - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith( ERR_MSG_CANNOT_OPEN_SERVER + " \"" + wrongUserName + "\" requested by the login.") || e.getMessage().startsWith(ERR_TCPIP_CONNECTION)); @@ -129,7 +131,7 @@ public void testNotProvideWithConnectionStringUser() throws SQLException { } String wrongUserName = azureUserName.split("@")[1]; - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith( ERR_MSG_CANNOT_OPEN_SERVER + " \"" + wrongUserName + "\" requested by the login.") || e.getMessage().startsWith(ERR_TCPIP_CONNECTION)); @@ -145,7 +147,7 @@ public void testSQLPasswordWithAzureDBWithConnectionStringUserName() throws SQLE if (!(e instanceof SQLServerException)) { fail(EXPECTED_EXCEPTION_NOT_THROWN); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith(ERR_MSG_CANNOT_OPEN_SERVER) || e.getMessage().startsWith(ERR_TCPIP_CONNECTION)); } @@ -168,7 +170,7 @@ public void testSQLPasswordWithAzureDBWithDatasource() throws SQLException { fail(EXPECTED_EXCEPTION_NOT_THROWN); } String wrongUserName = azureUserName.split("@")[1]; - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith( ERR_MSG_CANNOT_OPEN_SERVER + " \"" + wrongUserName + "\" requested by the login.") || e.getMessage().startsWith(ERR_TCPIP_CONNECTION)); @@ -185,7 +187,7 @@ public void testSQLPasswordWithAzureDBWithConnectionStringUser() throws SQLExcep fail(EXPECTED_EXCEPTION_NOT_THROWN); } String wrongUserName = azureUserName.split("@")[1]; - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith( ERR_MSG_CANNOT_OPEN_SERVER + " \"" + wrongUserName + "\" requested by the login.") || e.getMessage().startsWith(ERR_TCPIP_CONNECTION)); @@ -206,7 +208,7 @@ public void testSQLPasswordWithUntrustedSqlDB() throws SQLException { fail(EXPECTED_EXCEPTION_NOT_THROWN); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith(ERR_MSG_CANNOT_OPEN_SERVER) || e.getMessage().startsWith(ERR_TCPIP_CONNECTION)); } @@ -219,7 +221,7 @@ public void testADPasswordUnregisteredUserWithConnectionStringUserName() throws + ";Authentication=" + SqlAuthentication.ActiveDirectoryPassword.toString())) { fail(EXPECTED_EXCEPTION_NOT_THROWN); } catch (SQLServerException e) { - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage() .contains(ERR_MSG_FAILED_AUTHENTICATE + " the user " + badUserName + " in Active Directory (Authentication=ActiveDirectoryPassword).") @@ -240,7 +242,7 @@ public void testADPasswordUnregisteredUserWithDatasource() throws SQLException { try (Connection connection = ds.getConnection()) {} fail(EXPECTED_EXCEPTION_NOT_THROWN); } catch (SQLServerException e) { - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage() .contains(ERR_MSG_FAILED_AUTHENTICATE + " the user " + badUserName + " in Active Directory (Authentication=ActiveDirectoryPassword).") @@ -254,7 +256,7 @@ public void testADPasswordUnregisteredUserWithConnectionStringUser() throws SQLE + azurePassword + ";Authentication=" + SqlAuthentication.ActiveDirectoryPassword.toString())) { fail(EXPECTED_EXCEPTION_NOT_THROWN); } catch (SQLServerException e) { - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage() .contains(ERR_MSG_FAILED_AUTHENTICATE + " the user " + badUserName + " in Active Directory (Authentication=ActiveDirectoryPassword).") @@ -275,8 +277,9 @@ public void testAuthenticationAgainstSQLServerWithActivedirectorypassword() thro if (!(e instanceof SQLServerException)) { fail(EXPECTED_EXCEPTION_NOT_THROWN); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), e.getMessage().contains(ERR_MSG_FAILED_AUTHENTICATE - + " the user " + badUserName + " in Active Directory (Authentication=ActiveDirectoryPassword).")); + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), + e.getMessage().contains(ERR_MSG_FAILED_AUTHENTICATE + " the user " + badUserName + + " in Active Directory (Authentication=ActiveDirectoryPassword).")); } } @@ -316,7 +319,7 @@ public void testNotSpecifiedWithConnectionStringUserName() throws SQLException { } String wrongUserName = azureUserName.split("@")[1]; - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith( ERR_MSG_CANNOT_OPEN_SERVER + " \"" + wrongUserName + "\" requested by the login.") || e.getMessage().startsWith(ERR_TCPIP_CONNECTION)); @@ -343,7 +346,7 @@ public void testNotSpecifiedWithDataSource() throws SQLException { } String wrongUserName = azureUserName.split("@")[1]; - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith( ERR_MSG_CANNOT_OPEN_SERVER + " \"" + wrongUserName + "\" requested by the login.") || e.getMessage().startsWith(ERR_TCPIP_CONNECTION)); @@ -361,7 +364,7 @@ public void testNotSpecifiedWithConnectionStringUser() throws SQLException { } String wrongUserName = azureUserName.split("@")[1]; - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith( ERR_MSG_CANNOT_OPEN_SERVER + " \"" + wrongUserName + "\" requested by the login.") || e.getMessage().startsWith(ERR_TCPIP_CONNECTION)); @@ -390,7 +393,7 @@ public void testADPasswordWrongPasswordWithConnectionStringUserName() throws SQL fail(EXPECTED_EXCEPTION_NOT_THROWN); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), e.getMessage() + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage() .contains(ERR_MSG_FAILED_AUTHENTICATE + " the user " + azureUserName + " in Active Directory (Authentication=ActiveDirectoryPassword).") && (e.getCause().getCause().getMessage().toLowerCase().contains("invalid username or password") @@ -415,7 +418,7 @@ public void testADPasswordWrongPasswordWithDatasource() throws SQLException { fail(EXPECTED_EXCEPTION_NOT_THROWN); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), e.getMessage() + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage() .contains(ERR_MSG_FAILED_AUTHENTICATE + " the user " + azureUserName + " in Active Directory (Authentication=ActiveDirectoryPassword).") && (e.getCause().getCause().getMessage().toLowerCase().contains("invalid username or password") @@ -434,7 +437,7 @@ public void testADPasswordWrongPasswordWithConnectionStringUser() throws SQLExce fail(EXPECTED_EXCEPTION_NOT_THROWN); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), e.getMessage() + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage() .contains(ERR_MSG_FAILED_AUTHENTICATE + " the user " + azureUserName + " in Active Directory (Authentication=ActiveDirectoryPassword).") && (e.getCause().getCause().getMessage().toLowerCase().contains("invalid username or password") @@ -459,7 +462,7 @@ public void testSetAuthenticationWithIntegratedSecurityTrueWithDatasource() thro if (!(e instanceof SQLServerException)) { fail(EXPECTED_EXCEPTION_NOT_THROWN); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith(ERR_MSG_NOT_AUTH_AND_IS)); } } @@ -474,7 +477,7 @@ public void testSetAuthenticationWithIntegratedSecurityTrueWithConnectionStringU if (!(e instanceof SQLServerException)) { fail(EXPECTED_EXCEPTION_NOT_THROWN); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith(ERR_MSG_NOT_AUTH_AND_IS)); } } @@ -489,7 +492,7 @@ public void testSetAuthenticationWithIntegratedSecurityTrueWithConnectionStringU if (!(e instanceof SQLServerException)) { fail(EXPECTED_EXCEPTION_NOT_THROWN); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith(ERR_MSG_NOT_AUTH_AND_IS)); } } @@ -509,7 +512,7 @@ public void testADIntegratedWithUserAndPasswordWithDataSource() throws SQLExcept if (!(e instanceof SQLServerException)) { fail(EXPECTED_EXCEPTION_NOT_THROWN); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith(ERR_MSG_NOT_AUTH_AND_USER_PASSWORD)); } } @@ -524,7 +527,7 @@ public void testADIntegratedWithUserAndPasswordWithConnectionStringUserName() th if (!(e instanceof SQLServerException)) { fail(EXPECTED_EXCEPTION_NOT_THROWN); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith(ERR_MSG_NOT_AUTH_AND_USER_PASSWORD)); } } @@ -538,7 +541,7 @@ public void testADIntegratedWithUserAndPasswordWithConnectionStringUser() throws if (!(e instanceof SQLServerException)) { fail(EXPECTED_EXCEPTION_NOT_THROWN); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith(ERR_MSG_NOT_AUTH_AND_USER_PASSWORD)); } } @@ -558,7 +561,7 @@ public void testSetBothAccessTokenAndAuthentication() throws SQLException { if (!(e instanceof SQLServerException)) { fail(EXPECTED_EXCEPTION_NOT_THROWN); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith(ERR_MSG_CANNOT_SET_ACCESS_TOKEN)); } } @@ -578,7 +581,7 @@ public void testAccessTokenWithIntegratedSecurityTrue() throws SQLException { if (!(e instanceof SQLServerException)) { fail(EXPECTED_EXCEPTION_NOT_THROWN); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith(ERR_MSG_CANNOT_SET_ACCESS_TOKEN)); } } @@ -600,7 +603,7 @@ public void testAccessTokenWithUserAndPasswordWithDatasource() throws SQLExcepti if (!(e instanceof SQLServerException)) { fail(EXPECTED_EXCEPTION_NOT_THROWN); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith(ERR_MSG_CANNOT_SET_ACCESS_TOKEN)); } } @@ -617,7 +620,7 @@ public void testAccessTokenWithUserAndPasswordWithConnectionStringUserName() thr if (!(e instanceof SQLServerException)) { fail(EXPECTED_EXCEPTION_NOT_THROWN); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith(ERR_MSG_CANNOT_SET_ACCESS_TOKEN)); } } @@ -634,7 +637,7 @@ public void testAccessTokenWithUserAndPasswordWithConnectionStringUser() throws if (!(e instanceof SQLServerException)) { fail(EXPECTED_EXCEPTION_NOT_THROWN); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith(ERR_MSG_CANNOT_SET_ACCESS_TOKEN)); } } @@ -654,7 +657,7 @@ public void testAccessTokenEmpty() throws SQLException { if (!(e instanceof SQLServerException)) { fail(EXPECTED_EXCEPTION_NOT_THROWN); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().contains(ERR_MSG_ACCESS_TOKEN_EMPTY)); } } @@ -674,7 +677,7 @@ public void testADPasswordWithoutUser() throws SQLException { if (!(e instanceof SQLServerException)) { fail(EXPECTED_EXCEPTION_NOT_THROWN); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith(ERR_MSG_BOTH_USERNAME_PASSWORD_ACTIVEPASSWORD)); } } @@ -694,7 +697,7 @@ public void testADPasswordWithoutPasswordWithDatasource() throws SQLException { if (!(e instanceof SQLServerException)) { fail(EXPECTED_EXCEPTION_NOT_THROWN); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith(ERR_MSG_BOTH_USERNAME_PASSWORD_ACTIVEPASSWORD)); } } @@ -708,7 +711,7 @@ public void testADPasswordWithoutPasswordWithConnectionStringUserName() throws S if (!(e instanceof SQLServerException)) { fail(EXPECTED_EXCEPTION_NOT_THROWN); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith(ERR_MSG_BOTH_USERNAME_PASSWORD_ACTIVEPASSWORD)); } } @@ -722,7 +725,7 @@ public void testADPasswordWithoutPasswordWithConnectionStringUser() throws SQLEx if (!(e instanceof SQLServerException)) { fail(EXPECTED_EXCEPTION_NOT_THROWN); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith(ERR_MSG_BOTH_USERNAME_PASSWORD_ACTIVEPASSWORD)); } } @@ -742,7 +745,7 @@ public void testSqlPasswordWithoutUser() throws SQLException { if (!(e instanceof SQLServerException)) { fail(EXPECTED_EXCEPTION_NOT_THROWN); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith(ERR_MSG_BOTH_USERNAME_PASSWORD_SQLPASSWORD)); } } @@ -762,7 +765,7 @@ public void testSqlPasswordWithoutPasswordWithDatasource() throws SQLException { if (!(e instanceof SQLServerException)) { fail(EXPECTED_EXCEPTION_NOT_THROWN); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith(ERR_MSG_BOTH_USERNAME_PASSWORD_SQLPASSWORD)); } } @@ -776,7 +779,7 @@ public void testSqlPasswordWithoutPasswordWithConnectionStringUserName() throws if (!(e instanceof SQLServerException)) { fail(EXPECTED_EXCEPTION_NOT_THROWN); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith(ERR_MSG_BOTH_USERNAME_PASSWORD_SQLPASSWORD)); } } @@ -790,7 +793,7 @@ public void testSqlPasswordWithoutPasswordWithConnectionStringUser() throws SQLE if (!(e instanceof SQLServerException)) { fail(EXPECTED_EXCEPTION_NOT_THROWN); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith(ERR_MSG_BOTH_USERNAME_PASSWORD_SQLPASSWORD)); } } @@ -811,8 +814,34 @@ public void testInvalidAuthentication() throws SQLException { if (!(e instanceof SQLServerException)) { fail(EXPECTED_EXCEPTION_NOT_THROWN); } - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().startsWith("The authentication value") && e.getMessage().endsWith("is not valid.")); } } + + @Test + public void testInteractiveAuthTimeout() throws SQLException { + try { + SQLServerDataSource ds = new SQLServerDataSource(); + ds.setServerName(azureServer); + ds.setUser(azureUserName); + ds.setDatabaseName(azureDatabase); + ds.setAuthentication("ActiveDirectoryInteractive"); + + ds.setEncrypt(false); + ds.setTrustServerCertificate(true); + try (Connection connection = ds.getConnection()) {} + fail(EXPECTED_EXCEPTION_NOT_THROWN); + } catch (Exception e) { + if (!(e instanceof SQLServerException)) { + fail(EXPECTED_EXCEPTION_NOT_THROWN); + } + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage() + "," + e.getCause(), + e.getMessage() + .contains(ERR_MSG_FAILED_AUTHENTICATE + " the user " + azureUserName + + " in Active Directory (Authentication=ActiveDirectoryInteractive).") + && (isWindows ? e.getCause().getMessage() + .contains(TestResource.getResource("R_noAuthorizationCode")) : true)); + } + } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/FedauthCommon.java b/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/FedauthCommon.java index b35ee72e64..de29d91580 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/FedauthCommon.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/FedauthCommon.java @@ -42,6 +42,8 @@ public class FedauthCommon extends AbstractTest { static String azureUserName = null; static String azurePassword = null; static String azureGroupUserName = null; + static String azureAADPrincipialId = null; + static String azureAADPrincipialSecret = null; static boolean enableADIntegrated = false; @@ -54,7 +56,7 @@ public class FedauthCommon extends AbstractTest { static String[] fedauthJksPathsLinux = null; static String[] fedauthJavaKeyAliases = null; - static final String INVALID_EXCEPION_MSG = TestResource.getResource("R_invalidExceptionMessage"); + static final String INVALID_EXCEPTION_MSG = TestResource.getResource("R_invalidExceptionMessage"); static final String EXPECTED_EXCEPTION_NOT_THROWN = TestResource.getResource("R_expectedExceptionNotThrown"); static final String ERR_MSG_LOGIN_FAILED = TestResource.getResource("R_loginFailed"); static final String ERR_MSG_BOTH_USERNAME_PASSWORD_ACTIVEPASSWORD = TestUtils.R_BUNDLE @@ -83,7 +85,8 @@ enum SqlAuthentication { NotSpecified, SqlPassword, ActiveDirectoryPassword, - ActiveDirectoryIntegrated; + ActiveDirectoryIntegrated, + ActiveDirectoryServicePrincipal; static SqlAuthentication valueOfString(String value) throws SQLServerException { SqlAuthentication method = null; @@ -98,6 +101,9 @@ static SqlAuthentication valueOfString(String value) throws SQLServerException { } else if (value.toLowerCase(Locale.US) .equalsIgnoreCase(SqlAuthentication.ActiveDirectoryIntegrated.toString())) { method = SqlAuthentication.ActiveDirectoryIntegrated; + } else if (value.toLowerCase(Locale.US) + .equalsIgnoreCase(SqlAuthentication.ActiveDirectoryServicePrincipal.toString())) { + method = SqlAuthentication.ActiveDirectoryServicePrincipal; } return method; } @@ -118,6 +124,8 @@ public static void getConfigs() throws Exception { azureUserName = getConfiguredProperty("azureUserName"); azurePassword = getConfiguredProperty("azurePassword"); azureGroupUserName = getConfiguredProperty("azureGroupUserName"); + azureAADPrincipialId = getConfiguredProperty("AADSecurePrincipalId"); + azureAADPrincipialSecret = getConfiguredProperty("AADSecurePrincipalSecret"); String prop = getConfiguredProperty("enableADIntegrated"); enableADIntegrated = (null != prop && prop.equalsIgnoreCase("true")) ? true : false; diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/FedauthTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/FedauthTest.java index 45308fbaeb..eae81d8e07 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/FedauthTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/FedauthTest.java @@ -4,8 +4,9 @@ */ package com.microsoft.sqlserver.jdbc.fedauth; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.Assert.fail; import java.io.File; import java.io.FileInputStream; @@ -238,6 +239,62 @@ public void testCorrectAccessTokenDS() throws SQLException { } } + /** + * Test the actual AAD Service Principal Authentication using connection string, data source and SSL encryption. + */ + @Test + public void testAADServicePrincipalAuth() { + String url = "jdbc:sqlserver://" + azureServer + ";database=" + azureDatabase + ";authentication=" + + SqlAuthentication.ActiveDirectoryServicePrincipal + ";AADSecurePrincipalId=" + azureAADPrincipialId + + ";AADSecurePrincipalSecret=" + azureAADPrincipialSecret; + String urlEncrypted = url + ";encrypt=true;trustServerCertificate=true;"; + SQLServerDataSource ds = new SQLServerDataSource(); + updateDataSource(url, ds); + try (Connection conn1 = DriverManager.getConnection(url); Connection conn2 = ds.getConnection(); + Connection conn3 = DriverManager.getConnection(urlEncrypted)) { + assertNotNull(conn1); + assertNotNull(conn2); + assertNotNull(conn3); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + /** + * Test invalid connection property combinations when using AAD Service Principal Authentication. + */ + @Test + public void testAADServicePrincipalAuthWrong() { + String baseUrl = "jdbc:sqlserver://" + azureServer + ";database=" + azureDatabase + ";authentication=" + + SqlAuthentication.ActiveDirectoryServicePrincipal + ";"; + // Wrong AADSecurePrincipalSecret provided. + String url = baseUrl + "AADSecurePrincipalId=" + azureAADPrincipialId + ";AADSecurePrincipalSecret=wrongSecret"; + validateException(url, "R_MSALExecution"); + + // Wrong AADSecurePrincipalId provided. + url = baseUrl + "AADSecurePrincipalId=wrongId;AADSecurePrincipalSecret=" + azureAADPrincipialSecret; + validateException(url, "R_MSALExecution"); + + // AADSecurePrincipalSecret not provided. + url = baseUrl + "AADSecurePrincipalId=" + azureAADPrincipialId; + validateException(url, "R_NoUserPasswordForActiveServicePrincipal"); + + // AADSecurePrincipalId not provided. + url = baseUrl + "AADSecurePrincipalSecret=" + azureAADPrincipialSecret; + validateException(url, "R_NoUserPasswordForActiveServicePrincipal"); + + // Both AADSecurePrincipalId and AADSecurePrincipalSecret not provided. + validateException(baseUrl, "R_NoUserPasswordForActiveServicePrincipal"); + } + + private static void validateException(String url, String resourceKey) { + try (Connection conn = DriverManager.getConnection(url)) { + fail(TestResource.getResource("R_expectedFailPassed")); + } catch (SQLException e) { + assertTrue(e.getMessage().matches(TestUtils.formatErrorMsg(resourceKey))); + } + } + private void testValid(String authentication, boolean encrypt, boolean trustServerCertificate) throws SQLException { try { SQLServerDataSource ds = new SQLServerDataSource(); @@ -259,10 +316,10 @@ private void testValid(String authentication, boolean encrypt, boolean trustServ if (authentication.toLowerCase().contains("activedirectorypassword")) { fail(e.getMessage()); } else if (authentication.toLowerCase().contains("activedirectoryintegrated")) { - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), e.getMessage().contains(ERR_MSG_LOGIN_FAILED) + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().contains(ERR_MSG_LOGIN_FAILED) || e.getMessage().contains(ERR_MSG_FAILED_AUTHENTICATE)); } else { - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().contains(ERR_MSG_CANNOT_OPEN_SERVER) || e.getMessage().startsWith(ERR_TCPIP_CONNECTION)); } @@ -293,10 +350,10 @@ private void testNotValid(String authentication, boolean encrypt, } } catch (Exception e) { if (authentication.toLowerCase().contains("activedirectory")) { - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), e.getMessage().contains(ERR_MSG_LOGIN_FAILED) + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().contains(ERR_MSG_LOGIN_FAILED) || e.getMessage().contains(ERR_MSG_FAILED_AUTHENTICATE)); } else { - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().contains(ERR_MSG_CANNOT_OPEN_SERVER) || e.getMessage().startsWith(ERR_TCPIP_CONNECTION)); } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/PooledConnectionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/PooledConnectionTest.java index 9183b66837..2aaaf32bee 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/PooledConnectionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/fedauth/PooledConnectionTest.java @@ -168,7 +168,7 @@ private void testPooledConnectionMultiThread(long testingTimeInSeconds, try (Connection connection2 = pc.getConnection()) { testUserName(connection2, azureUserName, authentication); } catch (SQLException e) { - assertTrue(INVALID_EXCEPION_MSG + ": " + e.getMessage(), + assertTrue(INVALID_EXCEPTION_MSG + ": " + e.getMessage(), e.getMessage().contains(ERR_MSG_CONNECTION_CLOSED) || e.getMessage().contains(ERR_MSG_CONNECTION_IS_CLOSED) || e.getMessage().contains(ERR_MSG_HAS_CLOSED) diff --git a/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java b/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java index 739cdb6ca5..d4e52f771f 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java @@ -353,8 +353,18 @@ protected static ISQLServerDataSource updateDataSource(String connectionString, case Constants.CLIENT_KEY_PASSWORD: ds.setClientKeyPassword(value); break; + case Constants.AAD_SECURE_PRINCIPAL_ID: + ds.setAADSecurePrincipalId(value); + break; + case Constants.AAD_SECURE_PRINCIPAL_SECRET: + ds.setAADSecurePrincipalSecret(value); + break; case Constants.SEND_TEMPORAL_DATATYPES_AS_STRING_FOR_BULK_COPY: ds.setSendTemporalDataTypesAsStringForBulkCopy(Boolean.parseBoolean(value)); + break; + case Constants.MAX_RESULT_BUFFER: + ds.setMaxResultBuffer(value); + break; default: break; } diff --git a/src/test/java/com/microsoft/sqlserver/testframework/Constants.java b/src/test/java/com/microsoft/sqlserver/testframework/Constants.java index 4b804b0aab..2207451553 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/Constants.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/Constants.java @@ -134,6 +134,7 @@ private Constants() {} public static final String CANCEL_QUERY_TIMEOUT = "CANCELQUERYTIMEOUT"; public static final String ENCRYPT = "ENCRYPT"; public static final String HOST_NAME_IN_CERTIFICATE = "HOSTNAMEINCERTIFICATE"; + public static final String MAX_RESULT_BUFFER = "MAXRESULTBUFFER"; // End: Connection Properties parsed in AbstractTest class for creating DataSource. // Other Connection Properties set in FipsTest @@ -156,6 +157,8 @@ private Constants() {} public static final String KEYSTORE_LOCATION = "KEYSTORELOCATION"; public static final String CLIENT_CERTIFICATE = "CLIENTCERTIFICATE"; public static final String CLIENT_KEY = "CLIENTKEY"; + public static final String AAD_SECURE_PRINCIPAL_ID = "AADSECUREPRINCIPALID"; + public static final String AAD_SECURE_PRINCIPAL_SECRET = "AADSECUREPRINCIPALSECRET"; public static final String CLIENT_KEY_PASSWORD = "CLIENTKEYPASSWORD"; public static final String SEND_TEMPORAL_DATATYPES_AS_STRING_FOR_BULK_COPY = "SENDTEMPORALDATATYPESASSTRINGFORBULKCOPY"; public static final String CONFIG_PROPERTIES_FILE = "config.properties";