diff --git a/PSql/Resolve-SqlClient.ps1 b/PSql/Resolve-SqlClient.ps1
index 527b5b0..f5fa793 100644
--- a/PSql/Resolve-SqlClient.ps1
+++ b/PSql/Resolve-SqlClient.ps1
@@ -6,3 +6,6 @@ else
{
Add-Type -Path (Join-Path $PSScriptRoot runtimes/unix/lib/netcoreapp3.1/Microsoft.Data.SqlClient.dll)
}
+
+# Required for Azure Active Directory authentication modes
+Add-Type -Path (Join-Path $PSScriptRoot Microsoft.Identity.Client.dll)
diff --git a/PSql/_Commands/NewSqlContextCommand.cs b/PSql/_Commands/NewSqlContextCommand.cs
index fb0f0ef..e5387dd 100644
--- a/PSql/_Commands/NewSqlContextCommand.cs
+++ b/PSql/_Commands/NewSqlContextCommand.cs
@@ -18,30 +18,35 @@ private const string
// -ResourceGroupName
[Alias("ResourceGroup")]
- [Parameter(ParameterSetName = AzureName, Position = 1, Mandatory = true, ValueFromPipelineByPropertyName = true)]
+ [Parameter(ParameterSetName = AzureName, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
public string ResourceGroupName { get; set; }
// -ServerName
[Alias("Server")]
[Parameter(ParameterSetName = GenericName, Position = 0, ValueFromPipelineByPropertyName = true)]
- [Parameter(ParameterSetName = AzureName, Position = 2, Mandatory = true, ValueFromPipelineByPropertyName = true)]
+ [Parameter(ParameterSetName = AzureName, Position = 1, Mandatory = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
public string ServerName { get; set; }
// -DatabaseName
[Alias("Database")]
[Parameter(ParameterSetName = GenericName, Position = 1, ValueFromPipelineByPropertyName = true)]
- [Parameter(ParameterSetName = AzureName, Position = 3, Mandatory = true, ValueFromPipelineByPropertyName = true)]
+ [Parameter(ParameterSetName = AzureName, Position = 2, Mandatory = true, ValueFromPipelineByPropertyName = true)]
[ValidateNotNullOrEmpty]
public string DatabaseName { get; set; }
// -Credential
- [Parameter(ParameterSetName = GenericName, Position = 2, ValueFromPipelineByPropertyName = true)]
- [Parameter(ParameterSetName = AzureName, Position = 4, Mandatory = true, ValueFromPipelineByPropertyName = true)]
+ [Parameter(ParameterSetName = GenericName, Position = 2, ValueFromPipelineByPropertyName = true)]
+ [Parameter(ParameterSetName = AzureName, Position = 3, ValueFromPipelineByPropertyName = true)]
[Credential]
public PSCredential Credential { get; set; } = PSCredential.Empty;
+ // -AuthenticationMode
+ [Alias("Auth")]
+ [Parameter(ParameterSetName = AzureName, ValueFromPipelineByPropertyName = true)]
+ public AzureAuthenticationMode AuthenticationMode { get; set; }
+
// -EncryptionMode
[Alias("Encryption")]
[Parameter(ParameterSetName = GenericName, ValueFromPipelineByPropertyName = true)]
@@ -70,11 +75,25 @@ private const string
[ValidateRange("0:00:00", "24855.03:14:07")]
public TimeSpan? ConnectTimeout { get; set; }
+ // -ExposeCredentialInConnectionString
+ [Parameter(ValueFromPipelineByPropertyName = true)]
+ public SwitchParameter ExposeCredentialInConnectionString { get; set; }
+
+ // -Pooling
+ [Parameter(ValueFromPipelineByPropertyName = true)]
+ public SwitchParameter Pooling { get; set; }
+
+ // -MultipleActiveResultSets
+ [Alias("Mars")]
+ [Parameter(ValueFromPipelineByPropertyName = true)]
+ public SwitchParameter MultipleActiveResultSets { get; set; } = true;
+
protected override void ProcessRecord()
{
var context = Azure.IsPresent
- ? new AzureSqlContext { ResourceGroupName = ResourceGroupName }
- : new SqlContext { EncryptionMode = EncryptionMode };
+ ? new AzureSqlContext { ResourceGroupName = ResourceGroupName ,
+ AuthenticationMode = AuthenticationMode }
+ : new SqlContext { EncryptionMode = EncryptionMode };
var credential = Credential.IsNullOrEmpty()
? null
@@ -88,6 +107,10 @@ protected override void ProcessRecord()
context.ApplicationName = ApplicationName;
context.ApplicationIntent = ReadOnlyIntent ? ReadOnly : ReadWrite;
+ context.ExposeCredentialInConnectionString = ExposeCredentialInConnectionString;
+ context.EnableConnectionPooling = Pooling;
+ context.EnableMultipleActiveResultSets = MultipleActiveResultSets;
+
WriteObject(context);
}
}
diff --git a/PSql/_Data/AzureAuthenticationMode.cs b/PSql/_Data/AzureAuthenticationMode.cs
new file mode 100644
index 0000000..308cb99
--- /dev/null
+++ b/PSql/_Data/AzureAuthenticationMode.cs
@@ -0,0 +1,55 @@
+using Sam = Microsoft.Data.SqlClient.SqlAuthenticationMethod;
+
+namespace PSql
+{
+ ///
+ /// Modes for authentiating connections to Azure SQL Database and
+ /// compatible databases.
+ ///
+ public enum AzureAuthenticationMode
+ {
+ ///
+ /// Default authentication mode. The actual authentication mode
+ /// depends on the value of the
+ /// property. If the property is non-null, this mode selects
+ /// SQL authentication using the credential. If the property is
+ /// null, this mode selects Azure AD integrated
+ /// authentication.
+ ///
+ Default = Sam.NotSpecified,
+
+ ///
+ /// SQL authentication mode. The
+ /// property should contain the name and password stored for a server
+ /// login or contained database user.
+ ///
+ SqlPassword = Sam.SqlPassword,
+
+ ///
+ /// Azure Active Directory password authentication mode. The
+ /// property should contain the
+ /// name and password of an Azure AD principal.
+ ///
+ AadPassword = Sam.ActiveDirectoryPassword,
+
+ ///
+ /// Azure Active Directory integrated authentication mode. The
+ /// identity of the process should be an Azure AD principal.
+ ///
+ AadIntegrated = Sam.ActiveDirectoryIntegrated,
+
+ ///
+ /// Azure Active Directory interactive authentication mode, also
+ /// known as Universal Authentication with MFA. Authentication uses
+ /// an interactive flow and supports multiple factors.
+ ///
+ AadInteractive = Sam.ActiveDirectoryInteractive,
+
+ ///
+ /// Azure Active Directory service principal authentication mode.
+ /// The property contains the
+ /// client ID and secret of an Azure AD service principal.
+ ///
+ AadServicePrincipal = Sam.ActiveDirectoryServicePrincipal
+ }
+}
diff --git a/PSql/_Data/AzureSqlContext.cs b/PSql/_Data/AzureSqlContext.cs
index 317bd33..48dd1f2 100644
--- a/PSql/_Data/AzureSqlContext.cs
+++ b/PSql/_Data/AzureSqlContext.cs
@@ -14,6 +14,7 @@ public class AzureSqlContext : SqlContext
{
public AzureSqlContext()
{
+ // Encryption is required for connections to Azure SQL Database
EncryptionMode = EncryptionMode.Full;
}
@@ -21,11 +22,10 @@ public AzureSqlContext()
public string ServerFullName { get; private set; }
+ public AzureAuthenticationMode AuthenticationMode { get; set; }
+
protected override void BuildConnectionString(SqlConnectionStringBuilder builder)
{
- if (Credential.IsNullOrEmpty())
- throw new NotSupportedException("A credential is required when connecting to Azure SQL Database.");
-
base.BuildConnectionString(builder);
builder.DataSource = ServerFullName ?? ResolveServerFullName();
@@ -34,13 +34,56 @@ protected override void BuildConnectionString(SqlConnectionStringBuilder builder
builder.InitialCatalog = MasterDatabaseName;
}
+ protected override void ConfigureAuthentication(SqlConnectionStringBuilder builder)
+ {
+ var auth = (SqlAuthenticationMethod) AuthenticationMode;
+
+ switch (auth)
+ {
+ case SqlAuthenticationMethod.NotSpecified when Credential != null:
+ auth = SqlAuthenticationMethod.SqlPassword;
+ break;
+
+ case SqlAuthenticationMethod.NotSpecified:
+ auth = SqlAuthenticationMethod.ActiveDirectoryIntegrated;
+ break;
+
+ case SqlAuthenticationMethod.SqlPassword:
+ case SqlAuthenticationMethod.ActiveDirectoryPassword:
+ case SqlAuthenticationMethod.ActiveDirectoryServicePrincipal:
+ if (Credential.IsNullOrEmpty())
+ throw new NotSupportedException("A credential is required when connecting to Azure SQL Database.");
+ break;
+ }
+
+ builder.Authentication = auth;
+ }
+
protected override void ConfigureEncryption(SqlConnectionStringBuilder builder)
{
+ // Encryption is required for connections to Azure SQL Database
builder.Encrypt = true;
+
+ // Always verify server identity
+ // builder.TrustServerCertificate defaults to false
}
private string ResolveServerFullName()
{
+ // Check if ServerName should be used as ServerFullName verbatim
+
+ if (string.IsNullOrEmpty(ServerName))
+ throw new InvalidOperationException("ServerName is required.");
+
+ var shouldUseServerNameVerbatim
+ = ServerName.Contains('.', StringComparison.Ordinal)
+ || string.IsNullOrEmpty(ResourceGroupName);
+
+ if (shouldUseServerNameVerbatim)
+ return ServerName;
+
+ // Resolve ServerFullName using Az cmdlets
+
var value = ScriptBlock
.Create("param ($x) Get-AzSqlServer @x -ea Stop")
.Invoke(new Dictionary
diff --git a/PSql/_Data/SqlContext.cs b/PSql/_Data/SqlContext.cs
index f54f281..ad337cd 100644
--- a/PSql/_Data/SqlContext.cs
+++ b/PSql/_Data/SqlContext.cs
@@ -31,6 +31,12 @@ protected const string
public ApplicationIntent ApplicationIntent { get; set; }
+ public bool ExposeCredentialInConnectionString { get; set; }
+
+ public bool EnableConnectionPooling { get; set; }
+
+ public bool EnableMultipleActiveResultSets { get; set; }
+
internal SqlConnection CreateConnection(string databaseName)
{
var builder = new SqlConnectionStringBuilder();
@@ -62,14 +68,9 @@ protected virtual void BuildConnectionString(SqlConnectionStringBuilder builder)
//else
// server determines database
- // Authentication
- if (Credential.IsNullOrEmpty())
- builder.IntegratedSecurity = true;
- //else
- // will provide credential as a SqlCredential object
-
- // Encryption & Server Identity Check
- ConfigureEncryption(builder);
+ // Security
+ ConfigureAuthentication (builder);
+ ConfigureEncryption (builder);
// Timeout
if (ConnectTimeout.HasValue)
@@ -88,7 +89,18 @@ protected virtual void BuildConnectionString(SqlConnectionStringBuilder builder)
builder.ApplicationIntent = ApplicationIntent;
// Other
- builder.Pooling = false;
+ builder.PersistSecurityInfo = ExposeCredentialInConnectionString;
+ builder.MultipleActiveResultSets = EnableMultipleActiveResultSets;
+ builder.Pooling = EnableConnectionPooling;
+ }
+
+ protected virtual void ConfigureAuthentication(SqlConnectionStringBuilder builder)
+ {
+ // Authentication
+ if (Credential.IsNullOrEmpty())
+ builder.IntegratedSecurity = true;
+ //else
+ // will provide credential as a SqlCredential object
}
protected virtual void ConfigureEncryption(SqlConnectionStringBuilder builder)
@@ -105,11 +117,14 @@ protected virtual void ConfigureEncryption(SqlConnectionStringBuilder builder)
private (bool, bool) TranslateEncryptionMode(EncryptionMode mode)
{
+ // tuple: (useEncryption, useServerIdentityCheck)
+
switch (mode)
{
- case EncryptionMode.None: return (false, false);
- case EncryptionMode.Unverified: return (true, false);
- case EncryptionMode.Full: return (true, true );
+ // ( ENCRYPT, VERIFY )
+ case EncryptionMode.None: return ( false, false );
+ case EncryptionMode.Unverified: return ( true, false );
+ case EncryptionMode.Full: return ( true, true );
case EncryptionMode.Default:
default:
var isRemote = !GetIsLocal();
diff --git a/PSql/en-US/PSql.dll-help.xml b/PSql/en-US/PSql.dll-help.xml
index 6b5a82e..47314ac 100644
--- a/PSql/en-US/PSql.dll-help.xml
+++ b/PSql/en-US/PSql.dll-help.xml
@@ -607,28 +607,41 @@
ConnectTimeout
TimeSpan
+
+ ExposeCredentialInConnectionString
+
+
+ Pooling
+
+
+ MultipleActiveResultSets
+
New-SqlContext
Azure
-
+
ResourceGroupName
string
-
+
ServerName
string
-
+
DatabaseName
string
-
+
Credential
PSCredential
+
+ AuthenticationMode
+ { Default | SqlPassword | AadPassword | AadIntegrated | AadInteractive | AadServicePrincipal }
+
ReadOnlyIntent
@@ -644,6 +657,15 @@
ConnectTimeout
TimeSpan
+
+ ExposeCredentialInConnectionString
+
+
+ Pooling
+
+
+ MultipleActiveResultSets
+
@@ -661,7 +683,7 @@
False
-
+
ResourceGroupName
The name of the Azure resource group containing the virtual database server. Requires the -Azure switch.
@@ -673,11 +695,11 @@
None
-
+
ServerName
The name of the database server.
- When -Azure is specified, this parameter specifies the Azure resource name of the virtual database server.
+ When both -Azure and -ResourceGroupName are specified, this parameter specifies the Azure resource name of the virtual database server.
Otherwise, this parameter specifies the DNS name of the database server, optionally suffixed by a backslash and a database engine instance name. Examples: db.example.com, db.example.com\instance2
string
@@ -687,7 +709,7 @@
None with -Azure; otherwise, a value specifying the default instance on the local machine
-
+
DatabaseName
The name of the database.
@@ -700,7 +722,7 @@
None
-
+
Credential
The credential to use to authenticate with the database server.
@@ -713,6 +735,68 @@
None
+
+ AuthenticationMode
+
+ The method to use to authenticate with Azure SQL Database or compatible database.
+
+ PSql.AuthenticationMode
+
+ PSql.AuthenticationMode
+
+ None
+
+
+ Default
+
+
+ The default authentication mode. Equivalent to SqlPassword if -Credential is specified, and AadIntegrated otherwise.
+
+
+
+
+ SqlPassword
+
+
+ SQL authentication mode. -Credential is required and should match the name and password stored for a server login or contained database user.
+
+
+
+
+ AadPassword
+
+
+ Azure Active Directory password authentication mode. -Credential is required and should match the name and password of an Azure AD principal.
+
+
+
+
+ AadIntegrated
+
+
+ Azure Active Directory integrated authentication mode. The identity of the process should be an Azure AD principal. -Credential is not required.
+
+
+
+
+ AadInteractive
+
+
+ Azure Active Directory interactive authentication mode, also known as Universal Authentication with MFA. Authentication uses an interactive flow and supports multiple factors. -Credential is not required.
+
+
+
+
+ AadServicePrincipal
+
+
+ Azure Active Directory service principal authentication mode. -Credential is required and should match client ID and secret of an Azure AD service principal.
+
+
+
+
+
+
EncryptionMode
@@ -795,6 +879,43 @@
None
+
+ ExposeCredentialInConnectionString
+
+ Specifies that the credential used for authentication should be exposed in connections' ConnectionString property. This is a potential security risk, so use only when necessary.
+
+ SwitchParameter
+
+ System.Management.Automation.SwitchParameter
+
+ False
+
+
+
+ Pooling
+
+ Specifies that connections may be pooled to reduce setup and teardown time. Pooling is useful when making many connections with identical connection strings.
+
+ SwitchParameter
+
+ System.Management.Automation.SwitchParameter
+
+ False
+
+
+
+ MultipleActiveResultSets
+
+ Specifies that connections support execution of multiple batches concurrently, with limitations.
+ For more informatino, see Multiple Active Result Sets (MARS): https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/sql/multiple-active-result-sets-mars .
+
+ SwitchParameter
+
+ System.Management.Automation.SwitchParameter
+
+ False
+
+
diff --git a/README.md b/README.md
index 00ed488..bbee731 100644
--- a/README.md
+++ b/README.md
@@ -5,3 +5,10 @@ Cmdlets for SQL Server and Azure SQL databases.
## Status
Experimental, but based on previous work already used in production code.
+
+## Contributors
+
+Many thanks to the following contributors:
+
+**@Jezour**:
+ [#1](https://github.com/sharpjs/PSql/pull/1)