diff --git a/CHANGELOG.md b/CHANGELOG.md index d39fc1661..47d3efadd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,8 @@ The new optional parameters are respectively SqlSvcStartupType, AgtSvcStartupType, AsSvcStartupType, IsSvcStartupType and RsSvcStartupType ([issue #1165](https://github.com/PowerShell/SqlServerDsc/issues/1165). [Maxime Daniou (@mdaniou)](https://github.com/mdaniou) +- New DSC resource SqlServerSecureConnection + - New resource to configure a SQL Server instance for encrypted SQL connections. ## 11.4.0.0 diff --git a/DSCResources/MSFT_SqlServerSecureConnection/MSFT_SqlServerSecureConnection.psm1 b/DSCResources/MSFT_SqlServerSecureConnection/MSFT_SqlServerSecureConnection.psm1 new file mode 100644 index 000000000..e14fd931c --- /dev/null +++ b/DSCResources/MSFT_SqlServerSecureConnection/MSFT_SqlServerSecureConnection.psm1 @@ -0,0 +1,568 @@ +Import-Module -Name (Join-Path -Path (Split-Path (Split-Path $PSScriptRoot -Parent) -Parent) ` + -ChildPath 'SqlServerDscHelper.psm1') -Force + +$script:localizedData = Get-LocalizedData -ResourceName 'MSFT_SqlServerSecureConnection' + +<# + .SYNOPSIS + Gets the SQL Server Encryption status. + + .PARAMETER InstanceName + Name of the SQL Server instance to be configured. + + .PARAMETER Thumbprint + Thumbprint of the certificate being used for encryption. If parameter Ensure is set to 'Absent', then the parameter Thumbprint can be set to an empty string. + + .PARAMETER ForceEncryption + If all connections to the SQL instance should be encrypted. If this parameter is not assigned a value, the default is that all connections must be encrypted. + + .PARAMETER Ensure + If Encryption should be Enabled (Present) or Disabled (Absent). + + .PARAMETER ServiceAccount + Name of the account running the SQL Server service. +#> +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $InstanceName, + + [Parameter(Mandatory = $true)] + [System.String] + [AllowEmptyString()] + $Thumbprint, + + [Parameter()] + [System.Boolean] + $ForceEncryption = $true, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter(Mandatory = $true)] + [System.String] + $ServiceAccount + ) + + Write-Verbose -Message ( + $script:localizedData.GetEncryptionSettings ` + -f $InstanceName + ) + + $encryptionSettings = Get-EncryptedConnectionSetting -InstanceName $InstanceName + + Write-Verbose -Message ( + $script:localizedData.EncryptedSettings ` + -f $encryptionSettings.Certificate, $encryptionSettings.ForceEncryption + ) + + if ($Ensure -eq 'Present') + { + $ensureValue = 'Present' + $certificateSettings = Test-CertificatePermission -Thumbprint $Thumbprint -ServiceAccount $ServiceAccount + if ($encryptionSettings.Certificate -ine $Thumbprint) + { + Write-Verbose -Message ( + $script:localizedData.ThumbprintResult ` + -f $encryptionSettings.Certificate, $Thumbprint + ) + $ensureValue = 'Absent' + } + + if ($encryptionSettings.ForceEncryption -ne $ForceEncryption) + { + Write-Verbose -Message ( + $script:localizedData.ForceEncryptionResult ` + -f $encryptionSettings.ForceEncryption, $ForceEncryption + ) + $ensureValue = 'Absent' + } + + if (-not $certificateSettings) + { + Write-Verbose -Message ( + $script:localizedData.CertificateSettings ` + -f 'Configured' + ) + + $ensureValue = 'Absent' + } + else + { + Write-Verbose -Message ( + $script:localizedData.CertificateSettings ` + -f 'Not Configured' + ) + + } + } + else + { + $ensureValue = 'Absent' + if ($encryptionSettings.ForceEncryption -eq $false) + { + Write-Verbose -Message ( + $script:localizedData.EncryptionOff + ) + } + else + { + $ensureValue = 'Present' + Write-Verbose -Message ( + $script:localizedData.ForceEncryptionResult ` + -f $encryptionSettings.ForceEncryption, $false + ) + } + + if ($encryptionSettings.Certificate -eq '') + { + $certificateValue = 'Empty' + } + else + { + $ensureValue = 'Present' + Write-Verbose -Message ( + $script:localizedData.ThumbprintResult ` + -f $encryptionSettings.Certificate, 'Empty' + ) + $certificateValue = $encryptionSettings.Certificate + } + Write-Verbose -Message ( + $script:localizedData.EncryptedSettings ` + -f $certificateValue, $encryptionSettings.ForceEncryption + ) + } + + return @{ + InstanceName = [System.String] $InstanceName + Thumbprint = [System.String] $encryptionSettings.Certificate + ForceEncryption = [System.Boolean] $encryptionSettings.ForceEncryption + Ensure = [System.String] $ensureValue + ServiceAccount = [System.String] $ServiceAccount + } +} + +<# + .SYNOPSIS + Enables SQL Server Encryption Connection. + + .PARAMETER InstanceName + Name of the SQL Server instance to be configured. + + .PARAMETER Thumbprint + Thumbprint of the certificate being used for encryption. If parameter Ensure is set to 'Absent', then the parameter Thumbprint can be set to an empty string. + + .PARAMETER ForceEncryption + If all connections to the SQL instance should be encrypted. If this parameter is not assigned a value, the default is that all connections must be encrypted. + + .PARAMETER Ensure + If Encryption should be Enabled (Present) or Disabled (Absent). + + .PARAMETER ServiceAccount + Name of the account running the SQL Server service. +#> +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $InstanceName, + + [Parameter(Mandatory = $true)] + [System.String] + [AllowEmptyString()] + $Thumbprint, + + [Parameter()] + [System.Boolean] + $ForceEncryption = $true, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter(Mandatory = $true)] + [System.String] + $ServiceAccount + ) + + $parameters = @{ + InstanceName = $InstanceName + Thumbprint = $Thumbprint + ForceEncryption = $ForceEncryption + Ensure = $Ensure + ServiceAccount = $ServiceAccount + } + + $encryptionState = Get-TargetResource @parameters + + if ($Ensure -eq 'Present') + { + if ($ForceEncryption -ne $encryptionState.ForceEncryption -or $Thumbprint -ne $encryptionState.Thumbprint) + { + Write-Verbose -Message ( + $script:localizedData.SetEncryptionSetting ` + -f $InstanceName, $Thumbprint, $ForceEncryption + ) + Set-EncryptedConnectionSetting -InstanceName $InstanceName -Thumbprint $Thumbprint -ForceEncryption $ForceEncryption + } + + if ((Test-CertificatePermission -Thumbprint $Thumbprint -ServiceAccount $ServiceAccount) -eq $false) + { + Write-Verbose -Message ( + $script:localizedData.SetCertificatePermission ` + -f $Thumbprint, $ServiceAccount + ) + Set-CertificatePermission -Thumbprint $Thumbprint -ServiceAccount $ServiceAccount + } + } + else + { + Write-Verbose -Message ( + $script:localizedData.RemoveEncryptionSetting ` + -f $InstanceName + ) + Set-EncryptedConnectionSetting -InstanceName $InstanceName -Thumbprint '' -ForceEncryption $false + } + + Write-Verbose -Message ( + $script:localizedData.RestartingService ` + -f $InstanceName + ) + Restart-SqlService -SQLServer localhost -SQLInstanceName $InstanceName +} + +<# + .SYNOPSIS + Tests the SQL Server Encryption configuration. + + .PARAMETER InstanceName + Name of the SQL Server instance to be configured. + + .PARAMETER Thumbprint + Thumbprint of the certificate being used for encryption. If parameter Ensure is set to 'Absent', then the parameter Thumbprint can be set to an empty string. + + .PARAMETER ForceEncryption + If all connections to the SQL instance should be encrypted. If this parameter is not assigned a value, the default is, set to true, that all connections must be encrypted. + + .PARAMETER Ensure + If Encryption should be Enabled (Present) or Disabled (Absent). + + .PARAMETER ServiceAccount + Name of the account running the SQL Server service. +#> +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $InstanceName, + + [Parameter(Mandatory = $true)] + [System.String] + [AllowEmptyString()] + $Thumbprint, + + [Parameter()] + [System.Boolean] + $ForceEncryption = $true, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter(Mandatory = $true)] + [System.String] + $ServiceAccount + ) + + $parameters = @{ + InstanceName = $InstanceName + Thumbprint = $Thumbprint + ForceEncryption = $ForceEncryption + Ensure = $Ensure + ServiceAccount = $ServiceAccount + } + + Write-Verbose -Message ( + $script:localizedData.TestingConfiguration ` + -f $InstanceName + ) + + $encryptionState = Get-TargetResource @parameters + + return $Ensure -eq $encryptionState.Ensure +} + +<# + .SYNOPSIS + Gets the SQL Server Encryption settings. Returns Certificate thumbprint and ForceEncryption setting. + + .PARAMETER InstanceName + Name of the SQL Server Instance to be configured. +#> +function Get-SqlEncryptionValue +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $InstanceName + ) + + $sqlInstance = Get-Item 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\SQL' + if ($sqlInstance) + { + try + { + $sqlInstanceId = (Get-ItemProperty -Path $sqlInstance.PSPath -Name $InstanceName).$InstanceName + } + catch + { + throw ($script:localizedData.InstanceNotFound -f $InstanceName) + } + return Get-Item "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$sqlInstanceId\MSSQLServer\SuperSocketNetLib" + } +} + +<# + .SYNOPSIS + Gets the SQL Server Encryption settings. Returns Certificate thumbprint and ForceEncryption setting. + + .PARAMETER InstanceName + Name of the SQL Server Instance to be configured. +#> +function Get-EncryptedConnectionSetting +{ + [CmdletBinding()] + [OutputType([Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [string] + $InstanceName + ) + + $superSocketNetLib = Get-SqlEncryptionValue -InstanceName $InstanceName + if ($superSocketNetLib) + { + return @{ + ForceEncryption = [System.Boolean](Get-ItemProperty -Path $superSocketNetLib.PSPath -Name 'ForceEncryption').ForceEncryption + Certificate = (Get-ItemProperty -Path $superSocketNetLib.PSPath -Name 'Certificate').Certificate + } + } + return $null +} + +<# + .SYNOPSIS + Sets the SQL Server Encryption settings. + + .PARAMETER InstanceName + Name of the SQL Server Instance to be configured. + + .PARAMETER Thumbprint + Thumbprint of the certificate being used for encryption. + + .PARAMETER ForceEncryption + If all connections to the SQL instance should be encrypted. +#> +function Set-EncryptedConnectionSetting +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string] + $InstanceName, + + [Parameter(Mandatory = $true)] + [AllowEmptyString()] + [string] + $Thumbprint, + + [Parameter(Mandatory = $true)] + [System.Boolean] + $ForceEncryption + ) + + $superSocketNetLib = Get-SqlEncryptionValue -InstanceName $InstanceName + if ($superSocketNetLib) + { + Set-ItemProperty -Path $superSocketNetLib.PSPath -Name 'Certificate' -Value $Thumbprint + Set-ItemProperty -Path $superSocketNetLib.PSPath -Name 'ForceEncryption' -Value $([int]$ForceEncryption) + } + else + { + throw $script:localizedData.CouldNotFindEncryptionValues ` + -f $InstanceName + } +} + +<# + .SYNOPSIS + Gets the permissions of the private key on the certificate. + + .PARAMETER Thumbprint + Thumbprint of the certificate being used for encryption. +#> + +function Get-CertificateAcl +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $Thumbprint + ) + + $cert = Get-ChildItem -Path cert:\LocalMachine\My | Where-Object -FilterScript { $PSItem.Thumbprint -eq $Thumbprint } + + # Location of the machine related keys + $keyPath = $env:ProgramData + '\Microsoft\Crypto\RSA\MachineKeys\' + $keyName = $cert.PrivateKey.CspKeyContainerInfo.UniqueKeyContainerName + $keyFullPath = $keyPath + $keyName + + Write-Verbose -Message ( + $script:localizedData.PrivateKeyPath ` + -f $keyFullPath + ) + + try + { + # Get the current acl of the private key + return @{ + ACL = (Get-Item $keyFullPath).GetAccessControl() + Path = $keyFullPath + } + } + catch + { + throw $_ + } +} + +<# + .SYNOPSIS + Gives the service account read permissions to the private key on the certificate. + + .PARAMETER Thumbprint + Thumbprint of the certificate being used for encryption. + + .PARAMETER ServiceAccount + The service account running SQL Server service. +#> +function Set-CertificatePermission +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $Thumbprint, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $ServiceAccount + ) + + # Specify the user, the permissions and the permission type + $permission = "$($ServiceAccount)", 'Read', 'Allow' + $accessRule = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList $permission + + try + { + # Get the current acl of the private key + $acl = Get-CertificateAcl -Thumbprint $Thumbprint + + # Add the new ace to the acl of the private key + $acl.ACL.AddAccessRule($accessRule) + + # Write back the new acl + Set-Acl -Path $acl.Path -AclObject $acl.ACL + } + catch + { + throw $_ + } +} + +<# + .SYNOPSIS + Test if the service account has read permissions to the private key on the certificate. + + .PARAMETER Thumbprint + Thumbprint of the certificate being used for encryption. + + .PARAMETER ServiceAccount + The service account running SQL Server service. +#> +function Test-CertificatePermission +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $Thumbprint, + + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $ServiceAccount + ) + + # Specify the user, the permissions and the permission type + $permission = "$($ServiceAccount)", 'Read', 'Allow' + $accessRule = New-Object -TypeName System.Security.AccessControl.FileSystemAccessRule -ArgumentList $permission + + try + { + # Get the current acl of the private key + $acl = Get-CertificateAcl -Thumbprint $Thumbprint + + [array] $permissions = $acl.ACL.Access.Where( {$_.IdentityReference -eq $accessRule.IdentityReference}) + if ($permissions.Count -eq 0) + { + return $false + } + + $rights = $permissions[0].FileSystemRights.value__ + + #check if the rights contains Read permission, 131209 is the bitwise number for read. This allows the permissions to be higher then read. + if (($rights -bor 131209) -ne $rights) + { + return $false + } + + return $true + } + catch + { + return $false + } +} + +Export-ModuleMember -Function *-TargetResource diff --git a/DSCResources/MSFT_SqlServerSecureConnection/MSFT_SqlServerSecureConnection.schema.mof b/DSCResources/MSFT_SqlServerSecureConnection/MSFT_SqlServerSecureConnection.schema.mof new file mode 100644 index 000000000..f05c80391 --- /dev/null +++ b/DSCResources/MSFT_SqlServerSecureConnection/MSFT_SqlServerSecureConnection.schema.mof @@ -0,0 +1,11 @@ + +[ClassVersion("1.0.0.0"), FriendlyName("SqlServerSecureConnection")] +class MSFT_SqlServerSecureConnection : OMI_BaseResource +{ + [Key, Description("Name of the SQL Server instance to be configured.")] String InstanceName; + [Required, Description("Thumbprint of the certificate being used for encryption. If parameter Ensure is set to 'Absent', then the parameter Certificate can be set to an empty string.")] String Thumbprint; + [Write, Description("If all connections to the SQL instance should be encrypted. If this parameter is not assigned a value, the default is, set to true, that all connections must be encrypted.")] boolean ForceEncryption; + [Required, Description("Name of the account running the SQL Server service.")] String ServiceAccount; + [Write, Description("If Encryption should be Enabled (Present) or Disabled (Absent)."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; +}; + diff --git a/DSCResources/MSFT_SqlServerSecureConnection/en-US/MSFT_SqlServerSecureConnection.strings.psd1 b/DSCResources/MSFT_SqlServerSecureConnection/en-US/MSFT_SqlServerSecureConnection.strings.psd1 new file mode 100644 index 000000000..9bbaf8d92 --- /dev/null +++ b/DSCResources/MSFT_SqlServerSecureConnection/en-US/MSFT_SqlServerSecureConnection.strings.psd1 @@ -0,0 +1,21 @@ +<# + Localized resources for MSFT_SqlServerSecureConnection +#> + +ConvertFrom-StringData @' + GetEncryptionSettings = Getting encryption settings for instance '{0}'. + CertificateSettings = Certificate permissions are {0}. + EncryptedSettings = Found thumbprint of '{0}', with Force Encryption set to '{1}'. + SetEncryptionSetting = Securing instance '{0}' with Thumbprint: '{1}' and Force Encryption: '{2}'. + RemoveEncryptionSetting = Removing SQL Server secure connection from instance '{0}'. + SetCertificatePermission = Adding read permissions to certificate '{0}' for account '{1}'. + RestartingService = Restarting SQL Server service for instance '{0}'. + TestingConfiguration = Determine if the Secure Connection is in the desired state. + ThumbprintResult = Thumbprint was '{0}' but expected '{1}'. + ForceEncryptionResult = ForceEncryption was '{0}' but expected '{1}'. + CertificateResult = Certificate permissions was '{0}' but expected 'True'. + EncryptionOff = SQL Secure Connection is Disabled. + InstanceNotFound = SQL instance '{0}' not found on SQL Server. + PrivateKeyPath = Certificate private key is located at '{0}'. + CouldNotFindEncryptionValues = Could not find encryption values in registry for instance '{0}'. +'@ diff --git a/Examples/README.md b/Examples/README.md index 07c10adc3..64d5bc090 100644 --- a/Examples/README.md +++ b/Examples/README.md @@ -34,6 +34,7 @@ These are the links to the examples for each individual resource. - [SqlServerPermission](Resources/SqlServerPermission) - [SqlServerReplication](Resources/SqlServerReplication) - [SqlServerRole](Resources/SqlServerRole) +- [SqlServerSecureConnection](Resources/SqlServerSecureConnection) - [SqlServiceAccount](Resources/SqlServiceAccount) - [SqlSetup](Resources/SqlSetup) - [SqlWaitForAG](Resources/SqlWaitForAG) diff --git a/Examples/Resources/SqlServerSecureConnection/1-ForceSecureConnection.ps1 b/Examples/Resources/SqlServerSecureConnection/1-ForceSecureConnection.ps1 new file mode 100644 index 000000000..f4c9048ca --- /dev/null +++ b/Examples/Resources/SqlServerSecureConnection/1-ForceSecureConnection.ps1 @@ -0,0 +1,19 @@ +<# +.EXAMPLE + This example performs a standard Sql encryption setup. Forcing all connections to be encrypted. +#> +Configuration Example +{ + Import-DscResource -ModuleName SqlServerDsc + + node localhost { + SqlServerSecureConnection ForceSecureConnection + { + InstanceName = 'MSSQLSERVER' + Thumbprint = 'fb0b82c94b80da26cf0b86f10ec0c50ae7864a2c' + ForceEncryption = $true + Ensure = 'Present' + ServiceAccount = 'SqlSvc' + } + } +} diff --git a/Examples/Resources/SqlServerSecureConnection/2-SecureConnectionNotForced.ps1 b/Examples/Resources/SqlServerSecureConnection/2-SecureConnectionNotForced.ps1 new file mode 100644 index 000000000..0e544b0cb --- /dev/null +++ b/Examples/Resources/SqlServerSecureConnection/2-SecureConnectionNotForced.ps1 @@ -0,0 +1,19 @@ +<# +.EXAMPLE + This example performs a standard Sql encryption setup. Forcing all connections to be encrypted. +#> +Configuration Example +{ + Import-DscResource -ModuleName SqlServerDsc + + node localhost { + SqlServerSecureConnection SecureConnectionNotForced + { + InstanceName = 'MSSQLSERVER' + Thumbprint = 'fb0b82c94b80da26cf0b86f10ec0c50ae7864a2c' + ForceEncryption = $false + Ensure = 'Present' + ServiceAccount = 'SqlSvc' + } + } +} diff --git a/Examples/Resources/SqlServerSecureConnection/3-SecureConnectionAbsent.ps1 b/Examples/Resources/SqlServerSecureConnection/3-SecureConnectionAbsent.ps1 new file mode 100644 index 000000000..acf810862 --- /dev/null +++ b/Examples/Resources/SqlServerSecureConnection/3-SecureConnectionAbsent.ps1 @@ -0,0 +1,18 @@ +<# +.EXAMPLE + This example performs a standard Sql encryption setup. Forcing all connections to be encrypted. +#> +Configuration Example +{ + Import-DscResource -ModuleName SqlServerDsc + + node localhost { + SqlServerSecureConnection SecureConnectionAbsent + { + InstanceName = 'MSSQLSERVER' + Thumbprint = '' + Ensure = 'Absent' + ServiceAccount = 'SqlSvc' + } + } +} diff --git a/README.md b/README.md index d85c7fab3..10cc4aef4 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,9 @@ A full list of changes in each version can be found in the [change log](CHANGELO to manage database recovery model. * [**SqlDatabaseRole**](#sqldatabaserole) resource to manage SQL database roles. -* [**SqlRS**](#sqlrs) configures SQL Server Reporting +* [**SqlServerSecureConnection**](#sqlserversecureconnection) resource to + enable encrypted SQL connections. +* [**SqlRS**](#sqlrs) configures SQL Server Reporting. Services to use a database engine in another instance. * [**SqlScript**](#sqlscript) resource to extend DSC Get/Set/Test functionality to T-SQL. @@ -702,6 +704,48 @@ Read more about database role in this article [CREATE ROLE (Transact-SQL)](https All issues are not listed here, see [here for all open issues](https://github.com/PowerShell/SqlServerDsc/issues?q=is%3Aissue+is%3Aopen+in%3Atitle+SqlDatabaseRole). +### SqlServerSecureConnection + +Configures SQL connections to be encrypted. +Read more about encrypted connections in this article [Enable Encrypted Connections](https://docs.microsoft.com/en-us/sql/database-engine/configure-windows/enable-encrypted-connections-to-the-database-engine). + +#### Requirements + +* Target machine must be running Windows Server 2008 R2 or later. +* You must have a Certificate that is trusted and issued for + `ServerAuthentication`. +* The name of the Certificate must be the fully qualified domain name (FQDN) + of the computer. +* The Certificate must be installed in the LocalMachine Personal store. +* If `PsDscRunAsCredential` common parameter is used to run the resource, the + specified credential must have permissions to connect to the SQL Server instance + specified in `InstanceName`. + +#### Parameters + +* **`[String]` InstanceName** _(Key)_: Name of the SQL Server instance to be + configured. +* **`[String]` Thumbprint** _(Required)_: Thumbprint of the certificate being + used for encryption. If parameter Ensure is set to 'Absent', then the + parameter Certificate can be set to an empty string. +* **`[String]` ServiceAccount** _(Required)_: Name of the account running the + SQL Server service. +* **`[String]` Ensure** _(Write)_: If Encryption should be Enabled (Present) + or Disabled (Absent). { *Present* | Absent }. Defaults to Present. +* **`[Boolean]` ForceEncryption** _(Write)_: If all connections to the SQL + instance should be encrypted. If this parameter is not assigned a value, + the default is, set to *True*, that all connections must be encrypted. + +#### Examples + +* [Force Secure Connection](Examples/Resources/SqlServerSecureConnection/1-ForceSecureConnection.ps1). +* [Secure Connection but not required](Examples/Resources/SqlServerSecureConnection/2-SecureConnectionNotForced.ps1). +* [Secure Connection disabled](Examples/Resources/SqlServerSecureConnection/3-SecureConnectionAbsent.ps1). + +#### Known issues + +All issues are not listed here, see [here for all open issues](https://github.com/PowerShell/SqlServerDsc/issues?q=is%3Aissue+is%3Aopen+in%3Atitle+SqlServerSecureConnection). + ### SqlRS Initializes and configures SQL Reporting Services server. diff --git a/Tests/Integration/MSFT_SqlServerSecureConnection.Integration.Tests.ps1 b/Tests/Integration/MSFT_SqlServerSecureConnection.Integration.Tests.ps1 new file mode 100644 index 000000000..7d39536ce --- /dev/null +++ b/Tests/Integration/MSFT_SqlServerSecureConnection.Integration.Tests.ps1 @@ -0,0 +1,154 @@ +<# + This is used to make sure the integration test run in the correct order. + The integration test should run after the integration tests SqlServerLogin + and SqlServerRole, so any problems in those will be caught first, since + these integration tests are using those resources. +#> +[Microsoft.DscResourceKit.IntegrationTest(OrderNumber = 5)] +param() + +$script:DSCModuleName = 'SqlServerDsc' +$script:DSCResourceFriendlyName = 'SqlServerSecureConnection' +$script:DSCResourceName = "MSFT_$($script:DSCResourceFriendlyName)" + +if (-not $env:APPVEYOR -eq $true) +{ + Write-Warning -Message ('Integration test for {0} will be skipped unless $env:APPVEYOR equals $true' -f $script:DSCResourceName) + return +} + +#region HEADER +# Integration Test Template Version: 1.1.2 +[String] $script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone', 'https://github.com/PowerShell/DscResource.Tests.git', (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests')) +} + +Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath (Join-Path -Path 'DSCResource.Tests' -ChildPath 'TestHelper.psm1')) -Force +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:DSCModuleName ` + -DSCResourceName $script:DSCResourceName ` + -TestType Integration + +$testRootFolderPath = Split-Path -Path $PSScriptRoot -Parent +Import-Module -Name (Join-Path -Path $testRootFolderPath -ChildPath (Join-Path -Path 'TestHelpers' -ChildPath 'CommonTestHelper.psm1')) -Force + +#endregion + +$mockSqlServicePrimaryAccountUserName = "$env:COMPUTERNAME\svc-SqlPrimary" + +$null = New-SQLSelfSignedCertificate +$mockSqlPrivateKeyPassword = ConvertTo-SecureString -String '1234' -AsPlainText -Force +Import-PfxCertificate -FilePath $env:SqlPrivateCertificatePath -Password $mockSqlPrivateKeyPassword -Exportable -CertStoreLocation 'Cert:\LocalMachine\Root' +Import-PfxCertificate -FilePath $env:SqlPrivateCertificatePath -Password $mockSqlPrivateKeyPassword -Exportable -CertStoreLocation 'Cert:\LocalMachine\My' +try +{ + $configFile = Join-Path -Path $PSScriptRoot -ChildPath "$($script:DSCResourceName).config.ps1" + . $configFile + + Describe "$($script:DSCResourceName)_Integration" { + BeforeAll { + $resourceId = "[$($script:DSCResourceFriendlyName)]Integration_Test" + } + + $configurationName = "$($script:DSCResourceName)_AddSecureConnection_Config" + + Context ('When using configuration {0}' -f $configurationName) { + It 'Should compile and apply the MOF without throwing' { + { + $configurationParameters = @{ + SqlServicePrimaryUserName = $mockSqlServicePrimaryAccountUserName + OutputPath = $TestDrive + # The variable $ConfigurationData was dot-sourced above. + ConfigurationData = $ConfigurationData + } + + & $configurationName @configurationParameters + + $startDscConfigurationParameters = @{ + Path = $TestDrive + ComputerName = 'localhost' + Wait = $true + Verbose = $true + Force = $true + ErrorAction = 'Stop' + } + + Start-DscConfiguration @startDscConfigurationParameters + } | Should -Not -Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { + $script:currentConfiguration = Get-DscConfiguration -Verbose -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + $resourceCurrentState = $script:currentConfiguration | Where-Object -FilterScript { + $_.ConfigurationName -eq $configurationName + } | Where-Object -FilterScript { + $_.ResourceId -eq $resourceId + } + + $resourceCurrentState.Thumbprint | Should -Be $env:SqlCertificateThumbprint + $resourceCurrentState.ForceEncryption | Should -Be $true + } + } + + $configurationName = "$($script:DSCResourceName)_RemoveSecureConnection_Config" + + Context ('When using configuration {0}' -f $configurationName) { + It 'Should compile and apply the MOF without throwing' { + { + $configurationParameters = @{ + SqlServicePrimaryUserName = $mockSqlServicePrimaryAccountUserName + OutputPath = $TestDrive + # The variable $ConfigurationData was dot-sourced above. + ConfigurationData = $ConfigurationData + } + + & $configurationName @configurationParameters + + $startDscConfigurationParameters = @{ + Path = $TestDrive + ComputerName = 'localhost' + Wait = $true + Verbose = $true + Force = $true + ErrorAction = 'Stop' + } + + Start-DscConfiguration @startDscConfigurationParameters + } | Should -Not -Throw + } + + It 'Should be able to call Get-DscConfiguration without throwing' { + { + $script:currentConfiguration = Get-DscConfiguration -Verbose -ErrorAction Stop + } | Should -Not -Throw + } + + It 'Should have set the resource and all the parameters should match' { + $resourceCurrentState = $script:currentConfiguration | Where-Object -FilterScript { + $_.ConfigurationName -eq $configurationName + } | Where-Object -FilterScript { + $_.ResourceId -eq $resourceId + } + + $resultObject.Thumbprint | Should -BeNullOrEmpty + $resourceCurrentState.ForceEncryption | Should -Be $false + } + } + } +} +finally +{ + #region FOOTER + + Restore-TestEnvironment -TestEnvironment $TestEnvironment + + #endregion +} diff --git a/Tests/Integration/MSFT_SqlServerSecureConnection.config.ps1 b/Tests/Integration/MSFT_SqlServerSecureConnection.config.ps1 new file mode 100644 index 000000000..b69c9d317 --- /dev/null +++ b/Tests/Integration/MSFT_SqlServerSecureConnection.config.ps1 @@ -0,0 +1,62 @@ +$ConfigurationData = @{ + AllNodes = @( + @{ + NodeName = 'localhost' + ServerName = $env:COMPUTERNAME + InstanceName = 'DSCSQL2016' + + Thumbprint = $env:SqlCertificateThumbprint + + CertificateFile = $env:DscPublicCertificatePath + } + ) +} + +Configuration MSFT_SqlServerSecureConnection_AddSecureConnection_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] + $SqlServicePrimaryUserName + ) + + Import-DscResource -ModuleName 'SqlServerDsc' + + node localhost + { + SqlServerSecureConnection 'Integration_Test' + { + InstanceName = $Node.InstanceName + Ensure = 'Present' + Thumbprint = $Node.Thumbprint + ServiceAccount = $SqlServicePrimaryUserName + ForceEncryption = $true + } + } +} + +Configuration MSFT_SqlServerSecureConnection_RemoveSecureConnection_Config +{ + param + ( + [Parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [String] + $SqlServicePrimaryUserName + ) + + Import-DscResource -ModuleName 'SqlServerDsc' + + node localhost + { + SqlServerSecureConnection 'Integration_Test' + { + InstanceName = $Node.InstanceName + Ensure = 'Absent' + Thumbprint = '' + ServiceAccount = $SqlServicePrimaryUserName + } + } +} diff --git a/Tests/TestHelpers/CommonTestHelper.psm1 b/Tests/TestHelpers/CommonTestHelper.psm1 index fdcce7fa9..ad6115b3d 100644 --- a/Tests/TestHelpers/CommonTestHelper.psm1 +++ b/Tests/TestHelpers/CommonTestHelper.psm1 @@ -228,3 +228,61 @@ function Get-NetIPAddressNetwork return $networkObject } + + + +<# + .SYNOPSIS + This command will create a new self-signed certificate to be used to + secure Sql Server connection. + + .OUTPUTS + Returns the created certificate. Writes the path to the public + certificate in the machine environment variable $env:sqlPrivateCertificatePath, + and the certificate thumbprint in the machine environment variable + $env:SqlCertificateThumbprint. +#> +function New-SQLSelfSignedCertificate +{ + $sqlPublicCertificatePath = Join-Path -Path $env:temp -ChildPath 'SqlPublicKey.cer' + $sqlPrivateCertificatePath = Join-Path -Path $env:temp -ChildPath 'SqlPrivateKey.cer' + $sqlPriavteKeyPassword = ConvertTo-SecureString -String "1234" -Force -AsPlainText + + $certificateSubject = $env:COMPUTERNAME + + <# + There are build workers still on Windows Server 2012 R2 so let's + use the alternate method of New-SelfSignedCertificate. + #> + Install-Module -Name PSPKI -Scope CurrentUser -Force + Import-Module -Name PSPKI + + $newSelfSignedCertificateExParameters = @{ + Subject = "CN=$certificateSubject" + EKU = 'Server Authentication' + KeyUsage = 'KeyEncipherment, DataEncipherment' + SAN = "dns:$certificateSubject" + FriendlyName = 'Sql Encryption certificate' + Path = $sqlPrivateCertificatePath + Password = $sqlPriavteKeyPassword + Exportable = $true + KeyLength = 2048 + ProviderName = 'Microsoft Enhanced Cryptographic Provider v1.0' + AlgorithmName = 'RSA' + SignatureAlgorithm = 'SHA256' + } + + $certificate = New-SelfSignedCertificateEx @newSelfSignedCertificateExParameters + + Write-Info -Message ('Created self-signed certificate ''{0}'' with thumbprint ''{1}''.' -f $certificate.Subject, $certificate.Thumbprint) + + # Update a machine and session environment variable with the path to the private certificate. + Set-EnvironmentVariable -Name 'SqlPrivateCertificatePath' -Value $sqlPrivateCertificatePath -Machine + Write-Info -Message ('Environment variable $env:SqlPrivateCertificatePath set to ''{0}''' -f $env:SqlPrivateCertificatePath) + + # Update a machine and session environment variable with the thumbprint of the certificate. + Set-EnvironmentVariable -Name 'SqlCertificateThumbprint' -Value $certificate.Thumbprint -Machine + Write-Info -Message ('Environment variable $env:SqlCertificateThumbprint set to ''{0}''' -f $env:SqlCertificateThumbprint) + + return $certificate +} diff --git a/Tests/Unit/MSFT_SqlServerSecureConnection.Tests.ps1 b/Tests/Unit/MSFT_SqlServerSecureConnection.Tests.ps1 new file mode 100644 index 000000000..d7ac4e1bf --- /dev/null +++ b/Tests/Unit/MSFT_SqlServerSecureConnection.Tests.ps1 @@ -0,0 +1,749 @@ +$script:DSCModuleName = 'SqlServerDsc' +$script:DSCResourceName = 'MSFT_SqlServerSecureConnection' + +#region HEADER + +# Unit Test Template Version: 1.2.0 +$script:moduleRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot) +if ( (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests'))) -or ` + (-not (Test-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1'))) ) +{ + & git @('clone', 'https://github.com/PowerShell/DscResource.Tests.git', (Join-Path -Path $script:moduleRoot -ChildPath '\DSCResource.Tests\')) +} + +Import-Module -Name (Join-Path -Path $script:moduleRoot -ChildPath 'DSCResource.Tests\TestHelper.psm1') -Force + +$TestEnvironment = Initialize-TestEnvironment ` + -DSCModuleName $script:DSCModuleName ` + -DSCResourceName $script:DSCResourceName ` + -TestType Unit + +#endregion HEADER + +function Invoke-TestSetup +{ + Import-Module -Name (Join-Path -Path (Join-Path -Path (Join-Path -Path (Join-Path -Path $script:moduleRoot -ChildPath 'Tests') -ChildPath 'Unit') -ChildPath 'Stubs') -ChildPath 'SQLPSStub.psm1') -Global -Force +} + +function Invoke-TestCleanup +{ + Restore-TestEnvironment -TestEnvironment $TestEnvironment +} + +# Begin Testing +try +{ + Invoke-TestSetup + + InModuleScope $script:DSCResourceName { + class MockedAccessControl + { + [hashtable]$Access = @{ + IdentityReference = 'Everyone' + FileSystemRights = @{ + value__ = '131209' + } + } + + [void] AddAccessRule([System.Security.AccessControl.FileSystemAccessRule] $object) + { + + } + } + + class MockedGetItem + { + [string] $Thumbprint = '12345678' + [string] $PSPath = 'PathToItem' + [string] $Path = 'PathToItem' + [MockedAccessControl]$ACL = [MockedAccessControl]::new() + [hashtable]$PrivateKey = @{ + CspKeyContainerInfo = @{ + UniqueKeyContainerName = 'key' + } + } + + [MockedGetItem] GetAccessControl() + { + return $this + } + } + + $mockNamedInstanceName = 'INSTANCE' + $mockDefaultInstanceName = 'MSSQLSERVER' + $mockThumbprint = '123456789' + $mockServiceAccount = 'SqlSvc' + + Describe 'SqlServerSecureConnection\Get-TargetResource' -Tag 'Get' { + BeforeAll { + $mockDynamic_SqlBuildVersion = '13.0.4001.0' + + $defaultParameters = @{ + InstanceName = $mockNamedInstanceName + Thumbprint = $mockThumbprint + ServiceAccount = $mockServiceAccount + ForceEncryption = $true + Ensure = 'Present' + } + } + + Context 'When the system is in the desired state and Ensure is Present' { + Mock -CommandName Get-EncryptedConnectionSetting -MockWith { + return @{ + ForceEncryption = $true + Certificate = $mockThumbprint + } + } -Verifiable + Mock -CommandName Test-CertificatePermission -MockWith { return $true } + + It 'Should return the the state of present' { + $resultGetTargetResource = Get-TargetResource @defaultParameters + $resultGetTargetResource.InstanceName | Should -Be $mockNamedInstanceName + $resultGetTargetResource.Thumbprint | Should -Be $mockThumbprint + $resultGetTargetResource.ServiceAccount | Should -Be $mockServiceAccount + $resultGetTargetResource.ForceEncryption | Should -Be $true + $resultGetTargetResource.Ensure | Should -Be 'Present' + + Assert-MockCalled -CommandName Get-EncryptedConnectionSetting -Exactly -Times 1 -Scope It + } + + } + + Context 'When the system is not in the desired state and Ensure is Present' { + Mock -CommandName Get-EncryptedConnectionSetting -MockWith { + return @{ + ForceEncryption = $false + Certificate = '987654321' + } + } -Verifiable + Mock -CommandName Test-CertificatePermission -MockWith { return $false } + + It "Should return the state of absent when certificate thumbprint and certificate permissions don't match." { + $resultGetTargetResource = Get-TargetResource @defaultParameters + $resultGetTargetResource.InstanceName | Should -Be $mockNamedInstanceName + $resultGetTargetResource.Thumbprint | Should -Not -Be $mockThumbprint + $resultGetTargetResource.ServiceAccount | Should -Be $mockServiceAccount + $resultGetTargetResource.ForceEncryption | Should -Be $false + $resultGetTargetResource.Ensure | Should -Be 'Absent' + + Assert-MockCalled -CommandName Get-EncryptedConnectionSetting -Exactly -Times 1 -Scope It + } + } + + Context 'When the system is not in the desired state and Ensure is Present' { + Mock -CommandName Get-EncryptedConnectionSetting -MockWith { + return @{ + ForceEncryption = $true + Certificate = '987654321' + } + } -Verifiable + Mock -CommandName Test-CertificatePermission -MockWith { return $true } + + It 'Should return the state of absent when certificate permissions match but encryption settings dont' { + $resultGetTargetResource = Get-TargetResource @defaultParameters + $resultGetTargetResource.InstanceName | Should -Be $mockNamedInstanceName + $resultGetTargetResource.Thumbprint | Should -Not -Be $mockThumbprint + $resultGetTargetResource.ServiceAccount | Should -Be $mockServiceAccount + $resultGetTargetResource.ForceEncryption | Should -Be $true + $resultGetTargetResource.Ensure | Should -Be 'Absent' + + Assert-MockCalled -CommandName Get-EncryptedConnectionSetting -Exactly -Times 1 -Scope It + } + } + + Context 'When the system is not in the desired state and Ensure is Present' { + Mock -CommandName Get-EncryptedConnectionSetting -MockWith { + return @{ + ForceEncryption = $true + Certificate = $mockThumbprint + } + } -Verifiable + Mock -CommandName Test-CertificatePermission -MockWith { return $false } + + It 'Should return the state of absent when certificate permissions dont match but encryption settings do' { + $resultGetTargetResource = Get-TargetResource @defaultParameters + $resultGetTargetResource.InstanceName | Should -Be $mockNamedInstanceName + $resultGetTargetResource.Thumbprint | Should -Be $mockThumbprint + $resultGetTargetResource.ServiceAccount | Should -Be $mockServiceAccount + $resultGetTargetResource.ForceEncryption | Should -Be $true + $resultGetTargetResource.Ensure | Should -Be 'Absent' + + Assert-MockCalled -CommandName Get-EncryptedConnectionSetting -Exactly -Times 1 -Scope It + } + } + + $defaultParameters.Ensure = 'Absent' + + Context 'When the system is in the desired state and Ensure is Absent' { + Mock -CommandName Get-EncryptedConnectionSetting -MockWith { + return @{ + ForceEncryption = $false + Certificate = '' + } + } -Verifiable + Mock -CommandName Test-CertificatePermission -MockWith { return $true } + + It 'Should return the the state of absent' { + $resultGetTargetResource = Get-TargetResource @defaultParameters + $resultGetTargetResource.InstanceName | Should -Be $mockNamedInstanceName + $resultGetTargetResource.Thumbprint | Should -BeNullOrEmpty + $resultGetTargetResource.ServiceAccount | Should -Be $mockServiceAccount + $resultGetTargetResource.ForceEncryption | Should -Be $false + $resultGetTargetResource.Ensure | Should -Be 'Absent' + + Assert-MockCalled -CommandName Get-EncryptedConnectionSetting -Exactly -Times 1 -Scope It + } + } + + Context 'When the system is not in the desired state and Ensure is Absent' { + Mock -CommandName Get-EncryptedConnectionSetting -MockWith { + return @{ + ForceEncryption = $true + Certificate = $mockThumbprint + } + } -Verifiable + Mock -CommandName Test-CertificatePermission -MockWith { return $true } + + It 'Should return the the state of present' { + $resultGetTargetResource = Get-TargetResource @defaultParameters + $resultGetTargetResource.InstanceName | Should -Be $mockNamedInstanceName + $resultGetTargetResource.Thumbprint | Should -Be $mockThumbprint + $resultGetTargetResource.ServiceAccount | Should -Be $mockServiceAccount + $resultGetTargetResource.ForceEncryption | Should -Be $true + $resultGetTargetResource.Ensure | Should -Be 'Present' + + Assert-MockCalled -CommandName Get-EncryptedConnectionSetting -Exactly -Times 1 -Scope It + } + } + + Context 'When the system is not in the desired state and Ensure is Absent' { + Mock -CommandName Get-EncryptedConnectionSetting -MockWith { + return @{ + ForceEncryption = $false + Certificate = $mockThumbprint + } + } -Verifiable + Mock -CommandName Test-CertificatePermission -MockWith { return $true } + + It 'Should return the the state of present when ForceEncryption is False but a thumbprint exist.' { + $resultGetTargetResource = Get-TargetResource @defaultParameters + $resultGetTargetResource.InstanceName | Should -Be $mockNamedInstanceName + $resultGetTargetResource.Thumbprint | Should -Be $mockThumbprint + $resultGetTargetResource.ServiceAccount | Should -Be $mockServiceAccount + $resultGetTargetResource.ForceEncryption | Should -Be $false + $resultGetTargetResource.Ensure | Should -Be 'Present' + + Assert-MockCalled -CommandName Get-EncryptedConnectionSetting -Exactly -Times 1 -Scope It + } + } + + Context 'When the system is not in the desired state and Ensure is Absent' { + Mock -CommandName Get-EncryptedConnectionSetting -MockWith { + return @{ + ForceEncryption = $true + Certificate = '' + } + } -Verifiable + Mock -CommandName Test-CertificatePermission -MockWith { return $true } + + It 'Should return the the state of present when Certificate is null but ForceEncryption is True.' { + $resultGetTargetResource = Get-TargetResource @defaultParameters + $resultGetTargetResource.InstanceName | Should -Be $mockNamedInstanceName + $resultGetTargetResource.Thumbprint | Should -BeNullOrEmpty + $resultGetTargetResource.ServiceAccount | Should -Be $mockServiceAccount + $resultGetTargetResource.ForceEncryption | Should -Be $true + $resultGetTargetResource.Ensure | Should -Be 'Present' + + Assert-MockCalled -CommandName Get-EncryptedConnectionSetting -Exactly -Times 1 -Scope It + } + } + } + + Describe 'SqlServerSecureConnection\Set-TargetResource' -Tag 'Set' { + BeforeAll { + $defaultParameters = @{ + InstanceName = $mockNamedInstanceName + Thumbprint = $mockThumbprint + ServiceAccount = $mockServiceAccount + ForceEncryption = $true + Ensure = 'Present' + } + + $defaultAbsentParameters = @{ + InstanceName = $mockNamedInstanceName + Thumbprint = $mockThumbprint + ServiceAccount = $mockServiceAccount + ForceEncryption = $true + Ensure = 'Absent' + } + + Mock -CommandName Set-EncryptedConnectionSetting -Verifiable + Mock -CommandName Set-CertificatePermission -Verifiable + Mock -CommandName Restart-SqlService -Verifiable + } + + Context 'When the system is not in the desired state' { + + Context 'When only certificate permissions are set' { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + InstanceName = $mockNamedInstanceName + Thumbprint = '987654321' + ServiceAccount = $mockServiceAccount + ForceEncryption = $false + Ensure = 'Present' + } + } + Mock -CommandName Test-CertificatePermission -MockWith { return $true } + + It 'Should configure only ForceEncryption and Certificate thumbprint' { + { Set-TargetResource @defaultParameters } | Should -Not -Throw + + Assert-MockCalled -CommandName Set-EncryptedConnectionSetting -Exactly -Times 1 -Scope It + + Assert-MockCalled -CommandName Set-CertificatePermission -Exactly -Times 0 -Scope It + + Assert-MockCalled -CommandName Restart-SqlService -Exactly -Times 1 -Scope It + } + } + + Context 'When there is no certificate permissions set' { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + InstanceName = $mockNamedInstanceName + Thumbprint = $mockThumbprint + ServiceAccount = $mockServiceAccount + ForceEncryption = $true + Ensure = 'Present' + } + } + Mock -CommandName Test-CertificatePermission -MockWith { return $false } + It 'Should configure only certificate permissions' { + { Set-TargetResource @defaultParameters } | Should -Not -Throw + + Assert-MockCalled -CommandName Set-EncryptedConnectionSetting -Exactly -Times 0 -Scope It + + Assert-MockCalled -CommandName Set-CertificatePermission -Exactly -Times 1 -Scope It + + Assert-MockCalled -CommandName Restart-SqlService -Exactly -Times 1 -Scope It + } + } + + Context 'When no settings are configured' { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + InstanceName = $mockNamedInstanceName + Thumbprint = '987654321' + ServiceAccount = $mockServiceAccount + ForceEncryption = $false + Ensure = 'Present' + } + } + Mock -CommandName Test-CertificatePermission -MockWith { return $false } + + It 'Should configure Encryption settings and certificate permissions' { + { Set-TargetResource @defaultParameters } | Should -Not -Throw + + Assert-MockCalled -CommandName Set-EncryptedConnectionSetting -Exactly -Times 1 -Scope It + + Assert-MockCalled -CommandName Set-CertificatePermission -Exactly -Times 1 -Scope It + + Assert-MockCalled -CommandName Restart-SqlService -Exactly -Times 1 -Scope It + } + } + + Context 'When ensure is absent' { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + InstanceName = $mockNamedInstanceName + Thumbprint = $mockThumbprint + ServiceAccount = $mockServiceAccount + ForceEncryption = $true + Ensure = 'Absent' + } + } + Mock -CommandName Test-CertificatePermission -MockWith { return $false } + + It 'Should configure Encryption settings setting certificate to empty string' { + { Set-TargetResource @defaultAbsentParameters } | Should -Not -Throw + + Assert-MockCalled -CommandName Set-EncryptedConnectionSetting -Exactly -Times 1 -Scope It + + Assert-MockCalled -CommandName Set-CertificatePermission -Exactly -Times 0 -Scope It + + Assert-MockCalled -CommandName Restart-SqlService -Exactly -Times 1 -Scope It + } + } + } + } + + Describe 'SqlServerSecureConnection\Test-TargetResource' -Tag 'Test' { + Context 'When the system is not in the desired state' { + Context 'When ForceEncryption is not configured properly' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + InstanceName = $mockNamedInstanceName + Thumbprint = $mockThumbprint + ServiceAccount = $mockServiceAccount + ForceEncryption = $false + Ensure = 'Absent' + } + } -Verifiable + + $testParameters = @{ + InstanceName = $mockNamedInstanceName + Thumbprint = $mockThumbprint + ServiceAccount = $mockServiceAccount + ForceEncryption = $true + Ensure = 'Present' + } + } + + It 'Should return state as not in desired state' { + $resultTestTargetResource = Test-TargetResource @testParameters + $resultTestTargetResource | Should -Be $false + } + } + + Context 'When Thumbprint is not configured properly' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + InstanceName = $mockNamedInstanceName + Thumbprint = '987654321' + ServiceAccount = $mockServiceAccount + ForceEncryption = $true + Ensure = 'Absent' + } + } -Verifiable + + $testParameters = @{ + InstanceName = $mockNamedInstanceName + Thumbprint = $mockThumbprint + ServiceAccount = $mockServiceAccount + ForceEncryption = $true + Ensure = 'Present' + } + } + + It 'Should return state as not in desired state' { + $resultTestTargetResource = Test-TargetResource @testParameters + $resultTestTargetResource | Should -Be $false + } + } + + Context 'When certificate permission is not set' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + InstanceName = $mockNamedInstanceName + Thumbprint = $mockThumbprint + ServiceAccount = $mockServiceAccount + ForceEncryption = $true + Ensure = 'Absent' + } + } -Verifiable + + Mock -CommandName Test-CertificatePermission -MockWith { return $false } + + $testParameters = @{ + InstanceName = $mockNamedInstanceName + Thumbprint = $mockThumbprint + ServiceAccount = $mockServiceAccount + ForceEncryption = $true + Ensure = 'Present' + } + } + + It 'Should return state as not in desired state' { + $resultTestTargetResource = Test-TargetResource @testParameters + $resultTestTargetResource | Should -Be $false + } + } + + Context 'When Ensure is Absent' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + InstanceName = $mockNamedInstanceName + Thumbprint = $mockThumbprint + ServiceAccount = $mockServiceAccount + ForceEncryption = $true + Ensure = 'Absent' + } + } -Verifiable + + $testParameters = @{ + InstanceName = $mockNamedInstanceName + Thumbprint = $mockThumbprint + ServiceAccount = $mockServiceAccount + ForceEncryption = $true + Ensure = 'Present' + } + } + + It 'Should return state as not in desired state' { + $resultTestTargetResource = Test-TargetResource @testParameters + $resultTestTargetResource | Should -Be $false + } + } + } + + Context 'When the system is in the desired state' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + InstanceName = $mockNamedInstanceName + Thumbprint = $mockThumbprint + ServiceAccount = $mockServiceAccount + ForceEncryption = $true + Ensure = 'Present' + } + } -Verifiable + + Mock -CommandName Test-CertificatePermission -MockWith { return $true } + + $testParameters = @{ + InstanceName = $mockNamedInstanceName + Thumbprint = $mockThumbprint + ServiceAccount = $mockServiceAccount + ForceEncryption = $true + Ensure = 'Present' + } + } + + It 'Should return state as in desired state' { + $resultTestTargetResource = Test-TargetResource @testParameters + $resultTestTargetResource | Should -Be $true + } + } + } + + Describe 'SqlServerSecureConnection\Get-EncryptedConnectionSetting' -Tag 'Helper' { + + Mock -CommandName 'Get-ItemProperty' -MockWith { + return @{ + ForceEncryption = '1' + } + } -ParameterFilter { $Name -eq 'ForceEncryption' } -Verifiable + Mock -CommandName 'Get-ItemProperty' -MockWith { + return @{ + Certificate = '12345678' + } + } -ParameterFilter { $Name -eq 'Certificate' } -Verifiable + + Context 'When calling a method that execute successfully' { + BeforeAll { + Mock -CommandName 'Get-SqlEncryptionValue' -MockWith { + return [MockedGetItem]::new() + } -Verifiable + } + + It 'Should return hashtable with ForceEncryption and Certificate' { + $result = Get-EncryptedConnectionSetting -InstanceName 'NamedInstance' + $result.Certificate | Should -Be '12345678' + $result.ForceEncryption | Should -Be 1 + $result | Should -BeOfType [Hashtable] + Assert-VerifiableMock + } + } + + Context 'When calling a method that executes unsuccesfuly' { + BeforeAll { + Mock -CommandName 'Get-SqlEncryptionValue' -MockWith { + return $null + } + } + + It 'Should return null' { + $result = Get-EncryptedConnectionSetting -InstanceName 'NamedInstance' + $result | Should -BeNullOrEmpty + Assert-VerifiableMock + } + } + } + + Describe 'SqlServerSecureConnection\Set-EncryptedConnectionSetting' -Tag 'Helper' { + Context 'When calling a method that execute successfully' { + BeforeAll { + Mock -CommandName 'Get-SqlEncryptionValue' -MockWith { + return [MockedGetItem]::new() + } + Mock -CommandName 'Set-ItemProperty' -Verifiable + } + + It 'Should not throw' { + { Set-EncryptedConnectionSetting -InstanceName 'NamedInstance' -Thumbprint '12345678' -ForceEncryption $true } | Should -Not -Throw + Assert-VerifiableMock + } + } + + Context 'When calling a method that executes unsuccesfuly' { + BeforeAll { + Mock -CommandName 'Get-SqlEncryptionValue' -MockWith { + return $null + } -Verifiable + Mock -CommandName 'Set-ItemProperty' + } + + It 'Should throw' { + { Set-EncryptedConnectionSetting -InstanceName 'NamedInstance' -Thumbprint '12345678' -ForceEncryption $true } | Should -Throw + Assert-MockCalled -CommandName 'Set-ItemProperty' -Times 0 + Assert-VerifiableMock + } + } + } + + Describe 'SqlServerSecureConnection\Test-CertificatePermission' -Tag 'Helper' { + Context 'When calling a method that execute successfully' { + BeforeAll { + Mock -CommandName 'Get-CertificateAcl' -MockWith { + return [MockedGetItem]::new() + } -Verifiable + } + + It 'Should return True' { + $result = Test-CertificatePermission -Thumbprint '12345678' -ServiceAccount 'Everyone' + $result | Should -be $true + Assert-VerifiableMock + } + } + + Context 'When calling a method that execute successfully' { + BeforeAll { + Mock -CommandName 'Get-CertificateAcl' -MockWith { + $mockedItem = [MockedGetItem]::new() + $mockedItem.ACL = $null + return $mockedItem + } -Verifiable + } + + It 'Should return False when no permissions were found' { + $result = Test-CertificatePermission -Thumbprint '12345678' -ServiceAccount 'Everyone' + $result | Should -be $False + Assert-VerifiableMock + } + } + + Context 'When calling a method that execute successfully' { + BeforeAll { + Mock -CommandName 'Get-CertificateAcl' -MockWith { + $mockedItem = [MockedGetItem]::new() + $mockedItem.ACL.FileSystemRights.Value__ = 1 + return $mockedItem + } -Verifiable + } + + It 'Should return False when the wrong permissions are added' { + $result = Test-CertificatePermission -Thumbprint '12345678' -ServiceAccount 'Everyone' + $result | Should -be $False + Assert-VerifiableMock + } + } + + Context 'When calling a method that executes unsuccesfuly' { + BeforeAll { + Mock -CommandName 'Get-CertificateAcl' -MockWith { + return $null + } -Verifiable + } + + It 'Should return False' { + $result = Test-CertificatePermission -Thumbprint '12345678' -ServiceAccount 'Everyone' + $result | Should -be $false + Assert-VerifiableMock + } + } + } + + Describe 'SqlServerSecureConnection\Set-CertificatePermission' -Tag 'Helper' { + Context 'When calling a method that execute successfully' { + BeforeAll { + Mock -CommandName 'Get-CertificateAcl' -MockWith { + return [MockedGetItem]::new() + } -Verifiable + Mock -CommandName 'Set-Acl' -Verifiable + } + + It 'Should not throw' { + { Set-CertificatePermission -Thumbprint '12345678' -ServiceAccount 'Everyone' } | Should -Not -Throw + Assert-VerifiableMock + } + } + + Context 'When calling a method that executes unsuccesfuly' { + BeforeAll { + Mock -CommandName 'Get-Item' -MockWith { + return $null + } -Verifiable + Mock -CommandName 'Get-ChildItem' -MockWith { + return $null + } -Verifiable + } + + It 'Should throw' { + { Set-CertificatePermission -Thumbprint '12345678' -ServiceAccount 'Everyone' } | Should -Throw + Assert-VerifiableMock + } + } + } + + Describe 'SqlServerSecureConnection\Get-CertificateAcl' -Tag 'Helper' { + Context 'When calling a method that execute successfully' { + BeforeAll { + Mock -CommandName 'Get-ChildItem' -MockWith { + return [MockedGetItem]::new() + } -Verifiable + + Mock -CommandName 'Get-Item' -MockWith { + return [MockedGetItem]::new() + } -Verifiable + } + + It 'Should not throw' { + { Get-CertificateAcl -Thumbprint '12345678' } | Should -Not -Throw + Assert-VerifiableMock + } + } + } + + Describe 'SqlServerSecureConnection\Get-SqlEncryptionValue' -Tag 'Helper' { + Context 'When calling a method that execute successfully' { + BeforeAll { + Mock -CommandName 'Get-ItemProperty' -MockWith { + return [MockedGetItem]::new() + } -Verifiable + Mock -CommandName 'Get-Item' -MockWith { + return [MockedGetItem]::new() + } -Verifiable + } + + It 'Should not throw' { + { Get-SqlEncryptionValue -InstanceName $mockNamedInstanceName } | Should -Not -Throw + Assert-VerifiableMock + } + } + + Context 'When calling a method that execute unsuccessfully' { + BeforeAll { + Mock -CommandName 'Get-ItemProperty' -MockWith { + throw "Error" + } -Verifiable + Mock -CommandName 'Get-Item' -MockWith { + return [MockedGetItem]::new() + } -Verifiable + } + + It 'Should throw with expected message' { + { Get-SqlEncryptionValue -InstanceName $mockNamedInstanceName } | Should -Throw -ExpectedMessage "SQL instance '$mockNamedInstanceName' not found on SQL Server." + Assert-VerifiableMock + } + } + } + } +} +finally +{ + Invoke-TestCleanup +}