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)