diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bf3e0fa4..082487141 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,14 @@ - Changed the logic of 'Build the argument string to be passed to setup' to not quote the value if root directory is specified ([issue #1254](https://github.com/PowerShell/SqlServerDsc/issues/1254)). +- Changes to SqlAGDatabase + - Fix MatchDatabaseOwner to check for CONTROL SERVER, IMPERSONATE LOGIN, or + CONTROL LOGIN permission in addition to IMPERSONATE ANY LOGIN. + - Update and fix MatchDatabaseOwner help text. +- Changes to xSQLServerHelper + - New-TerminatingError error text for a missing localized message now matches + the output even if the "missing localized message" localized message is + also missing. ## 12.3.0.0 diff --git a/DSCResources/MSFT_SqlAGDatabase/MSFT_SqlAGDatabase.psm1 b/DSCResources/MSFT_SqlAGDatabase/MSFT_SqlAGDatabase.psm1 index 6660f8fec..58381970c 100644 --- a/DSCResources/MSFT_SqlAGDatabase/MSFT_SqlAGDatabase.psm1 +++ b/DSCResources/MSFT_SqlAGDatabase/MSFT_SqlAGDatabase.psm1 @@ -138,11 +138,12 @@ function Get-TargetResource .PARAMETER MatchDatabaseOwner If set to $true, this ensures the database owner of the database on the primary replica is the owner of the database on all secondary replicas. This requires the database owner is available - as a login on all replicas and that the PSDscRunAsCredential has impersonate permissions. + as a login on all replicas and that the PsDscRunAsCredential has impersonate any login, control + server, impersonate login, or control login permissions. - If set to $false, the owner of the database will be the PSDscRunAsCredential. + If set to $false, the owner of the database will be the PsDscRunAsCredential. - The default is '$true'. + The default is '$false'. .PARAMETER ProcessOnlyOnActiveNode Specifies that the resource will only determine if a change is needed if the target node is the active host of the SQL Server Instance. @@ -232,27 +233,32 @@ function Set-TargetResource # Get only the secondary replicas. Some tests do not need to be performed on the primary replica $secondaryReplicas = $availabilityGroup.AvailabilityReplicas | Where-Object -FilterScript { $_.Role -ne 'Primary' } - # Ensure the appropriate permissions are in place on all the replicas - if ( $MatchDatabaseOwner ) + foreach ( $databaseToAddToAvailabilityGroup in $databasesToAddToAvailabilityGroup ) { - $impersonatePermissionsStatus = @{} + $databaseObject = $primaryServerObject.Databases[$databaseToAddToAvailabilityGroup] - foreach ( $availabilityGroupReplica in $secondaryReplicas ) + # Ensure the appropriate permissions are in place on all the replicas + if ( $MatchDatabaseOwner ) { - $currentAvailabilityGroupReplicaServerObject = Connect-SQL -ServerName $availabilityGroupReplica.Name - $impersonatePermissionsStatus.Add( - $availabilityGroupReplica.Name, - ( Test-ImpersonatePermissions -ServerObject $currentAvailabilityGroupReplicaServerObject ) - ) - } + $impersonatePermissionsStatus = @{} - if ( $impersonatePermissionsStatus.Values -contains $false ) - { - $impersonatePermissionsMissingParameters = @( - [System.Security.Principal.WindowsIdentity]::GetCurrent().Name, - ( ( $impersonatePermissionsStatus.GetEnumerator() | Where-Object -FilterScript { -not $_.Value } | Select-Object -ExpandProperty Key ) -join ', ' ) - ) - throw ($script:localizedData.ImpersonatePermissionsMissing -f $impersonatePermissionsMissingParameters ) + foreach ( $availabilityGroupReplica in $secondaryReplicas ) + { + $currentAvailabilityGroupReplicaServerObject = Connect-SQL -ServerName $availabilityGroupReplica.Name + $impersonatePermissionsStatus.Add( + $availabilityGroupReplica.Name, + ( Test-ImpersonatePermissions -ServerObject $currentAvailabilityGroupReplicaServerObject -Securable $databaseObject.Owner ) + ) + } + + if ( $impersonatePermissionsStatus.Values -contains $false ) + { + $impersonatePermissionsMissingParameters = @( + [System.Security.Principal.WindowsIdentity]::GetCurrent().Name, + ( ( $impersonatePermissionsStatus.GetEnumerator() | Where-Object -FilterScript { -not $_.Value } | Select-Object -ExpandProperty Key ) -join ', ' ) + ) + throw ($script:localizedData.ImpersonatePermissionsMissing -f $impersonatePermissionsMissingParameters ) + } } } @@ -481,15 +487,36 @@ function Set-TargetResource $restoreDatabaseQueryStringBuilder.Append($databaseFullBackupFile) | Out-Null $restoreDatabaseQueryStringBuilder.AppendLine('''') | Out-Null $restoreDatabaseQueryStringBuilder.Append('WITH NORECOVERY') | Out-Null + if ( $MatchDatabaseOwner ) + { + $restoreDatabaseQueryStringBuilder.AppendLine() | Out-Null + $restoreDatabaseQueryStringBuilder.Append('REVERT') | Out-Null + } $restoreDatabaseQueryString = $restoreDatabaseQueryStringBuilder.ToString() - # Build the parameters to restore the transaction log - $restoreSqlDatabaseLogParameters = @{ - Database = $databaseToAddToAvailabilityGroup - BackupFile = $databaseLogBackupFile - RestoreAction = 'Log' - NoRecovery = $true + # Need to restore the database with a query in order to impersonate the correct login + $restoreLogQueryStringBuilder = New-Object -TypeName System.Text.StringBuilder + + if ( $MatchDatabaseOwner ) + { + $restoreLogQueryStringBuilder.Append('EXECUTE AS LOGIN = ''') | Out-Null + $restoreLogQueryStringBuilder.Append($databaseObject.Owner) | Out-Null + $restoreLogQueryStringBuilder.AppendLine('''') | Out-Null + } + + $restoreLogQueryStringBuilder.Append('RESTORE DATABASE [') | Out-Null + $restoreLogQueryStringBuilder.Append($databaseToAddToAvailabilityGroup) | Out-Null + $restoreLogQueryStringBuilder.AppendLine(']') | Out-Null + $restoreLogQueryStringBuilder.Append('FROM DISK = ''') | Out-Null + $restoreLogQueryStringBuilder.Append($databaseLogBackupFile) | Out-Null + $restoreLogQueryStringBuilder.AppendLine('''') | Out-Null + $restoreLogQueryStringBuilder.Append('WITH NORECOVERY') | Out-Null + if ( $MatchDatabaseOwner ) + { + $restoreLogQueryStringBuilder.AppendLine() | Out-Null + $restoreLogQueryStringBuilder.Append('REVERT') | Out-Null } + $restoreLogQueryString = $restoreLogQueryStringBuilder.ToString() try { @@ -502,7 +529,7 @@ function Set-TargetResource # Restore the database Invoke-Query -SQLServer $currentAvailabilityGroupReplicaServerObject.NetName -SQLInstanceName $currentAvailabilityGroupReplicaServerObject.ServiceName -Database master -Query $restoreDatabaseQueryString - Restore-SqlDatabase -InputObject $currentAvailabilityGroupReplicaServerObject @restoreSqlDatabaseLogParameters + Invoke-Query -SQLServer $currentAvailabilityGroupReplicaServerObject.NetName -SQLInstanceName $currentAvailabilityGroupReplicaServerObject.ServiceName -Database master -Query $restoreLogQueryString # Add the database to the Availability Group Add-SqlAvailabilityDatabase -InputObject $currentReplicaAvailabilityGroupObject -Database $databaseToAddToAvailabilityGroup @@ -604,11 +631,12 @@ function Set-TargetResource .PARAMETER MatchDatabaseOwner If set to $true, this ensures the database owner of the database on the primary replica is the owner of the database on all secondary replicas. This requires the database owner is available - as a login on all replicas and that the PSDscRunAsCredential has impersonate permissions. + as a login on all replicas and that the PsDscRunAsCredential has impersonate any login, control + server, impersonate login, or control login permissions. - If set to $false, the owner of the database will be the PSDscRunAsCredential. + If set to $false, the owner of the database will be the PsDscRunAsCredential. - The default is '$true'. + The default is '$false'. .PARAMETER ProcessOnlyOnActiveNode Specifies that the resource will only determine if a change is needed if the target node is the active host of the SQL Server Instance. diff --git a/DSCResources/MSFT_SqlAGDatabase/MSFT_SqlAGDatabase.schema.mof b/DSCResources/MSFT_SqlAGDatabase/MSFT_SqlAGDatabase.schema.mof index a4c183253..d25a1bae3 100644 --- a/DSCResources/MSFT_SqlAGDatabase/MSFT_SqlAGDatabase.schema.mof +++ b/DSCResources/MSFT_SqlAGDatabase/MSFT_SqlAGDatabase.schema.mof @@ -8,7 +8,7 @@ class MSFT_SqlAGDatabase : OMI_BaseResource [Required, Description("The path used to seed the availability group replicas. This should be a path that is accessible by all of the replicas")] String BackupPath; [Write, Description("Specifies the membership of the database(s) in the availability group. The options are: Present: The defined database(s) are added to the availability group. All other databases that may be a member of the availability group are ignored. Absent: The defined database(s) are removed from the availability group. All other databases that may be a member of the availability group are ignored. The default is 'Present'."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] String Ensure; [Write, Description("When used with 'Ensure = 'Present'' it ensures the specified database(s) are the only databases that are a member of the specified Availability Group. This parameter is ignored when 'Ensure' is 'Absent'.")] Boolean Force; - [Write, Description("If set to $true, this ensures the database owner of the database on the primary replica is the owner of the database on all secondary replicas. This requires the database owner is available as a login on all replicas and that the PsDscRunAsCredential has impersonate permissions. If set to $false, the owner of the database will be the PsDscRunAsCredential. The default is '$true'")] Boolean MatchDatabaseOwner; + [Write, Description("If set to $true, this ensures the database owner of the database on the primary replica is the owner of the database on all secondary replicas. This requires the database owner is available as a login on all replicas and that the PsDscRunAsCredential has impersonate any login, control server, impersonate login, or control login permissions. If set to $false, the owner of the database will be the PsDscRunAsCredential. The default is '$false'")] Boolean MatchDatabaseOwner; [Write, Description("Specifies that the resource will only determine if a change is needed if the target node is the active host of the SQL Server Instance.")] Boolean ProcessOnlyOnActiveNode; [Read, Description("Determines if the current node is actively hosting the SQL Server instance.")] Boolean IsActiveNode; }; diff --git a/DSCResources/MSFT_SqlAGDatabase/en-US/MSFT_SqlAGDatabase.strings.psd1 b/DSCResources/MSFT_SqlAGDatabase/en-US/MSFT_SqlAGDatabase.strings.psd1 index 112cf9e1e..6e08e9903 100644 --- a/DSCResources/MSFT_SqlAGDatabase/en-US/MSFT_SqlAGDatabase.strings.psd1 +++ b/DSCResources/MSFT_SqlAGDatabase/en-US/MSFT_SqlAGDatabase.strings.psd1 @@ -7,7 +7,7 @@ ConvertFrom-StringData @' DatabaseShouldBeMember = The following databases should be a member of the availability group '{0}': {1}. DatabaseShouldNotBeMember = The following databases should not be a member of the availability group '{0}': {1}. DatabasesNotFound = The following databases were not found in the instance: {0}. - ImpersonatePermissionsMissing = The login '{0}' is missing impersonate permissions in the instances '{1}'. + ImpersonatePermissionsMissing = The login '{0}' is missing impersonate any login, control server, impersonate login, or control login permissions in the instances '{1}'. NotActiveNode = The node '{0}' is not actively hosting the instance '{1}'. Exiting the test. ParameterNotOfType = The parameter '{0}' is not of the type '{1}'. ParameterNullOrEmpty = The parameter '{0}' is NULL or empty. diff --git a/DSCResources/MSFT_SqlAGDatabase/en-US/about_SqlAGDatabase.help.txt b/DSCResources/MSFT_SqlAGDatabase/en-US/about_SqlAGDatabase.help.txt index 706886d20..214e0e88c 100644 --- a/DSCResources/MSFT_SqlAGDatabase/en-US/about_SqlAGDatabase.help.txt +++ b/DSCResources/MSFT_SqlAGDatabase/en-US/about_SqlAGDatabase.help.txt @@ -40,11 +40,12 @@ PARAMETER Force PARAMETER MatchDatabaseOwner If set to $true, this ensures the database owner of the database on the primary replica is the owner of the database on all secondary replicas. This requires the database owner is available - as a login on all replicas and that the PSDscRunAsCredential has impersonate permissions. + as a login on all replicas and that the PsDscRunAsCredential has impersonate any login, control + server, impersonate login, or control login permissions. - If set to $false, the owner of the database will be the PSDscRunAsCredential. + If set to $false, the owner of the database will be the PsDscRunAsCredential. - The default is '$true'. + The default is '$false'. .PARAMETER ProcessOnlyOnActiveNode Specifies that the resource will only determine if a change is needed if the target node is the active host of the SQL Server Instance. diff --git a/README.md b/README.md index e5b5bfd79..9ce9bfccb 100644 --- a/README.md +++ b/README.md @@ -286,10 +286,11 @@ group. Availability Group. This parameter is ignored when 'Ensure' is 'Absent'. * **`[Boolean]` MatchDatabaseOwner** _(Write)_: If set to $true, this ensures the database owner of the database on the primary replica is the owner of the database - on all secondary replicas. This requires the database owner is available as a - login on all replicas and that the PsDscRunAsCredential has impersonate permissions. + on all secondary replicas. This requires the database owner is available + as a login on all replicas and that the PsDscRunAsCredential has impersonate any + login, control server, impersonate login, or control login permissions. If set to $false, the owner of the database will be the PsDscRunAsCredential. - The default is '$true'. + The default is '$false'. * **`[Boolean]` ProcessOnlyOnActiveNode** _(Write)_: Specifies that the resource will only determine if a change is needed if the target node is the active host of the SQL Server Instance. diff --git a/SqlServerDscHelper.psm1 b/SqlServerDscHelper.psm1 index 059a2d35f..8f3f682fc 100644 --- a/SqlServerDscHelper.psm1 +++ b/SqlServerDscHelper.psm1 @@ -289,7 +289,7 @@ function New-TerminatingError if (!$errorMessage) { - $errorMessage = ("No Localization key found for key: {0}" -f $ErrorType) + $errorMessage = ("No Localization key found for ErrorType: '{0}'." -f $ErrorType) } } @@ -1059,6 +1059,43 @@ function Update-AvailabilityGroupReplica } } +<# + .SYNOPSIS + Impersonates a login and determines whether required permissions are present. + + .PARAMETER SQLServer + String containing the host name of the SQL Server to connect to. + + .PARAMETER SQLInstanceName + String containing the SQL Server Database Engine instance to connect to. + + .PARAMETER LoginName + String containing the login (user) which should be checked for a permission. + + .PARAMETER Permissions + This is a list that represents a SQL Server set of database permissions. + + .PARAMETER SecurableClass + String containing the class of permissions to test. It can be: + SERVER: A permission that is applicable against server objects. + LOGIN: A permission that is applicable against login objects. + + Default is 'SERVER'. + + .PARAMETER SecurableName + String containing the name of the object against which permissions exist, e.g. if SecurableClass is LOGIN this is the name of a login permissions may exist against. + + Default is $null. + + .NOTES + These SecurableClass are not yet in this module yet and so are not implemented: + 'APPLICATION ROLE', 'ASSEMBLY', 'ASYMMETRIC KEY', 'CERTIFICATE', + 'CONTRACT', 'DATABASE', 'ENDPOINT', 'FULLTEXT CATALOG', + 'MESSAGE TYPE', 'OBJECT', 'REMOTE SERVICE BINDING', 'ROLE', + 'ROUTE', 'SCHEMA', 'SERVICE', 'SYMMETRIC KEY', 'TYPE', 'USER', + 'XML SCHEMA COLLECTION' + +#> function Test-LoginEffectivePermissions { param @@ -1079,7 +1116,16 @@ function Test-LoginEffectivePermissions [Parameter(Mandatory = $true)] [System.String[]] - $Permissions + $Permissions, + + [ValidateSet('SERVER', 'LOGIN')] + [Parameter()] + [System.String] + $SecurableClass = 'SERVER', + + [Parameter()] + [System.String] + $SecurableName ) # Assume the permissions are not present @@ -1092,12 +1138,24 @@ function Test-LoginEffectivePermissions WithResults = $true } - $queryToGetEffectivePermissionsForLogin = " - EXECUTE AS LOGIN = '$LoginName' - SELECT DISTINCT permission_name - FROM fn_my_permissions(null,'SERVER') - REVERT - " + if ( [System.String]::IsNullOrEmpty($SecurableName) ) + { + $queryToGetEffectivePermissionsForLogin = " + EXECUTE AS LOGIN = '$LoginName' + SELECT DISTINCT permission_name + FROM fn_my_permissions(null,'$SecurableClass') + REVERT + " + } + else + { + $queryToGetEffectivePermissionsForLogin = " + EXECUTE AS LOGIN = '$LoginName' + SELECT DISTINCT permission_name + FROM fn_my_permissions('$SecurableName','$SecurableClass') + REVERT + " + } Write-Verbose -Message ($script:localizedData.GetEffectivePermissionForLogin -f $LoginName, $sqlInstanceName) -Verbose @@ -1233,6 +1291,10 @@ function Get-PrimaryReplicaServerObject .PARAMETER ServerObject The server object on which to perform the test. + + .PARAMETER SecurableName + If set then impersonate permission on this specific securable (e.g. login) is also checked. + #> function Test-ImpersonatePermissions { @@ -1240,9 +1302,14 @@ function Test-ImpersonatePermissions ( [Parameter(Mandatory = $true)] [Microsoft.SqlServer.Management.Smo.Server] - $ServerObject + $ServerObject, + + [Parameter()] + [System.String] + $SecurableName ) + # The impersonate any login permission only exists in SQL 2014 and above $testLoginEffectivePermissionsParams = @{ SQLServer = $ServerObject.ComputerNamePhysicalNetBIOS SQLInstanceName = $ServerObject.ServiceName @@ -1251,12 +1318,77 @@ function Test-ImpersonatePermissions } $impersonatePermissionsPresent = Test-LoginEffectivePermissions @testLoginEffectivePermissionsParams + if ($impersonatePermissionsPresent) + { + New-VerboseMessage -Message ( 'The login "{0}" has impersonate any login permissions on the instance "{1}\{2}".' -f $testLoginEffectivePermissionsParams.LoginName, $testLoginEffectivePermissionsParams.SQLServer, $testLoginEffectivePermissionsParams.SQLInstanceName ) + return $impersonatePermissionsPresent + } + else + { + New-VerboseMessage -Message ( 'The login "{0}" does not have impersonate any login permissions on the instance "{1}\{2}".' -f $testLoginEffectivePermissionsParams.LoginName, $testLoginEffectivePermissionsParams.SQLServer, $testLoginEffectivePermissionsParams.SQLInstanceName ) + } - if ( -not $impersonatePermissionsPresent ) + # Check for sysadmin / control server permission which allows impersonation + $testLoginEffectivePermissionsParams = @{ + SQLServer = $ServerObject.ComputerNamePhysicalNetBIOS + SQLInstanceName = $ServerObject.ServiceName + LoginName = $ServerObject.ConnectionContext.TrueLogin + Permissions = @('CONTROL SERVER') + } + $impersonatePermissionsPresent = Test-LoginEffectivePermissions @testLoginEffectivePermissionsParams + if ($impersonatePermissionsPresent) + { + New-VerboseMessage -Message ( 'The login "{0}" has control server permissions on the instance "{1}\{2}".' -f $testLoginEffectivePermissionsParams.LoginName, $testLoginEffectivePermissionsParams.SQLServer, $testLoginEffectivePermissionsParams.SQLInstanceName ) + return $impersonatePermissionsPresent + } + else { - New-VerboseMessage -Message ( 'The login "{0}" does not have impersonate permissions on the instance "{1}\{2}".' -f $testLoginEffectivePermissionsParams.LoginName, $testLoginEffectivePermissionsParams.SQLServer, $testLoginEffectivePermissionsParams.SQLInstanceName ) + New-VerboseMessage -Message ( 'The login "{0}" does not have control server permissions on the instance "{1}\{2}".' -f $testLoginEffectivePermissionsParams.LoginName, $testLoginEffectivePermissionsParams.SQLServer, $testLoginEffectivePermissionsParams.SQLInstanceName ) } + if ( -not [System.String]::IsNullOrEmpty($SecurableName) ) { + # Check for login-specific impersonation permissions + $testLoginEffectivePermissionsParams = @{ + SQLServer = $ServerObject.ComputerNamePhysicalNetBIOS + SQLInstanceName = $ServerObject.ServiceName + LoginName = $ServerObject.ConnectionContext.TrueLogin + Permissions = @('IMPERSONATE') + SecurableClass = 'LOGIN' + SecurableName = $SecurableName + } + $impersonatePermissionsPresent = Test-LoginEffectivePermissions @testLoginEffectivePermissionsParams + if ($impersonatePermissionsPresent) + { + New-VerboseMessage -Message ( 'The login "{0}" has impersonate permissions on the instance "{1}\{2}" for the login "{3}".' -f $testLoginEffectivePermissionsParams.LoginName, $testLoginEffectivePermissionsParams.SQLServer, $testLoginEffectivePermissionsParams.SQLInstanceName, $SecurableName ) + return $impersonatePermissionsPresent + } + else + { + New-VerboseMessage -Message ( 'The login "{0}" does not have impersonate permissions on the instance "{1}\{2}" for the login "{3}".' -f $testLoginEffectivePermissionsParams.LoginName, $testLoginEffectivePermissionsParams.SQLServer, $testLoginEffectivePermissionsParams.SQLInstanceName, $SecurableName ) + } + + # Check for login-specific control permissions + $testLoginEffectivePermissionsParams = @{ + SQLServer = $ServerObject.ComputerNamePhysicalNetBIOS + SQLInstanceName = $ServerObject.ServiceName + LoginName = $ServerObject.ConnectionContext.TrueLogin + Permissions = @('CONTROL') + SecurableClass = 'LOGIN' + SecurableName = $SecurableName + } + $impersonatePermissionsPresent = Test-LoginEffectivePermissions @testLoginEffectivePermissionsParams + if ($impersonatePermissionsPresent) + { + New-VerboseMessage -Message ( 'The login "{0}" has control permissions on the instance "{1}\{2}" for the login "{3}".' -f $testLoginEffectivePermissionsParams.LoginName, $testLoginEffectivePermissionsParams.SQLServer, $testLoginEffectivePermissionsParams.SQLInstanceName, $SecurableName ) + return $impersonatePermissionsPresent + } + else + { + New-VerboseMessage -Message ( 'The login "{0}" does not have control permissions on the instance "{1}\{2}" for the login "{3}".' -f $testLoginEffectivePermissionsParams.LoginName, $testLoginEffectivePermissionsParams.SQLServer, $testLoginEffectivePermissionsParams.SQLInstanceName, $SecurableName ) + } + } + + New-VerboseMessage -Message ( 'The login "{0}" does not have any impersonate permissions required on the instance "{1}\{2}".' -f $testLoginEffectivePermissionsParams.LoginName, $testLoginEffectivePermissionsParams.SQLServer, $testLoginEffectivePermissionsParams.SQLInstanceName ) return $impersonatePermissionsPresent } @@ -1572,7 +1704,7 @@ function Get-ServiceAccount function Find-ExceptionByNumber { # Define parameters - param + param ( [Parameter(Mandatory = $true)] [System.Exception] diff --git a/Tests/Unit/MSFT_SqlAGDatabase.Tests.ps1 b/Tests/Unit/MSFT_SqlAGDatabase.Tests.ps1 index 43f35edb3..788ff9f74 100644 --- a/Tests/Unit/MSFT_SqlAGDatabase.Tests.ps1 +++ b/Tests/Unit/MSFT_SqlAGDatabase.Tests.ps1 @@ -88,6 +88,8 @@ try $mockAvailabilityGroupObjectName = 'AvailabilityGroup1' $mockAvailabilityGroupWithoutDatabasesObjectName = 'AvailabilityGroupWithoutDatabases' $mockAvailabilityGroupObjectWithPrimaryReplicaOnAnotherServerName = 'AvailabilityGroup2' + $mockTrueLogin = 'Login1' + $mockDatabaseOwner = 'DatabaseOwner1' #endregion mock names @@ -279,6 +281,7 @@ try $newDatabaseObject.LogFiles = @{ FileName = ( [IO.Path]::Combine( $mockLogFilePath, "$($mockPresentDatabaseName).ldf" ) ) } + $newDatabaseObject.Owner = $mockDatabaseOwner # Add the database object to the database collection $mockDatabaseObjects.Add($newDatabaseObject) @@ -299,6 +302,7 @@ try $newDatabaseObject.LogFiles = @{ FileName = ( [IO.Path]::Combine( $mockLogFilePathIncorrect, "$($mockPresentDatabaseName).ldf" ) ) } + $newDatabaseObject.Owner = $mockDatabaseOwner # Add the database object to the database collection $mockDatabaseObjectsWithIncorrectFileNames.Add($newDatabaseObject) @@ -315,6 +319,9 @@ try $mockServerObject.AvailabilityGroups.Add($mockAvailabilityGroupObject.Clone()) $mockServerObject.AvailabilityGroups.Add($mockAvailabilityGroupWithoutDatabasesObject.Clone()) $mockServerObject.AvailabilityGroups.Add($mockAvailabilityGroupObjectWithPrimaryReplicaOnAnotherServer.Clone()) + $mockServerObject.ComputerNamePhysicalNetBIOS = $mockServerObjectDomainInstanceName + $mockServerObject.ConnectionContext = New-Object -TypeName Microsoft.SqlServer.Management.Smo.ServerConnection + $mockServerObject.ConnectionContext.TrueLogin = $mockTrueLogin $mockServerObject.Databases = $mockDatabaseObjects $mockServerObject.DomainInstanceName = $mockServerObjectDomainInstanceName $mockServerObject.NetName = $mockServerObjectDomainInstanceName @@ -328,6 +335,9 @@ try $mockServer2Object.AvailabilityGroups.Add($mockAvailabilityGroupObject.Clone()) $mockServer2Object.AvailabilityGroups.Add($mockAvailabilityGroupWithoutDatabasesObject.Clone()) $mockServer2Object.AvailabilityGroups.Add($mockAvailabilityGroupObjectWithPrimaryReplicaOnAnotherServer.Clone()) + $mockServer2Object.ComputerNamePhysicalNetBIOS = $mockPrimaryServerObjectDomainInstanceName + $mockServer2Object.ConnectionContext = New-Object -TypeName Microsoft.SqlServer.Management.Smo.ServerConnection + $mockServer2Object.ConnectionContext.TrueLogin = $mockTrueLogin $mockServer2Object.Databases = $mockDatabaseObjects $mockServer2Object.DomainInstanceName = $mockPrimaryServerObjectDomainInstanceName $mockServer2Object.NetName = $mockPrimaryServerObjectDomainInstanceName @@ -370,7 +380,8 @@ WITH NORECOVERY' $Query -like 'EXECUTE AS LOGIN = * RESTORE DATABASE * FROM DISK = * -WITH NORECOVERY' +WITH NORECOVERY* +REVERT' } #endregion Invoke Query Mock @@ -454,7 +465,6 @@ WITH NORECOVERY' Mock -CommandName Join-Path -MockWith { [IO.Path]::Combine($databaseMembershipClass.BackupPath,"$($database.Name)_Log_$(Get-Date -Format 'yyyyMMddhhmmss').trn") } -Verifiable -ParameterFilter { $ChildPath -like '*_Log_*.trn' } Mock -CommandName New-TerminatingError { $ErrorType } -Verifiable Mock -CommandName Remove-Item -Verifiable - Mock -CommandName Restore-SqlDatabase -Verifiable } BeforeEach { @@ -501,13 +511,12 @@ WITH NORECOVERY' Assert-MockCalled -CommandName Import-SQLPSModule -Scope It -Times 1 -Exactly Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 2 -Exactly -ParameterFilter { $Query -like 'EXEC master.dbo.xp_fileexist *' } Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 0 -Exactly -ParameterFilter $mockInvokeQueryParameterRestoreDatabase - Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 1 -Exactly -ParameterFilter $mockInvokeQueryParameterRestoreDatabaseWithExecuteAs + Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 2 -Exactly -ParameterFilter $mockInvokeQueryParameterRestoreDatabaseWithExecuteAs Assert-MockCalled -CommandName Join-Path -Scope It -Times 1 -Exactly -ParameterFilter { $ChildPath -like '*_Full_*.bak' } Assert-MockCalled -CommandName Join-Path -Scope It -Times 1 -Exactly -ParameterFilter { $ChildPath -like '*_Log_*.trn' } Assert-MockCalled -CommandName New-TerminatingError -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-Item -Scope It -Times 1 -Exactly Assert-MockCalled -CommandName Remove-SqlAvailabilityDatabase -Scope It -Times 0 -Exactly - Assert-MockCalled -CommandName Restore-SqlDatabase -Scope It -Times 1 -Exactly Assert-MockCalled -CommandName Test-ImpersonatePermissions -Scope It -Times 1 -Exactly } @@ -530,13 +539,12 @@ WITH NORECOVERY' Assert-MockCalled -CommandName Import-SQLPSModule -Scope It -Times 1 -Exactly Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 2 -Exactly -ParameterFilter { $Query -like 'EXEC master.dbo.xp_fileexist *' } Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 0 -Exactly -ParameterFilter $mockInvokeQueryParameterRestoreDatabase - Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 1 -Exactly -ParameterFilter $mockInvokeQueryParameterRestoreDatabaseWithExecuteAs + Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 2 -Exactly -ParameterFilter $mockInvokeQueryParameterRestoreDatabaseWithExecuteAs Assert-MockCalled -CommandName Join-Path -Scope It -Times 1 -Exactly -ParameterFilter { $ChildPath -like '*_Full_*.bak' } Assert-MockCalled -CommandName Join-Path -Scope It -Times 1 -Exactly -ParameterFilter { $ChildPath -like '*_Log_*.trn' } Assert-MockCalled -CommandName New-TerminatingError -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-Item -Scope It -Times 1 -Exactly Assert-MockCalled -CommandName Remove-SqlAvailabilityDatabase -Scope It -Times 0 -Exactly - Assert-MockCalled -CommandName Restore-SqlDatabase -Scope It -Times 1 -Exactly Assert-MockCalled -CommandName Test-ImpersonatePermissions -Scope It -Times 1 -Exactly } @@ -565,7 +573,6 @@ WITH NORECOVERY' Assert-MockCalled -CommandName New-TerminatingError -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-Item -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-SqlAvailabilityDatabase -Scope It -Times 0 -Exactly - Assert-MockCalled -CommandName Restore-SqlDatabase -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Test-ImpersonatePermissions -Scope It -Times 0 -Exactly } @@ -587,21 +594,20 @@ WITH NORECOVERY' Assert-MockCalled -CommandName Get-PrimaryReplicaServerObject -Scope It -Times 0 -Exactly -ParameterFilter { $AvailabilityGroup.PrimaryReplicaServerName -eq 'Server2' } Assert-MockCalled -CommandName Import-SQLPSModule -Scope It -Times 1 -Exactly Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 2 -Exactly -ParameterFilter { $Query -like 'EXEC master.dbo.xp_fileexist *' } - Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 1 -Exactly -ParameterFilter $mockInvokeQueryParameterRestoreDatabase + Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 2 -Exactly -ParameterFilter $mockInvokeQueryParameterRestoreDatabase Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 0 -Exactly -ParameterFilter $mockInvokeQueryParameterRestoreDatabaseWithExecuteAs Assert-MockCalled -CommandName Join-Path -Scope It -Times 1 -Exactly -ParameterFilter { $ChildPath -like '*_Full_*.bak' } Assert-MockCalled -CommandName Join-Path -Scope It -Times 1 -Exactly -ParameterFilter { $ChildPath -like '*_Log_*.trn' } Assert-MockCalled -CommandName New-TerminatingError -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-Item -Scope It -Times 1 -Exactly Assert-MockCalled -CommandName Remove-SqlAvailabilityDatabase -Scope It -Times 0 -Exactly - Assert-MockCalled -CommandName Restore-SqlDatabase -Scope It -Times 1 -Exactly Assert-MockCalled -CommandName Test-ImpersonatePermissions -Scope It -Times 0 -Exactly } It 'Should throw the correct error when "MatchDatabaseOwner" is $true and the current login does not have impersonate permissions' { Mock -CommandName Test-ImpersonatePermissions -MockWith { $false } -Verifiable - { Set-TargetResource @mockSetTargetResourceParameters } | Should -Throw "The login '$([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)' is missing impersonate permissions in the instances 'Server2'." + { Set-TargetResource @mockSetTargetResourceParameters } | Should -Throw "The login '$([System.Security.Principal.WindowsIdentity]::GetCurrent().Name)' is missing impersonate any login, control server, impersonate login, or control login permissions in the instances 'Server2'." Assert-MockCalled -CommandName Add-SqlAvailabilityDatabase -Scope It -Times 0 -Exactly -ParameterFilter { $InputObject.PrimaryReplicaServerName -eq 'Server1' -and $InputObject.LocalReplicaRole -eq 'Primary' } Assert-MockCalled -CommandName Add-SqlAvailabilityDatabase -Scope It -Times 0 -Exactly -ParameterFilter { $InputObject.PrimaryReplicaServerName -eq 'Server1' -and $InputObject.LocalReplicaRole -eq 'Secondary' } @@ -623,7 +629,6 @@ WITH NORECOVERY' Assert-MockCalled -CommandName New-TerminatingError -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-Item -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-SqlAvailabilityDatabase -Scope It -Times 0 -Exactly - Assert-MockCalled -CommandName Restore-SqlDatabase -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Test-ImpersonatePermissions -Scope It -Times 1 -Exactly } @@ -676,8 +681,7 @@ WITH NORECOVERY' Assert-MockCalled -CommandName New-TerminatingError -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-Item -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-SqlAvailabilityDatabase -Scope It -Times 0 -Exactly - Assert-MockCalled -CommandName Restore-SqlDatabase -Scope It -Times 0 -Exactly - Assert-MockCalled -CommandName Test-ImpersonatePermissions -Scope It -Times 1 -Exactly + Assert-MockCalled -CommandName Test-ImpersonatePermissions -Scope It -Times 1 -Exactly $mockServerObject.Databases['DB1'].($prerequisiteCheck.Key) = $originalValue } @@ -708,7 +712,6 @@ WITH NORECOVERY' Assert-MockCalled -CommandName New-TerminatingError -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-Item -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-SqlAvailabilityDatabase -Scope It -Times 0 -Exactly - Assert-MockCalled -CommandName Restore-SqlDatabase -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Test-ImpersonatePermissions -Scope It -Times 1 -Exactly } @@ -746,8 +749,7 @@ WITH NORECOVERY' Assert-MockCalled -CommandName New-TerminatingError -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-Item -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-SqlAvailabilityDatabase -Scope It -Times 0 -Exactly - Assert-MockCalled -CommandName Restore-SqlDatabase -Scope It -Times 0 -Exactly - Assert-MockCalled -CommandName Test-ImpersonatePermissions -Scope It -Times 1 -Exactly + Assert-MockCalled -CommandName Test-ImpersonatePermissions -Scope It -Times 1 -Exactly $mockServerObject.Databases['DB1'].($filestreamProperty.Key) = $originalValue } @@ -779,7 +781,6 @@ WITH NORECOVERY' Assert-MockCalled -CommandName New-TerminatingError -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-Item -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-SqlAvailabilityDatabase -Scope It -Times 0 -Exactly - Assert-MockCalled -CommandName Restore-SqlDatabase -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Test-ImpersonatePermissions -Scope It -Times 1 -Exactly $mockServerObject.Databases['DB1'].ContainmentType = $originalValue @@ -812,7 +813,6 @@ WITH NORECOVERY' Assert-MockCalled -CommandName New-TerminatingError -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-Item -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-SqlAvailabilityDatabase -Scope It -Times 0 -Exactly - Assert-MockCalled -CommandName Restore-SqlDatabase -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Test-ImpersonatePermissions -Scope It -Times 1 -Exactly $mockServer2Object.Databases['DB1'].FileGroups.Files.FileName = $originalValue @@ -845,7 +845,6 @@ WITH NORECOVERY' Assert-MockCalled -CommandName New-TerminatingError -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-Item -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-SqlAvailabilityDatabase -Scope It -Times 0 -Exactly - Assert-MockCalled -CommandName Restore-SqlDatabase -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Test-ImpersonatePermissions -Scope It -Times 1 -Exactly $mockServer2Object.Databases['DB1'].LogFiles.FileName = $originalValue @@ -878,7 +877,6 @@ WITH NORECOVERY' Assert-MockCalled -CommandName New-TerminatingError -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-Item -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-SqlAvailabilityDatabase -Scope It -Times 0 -Exactly - Assert-MockCalled -CommandName Restore-SqlDatabase -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Test-ImpersonatePermissions -Scope It -Times 1 -Exactly $mockServerObject.Databases['DB1'].EncryptionEnabled = $false @@ -905,13 +903,12 @@ WITH NORECOVERY' Assert-MockCalled -CommandName Import-SQLPSModule -Scope It -Times 1 -Exactly Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 2 -Exactly -ParameterFilter { $Query -like 'EXEC master.dbo.xp_fileexist *' } Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 0 -Exactly -ParameterFilter $mockInvokeQueryParameterRestoreDatabase - Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 1 -Exactly -ParameterFilter $mockInvokeQueryParameterRestoreDatabaseWithExecuteAs + Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 2 -Exactly -ParameterFilter $mockInvokeQueryParameterRestoreDatabaseWithExecuteAs Assert-MockCalled -CommandName Join-Path -Scope It -Times 1 -Exactly -ParameterFilter { $ChildPath -like '*_Full_*.bak' } Assert-MockCalled -CommandName Join-Path -Scope It -Times 1 -Exactly -ParameterFilter { $ChildPath -like '*_Log_*.trn' } Assert-MockCalled -CommandName New-TerminatingError -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-Item -Scope It -Times 1 -Exactly Assert-MockCalled -CommandName Remove-SqlAvailabilityDatabase -Scope It -Times 0 -Exactly - Assert-MockCalled -CommandName Restore-SqlDatabase -Scope It -Times 1 -Exactly Assert-MockCalled -CommandName Test-ImpersonatePermissions -Scope It -Times 1 -Exactly } @@ -940,7 +937,6 @@ WITH NORECOVERY' Assert-MockCalled -CommandName New-TerminatingError -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-Item -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-SqlAvailabilityDatabase -Scope It -Times 0 -Exactly - Assert-MockCalled -CommandName Restore-SqlDatabase -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Test-ImpersonatePermissions -Scope It -Times 1 -Exactly } @@ -969,7 +965,6 @@ WITH NORECOVERY' Assert-MockCalled -CommandName New-TerminatingError -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-Item -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-SqlAvailabilityDatabase -Scope It -Times 0 -Exactly - Assert-MockCalled -CommandName Restore-SqlDatabase -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Test-ImpersonatePermissions -Scope It -Times 1 -Exactly } @@ -998,7 +993,6 @@ WITH NORECOVERY' Assert-MockCalled -CommandName New-TerminatingError -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-Item -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-SqlAvailabilityDatabase -Scope It -Times 0 -Exactly - Assert-MockCalled -CommandName Restore-SqlDatabase -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Test-ImpersonatePermissions -Scope It -Times 1 -Exactly } @@ -1021,13 +1015,12 @@ WITH NORECOVERY' Assert-MockCalled -CommandName Import-SQLPSModule -Scope It -Times 1 -Exactly Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 2 -Exactly -ParameterFilter { $Query -like 'EXEC master.dbo.xp_fileexist *' } Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 0 -Exactly -ParameterFilter $mockInvokeQueryParameterRestoreDatabase - Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 1 -Exactly -ParameterFilter $mockInvokeQueryParameterRestoreDatabaseWithExecuteAs + Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 2 -Exactly -ParameterFilter $mockInvokeQueryParameterRestoreDatabaseWithExecuteAs Assert-MockCalled -CommandName Join-Path -Scope It -Times 1 -Exactly -ParameterFilter { $ChildPath -like '*_Full_*.bak' } Assert-MockCalled -CommandName Join-Path -Scope It -Times 1 -Exactly -ParameterFilter { $ChildPath -like '*_Log_*.trn' } Assert-MockCalled -CommandName New-TerminatingError -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-Item -Scope It -Times 1 -Exactly Assert-MockCalled -CommandName Remove-SqlAvailabilityDatabase -Scope It -Times 0 -Exactly - Assert-MockCalled -CommandName Restore-SqlDatabase -Scope It -Times 1 -Exactly Assert-MockCalled -CommandName Test-ImpersonatePermissions -Scope It -Times 1 -Exactly } } @@ -1060,7 +1053,6 @@ WITH NORECOVERY' Assert-MockCalled -CommandName New-TerminatingError -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-Item -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-SqlAvailabilityDatabase -Scope It -Times 2 -Exactly - Assert-MockCalled -CommandName Restore-SqlDatabase -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Test-ImpersonatePermissions -Scope It -Times 0 -Exactly } @@ -1089,7 +1081,6 @@ WITH NORECOVERY' Assert-MockCalled -CommandName New-TerminatingError -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-Item -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-SqlAvailabilityDatabase -Scope It -Times 2 -Exactly - Assert-MockCalled -CommandName Restore-SqlDatabase -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Test-ImpersonatePermissions -Scope It -Times 0 -Exactly } @@ -1118,7 +1109,6 @@ WITH NORECOVERY' Assert-MockCalled -CommandName New-TerminatingError -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-Item -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-SqlAvailabilityDatabase -Scope It -Times 2 -Exactly - Assert-MockCalled -CommandName Restore-SqlDatabase -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Test-ImpersonatePermissions -Scope It -Times 0 -Exactly } } @@ -1146,13 +1136,12 @@ WITH NORECOVERY' Assert-MockCalled -CommandName Import-SQLPSModule -Scope It -Times 1 -Exactly Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 2 -Exactly -ParameterFilter { $Query -like 'EXEC master.dbo.xp_fileexist *' } Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 0 -Exactly -ParameterFilter $mockInvokeQueryParameterRestoreDatabase - Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 1 -Exactly -ParameterFilter $mockInvokeQueryParameterRestoreDatabaseWithExecuteAs + Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 2 -Exactly -ParameterFilter $mockInvokeQueryParameterRestoreDatabaseWithExecuteAs Assert-MockCalled -CommandName Join-Path -Scope It -Times 1 -Exactly -ParameterFilter { $ChildPath -like '*_Full_*.bak' } Assert-MockCalled -CommandName Join-Path -Scope It -Times 1 -Exactly -ParameterFilter { $ChildPath -like '*_Log_*.trn' } Assert-MockCalled -CommandName New-TerminatingError -Scope It -Times 0 -Exactly Assert-MockCalled -CommandName Remove-Item -Scope It -Times 1 -Exactly Assert-MockCalled -CommandName Remove-SqlAvailabilityDatabase -Scope It -Times 1 -Exactly - Assert-MockCalled -CommandName Restore-SqlDatabase -Scope It -Times 1 -Exactly Assert-MockCalled -CommandName Test-ImpersonatePermissions -Scope It -Times 1 -Exactly } } diff --git a/Tests/Unit/SqlServerDSCHelper.Tests.ps1 b/Tests/Unit/SqlServerDSCHelper.Tests.ps1 index 31aaafb39..451d7e6ff 100644 --- a/Tests/Unit/SqlServerDSCHelper.Tests.ps1 +++ b/Tests/Unit/SqlServerDSCHelper.Tests.ps1 @@ -681,68 +681,111 @@ InModuleScope $script:helperModuleName { Describe "Testing Test-LoginEffectivePermissions" { - $mockAllPermissionsPresent = @( + $mockAllServerPermissionsPresent = @( 'Connect SQL', 'Alter Any Availability Group', 'View Server State' ) - $mockPermissionsMissing = @( + $mockServerPermissionsMissing = @( 'Connect SQL', 'View Server State' ) - $mockInvokeQueryClusterServicePermissionsSet = @() # Will be set dynamically in the check + $mockAllLoginPermissionsPresent = @( + 'View Definition', + 'Impersonate' + ) + + $mockLoginPermissionsMissing = @( + 'View Definition' + ) - $mockInvokeQueryClusterServicePermissionsResult = { + $mockInvokeQueryPermissionsSet = @() # Will be set dynamically in the check + + $mockInvokeQueryPermissionsResult = { return New-Object -TypeName PSObject -Property @{ Tables = @{ Rows = @{ - permission_name = $mockInvokeQueryClusterServicePermissionsSet + permission_name = $mockInvokeQueryPermissionsSet } } } } - $testLoginEffectivePermissionsParams = @{ + $testLoginEffectiveServerPermissionsParams = @{ SQLServer = 'Server1' SQLInstanceName = 'MSSQLSERVER' Login = 'NT SERVICE\ClusSvc' Permissions = @() } + $testLoginEffectiveLoginPermissionsParams = @{ + SQLServer = 'Server1' + SQLInstanceName = 'MSSQLSERVER' + Login = 'NT SERVICE\ClusSvc' + Permissions = @() + SecurableClass = 'LOGIN' + SecurableName = 'Login1' + } + BeforeEach { - Mock -CommandName Invoke-Query -MockWith $mockInvokeQueryClusterServicePermissionsResult -Verifiable + Mock -CommandName Invoke-Query -MockWith $mockInvokeQueryPermissionsResult -Verifiable } Context 'When all of the permissions are present' { + It 'Should return $true when the desired server permissions are present' { + $mockInvokeQueryPermissionsSet = $mockAllServerPermissionsPresent.Clone() + $testLoginEffectiveServerPermissionsParams.Permissions = $mockAllServerPermissionsPresent.Clone() + + Test-LoginEffectivePermissions @testLoginEffectiveServerPermissionsParams | Should -Be $true + + Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 1 -Exactly + } - It 'Should return $true when the desired permissions are present' { - $mockInvokeQueryClusterServicePermissionsSet = $mockAllPermissionsPresent.Clone() - $testLoginEffectivePermissionsParams.Permissions = $mockAllPermissionsPresent.Clone() + It 'Should return $true when the desired login permissions are present' { + $mockInvokeQueryPermissionsSet = $mockAllLoginPermissionsPresent.Clone() + $testLoginEffectiveLoginPermissionsParams.Permissions = $mockAllLoginPermissionsPresent.Clone() - Test-LoginEffectivePermissions @testLoginEffectivePermissionsParams | Should -Be $true + Test-LoginEffectivePermissions @testLoginEffectiveLoginPermissionsParams | Should -Be $true - Assert-MockCalled -CommandName Invoke-Query -Times 1 -Exactly + Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 1 -Exactly } } Context 'When a permission is missing' { + It 'Should return $false when the desired server permissions are not present' { + $mockInvokeQueryPermissionsSet = $mockServerPermissionsMissing.Clone() + $testLoginEffectiveServerPermissionsParams.Permissions = $mockAllServerPermissionsPresent.Clone() - It 'Should return $false when the desired permissions are not present' { - $mockInvokeQueryClusterServicePermissionsSet = $mockPermissionsMissing.Clone() - $testLoginEffectivePermissionsParams.Permissions = $mockAllPermissionsPresent.Clone() + Test-LoginEffectivePermissions @testLoginEffectiveServerPermissionsParams | Should -Be $false - Test-LoginEffectivePermissions @testLoginEffectivePermissionsParams | Should -Be $false + Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 1 -Exactly + } + + It 'Should return $false when the specified login has no server permissions assigned' { + $mockInvokeQueryPermissionsSet = @() + $testLoginEffectiveServerPermissionsParams.Permissions = $mockAllServerPermissionsPresent.Clone() + + Test-LoginEffectivePermissions @testLoginEffectiveServerPermissionsParams | Should -Be $false + + Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 1 -Exactly + } + + It 'Should return $false when the desired login permissions are not present' { + $mockInvokeQueryPermissionsSet = $mockLoginPermissionsMissing.Clone() + $testLoginEffectiveLoginPermissionsParams.Permissions = $mockAllLoginPermissionsPresent.Clone() + + Test-LoginEffectivePermissions @testLoginEffectiveLoginPermissionsParams | Should -Be $false Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 1 -Exactly } - It 'Should return $false when the specified login has no permissions assigned' { - $mockInvokeQueryClusterServicePermissionsSet = @() - $testLoginEffectivePermissionsParams.Permissions = $mockAllPermissionsPresent.Clone() + It 'Should return $false when the specified login has no login permissions assigned' { + $mockInvokeQueryPermissionsSet = @() + $testLoginEffectiveLoginPermissionsParams.Permissions = $mockAllLoginPermissionsPresent.Clone() - Test-LoginEffectivePermissions @testLoginEffectivePermissionsParams | Should -Be $false + Test-LoginEffectivePermissions @testLoginEffectiveLoginPermissionsParams | Should -Be $false Assert-MockCalled -CommandName Invoke-Query -Scope It -Times 1 -Exactly } @@ -1229,8 +1272,24 @@ InModuleScope $script:helperModuleName { } } + $mockTestLoginEffectivePermissions_ImpersonateAnyLogin_ParameterFilter = { + $Permissions -eq @('IMPERSONATE ANY LOGIN') + } + + $mockTestLoginEffectivePermissions_ControlServer_ParameterFilter = { + $Permissions -eq @('CONTROL SERVER') + } + + $mockTestLoginEffectivePermissions_ImpersonateLogin_ParameterFilter = { + $Permissions -eq @('IMPERSONATE') + } + + $mockTestLoginEffectivePermissions_ControlLogin_ParameterFilter = { + $Permissions -eq @('CONTROL') + } + Describe 'Testing Test-ImpersonatePermissions' { - $mockConnectionContextObject = New-Object -TypeName Microsoft.SqlServer.Management.Smo.ConnectionContext + $mockConnectionContextObject = New-Object -TypeName Microsoft.SqlServer.Management.Smo.ServerConnection $mockConnectionContextObject.TrueLogin = 'Login1' $mockServerObject = New-Object -TypeName Microsoft.SqlServer.Management.Smo.Server @@ -1238,23 +1297,60 @@ InModuleScope $script:helperModuleName { $mockServerObject.ServiceName = 'MSSQLSERVER' $mockServerObject.ConnectionContext = $mockConnectionContextObject + BeforeEach { + Mock -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateAnyLogin_ParameterFilter -MockWith { $false } -Verifiable + Mock -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlServer_ParameterFilter -MockWith { $false } -Verifiable + Mock -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateLogin_ParameterFilter -MockWith { $false } -Verifiable + Mock -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlLogin_ParameterFilter -MockWith { $false } -Verifiable + } + Context 'When impersonate permissions are present for the login' { - Mock -CommandName Test-LoginEffectivePermissions -MockWith { $true } + It 'Should return true when the impersonate any login permissions are present for the login' { + Mock -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateAnyLogin_ParameterFilter -MockWith { $true } -Verifiable + Test-ImpersonatePermissions -ServerObject $mockServerObject | Should -Be $true + + Assert-MockCalled -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateAnyLogin_ParameterFilter -Scope It -Times 1 -Exactly + } - It 'Should return true when the impersonate permissions are present for the login'{ + It 'Should return true when the control server permissions are present for the login' { + Mock -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlServer_ParameterFilter -MockWith { $true } -Verifiable Test-ImpersonatePermissions -ServerObject $mockServerObject | Should -Be $true - Assert-MockCalled -CommandName Test-LoginEffectivePermissions -Scope It -Times 1 -Exactly + Assert-MockCalled -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlServer_ParameterFilter -Scope It -Times 1 -Exactly + } + + It 'Should return true when the impersonate login permissions are present for the login' { + Mock -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateLogin_ParameterFilter -MockWith { $true } -Verifiable + Test-ImpersonatePermissions -ServerObject $mockServerObject -SecurableName 'Login1' | Should -Be $true + + Assert-MockCalled -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateLogin_ParameterFilter -Scope It -Times 1 -Exactly + } + + It 'Should return true when the control login permissions are present for the login' { + Mock -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlLogin_ParameterFilter -MockWith { $true } -Verifiable + Test-ImpersonatePermissions -ServerObject $mockServerObject -SecurableName 'Login1' | Should -Be $true + + Assert-MockCalled -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlLogin_ParameterFilter -Scope It -Times 1 -Exactly } } Context 'When impersonate permissions are missing for the login' { - Mock -CommandName Test-LoginEffectivePermissions -MockWith { $false } -Verifiable - - It 'Should return false when the impersonate permissions are missing for the login'{ + It 'Should return false when the server permissions are missing for the login' { Test-ImpersonatePermissions -ServerObject $mockServerObject | Should -Be $false - Assert-MockCalled -CommandName Test-LoginEffectivePermissions -Scope It -Times 1 -Exactly + Assert-MockCalled -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateAnyLogin_ParameterFilter -Scope It -Times 1 -Exactly + Assert-MockCalled -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlServer_ParameterFilter -Scope It -Times 1 -Exactly + Assert-MockCalled -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateLogin_ParameterFilter -Scope It -Times 0 -Exactly + Assert-MockCalled -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlLogin_ParameterFilter -Scope It -Times 0 -Exactly + } + + It 'Should return false when the login permissions are missing for the login' { + Test-ImpersonatePermissions -ServerObject $mockServerObject -SecurableName 'Login1' | Should -Be $false + + Assert-MockCalled -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateAnyLogin_ParameterFilter -Scope It -Times 1 -Exactly + Assert-MockCalled -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlServer_ParameterFilter -Scope It -Times 1 -Exactly + Assert-MockCalled -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ImpersonateLogin_ParameterFilter -Scope It -Times 1 -Exactly + Assert-MockCalled -CommandName Test-LoginEffectivePermissions -ParameterFilter $mockTestLoginEffectivePermissions_ControlLogin_ParameterFilter -Scope It -Times 1 -Exactly } } } @@ -1703,10 +1799,29 @@ InModuleScope $script:helperModuleName { Describe 'Testing New-TerminatingError' -Tag NewWarningMessage { Context -Name 'When building a localized error message' -Fixture { It 'Should return the correct error record with the correct error message' { - $errorRecord = New-TerminatingError -ErrorType 'NoKeyFound' -FormatArgs 'Dummy error' $errorRecord.Exception.Message | Should -Be 'No Localization key found for ErrorType: ''Dummy error''.' } + + It 'Should return the correct error record with the correct error message including InnerException' { + $errorRecord = New-TerminatingError -ErrorType 'NoKeyFound' -FormatArgs 'Dummy error' -InnerException 'Dummy exception' + $errorRecord.Exception.Message | Should -Be 'No Localization key found for ErrorType: ''Dummy error''. InnerException: Dummy exception' + } + + It 'Should return the correct error record with a matching FullyQualifiedErrorId' { + $errorRecord = New-TerminatingError -ErrorType 'NoKeyFound' -FormatArgs 'Dummy error' + $errorRecord.FullyQualifiedErrorId | Should -Be 'SqlServerDSCHelper.NoKeyFound' + } + + It 'Should return the correct error record with a matching FullyQualifiedErrorId when there is no calling module' { + Mock -CommandName Get-PSCallStack -MockWith { + , [PSCustomObject] @{ + ScriptName = '' + } + } + $errorRecord = New-TerminatingError -ErrorType 'NoKeyFound' -FormatArgs 'Dummy error' + $errorRecord.FullyQualifiedErrorId | Should -Be 'NoKeyFound' + } } Context -Name 'When building a localized error message that does not exists' -Fixture { @@ -1714,6 +1829,16 @@ InModuleScope $script:helperModuleName { $errorRecord = New-TerminatingError -ErrorType 'UnknownDummyMessage' -FormatArgs 'Dummy error' $errorRecord.Exception.Message | Should -Be 'No Localization key found for ErrorType: ''UnknownDummyMessage''.' } + + It 'Should return the correct error record with the correct error message even if the NoKeyFound message is missing' { + $noKeyFound = $script:localizedData.NoKeyFound + $script:localizedData.Remove('NoKeyFound') + + $errorRecord = New-TerminatingError -ErrorType 'UnknownDummyMessage' -FormatArgs 'Dummy error' + $errorRecord.Exception.Message | Should -Be 'No Localization key found for ErrorType: ''UnknownDummyMessage''.' + + $script:localizedData.NoKeyFound = $noKeyFound + } } Assert-VerifiableMock diff --git a/Tests/Unit/Stubs/SMO.cs b/Tests/Unit/Stubs/SMO.cs index 9d507d8e3..12744e18a 100644 --- a/Tests/Unit/Stubs/SMO.cs +++ b/Tests/Unit/Stubs/SMO.cs @@ -238,7 +238,7 @@ public class Server public string MockGranteeName; public AvailabilityGroupCollection AvailabilityGroups = new AvailabilityGroupCollection(); - public ConnectionContext ConnectionContext; + public ServerConnection ConnectionContext; public string ComputerNamePhysicalNetBIOS; public DatabaseCollection Databases = new DatabaseCollection(); public string DisplayName; @@ -783,7 +783,7 @@ public void Create() // TypeName: Microsoft.SqlServer.Management.Common.ServerConnection // Used by: // SqlAGDatabase - public class ConnectionContext + public class ServerConnection { public string TrueLogin;