From e8e14af936e19951549559699d3214f192abe519 Mon Sep 17 00:00:00 2001 From: Shawn Sesna Date: Mon, 14 Jan 2019 00:27:12 -0800 Subject: [PATCH] SqlServiceAccount: Updated Get-ServiceObject to find SSIS service (#1246) - Changes to SqlServiceAccount - Fixed Get-ServiceObject when searching for Integration Services service. Unlike the rest of SQL Server services, the Integration Services service cannot be instanced, however you can have multiple versions installed. Get-Service object would return the correct service name that you are looking for, but it appends the version number at the end. Added parameter VersionNumber so the search would return the correct service name. - Added code to allow for using Managed Service Accounts. - Changes to SqlServerLogin - Fixed issue in Test-TargetResource to valid password on disabled accounts (issue #915). --- CHANGELOG.md | 13 +++ .../MSFT_SqlServerLogin.psm1 | 38 ++++++- .../MSFT_SqlServiceAccount.psm1 | 66 +++++++++-- .../MSFT_SqlServiceAccount.schema.mof | 1 + .../en-US/MSFT_SqlServiceAccount.strings.psd1 | 1 + DSCResources/MSFT_SqlSetup/MSFT_SqlSetup.psm1 | 43 ++----- README.md | 3 + SqlServerDscHelper.psm1 | 106 ++++++++++++++++++ Tests/Unit/MSFT_SqlServerLogin.Tests.ps1 | 105 +++++++++++++++++ Tests/Unit/MSFT_SqlServiceAccount.Tests.ps1 | 72 +++++++++++- Tests/Unit/SqlServerDSCHelper.Tests.ps1 | 61 ++++++++++ 11 files changed, 465 insertions(+), 44 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 22e62fe45..002d49421 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ ## Unreleased +- Changes to SqlServiceAccount + - Fixed Get-ServiceObject when searching for Integration Services service. + Unlike the rest of SQL Server services, the Integration Services service + cannot be instanced, however you can have multiple versions installed. + Get-Service object would return the correct service name that you + are looking for, but it appends the version number at the end. Added + parameter VersionNumber so the search would return the correct + service name. + - Added code to allow for using Managed Service Accounts. +- Changes to SqlServerLogin + - Fixed issue in Test-TargetResource to valid password on disabled accounts. + ([issue #915](https://github.com/PowerShell/SqlServerDsc/issues/915)). + ## 12.2.0.0 - Changes to SqlServerDsc diff --git a/DSCResources/MSFT_SqlServerLogin/MSFT_SqlServerLogin.psm1 b/DSCResources/MSFT_SqlServerLogin/MSFT_SqlServerLogin.psm1 index d180eaa14..094c47da1 100644 --- a/DSCResources/MSFT_SqlServerLogin/MSFT_SqlServerLogin.psm1 +++ b/DSCResources/MSFT_SqlServerLogin/MSFT_SqlServerLogin.psm1 @@ -420,8 +420,42 @@ function Test-TargetResource } catch { - New-VerboseMessage -Message "Password validation failed for the login '$Name'." - $testPassed = $false + # Check to see if the parameter of $Disabled is true + if ($Disabled) + { + <# + An exception occurred and $Disabled is true, we neeed + to check the error codes for expected error numbers. + Recursively search the Exception variable and inner + Exceptions for the specific numbers. + 18470 - Username and password are correct, but + account is disabled. + 18456 - Login failed for user. + #> + if ((Find-ExceptionByNumber -ExceptionToSearch $_.Exception -ErrorNumber 18470)) + { + New-VerboseMessage -Message "Password valid, but '$Name' is disabled." + } + elseif ((Find-ExceptionByNumber -ExceptionToSearch $_.Exception -ErrorNumber 18456)) + { + New-VerboseMessage -Message $_.Exception.message + + # The password was not correct, password validation failed + $testPassed = $false + } + else + { + New-VerboseMessage -Message "Unknown error: $($_.Exception.message)" + + # Something else went wrong, rethrow error + throw + } + } + else + { + New-VerboseMessage -Message "Password validation failed for the login '$Name'." + $testPassed = $false + } } } } diff --git a/DSCResources/MSFT_SqlServiceAccount/MSFT_SqlServiceAccount.psm1 b/DSCResources/MSFT_SqlServiceAccount/MSFT_SqlServiceAccount.psm1 index a79b9c4ed..498bfbd63 100644 --- a/DSCResources/MSFT_SqlServiceAccount/MSFT_SqlServiceAccount.psm1 +++ b/DSCResources/MSFT_SqlServiceAccount/MSFT_SqlServiceAccount.psm1 @@ -24,8 +24,13 @@ $script:localizedData = Get-LocalizedData -ResourceName 'MSFT_SqlServiceAccount' ** Not used in this function ** Credential of the service account that should be used. + .PARAMETER VersionNumber + ** Only used when specifying IntegrationServices ** + Version number of IntegrationServices. + .EXAMPLE Get-TargetResource -ServerName $env:COMPUTERNAME -InstanceName MSSQLSERVER -ServiceType DatabaseEngine -ServiceAccount $account + Get-TargetResource -ServerName $env:COMPUTERNAME -InstanceName MSSQLSERVER -ServiceType IntegrationServices -ServiceAccount $account -VersionNumber 130 #> function Get-TargetResource { @@ -48,11 +53,15 @@ function Get-TargetResource [Parameter(Mandatory = $true)] [System.Management.Automation.PSCredential] - $ServiceAccount + $ServiceAccount, + + [Parameter()] + [System.String] + $VersionNumber ) # Get the SMO Service object instance - $serviceObject = Get-ServiceObject -ServerName $ServerName -InstanceName $InstanceName -ServiceType $ServiceType + $serviceObject = Get-ServiceObject -ServerName $ServerName -InstanceName $InstanceName -ServiceType $ServiceType -VersionNumber $VersionNumber # If no service was found, throw an exception if (-not $serviceObject) @@ -98,8 +107,12 @@ function Get-TargetResource .PARAMETER Force Forces the service account to be updated. + .PARAMETER VersionNumber + Version number of IntegrationServices + .EXAMPLE Test-TargetResource -ServerName $env:COMPUTERNAME -InstanceName MSSQLSERVER -ServiceType DatabaseEngine -ServiceAccount $account + Test-TargetResource -ServerName $env:COMPUTERNAME -InstanceName MSSQLSERVER -SerticeType IntegrationServices -ServiceAccount $account -VersionNumber 130 #> function Test-TargetResource @@ -131,7 +144,11 @@ function Test-TargetResource [Parameter()] [System.Boolean] - $Force + $Force, + + [Parameter()] + [System.String] + $VersionNumber ) if ($Force) @@ -141,7 +158,7 @@ function Test-TargetResource } # Get the current state - $currentState = Get-TargetResource -ServerName $ServerName -InstanceName $InstanceName -ServiceType $ServiceType -ServiceAccount $ServiceAccount + $currentState = Get-TargetResource -ServerName $ServerName -InstanceName $InstanceName -ServiceType $ServiceType -ServiceAccount $ServiceAccount -VersionNumber $VersionNumber New-VerboseMessage -Message ($script:localizedData.CurrentServiceAccount -f $currentState.ServiceAccountName, $ServerName, $InstanceName) return ($currentState.ServiceAccountName -ieq $ServiceAccount.UserName) @@ -171,6 +188,9 @@ function Test-TargetResource .PARAMETER Force Forces the service account to be updated. + .PARAMETER VersionNumber + Version number of IntegrationServices + .EXAMPLE Set-TargetResource -ServerName $env:COMPUTERNAME -InstanceName MSSQLSERVER -ServiceType DatabaseEngine -ServiceAccount $account #> @@ -202,11 +222,15 @@ function Set-TargetResource [Parameter()] [System.Boolean] - $Force + $Force, + + [Parameter()] + [System.String] + $VersionNumber ) # Get the Service object - $serviceObject = Get-ServiceObject -ServerName $ServerName -InstanceName $InstanceName -ServiceType $ServiceType + $serviceObject = Get-ServiceObject -ServerName $ServerName -InstanceName $InstanceName -ServiceType $ServiceType -VersionNumber $VersionNumber # If no service was found, throw an exception if (-not $serviceObject) @@ -218,7 +242,8 @@ function Set-TargetResource try { New-VerboseMessage -Message ($script:localizedData.UpdatingServiceAccount -f $ServiceAccount.UserName, $serviceObject.Name) - $serviceObject.SetServiceAccount($ServiceAccount.UserName, $ServiceAccount.GetNetworkCredential().Password) + $account = Get-ServiceAccount -ServiceAccount $ServiceAccount + $serviceObject.SetServiceAccount($account.UserName, $account.Password) } catch { @@ -246,9 +271,13 @@ function Set-TargetResource .PARAMETER ServiceType Type of service to be managed. Must be one of the following: DatabaseEngine, SQLServerAgent, Search, IntegrationServices, AnalysisServices, ReportingServices, SQLServerBrowser, NotificationServices. + + .PARAMETER VersionNumber + Version number of IntegrationServices. .EXAMPLE Get-ServiceObject -ServerName $env:COMPUTERNAME -InstanceName MSSQLSERVER -ServiceType DatabaseEngine + Get-ServiceObject -ServerName $env:COMPUTERNAME -InstanceName MSSQLSERVER -ServiceType IntegrationServices -VersionNumber 130 #> function Get-ServiceObject { @@ -266,9 +295,20 @@ function Get-ServiceObject [Parameter(Mandatory = $true)] [ValidateSet('DatabaseEngine', 'SQLServerAgent', 'Search', 'IntegrationServices', 'AnalysisServices', 'ReportingServices', 'SQLServerBrowser', 'NotificationServices')] [System.String] - $ServiceType + $ServiceType, + + [Parameter()] + [System.String] + $VersionNumber ) + # Check to see if Integration services was specified, but no version specified + if (($ServiceType -eq 'IntegrationServices') -and ([String]::IsNullOrEmpty($VersionNumber))) + { + $errorMessage = $script:localizedData.MissingParameter -f $ServiceType + New-InvalidArgumentException -Message $errorMessage -ArgumentName 'VersionNumber' + } + # Load the SMO libraries Import-SQLPSModule @@ -281,9 +321,16 @@ function Get-ServiceObject # Get the service name for the specified instance and type $serviceNameFilter = Get-SqlServiceName -InstanceName $InstanceName -ServiceType $ServiceType + # Check the service type and append version number if IntegrationServices + if ($ServiceType -eq 'IntegrationServices') + { + # Append version number + $serviceNameFilter = '{0}{1}' -f $serviceNameFilter, $VersionNumber + } + # Get the Service object for the specified instance/type $serviceObject = $managedComputer.Services | Where-Object -FilterScript { - $_.Name -eq $serviceNameFilter + $_.Name -eq "$serviceNameFilter" } return $serviceObject @@ -440,3 +487,4 @@ function Get-SqlServiceName # Build the name of the service and return it return ($returnValue -f $serviceNamingScheme, $InstanceName) } + diff --git a/DSCResources/MSFT_SqlServiceAccount/MSFT_SqlServiceAccount.schema.mof b/DSCResources/MSFT_SqlServiceAccount/MSFT_SqlServiceAccount.schema.mof index 01e3d22f3..4a9c4788c 100644 --- a/DSCResources/MSFT_SqlServiceAccount/MSFT_SqlServiceAccount.schema.mof +++ b/DSCResources/MSFT_SqlServiceAccount/MSFT_SqlServiceAccount.schema.mof @@ -8,4 +8,5 @@ class MSFT_SqlServiceAccount : OMI_BaseResource [Write, Description("Determines whether the service is automatically restarted when a change to the configuration was needed.")] Boolean RestartService; [Write, Description("Forces the service account to be updated. Useful for password changes.")] Boolean Force; [Read, Description("Returns the service account username for the service.")] String ServiceAccountName; + [Write, Description("The version number for the service, mandatory with IntegrationServices ServiceType")] String VersionNumber; }; diff --git a/DSCResources/MSFT_SqlServiceAccount/en-US/MSFT_SqlServiceAccount.strings.psd1 b/DSCResources/MSFT_SqlServiceAccount/en-US/MSFT_SqlServiceAccount.strings.psd1 index 9facb6f92..0364b4b93 100644 --- a/DSCResources/MSFT_SqlServiceAccount/en-US/MSFT_SqlServiceAccount.strings.psd1 +++ b/DSCResources/MSFT_SqlServiceAccount/en-US/MSFT_SqlServiceAccount.strings.psd1 @@ -10,4 +10,5 @@ ConvertFrom-StringData @' SetServiceAccountFailed = Unable to set the service account for {0} on {1}. Message {2} UnknownServiceType = Unknown or unsupported service type '{0}' specified! NotInstanceAware = Service type '{0}' is not instance aware. + MissingParameter = Missing parameter detected for '{0}'! '@ diff --git a/DSCResources/MSFT_SqlSetup/MSFT_SqlSetup.psm1 b/DSCResources/MSFT_SqlSetup/MSFT_SqlSetup.psm1 index a49c38b80..c97b14f64 100644 --- a/DSCResources/MSFT_SqlSetup/MSFT_SqlSetup.psm1 +++ b/DSCResources/MSFT_SqlSetup/MSFT_SqlSetup.psm1 @@ -2250,42 +2250,23 @@ function Get-ServiceAccountParameters $ServiceType ) + # Get the service account properties + $accountParameters = Get-ServiceAccount -ServiceAccount $ServiceAccount $parameters = @{} - switch -Regex ($ServiceAccount.UserName.ToUpper()) - { - '^(?:NT ?AUTHORITY\\)?(SYSTEM|LOCALSERVICE|LOCAL SERVICE|NETWORKSERVICE|NETWORK SERVICE)$' - { - $parameters = @{ - "$($ServiceType)SVCACCOUNT" = "NT AUTHORITY\$($Matches[1])" - } - } - - '^(?:NT SERVICE\\)(.*)$' - { - $parameters = @{ - "$($ServiceType)SVCACCOUNT" = "NT SERVICE\$($Matches[1])" - } - } - - # Testing if account is a Managed Service Account, which ends with '$'. - '\$$' - { - $parameters = @{ - "$($ServiceType)SVCACCOUNT" = $ServiceAccount.UserName - } - } + # Assign the service type the account + $parameters = @{ + "$($ServiceType)SVCACCOUNT" = $accountParameters.UserName + } - # Normal local or domain service account. - default - { - $parameters = @{ - "$($ServiceType)SVCACCOUNT" = $ServiceAccount.UserName - "$($ServiceType)SVCPASSWORD" = $ServiceAccount.GetNetworkCredential().Password - } - } + # Check to see if password is null + if (![string]::IsNullOrEmpty($accountParameters.Password)) + { + # Add the password to the hashtable + $parameters.Add("$($ServiceType)SVCPASSWORD", $accountParameters.Password) } + return $parameters } diff --git a/README.md b/README.md index 82f102776..6249e99b0 100644 --- a/README.md +++ b/README.md @@ -1527,6 +1527,9 @@ Manage the service account for SQL Server services. * **`[Boolean]` Force** (Write): Forces the service account to be updated. Useful for password changes. This will cause `Set-TargetResource` to be run on each consecutive run. +* **`[String]` VersionNumber** (Write): The version number of the SQL Server, + mandatory for when IntegrationServices is used as **ServiceType**. + Eg. 130 for SQL 2016. #### Read-Only Properties from Get-TargetResource diff --git a/SqlServerDscHelper.psm1 b/SqlServerDscHelper.psm1 index 73e4bb76b..059a2d35f 100644 --- a/SqlServerDscHelper.psm1 +++ b/SqlServerDscHelper.psm1 @@ -1498,3 +1498,109 @@ function Invoke-SqlScript Invoke-SqlCmd @PSBoundParameters } + +<# + .SYNOPSIS + Builds service account parameters for service account. + + .PARAMETER ServiceAccount + Credential for the service account. +#> +function Get-ServiceAccount +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.Management.Automation.PSCredential] + $ServiceAccount + ) + + $accountParameters = @{} + + switch -Regex ($ServiceAccount.UserName.ToUpper()) + { + '^(?:NT ?AUTHORITY\\)?(SYSTEM|LOCALSERVICE|LOCAL SERVICE|NETWORKSERVICE|NETWORK SERVICE)$' + { + $accountParameters = @{ + "UserName" = "NT AUTHORITY\$($Matches[1])" + } + } + + '^(?:NT SERVICE\\)(.*)$' + { + $accountParameters = @{ + "UserName" = "NT SERVICE\$($Matches[1])" + } + } + + # Testing if account is a Managed Service Account, which ends with '$'. + '\$$' + { + $accountParameters = @{ + "UserName" = $ServiceAccount.UserName + } + } + + # Normal local or domain service account. + default + { + $accountParameters = @{ + "UserName" = $ServiceAccount.UserName + "Password" = $ServiceAccount.GetNetworkCredential().Password + } + } + } + + return $accountParameters +} + +<# + .SYNOPSIS + Recursevly searches Exception stack for specific error number. + + .PARAMETER ExceptionToSearch + The Exception object to test + + .PARAMETER ErrorNumber + The specific error number to look for + + .NOTES + This function allows us to more easily write mocks. +#> +function Find-ExceptionByNumber +{ + # Define parameters + param + ( + [Parameter(Mandatory = $true)] + [System.Exception] + $ExceptionToSearch, + + [Parameter(Mandatory = $true)] + [System.String] + $ErrorNumber + ) + + # Define working variables + $errorFound = $false + + # Check to see if the exception has an inner exception + if ($ExceptionToSearch.InnerException) + { + # Assign found to the returned recursive call + $errorFound = Find-ExceptionByNumber -ExceptionToSearch $ExceptionToSearch.InnerException -ErrorNumber $ErrorNumber + } + + # Check to see if it was found + if (!$errorFound) + { + # Check this exceptions message + $errorFound = $ExceptionToSearch.Number -eq $ErrorNumber + } + + # Return + return $errorFound +} + diff --git a/Tests/Unit/MSFT_SqlServerLogin.Tests.ps1 b/Tests/Unit/MSFT_SqlServerLogin.Tests.ps1 index 28575cf42..99c711671 100644 --- a/Tests/Unit/MSFT_SqlServerLogin.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlServerLogin.Tests.ps1 @@ -218,6 +218,12 @@ try return $mock } + $mockAccountDisabledException = New-Object System.Exception 'Account disabled' + $mockAccountDisabledException | Add-Member -Name 'Number' -Value 18470 -MemberType NoteProperty + $mockLoginFailedException = New-Object System.Exception 'Login failed' + $mockLoginFailedException | Add-Member -Name 'Number' -Value 18456 -MemberType NoteProperty + $mockException = New-Object System.Exception 'Something went wrong' + $mockException | Add-Member -Name 'Number' -Value 1 -MemberType NoteProperty #endregion Pester Test Initialization Describe 'MSFT_SqlServerLogin\Get-TargetResource' { @@ -373,6 +379,105 @@ try Assert-MockCalled -CommandName Connect-SQL -Scope It -Times 1 -Exactly } + + It 'Should be return $true when a login should be present but disabled' { + $mockTestTargetResourceParameters = $getTargetResource_KnownSqlLogin.Clone() + $mockTestTargetResourceParameters.Add('Ensure', 'Present') + $mockTestTargetResourceParameters.Add('Disabled', $true) + $mockTestTargetResourceParameters.Add('LoginType', 'SqlLogin') + $mockTestTargetResourceParameters.Add('LoginCredential', (New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @($mockTestTargetResourceParameters.Name, $mockSqlLoginPassword))) + + # Override mock declaration + Mock -CommandName Connect-SQL -MockWith {throw $mockAccountDisabledException} + + # Override Get-TargetResource + Mock -CommandName Get-TargetResource {return New-Object PSObject -Property @{ + Ensure = 'Present' + Name = $mockTestTargetResourceParameters.Name + LoginType = $mockTestTargetResourceParameters.LoginType + ServerName = 'Server1' + InstanceName = 'MSSQLERVER' + Disabled = $true + LoginMustChangePassword = $false + LoginPasswordPolicyEnforced = $true + LoginPasswordExpirationEnabled = $true + } + } + + # Call the test target + $result = Test-TargetResource @mockTestTargetResourceParameters + + Assert-MockCalled -CommandName Get-TargetResource -Scope It -Times 1 -Exactly + Assert-MockCAlled -CommandName Connect-SQL -Scope It -Times 1 -Exactly + + # Should be true + $result | Should -Be $true + } + + It 'Should be return $false when a login should be present but disabled and password incorrect' { + $mockTestTargetResourceParameters = $getTargetResource_KnownSqlLogin.Clone() + $mockTestTargetResourceParameters.Add('Ensure', 'Present') + $mockTestTargetResourceParameters.Add('Disabled', $true) + $mockTestTargetResourceParameters.Add('LoginType', 'SqlLogin') + $mockTestTargetResourceParameters.Add('LoginCredential', (New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @($mockTestTargetResourceParameters.Name, $mockSqlLoginPassword))) + + # Override mock declaration + Mock -CommandName Connect-SQL -MockWith {throw $mockLoginFailedException} + + # Override Get-TargetResource + Mock -CommandName Get-TargetResource {return New-Object PSObject -Property @{ + Ensure = 'Present' + Name = $mockTestTargetResourceParameters.Name + LoginType = $mockTestTargetResourceParameters.LoginType + ServerName = 'Server1' + InstanceName = 'MSSQLERVER' + Disabled = $true + LoginMustChangePassword = $false + LoginPasswordPolicyEnforced = $true + LoginPasswordExpirationEnabled = $true + } + } + + # Call the test target + $result = Test-TargetResource @mockTestTargetResourceParameters + + Assert-MockCalled -CommandName Get-TargetResource -Scope It -Times 1 -Exactly + Assert-MockCAlled -CommandName Connect-SQL -Scope It -Times 1 -Exactly + + # Should be true + $result | Should -Be $false + } + + It 'Should throw exception when unkown error occurred and account is disabled' { + $mockTestTargetResourceParameters = $getTargetResource_KnownSqlLogin.Clone() + $mockTestTargetResourceParameters.Add('Ensure', 'Present') + $mockTestTargetResourceParameters.Add('Disabled', $true) + $mockTestTargetResourceParameters.Add('LoginType', 'SqlLogin') + $mockTestTargetResourceParameters.Add('LoginCredential', (New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @($mockTestTargetResourceParameters.Name, $mockSqlLoginPassword))) + + # Override mock declaration + Mock -CommandName Connect-SQL -MockWith {throw $mockException} + + # Override Get-TargetResource + Mock -CommandName Get-TargetResource {return New-Object PSObject -Property @{ + Ensure = 'Present' + Name = $mockTestTargetResourceParameters.Name + LoginType = $mockTestTargetResourceParameters.LoginType + ServerName = 'Server1' + InstanceName = 'MSSQLERVER' + Disabled = $true + LoginMustChangePassword = $false + LoginPasswordPolicyEnforced = $true + LoginPasswordExpirationEnabled = $true + } + } + + # Call the test target + { Test-TargetResource @mockTestTargetResourceParameters } | Should -Throw + + Assert-MockCalled -CommandName Get-TargetResource -Scope It -Times 1 -Exactly + Assert-MockCAlled -CommandName Connect-SQL -Scope It -Times 1 -Exactly + } } Context 'When the desired state is Present' { diff --git a/Tests/Unit/MSFT_SqlServiceAccount.Tests.ps1 b/Tests/Unit/MSFT_SqlServiceAccount.Tests.ps1 index 1db06c2e6..5fc8b2dd0 100644 --- a/Tests/Unit/MSFT_SqlServiceAccount.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlServiceAccount.Tests.ps1 @@ -60,8 +60,11 @@ try $mockServiceAccountCredential = (New-Object -TypeName System.Management.Automation.PSCredential $mockDesiredServiceAccountName, (New-Object -TypeName System.Security.SecureString)) $mockDefaultServiceAccountName = 'NT SERVICE\MSSQLSERVER' $mockDefaultServiceAccountCredential = (New-Object -TypeName System.Management.Automation.PSCredential $mockDefaultServiceAccountName, (New-Object -TypeName System.Security.SecureString)) - $mockLocalServiceAccountName = "$($mockSqlServer)\SqlService" + $mockLocalServiceAccountName = '$($mockSqlServer)\SqlService' $mockLocalServiceAccountCredential = (New-Object -TypeName System.Management.Automation.PSCredential $mockLocalServiceAccountName, (New-Object -TypeName System.Security.SecureString)) + $mockManagedServiceAccountName = 'CONTOSO\sqlservice$' + $mockManagedServiceAccountCredential = (New-Object -TypeName System.Management.Automation.PSCredential $mockManagedServiceAccountName, (New-Object -TypeName System.Security.SecureString)) + $mockIntegrationServicesObject = @{Name = 'MsDtsServer130'} # Stores the result of SetServiceAccount calls $testServiceAccountUpdated = @{ @@ -170,6 +173,18 @@ try return $managedComputerObject } + $mockGetServiceObject_DefaultInstance_ManagedServiceAccount = { + $managedComputerObject = New-Object -TypeName PSObject -Property @{ + Name = $mockDefaultInstanceName + ServiceAccount = $mockManagedServiceAccountName + Type = 'SqlServer' + } + + $managedComputerObject | Add-Member @mockAddMemberParameters_SetServiceAccount + + return $managedComputerObject + } + $mockGetServiceObject_NamedInstance = { $managedComputerObject = New-Object -TypeName PSObject -Property @{ Name = ('MSSQL${0}' -f $mockNamedInstance) @@ -483,7 +498,7 @@ try ) # Get the service name - Get-SqlServiceName -InstanceName $mockDefaultInstanceName -ServiceType $ServiceType | Should -Be $ExpectedServiceName + Get-SqlServiceName -InstanceName $mockDefaultInstanceName -ServiceType $ServiceType | Should -Be $ExpectedServiceName # Ensure the mock is utilized Assert-MockCalled -CommandName Get-ChildItem -ParameterFilter $mockGetChildItem_ParameterFilter -Scope It -Exactly -Times 1 @@ -633,6 +648,36 @@ try Assert-MockCalled -CommandName New-Object -Scope It -Exactly -Times 1 } } + + Context 'When getting service IntegrationServices' { + Mock @mockNewObjectParameters_DefaultInstance + Mock -CommandName Get-SqlServiceName -MockWith { + return 'MsDtsServer' + } + Mock -CommandName New-Object -MockWith { + return @{ + Services = $mockIntegrationServicesObject + } + } + It 'Should throw an exception when VersionNumber is not specified'{ + $getServiceObjectParameters = $defaultGetServiceObjectParameters.Clone() + $getServiceObjectParameters.ServiceType = 'IntegrationServices' + $getServiceObjectParameters.InstanceName = 'MSSQLSERVER' + + $testErrorMessage = $script:localizedData.MissingParameter -f 'IntegrationServices' + + {Get-ServiceObject @getServiceObjectParameters} | Should -Throw $testErrorMessage + } + + It 'Should return service when VersionNumber is specified'{ + $getServiceObjectParameters = $defaultGetServiceObjectParameters.Clone() + $getServiceObjectParameters.ServiceType = 'IntegrationServices' + $getServiceObjectParameters.InstanceName = 'MSSQLSERVER' + $getServiceObjectParameters.VersionNumber = '130' + + Get-ServiceObject @getServiceObjectParameters | Should -Be $mockIntegrationServicesObject + } + } } Describe 'MSFT_SqlServerServiceAccount\Get-TargetResource' -Tag 'Get' { @@ -751,6 +796,29 @@ try Assert-MockCalled -CommandName Get-ServiceObject -Scope It -Exactly -Times 1 } } + + Context 'When the service account is a Managed Service Account' { + BeforeAll { + Mock -CommandName Get-ServiceObject -MockWith $mockGetServiceObject_DefaultInstance_ManagedServiceAccount + } + + $defaultGetTargetResourceParameters = @{ + ServerName = $mockSqlServer + InstanceName = $mockDefaultInstanceName + ServiceType = $mockServiceType + ServiceAccount = $mockManagedServiceAccountCredential + } + + It 'Should have the Managed Service Account' { + $currentState = Get-TargetResource @defaultGetTargetResourceParameters + + # Validate the managed service account + $currentState.ServiceAccountName | Should -Be $mockManagedServiceAccountName + + # Ensure the mocks were properly used + Assert-MockCalled -CommandName Get-ServiceObject -Scope It -Exactly -Times 1 + } + } } Describe 'MSFT_SqlServerServiceAccount\Test-TargetResource' -Tag 'Test' { diff --git a/Tests/Unit/SqlServerDSCHelper.Tests.ps1 b/Tests/Unit/SqlServerDSCHelper.Tests.ps1 index 6901308d8..2ab8817f1 100644 --- a/Tests/Unit/SqlServerDSCHelper.Tests.ps1 +++ b/Tests/Unit/SqlServerDSCHelper.Tests.ps1 @@ -142,6 +142,20 @@ InModuleScope $script:moduleName { $mockSetupCredentialSecurePassword = ConvertTo-SecureString -String $mockSetupCredentialPassword -AsPlainText -Force $mockSetupCredential = New-Object -TypeName PSCredential -ArgumentList ($mockSetupCredentialUserName, $mockSetupCredentialSecurePassword) + $mockLocalSystemAccountUserName = 'NT AUTHORITY\SYSTEM' + $mockLocalSystemAccountCredential = New-Object System.Management.Automation.PSCredential $mockLocalSystemAccountUserName, (ConvertTo-SecureString "Password1" -AsPlainText -Force) + $mockManagedServiceAccountUserName = 'CONTOSO\msa$' + $mockManagedServiceAccountCredential = New-Object System.Management.Automation.PSCredential $mockManagedServiceAccountUserName, (ConvertTo-SecureString "Password1" -AsPlainText -Force) + $mockDomainAccountUserName = 'CONTOSO\User1' + $mockLocalServiceAccountUserName = 'NT SERVICE\MyService' + $mockLocalServiceAccountCredential = New-Object System.Management.Automation.PSCredential $mockLocalServiceAccountUserName, (ConvertTo-SecureString "Password1" -AsPlainText -Force) + $mockDomainAccountCredential = New-Object System.Management.Automation.PSCredential $mockDomainAccountUserName, (ConvertTo-SecureString "Password1" -AsPlainText -Force) + $mockInnerException = New-Object System.Exception "This is a mock inner excpetion object" + $mockInnerException | Add-Member -Name 'Number' -Value 2 -MemberType NoteProperty + $mockException = New-Object System.Exception "This is a mock exception object", $mockInnerException + $mockException | Add-Member -Name 'Number' -Value 1 -MemberType NoteProperty + + Describe 'Testing Restart-SqlService' { Context 'Restart-SqlService standalone instance' { BeforeEach { @@ -2028,4 +2042,51 @@ InModuleScope $script:moduleName { } } } + + Describe 'Testing Get-ServiceAccount'{ + Context 'When getting service account' { + It 'Should return NT AUTHORITY\SYSTEM' { + $returnValue = Get-ServiceAccount -ServiceAccount $mockLocalSystemAccountCredential + + $returnValue.UserName | Should -Be $mockLocalSystemAccountUserName + $returnValue.Password | Should -BeNullOrEmpty + } + + It 'Should return Domain Account and Password' { + $returnValue = Get-ServiceAccount -ServiceAccount $mockDomainAccountCredential + + $returnValue.UserName | Should -Be $mockDomainAccountUserName + $returnValue.Password | Should -Be $mockDomainAccountCredential.GetNetworkCredential().Password + } + + It 'Should return managed service account' { + $returnValue = Get-ServiceAccount -ServiceAccount $mockManagedServiceAccountCredential + + $returnValue.UserName | Should -Be $mockManagedServiceAccountUserName + } + + It 'Should return local service account' { + $returnValue= Get-ServiceAccount -ServiceAccount $mockLocalServiceAccountCredential + + $returnValue.UserName | Should -Be $mockLocalServiceAccountUserName + $returnValue.Password | Should -BeNullOrEmpty + } + } + } + + Describe 'Testing Find-ExceptionByNumber'{ + Context 'When searching Exception objects'{ + It 'Should return true for main exception' { + Find-ExceptionByNumber -ExceptionToSearch $mockException -ErrorNumber 1 | Should -Be $true + } + + It 'Should return true for inner exception' { + Find-ExceptionByNumber -ExceptionToSearch $mockException -ErrorNumber 2 | Should -Be $true + } + + It 'Should return false when message not found' { + Find-ExceptionByNumber -ExceptionToSearch $mockException -ErrorNumber 3 | Should -Be $false + } + } + } }