diff --git a/CHANGELOG.md b/CHANGELOG.md index 94e77b64d..a83fa5ddf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -472,6 +472,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added some verbose messages to better indicate which CIM methods are run and when they are run. - Minor refactor to support running unit test with strict mode enabled. + - Added support for Power BI Report Server + - Added the ability to change the service account + - Added the ability to specify the databases name + ([issue #149](https://github.com/dsccommunity/SqlServerDsc/issues/149)) + - Added the ability to specify a HTTPS certificate + ([issue #587](https://github.com/dsccommunity/SqlServerDsc/issues/587)) - SqlLogin - Only enforces optional parameter `LoginType` when it is specified in the configuration. diff --git a/source/DSCResources/DSC_SqlRS/DSC_SqlRS.psm1 b/source/DSCResources/DSC_SqlRS/DSC_SqlRS.psm1 index 6022f6461..089d52bb4 100644 --- a/source/DSCResources/DSC_SqlRS/DSC_SqlRS.psm1 +++ b/source/DSCResources/DSC_SqlRS/DSC_SqlRS.psm1 @@ -8,7 +8,7 @@ $script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' <# .SYNOPSIS - Gets the SQL Reporting Services initialization status. + Gets the SQL Server Reporting Services properties. .PARAMETER InstanceName Name of the SQL Server Reporting Services instance to be configured. @@ -27,6 +27,12 @@ $script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US' on the SqlConnection object of the Microsoft.Data.SqlClient driver. This parameter can only be used when the module SqlServer v22.x.x is installed. + + .PARAMETER EncryptionKeyBackupPath + The path where the encryption key will be backed up to. + + .PARAMETER EncryptionKeyBackupPathCredential + The credential which is used to access the path specified in EncryptionKeyBackupPath. #> function Get-TargetResource { @@ -50,7 +56,15 @@ function Get-TargetResource [Parameter()] [ValidateSet('Mandatory', 'Optional', 'Strict')] [System.String] - $Encrypt + $Encrypt, + + [Parameter()] + [System.String] + $EncryptionKeyBackupPath, + + [Parameter()] + [System.Management.Automation.PSCredential] + $EncryptionKeyBackupPathCredential ) Write-Verbose -Message ( @@ -59,21 +73,42 @@ function Get-TargetResource $getTargetResourceResult = @{ InstanceName = $InstanceName - DatabaseServerName = $DatabaseServerName - DatabaseInstanceName = $DatabaseInstanceName + DatabaseServerName = $null + DatabaseInstanceName = $null + DatabaseName = $null ReportServerVirtualDirectory = $null ReportsVirtualDirectory = $null ReportServerReservedUrl = $null ReportsReservedUrl = $null + HttpsCertificateThumbprint = $null + HttpsIPAddress = $null + HttpsPort = $null UseSsl = $false IsInitialized = $false Encrypt = $Encrypt + WindowsServiceIdentityActual = $null + ServiceName = $null + ServiceAccountName = $null + EncryptionKeyBackupFile = $null } $reportingServicesData = Get-ReportingServicesData -InstanceName $InstanceName if ( $null -ne $reportingServicesData.Configuration ) { + #region Get Operating System Information + try + { + $wmiOperatingSystem = Get-CimInstance -ClassName Win32_OperatingSystem -Namespace 'root/cimv2' -ErrorAction Stop + } + catch + { + New-ObjectNotFoundException -Message ( $script:localizedData.GetOperatingSystemClassError ) -ErrorRecord $_ + } + + $language = $wmiOperatingSystem.OSLanguage + #endregion Get Operating System Information + if ( $reportingServicesData.Configuration.DatabaseServerName.Contains('\') ) { $getTargetResourceResult.DatabaseServerName = $reportingServicesData.Configuration.DatabaseServerName.Split('\')[0] @@ -86,57 +121,96 @@ function Get-TargetResource } $isInitialized = $reportingServicesData.Configuration.IsInitialized + $getTargetResourceResult.IsInitialized = [System.Boolean] $isInitialized + $getTargetResourceResult.ServiceName = $reportingServicesData.Configuration.ServiceName + $getTargetResourceResult.ServiceAccountName = $reportingServicesData.Configuration.WindowsServiceIdentityActual + $getTargetResourceResult.DatabaseName = $reportingServicesData.Configuration.DatabaseName + + if ( $reportingServicesData.Configuration.SecureConnectionLevel ) + { + $getTargetResourceResult.UseSsl = $true + } + else + { + $getTargetResourceResult.UseSsl = $false + } + + $getTargetResourceResult.ReportServerVirtualDirectory = $reportingServicesData.Configuration.VirtualDirectoryReportServer + $getTargetResourceResult.ReportsVirtualDirectory = $reportingServicesData.Configuration.VirtualDirectoryReportManager + + #region Get Reserved URLs + $invokeRsCimMethodParameters = @{ + CimInstance = $reportingServicesData.Configuration + MethodName = 'ListReservedUrls' + } + + $reservedUrls = Invoke-RsCimMethod @invokeRsCimMethodParameters - [System.Boolean] $getTargetResourceResult.IsInitialized = $isInitialized + $reportServerReservedUrl = @() + $reportsReservedUrl = @() - if ( $isInitialized ) + for ( $i = 0; $i -lt $reservedUrls.Application.Count; ++$i ) { - if ( $reportingServicesData.Configuration.SecureConnectionLevel ) + if ( $reservedUrls.Application[$i] -eq 'ReportServerWebService' ) { - $getTargetResourceResult.UseSsl = $true + $reportServerReservedUrl += $reservedUrls.UrlString[$i] } - else + + if ( $reservedUrls.Application[$i] -eq $reportingServicesData.ReportsApplicationName ) { - $getTargetResourceResult.UseSsl = $false + $reportsReservedUrl += $reservedUrls.UrlString[$i] } + } - $getTargetResourceResult.ReportServerVirtualDirectory = $reportingServicesData.Configuration.VirtualDirectoryReportServer - $getTargetResourceResult.ReportsVirtualDirectory = $reportingServicesData.Configuration.VirtualDirectoryReportManager + $getTargetResourceResult.ReportServerReservedUrl = $reportServerReservedUrl + $getTargetResourceResult.ReportsReservedUrl = $reportsReservedUrl + #endregion Get Reserved URLs - $invokeRsCimMethodParameters = @{ - CimInstance = $reportingServicesData.Configuration - MethodName = 'ListReservedUrls' + #region Get SSL Certificate Bindings + $invokeRsCimMethodListSSLCertificateBindingsParameters = @{ + CimInstance = $reportingServicesData.Configuration + MethodName = 'ListSSLCertificateBindings' + Arguments = @{ + LCID = $language } + } + $sslCertificateBindings = Invoke-RsCimMethod @invokeRsCimMethodListSSLCertificateBindingsParameters - $reservedUrls = Invoke-RsCimMethod @invokeRsCimMethodParameters + $getTargetResourceResult.HttpsCertificateThumbprint = $sslCertificateBindings | Select-Object -ExpandProperty CertificateHash -Unique -ErrorAction SilentlyContinue + $getTargetResourceResult.HttpsIPAddress = $sslCertificateBindings | Select-Object -ExpandProperty IPAddress -Unique -ErrorAction SilentlyContinue + $getTargetResourceResult.HttpsPort = $sslCertificateBindings | Select-Object -ExpandProperty Port -Unique -ErrorAction SilentlyContinue + #endregion Get SSL Certificate Bindings - $reportServerReservedUrl = @() - $reportsReservedUrl = @() + #region Get Encryption Key Backup + if ( $PSBoundParameters.ContainsKey('EncryptionKeyBackupPath') ) + { + $EncryptionKeyBackupPath = [Environment]::ExpandEnvironmentVariables($EncryptionKeyBackupPath) - for ( $i = 0; $i -lt $reservedUrls.Application.Count; ++$i ) + if ( $EncryptionKeyBackupPath -match '^\\\\') { - if ( $reservedUrls.Application[$i] -eq 'ReportServerWebService' ) - { - $reportServerReservedUrl += $reservedUrls.UrlString[$i] - } + $encryptionKeyBackupPathIsUnc = $true + } + else + { + $encryptionKeyBackupPathIsUnc = $false + } - if ( $reservedUrls.Application[$i] -eq $reportingServicesData.ReportsApplicationName ) - { - $reportsReservedUrl += $reservedUrls.UrlString[$i] - } + if ( $encryptionKeyBackupPathIsUnc -and $PSBoundParameters.ContainsKey('EncryptionKeyBackupPathCredential') ) + { + Connect-UncPath -RemotePath $EncryptionKeyBackupPath -SourceCredential $EncryptionKeyBackupPathCredential } - $getTargetResourceResult.ReportServerReservedUrl = $reportServerReservedUrl - $getTargetResourceResult.ReportsReservedUrl = $reportsReservedUrl - } - else - { - <# - Make sure the value returned is false, if the value returned was - either empty, $null or $false. Fic for issue #822. - #> - [System.Boolean] $getTargetResourceResult.IsInitialized = $false + $encryptionKeyBackupFileName = "$(Get-ComputerName)-$InstanceName.snk" + $encryptionKeyBackupFile = Join-Path -Path $EncryptionKeyBackupPath -ChildPath $encryptionKeyBackupFileName + + $getTargetResourceResult.EncryptionKeyBackupFile = ( Get-Item -Path $encryptionKeyBackupFile -ErrorAction SilentlyContinue ).Name + + if ( $encryptionKeyBackupPathIsUnc -and $PSBoundParameters.ContainsKey('EncryptionKeyBackupPathCredential') ) + { + Disconnect-UncPath -RemotePath $EncryptionKeyBackupPath + } } + #endregion Get Encryption Key Backup } else { @@ -150,7 +224,7 @@ function Get-TargetResource <# .SYNOPSIS - Initializes SQL Reporting Services. + Configures SQL Server Reporting Services. .PARAMETER InstanceName Name of the SQL Server Reporting Services instance to be configured. @@ -161,6 +235,18 @@ function Get-TargetResource .PARAMETER DatabaseInstanceName Name of the SQL Server instance to host the Reporting Service database. + .PARAMETER DatabaseName + Name of the the Reporting Services database. Default is "ReportServer". + + .PARAMETER LocalServiceAccountType + Name of the local account which the service will run as. This is + ignored if the _ServiceAccount_ parameter is supplied.. Default is + "VirtualAccount". + + .PARAMETER ServiceAccount + The service account that should be used when running the Windows + service. + .PARAMETER ReportServerVirtualDirectory Report Server Web Service virtual directory. Optional. @@ -175,6 +261,17 @@ function Get-TargetResource Report Manager/Report Web App URL reservations. Optional. If not specified, 'http://+:80' URL reservation will be used. + .PARAMETER HttpsCertificateThumbprint + The thumbprint of the certificate used to secure SSL communication. + + .PARAMETER HttpsIPAddress + The IP address to bind the certificate specified in the + CertificateThumbprint parameter to. Default is `0.0.0.0` which binds to + all IP addresses. + + .PARAMETER HttpsPort + The port used for SSL communication. Default is `443`. + .PARAMETER UseSsl If connections to the Reporting Services must use SSL. If this parameter is not assigned a value, the default is that Reporting @@ -194,6 +291,18 @@ function Get-TargetResource This parameter can only be used when the module SqlServer v22.x.x is installed. + .PARAMETER EncryptionKeyBackupPath + The path where the encryption key will be backed up to. + + .PARAMETER EncryptionKeyBackupPathCredential + The credential which is used to access the path specified in + "EncryptionKeyBackupPath". + + .PARAMETER EncryptionKeyBackupCredential + The credential which should be used to backup the encryption key. If no + credential is supplied, a randomized value will be generated for + internal use during runtime. + .NOTES To find out the parameter names for the methods in the class MSReportServer_ConfigurationSetting it's easy to list them using the @@ -221,7 +330,7 @@ function Get-TargetResource ``` SecureConnectionLevel (the parameter UseSsl): - The SecureConnectionLevel value can be 0,1,2 or 3, but since + The SecureConnectionLevel value can be 0, 1, 2, or 3, but since SQL Server 2008 R2 this was changed. So we are just setting it to 0 (off) and 1 (on). @@ -249,6 +358,24 @@ function Set-TargetResource [System.String] $DatabaseInstanceName, + [Parameter()] + [System.String] + $DatabaseName = 'ReportServer', + + [Parameter()] + [ValidateSet( + 'LocalService', + 'NetworkService', + 'System', + 'VirtualAccount' + )] + [System.String] + $LocalServiceAccountType = 'VirtualAccount', + + [Parameter()] + [System.Management.Automation.PSCredential] + $ServiceAccount, + [Parameter()] [System.String] $ReportServerVirtualDirectory, @@ -259,11 +386,23 @@ function Set-TargetResource [Parameter()] [System.String[]] - $ReportServerReservedUrl, + $ReportServerReservedUrl = @('http://+:80'), [Parameter()] [System.String[]] - $ReportsReservedUrl, + $ReportsReservedUrl = @('http://+:80'), + + [Parameter()] + [System.String] + $HttpsCertificateThumbprint, + + [Parameter()] + [System.String] + $HttpsIPAddress = '0.0.0.0', + + [Parameter()] + [System.Int32] + $HttpsPort = 443, [Parameter()] [System.Boolean] @@ -276,190 +415,220 @@ function Set-TargetResource [Parameter()] [ValidateSet('Mandatory', 'Optional', 'Strict')] [System.String] - $Encrypt + $Encrypt, + + [Parameter()] + [System.String] + $EncryptionKeyBackupPath, + + [Parameter()] + [System.Management.Automation.PSCredential] + $EncryptionKeyBackupPathCredential, + + [Parameter()] + [System.Management.Automation.PSCredential] + $EncryptionKeyBackupCredential + ) + + $defaultInstanceNames = @( + 'MSSQLSERVER' + 'PBIRS' + 'SSRS' ) + Import-SqlDscPreferredModule + $reportingServicesData = Get-ReportingServicesData -InstanceName $InstanceName if ( $null -ne $reportingServicesData.Configuration ) { - if ( $reportingServicesData.SqlVersion -ge 14 ) - { - if ( [string]::IsNullOrEmpty($ReportServerVirtualDirectory) ) - { - $ReportServerVirtualDirectory = 'ReportServer' - } - - if ( [string]::IsNullOrEmpty($ReportsVirtualDirectory) ) - { - $ReportsVirtualDirectory = 'Reports' - } + $restartReportingService = $false + $executeDatabaseRightsScript = $false - $reportingServicesServiceName = 'SQLServerReportingServices' - $reportingServicesDatabaseName = 'ReportServer' + $getTargetResourceParameters = @{ + InstanceName = $InstanceName + DatabaseServerName = $DatabaseServerName + DatabaseInstanceName = $DatabaseInstanceName } - elseif ( $InstanceName -eq 'MSSQLSERVER' ) - { - if ( [System.String]::IsNullOrEmpty($ReportServerVirtualDirectory) ) - { - $ReportServerVirtualDirectory = 'ReportServer' - } - if ( [System.String]::IsNullOrEmpty($ReportsVirtualDirectory) ) - { - $ReportsVirtualDirectory = 'Reports' - } + $currentConfig = Get-TargetResource @getTargetResourceParameters - $reportingServicesServiceName = 'ReportServer' - $reportingServicesDatabaseName = 'ReportServer' + #region Get Operating System Information + try + { + $wmiOperatingSystem = Get-CimInstance -ClassName Win32_OperatingSystem -Namespace 'root/cimv2' -ErrorAction Stop } - else + catch { - if ( [System.String]::IsNullOrEmpty($ReportServerVirtualDirectory) ) - { - $ReportServerVirtualDirectory = "ReportServer_$InstanceName" - } - - if ( [System.String]::IsNullOrEmpty($ReportsVirtualDirectory) ) - { - $ReportsVirtualDirectory = "Reports_$InstanceName" - } - - $reportingServicesServiceName = "ReportServer`$$InstanceName" - $reportingServicesDatabaseName = "ReportServer`$$InstanceName" + New-ObjectNotFoundException -Message ( $script:localizedData.GetOperatingSystemClassError ) -ErrorRecord $_ } - if ( $DatabaseInstanceName -eq 'MSSQLSERVER' ) + $language = $wmiOperatingSystem.OSLanguage + #endregion Get Operating System Information + + #region Backup Encryption Key + if ( -not $PSBoundParameters.ContainsKey('EncryptionKeyBackupCredential') ) { - $reportingServicesConnection = $DatabaseServerName + Write-Verbose -Message $script:localizedData.EncryptionKeyBackupCredentialNotSpecified -Verbose + $EncryptionKeyBackupCredential = New-EncryptionKeyBackupCredential } - else - { - $reportingServicesConnection = "$DatabaseServerName\$DatabaseInstanceName" + + Write-Verbose -Message ( $script:localizedData.ReportingServicesIsInitialized -f $DatabaseServerName, $DatabaseInstanceName, $currentConfig.IsInitialized ) -Verbose + $backupEncryptionKeyParameters = @{ + IsInitialized = $currentConfig.IsInitialized + EncryptionKeyBackupCredential = $EncryptionKeyBackupCredential + CimInstance = $reportingServicesData.Configuration } - $wmiOperatingSystem = Get-CimInstance -ClassName Win32_OperatingSystem -Namespace 'root/cimv2' -ErrorAction SilentlyContinue - if ( $null -eq $wmiOperatingSystem ) + if ( $PSBoundParameters.ContainsKey('EncryptionKeyBackupPath') -and $PSBoundParameters.ContainsKey('EncryptionKeyBackupPathCredential') ) { - throw 'Unable to find WMI object Win32_OperatingSystem.' + $backupEncryptionKeyParameters.Add('EncryptionKeyBackupPath', $EncryptionKeyBackupPath) + $backupEncryptionKeyParameters.Add('EncryptionKeyBackupPathCredential', $EncryptionKeyBackupPathCredential) } - $language = $wmiOperatingSystem.OSLanguage - $restartReportingService = $false + $backupEncryptionKeyResult = Backup-EncryptionKey @backupEncryptionKeyParameters + #endregion Backup Encryption Key - if ( -not $reportingServicesData.Configuration.IsInitialized ) - { - Write-Verbose -Message "Initializing Reporting Services on $DatabaseServerName\$DatabaseInstanceName." + #region Set the service account + $setServiceAccount = $false - # We will restart Reporting Services after initialization (unless SuppressRestart is set) - $restartReportingService = $true + if ( $PSBoundParameters.ContainsKey('ServiceAccount') -and ( $ServiceAccount.UserName -ne $currentConfig.ServiceAccountName) ) + { + $setServiceAccount = $true - # If no Report Server reserved URLs have been specified, use the default one. - if ( $null -eq $ReportServerReservedUrl ) - { - $ReportServerReservedUrl = @('http://+:80') + $invokeRsCimMethodSetWindowsServiceIdentityParameterArguments = @{ + Account = $ServiceAccount.UserName + Password = $ServiceAccount.GetNetworkCredential().Password + UseBuiltInAccount = $false } - - # If no Report Manager/Report Web App reserved URLs have been specified, use the default one. - if ( $null -eq $ReportsReservedUrl ) - { - $ReportsReservedUrl = @('http://+:80') + } + else + { + $getLocalServiceAccountNameParameters = @{ + LocalServiceAccountType = $LocalServiceAccountType + ServiceName = $currentConfig.ServiceName } + $localServiceAccountName = Get-LocalServiceAccountName @getLocalServiceAccountNameParameters - if ( $reportingServicesData.Configuration.VirtualDirectoryReportServer -ne $ReportServerVirtualDirectory ) + if ( $localServiceAccountName -ne $currentConfig.ServiceAccountName ) { - Write-Verbose -Message "Setting report server virtual directory on $DatabaseServerName\$DatabaseInstanceName to '$ReportServerVirtualDirectory'." + $setServiceAccount = $true - $invokeRsCimMethodParameters = @{ - CimInstance = $reportingServicesData.Configuration - MethodName = 'SetVirtualDirectory' - Arguments = @{ - Application = 'ReportServerWebService' - VirtualDirectory = $ReportServerVirtualDirectory - Lcid = $language - } + # SQL 2017+ cannot use NT AUTHORITY\SYSTEM or NT AUTHORITY\LocalService as the service account + if ( $reportingServicesData.SqlVersion -ge 14 -and $LocalServiceAccountType -in @('LocalService', 'System') ) + { + $localServiceAccountUnsupportedException = $script:localizedData.LocalServiceAccountUnsupportedException -f $LocalServiceAccountType, $reportingServicesData.SqlVersion + New-InvalidArgumentException -Message $localServiceAccountUnsupportedException -ArgumentName $LocalServiceAccountType } - Invoke-RsCimMethod @invokeRsCimMethodParameters - - $ReportServerReservedUrl | ForEach-Object -Process { - Write-Verbose -Message "Adding report server URL reservation on $DatabaseServerName\$DatabaseInstanceName`: $_." - - $invokeRsCimMethodParameters = @{ - CimInstance = $reportingServicesData.Configuration - MethodName = 'ReserveUrl' - Arguments = @{ - Application = 'ReportServerWebService' - UrlString = $_ - Lcid = $language - } - } - - Invoke-RsCimMethod @invokeRsCimMethodParameters + $invokeRsCimMethodSetWindowsServiceIdentityParameterArguments = @{ + Account = $localServiceAccountName + Password = '' + UseBuiltInAccount = $false } } + } - if ( $reportingServicesData.Configuration.VirtualDirectoryReportManager -ne $ReportsVirtualDirectory ) - { - Write-Verbose -Message "Setting reports virtual directory on $DatabaseServerName\$DatabaseInstanceName to '$ReportServerVirtualDirectory'." - - $invokeRsCimMethodParameters = @{ - CimInstance = $reportingServicesData.Configuration - MethodName = 'SetVirtualDirectory' - Arguments = @{ - Application = $reportingServicesData.ReportsApplicationName - VirtualDirectory = $ReportsVirtualDirectory - Lcid = $language - } - } + if ( $setServiceAccount ) + { + Write-Verbose -Message ( + $script:localizedData.SetServiceAccount -f @( + $invokeRsCimMethodSetWindowsServiceIdentityParameterArguments.Account + $currentConfig.ServiceAccountName + ) + ) -Verbose + + $invokeRsCimMethodSetWindowsServiceIdentityParameters = @{ + CimInstance = $reportingServicesData.Configuration + MethodName = 'SetWindowsServiceIdentity' + Arguments = $invokeRsCimMethodSetWindowsServiceIdentityParameterArguments + } - Invoke-RsCimMethod @invokeRsCimMethodParameters + Invoke-RsCimMethod @invokeRsCimMethodSetWindowsServiceIdentityParameters > $null - $ReportsReservedUrl | ForEach-Object -Process { - Write-Verbose -Message "Adding reports URL reservation on $DatabaseServerName\$DatabaseInstanceName`: $_." + $restartReportingService = $true + $executeDatabaseRightsScript = $true - $invokeRsCimMethodParameters = @{ - CimInstance = $reportingServicesData.Configuration - MethodName = 'ReserveUrl' - Arguments = @{ - Application = $reportingServicesData.ReportsApplicationName - UrlString = $_ - Lcid = $language - } - } + # Get the current configuration since it changed the reserved URLs + $currentConfig = Get-TargetResource @getTargetResourceParameters + Write-Verbose -Message ( $script:localizedData.ReportingServicesIsInitialized -f $DatabaseServerName, $DatabaseInstanceName, $currentConfig.IsInitialized ) -Verbose + } + #endregion Set the service account - Invoke-RsCimMethod @invokeRsCimMethodParameters - } - } + #region Database + if ( $DatabaseInstanceName -eq 'MSSQLSERVER' ) + { + $reportingServicesConnection = $DatabaseServerName + } + else + { + $reportingServicesConnection = "$DatabaseServerName\$DatabaseInstanceName" + } - Write-Verbose -Message "Generate database creation script on $DatabaseServerName\$DatabaseInstanceName for database '$reportingServicesDatabaseName'." + # Generate the database creation script + if ( $currentConfig.DatabaseName -ne $DatabaseName ) + { + Write-Verbose -Message ( $script:localizedData.TestDatabaseName -f $currentConfig.DatabaseName, $DatabaseName ) -Verbose + Write-Verbose -Message ( $script:localizedData.GenerateDatabaseCreateScript -f @( + $DatabaseServerName + $DatabaseInstanceName + $DatabaseName + )) -Verbose $invokeRsCimMethodParameters = @{ CimInstance = $reportingServicesData.Configuration MethodName = 'GenerateDatabaseCreationScript' Arguments = @{ - DatabaseName = $reportingServicesDatabaseName + DatabaseName = $DatabaseName IsSharePointMode = $false Lcid = $language } } $reportingServicesDatabaseScript = Invoke-RsCimMethod @invokeRsCimMethodParameters + } - # Determine RS service account - $reportingServicesServiceAccountUserName = (Get-CimInstance -ClassName Win32_Service | Where-Object -FilterScript { - $_.Name -eq $reportingServicesServiceName - }).StartName - - Write-Verbose -Message "Generate database rights script on $DatabaseServerName\$DatabaseInstanceName for database '$reportingServicesDatabaseName' and user '$reportingServicesServiceAccountUserName'." + # Generate the database rights script + if ( + $executeDatabaseRightsScript -or + $currentConfig.DatabaseName -ne $DatabaseName -or + $currentConfig.DatabaseServerName -ne $DatabaseServerName -or + $currentConfig.DatabaseInstanceName -ne $DatabaseInstanceName + ) + { + Write-Verbose -Message ( $script:localizedData.GenerateDatabaseRightsScript -f @( + $DatabaseServerName + $DatabaseInstanceName + $DatabaseName + )) -Verbose + + #region Determine if the database is local or remote + + # Get the local computer properties + $computerSystem = Get-CimInstance -ClassName Win32_ComputerSystem -Namespace 'root/cimv2' + + # Define an array of hostnames and IP addresses which identify the local computer + $localServerIdentifiers = @( + '.' + '(local)' + 'LOCAL' + 'localhost' + $computerSystem.DNSHostName + "$($computerSystem.DNSHostName).$($computerSystem.Domain)" + ) + ( Get-NetIPAddress | Select-Object -ExpandProperty IPAddress ) + + $localServerIdentifiersRegex = $localServerIdentifiers | Foreach-Object -Process { [System.Text.RegularExpressions.Regex]::Escape($_) } + $databaseServerIsRemote = $DatabaseServerName -notmatch "^$( $localServerIdentifiersRegex -join '|' )$" + Write-Verbose -Message ( $script:localizedData.DatabaseServerIsRemote -f $DatabaseServerName, $databaseServerIsRemote ) -Verbose + #endregion Determine if the database is local or remote $invokeRsCimMethodParameters = @{ CimInstance = $reportingServicesData.Configuration MethodName = 'GenerateDatabaseRightsScript' Arguments = @{ - DatabaseName = $reportingServicesDatabaseName - UserName = $reportingServicesServiceAccountUserName - IsRemote = $false + DatabaseName = $DatabaseName + UserName = $currentConfig.ServiceAccountName + IsRemote = $databaseServerIsRemote IsWindowsUser = $true } } @@ -489,15 +658,27 @@ function Set-TargetResource Invoke-SqlCmd @invokeSqlCmdParameters -Query $reportingServicesDatabaseScript.Script Invoke-SqlCmd @invokeSqlCmdParameters -Query $reportingServicesDatabaseRightsScript.Script + } - Write-Verbose -Message "Set database connection on $DatabaseServerName\$DatabaseInstanceName to database '$reportingServicesDatabaseName'." + # Set the database connection + if ( + $currentConfig.DatabaseName -ne $DatabaseName -or + $currentConfig.DatabaseServerName -ne $DatabaseServerName -or + $currentConfig.DatabaseInstanceName -ne $DatabaseInstanceName + ) + { + Write-Verbose -Message ( $script:localizedData.SetDatabaseConnection -f @( + $DatabaseServerName + $DatabaseInstanceName + $DatabaseName + )) -Verbose $invokeRsCimMethodParameters = @{ CimInstance = $reportingServicesData.Configuration MethodName = 'SetDatabaseConnection' Arguments = @{ Server = $reportingServicesConnection - DatabaseName = $reportingServicesDatabaseName + DatabaseName = $DatabaseName Username = '' Password = '' @@ -522,291 +703,496 @@ function Set-TargetResource Invoke-RsCimMethod @invokeRsCimMethodParameters - <# - When initializing SSRS 2019, the call to InitializeReportServer - always fails, even if IsInitialized flag is $false. - It also seems that simply restarting SSRS at this point initializes - it. + # Get the current configuration since the database connection was updated + $currentConfig = Get-TargetResource @getTargetResourceParameters + Write-Verbose -Message ( $script:localizedData.ReportingServicesIsInitialized -f $DatabaseServerName, $DatabaseInstanceName, $currentConfig.IsInitialized ) -Verbose + } - This has since been change to always restart Reporting Services service - for all versions to initialize the Reporting Services. If still not - initialized after restart, the CIM method InitializeReportServer will - also run after. + #endregion Database - We will ignore $SuppressRestart here. - #> - Write-Verbose -Message $script:localizedData.RestartToFinishInitialization + <# + Determine if the encryption key was backed up. If not, then back it up now + This is required for anything older than SQL 2017 + #> + if ( -not $backupEncryptionKeyResult ) + { + $backupEncryptionKeyResult = Backup-EncryptionKey @backupEncryptionKeyParameters + } - Restart-ReportingServicesService -InstanceName $InstanceName -WaitTime 30 + #region Virtual Directories + <# + SQL Server Reporting Services virtual directories (both + Report Server and Report Manager/Report Web App) are a + part of SQL Server Reporting Services URL reservations. + + The default SQL Server Reporting Services URL reservations are: + http://+:80/ReportServer/ (for Report Server) + and + http://+:80/Reports/ (for Report Manager/Report Web App) + + You can get them by running 'netsh http show urlacl' from + command line. + + In order to change a virtual directory, we first need to remove + existing URL reservations, change the appropriate virtual directory + setting and re-add URL reservations, which will then contain the + new virtual directory. + #> - $restartReportingService = $false + if ( [System.String]::IsNullOrEmpty($ReportServerVirtualDirectory) -and $InstanceName -notin $defaultInstanceNames ) + { + $ReportServerVirtualDirectory = "ReportServer_$InstanceName" + } + elseif ( [System.String]::IsNullOrEmpty($ReportServerVirtualDirectory) ) + { + $ReportServerVirtualDirectory = 'ReportServer' + } - $reportingServicesData = Get-ReportingServicesData -InstanceName $InstanceName + if ( [System.String]::IsNullOrEmpty($ReportsVirtualDirectory) -and $InstanceName -notin $defaultInstanceNames ) + { + $ReportsVirtualDirectory = "Reports_$InstanceName" + } + elseif ( [System.String]::IsNullOrEmpty($ReportsVirtualDirectory) ) + { + $ReportsVirtualDirectory = 'Reports' + } - <# - Only execute InitializeReportServer if SetDatabaseConnection hasn't - initialized Reporting Services already. Otherwise, executing - InitializeReportServer will fail on SQL Server Standard and - lower editions. - #> - if ( -not $reportingServicesData.Configuration.IsInitialized ) - { - Write-Verbose -Message "Did not help restarting the Reporting Services service, running the CIM method to initialize report server on $DatabaseServerName\$DatabaseInstanceName for instance ID '$($reportingServicesData.Configuration.InstallationID)'." + if ( -not [System.String]::IsNullOrEmpty($ReportServerVirtualDirectory) -and ($ReportServerVirtualDirectory -ne $currentConfig.ReportServerVirtualDirectory) ) + { + Write-Verbose -Message ( + $script:localizedData.SetReportServerVirtualDirectory -f @( + $DatabaseServerName + $DatabaseInstanceName + $ReportServerVirtualDirectory + ) + ) -Verbose - $restartReportingService = $true + $restartReportingService = $true + $currentConfig.ReportServerReservedUrl | ForEach-Object -Process { $invokeRsCimMethodParameters = @{ CimInstance = $reportingServicesData.Configuration - MethodName = 'InitializeReportServer' + MethodName = 'RemoveURL' Arguments = @{ - InstallationId = $reportingServicesData.Configuration.InstallationID + Application = 'ReportServerWebService' + UrlString = $_ + Lcid = $language } } Invoke-RsCimMethod @invokeRsCimMethodParameters } - else - { - Write-Verbose -Message "Reporting Services on $DatabaseServerName\$DatabaseInstanceName is initialized." + + $invokeRsCimMethodParameters = @{ + CimInstance = $reportingServicesData.Configuration + MethodName = 'SetVirtualDirectory' + Arguments = @{ + Application = 'ReportServerWebService' + VirtualDirectory = $ReportServerVirtualDirectory + Lcid = $language + } } - if ( $PSBoundParameters.ContainsKey('UseSsl') -and $UseSsl -ne $reportingServicesData.Configuration.SecureConnectionLevel ) - { - Write-Verbose -Message "Changing value for using SSL to '$UseSsl'." + Invoke-RsCimMethod @invokeRsCimMethodParameters + + # Get the current configuration since it changed the virtual directories + $currentConfig = Get-TargetResource @getTargetResourceParameters + Write-Verbose -Message ( $script:localizedData.ReportingServicesIsInitialized -f $DatabaseServerName, $DatabaseInstanceName, $currentConfig.IsInitialized ) -Verbose + } + + if ( -not [System.String]::IsNullOrEmpty($ReportsVirtualDirectory) -and ($ReportsVirtualDirectory -ne $currentConfig.ReportsVirtualDirectory) ) + { + Write-Verbose -Message ( + $script:localizedData.SetReportsVirtualDirectory -f @( + $DatabaseServerName + $DatabaseInstanceName + $ReportServerVirtualDirectory + ) + ) -Verbose - $restartReportingService = $true + $restartReportingService = $true + $currentConfig.ReportsReservedUrl | ForEach-Object -Process { $invokeRsCimMethodParameters = @{ CimInstance = $reportingServicesData.Configuration - MethodName = 'SetSecureConnectionLevel' + MethodName = 'RemoveURL' Arguments = @{ - Level = @(0, 1)[$UseSsl] + Application = $reportingServicesData.ReportsApplicationName + UrlString = $_ + Lcid = $language } } Invoke-RsCimMethod @invokeRsCimMethodParameters } - } - else - { - $getTargetResourceParameters = @{ - InstanceName = $InstanceName - DatabaseServerName = $DatabaseServerName - DatabaseInstanceName = $DatabaseInstanceName - } - - $currentConfig = Get-TargetResource @getTargetResourceParameters - <# - SQL Server Reporting Services virtual directories (both - Report Server and Report Manager/Report Web App) are a - part of SQL Server Reporting Services URL reservations. - - The default SQL Server Reporting Services URL reservations are: - http://+:80/ReportServer/ (for Report Server) - and - http://+:80/Reports/ (for Report Manager/Report Web App) - - You can get them by running 'netsh http show urlacl' from - command line. - - In order to change a virtual directory, we first need to remove - existing URL reservations, change the appropriate virtual directory - setting and re-add URL reservations, which will then contain the - new virtual directory. - #> + $invokeRsCimMethodParameters = @{ + CimInstance = $reportingServicesData.Configuration + MethodName = 'SetVirtualDirectory' + Arguments = @{ + Application = $reportingServicesData.ReportsApplicationName + VirtualDirectory = $ReportsVirtualDirectory + Lcid = $language + } + } - if ( -not [System.String]::IsNullOrEmpty($ReportServerVirtualDirectory) -and ($ReportServerVirtualDirectory -ne $currentConfig.ReportServerVirtualDirectory) ) - { - Write-Verbose -Message "Setting report server virtual directory on $DatabaseServerName\$DatabaseInstanceName to $ReportServerVirtualDirectory." + Invoke-RsCimMethod @invokeRsCimMethodParameters - $restartReportingService = $true + # Get the current configuration since it changed the virtual directories + $currentConfig = Get-TargetResource @getTargetResourceParameters + Write-Verbose -Message ( $script:localizedData.ReportingServicesIsInitialized -f $DatabaseServerName, $DatabaseInstanceName, $currentConfig.IsInitialized ) -Verbose + } + #endregion Virtual Directories - $currentConfig.ReportServerReservedUrl | ForEach-Object -Process { - $invokeRsCimMethodParameters = @{ - CimInstance = $reportingServicesData.Configuration - MethodName = 'RemoveURL' - Arguments = @{ - Application = 'ReportServerWebService' - UrlString = $_ - Lcid = $language - } - } + #region Reserved URLs + $compareParameters = @{ + ReferenceObject = $currentConfig.ReportServerReservedUrl + DifferenceObject = $ReportServerReservedUrl + } - Invoke-RsCimMethod @invokeRsCimMethodParameters - } + if ( ($null -ne $ReportServerReservedUrl) -and ($null -ne (Compare-Object @compareParameters)) ) + { + $restartReportingService = $true + $currentConfig.ReportServerReservedUrl | ForEach-Object -Process { $invokeRsCimMethodParameters = @{ CimInstance = $reportingServicesData.Configuration - MethodName = 'SetVirtualDirectory' + MethodName = 'RemoveURL' Arguments = @{ - Application = 'ReportServerWebService' - VirtualDirectory = $ReportServerVirtualDirectory - Lcid = $language + Application = 'ReportServerWebService' + UrlString = $_ + Lcid = $language } } Invoke-RsCimMethod @invokeRsCimMethodParameters - - $currentConfig.ReportServerReservedUrl | ForEach-Object -Process { - $invokeRsCimMethodParameters = @{ - CimInstance = $reportingServicesData.Configuration - MethodName = 'ReserveUrl' - Arguments = @{ - Application = 'ReportServerWebService' - UrlString = $_ - Lcid = $language - } - } - - Invoke-RsCimMethod @invokeRsCimMethodParameters - } } - if ( -not [System.String]::IsNullOrEmpty($ReportsVirtualDirectory) -and ($ReportsVirtualDirectory -ne $currentConfig.ReportsVirtualDirectory) ) - { - Write-Verbose -Message "Setting reports virtual directory on $DatabaseServerName\$DatabaseInstanceName to $ReportServerVirtualDirectory." - - $restartReportingService = $true - - $currentConfig.ReportsReservedUrl | ForEach-Object -Process { - $invokeRsCimMethodParameters = @{ - CimInstance = $reportingServicesData.Configuration - MethodName = 'RemoveURL' - Arguments = @{ - Application = $reportingServicesData.ReportsApplicationName - UrlString = $_ - Lcid = $language - } - } - - Invoke-RsCimMethod @invokeRsCimMethodParameters - } - + $ReportServerReservedUrl | ForEach-Object -Process { + Write-Verbose -Message "Adding report server URL reservation on $DatabaseServerName\$DatabaseInstanceName`: $_." $invokeRsCimMethodParameters = @{ CimInstance = $reportingServicesData.Configuration - MethodName = 'SetVirtualDirectory' + MethodName = 'ReserveUrl' Arguments = @{ - Application = $reportingServicesData.ReportsApplicationName - VirtualDirectory = $ReportsVirtualDirectory - Lcid = $language + Application = 'ReportServerWebService' + UrlString = $_ + Lcid = $language } } Invoke-RsCimMethod @invokeRsCimMethodParameters + } - $currentConfig.ReportsReservedUrl | ForEach-Object -Process { - $invokeRsCimMethodParameters = @{ - CimInstance = $reportingServicesData.Configuration - MethodName = 'ReserveUrl' - Arguments = @{ - Application = $reportingServicesData.ReportsApplicationName - UrlString = $_ - Lcid = $language - } + # Get the current configuration + $currentConfig = Get-TargetResource @getTargetResourceParameters + Write-Verbose -Message ( $script:localizedData.ReportingServicesIsInitialized -f $DatabaseServerName, $DatabaseInstanceName, $currentConfig.IsInitialized ) -Verbose + } + + $compareParameters = @{ + ReferenceObject = $currentConfig.ReportsReservedUrl + DifferenceObject = $ReportsReservedUrl + } + + if ( ($null -ne $ReportsReservedUrl) -and ($null -ne (Compare-Object @compareParameters)) ) + { + $restartReportingService = $true + + $currentConfig.ReportsReservedUrl | ForEach-Object -Process { + $invokeRsCimMethodParameters = @{ + CimInstance = $reportingServicesData.Configuration + MethodName = 'RemoveURL' + Arguments = @{ + Application = $reportingServicesData.ReportsApplicationName + UrlString = $_ + Lcid = $language } + } + + Invoke-RsCimMethod @invokeRsCimMethodParameters + } - Invoke-RsCimMethod @invokeRsCimMethodParameters + $ReportsReservedUrl | ForEach-Object -Process { + Write-Verbose -Message ( + $script:localizedData.AddReportsUrlReservation -f @( + $DatabaseServerName + $DatabaseInstanceName + $_ + ) + ) + + $invokeRsCimMethodParameters = @{ + CimInstance = $reportingServicesData.Configuration + MethodName = 'ReserveUrl' + Arguments = @{ + Application = $reportingServicesData.ReportsApplicationName + UrlString = $_ + Lcid = $language + } } + + Invoke-RsCimMethod @invokeRsCimMethodParameters } - $compareParameters = @{ - ReferenceObject = $currentConfig.ReportServerReservedUrl - DifferenceObject = $ReportServerReservedUrl + # Get the current configuration + $currentConfig = Get-TargetResource @getTargetResourceParameters + Write-Verbose -Message ( $script:localizedData.ReportingServicesIsInitialized -f $DatabaseServerName, $DatabaseInstanceName, $currentConfig.IsInitialized ) -Verbose + } + #endregion Reserved URLs + + #region SSL Certificate Bindings + $invokeRsCimMethodListSSLCertificateBindingsParameters = @{ + CimInstance = $reportingServicesData.Configuration + MethodName = 'ListSSLCertificateBindings' + Arguments = @{ + LCID = $language + } + } + $sslCertificateBindings = Invoke-RsCimMethod @invokeRsCimMethodListSSLCertificateBindingsParameters + + # Create a PSObject of the binding information to make it easier to work with + $sslCertificateBindingObjects = @() + for ( $i = 0; $i -lt $sslCertificateBindings.Application.Count; $i++ ) + { + $sslCertificateBindingObjects += [PSCustomObject] @{ + Application = $sslCertificateBindings.Application[$i] + CertificateHash = $sslCertificateBindings.CertificateHash[$i] + IPAddress = $sslCertificateBindings.IPAddress[$i] + Port = $sslCertificateBindings.Port[$i] + } + } + + if ( $sslCertificateBindingObjects.Count -gt 1 ) + { + # Get the bindings to remove + $sslCertificateBindingsToRemove = $sslCertificateBindingObjects | Where-Object -FilterScript { + $_.CertificateHash -ne $HttpsCertificateThumbprint -or + $_.IPAddress -ne $HttpsIPAddress -or + $_.Port -ne $HttpsPort } - if ( ($null -ne $ReportServerReservedUrl) -and ($null -ne (Compare-Object @compareParameters)) ) + $invokeRsCimMethodRemoveSSLCertificateBindingsParameters = @{ + CimInstance = $reportingServicesData.Configuration + MethodName = 'RemoveSSLCertificateBindings' + Arguments = $null + } + + foreach ( $sslCertificateBindingToRemove in $sslCertificateBindingsToRemove ) { - $restartReportingService = $true + $invokeRsCimMethodRemoveSSLCertificateBindingsParameterArguments = @{ + Application = $sslCertificateBindingToRemove.Application + CertificateHash = $sslCertificateBindingToRemove.CertificateHash + IPAddress = $sslCertificateBindingToRemove.IPAddress + Port = $sslCertificateBindingToRemove.Port + lcid = $language + } + $invokeRsCimMethodRemoveSSLCertificateBindingsParameters.Arguments = $invokeRsCimMethodRemoveSSLCertificateBindingsParameterArguments - $currentConfig.ReportServerReservedUrl | ForEach-Object -Process { - $invokeRsCimMethodParameters = @{ - CimInstance = $reportingServicesData.Configuration - MethodName = 'RemoveURL' - Arguments = @{ - Application = 'ReportServerWebService' - UrlString = $_ - Lcid = $language - } - } + $removeSslCertficateBindingResult = Invoke-RsCimMethod @invokeRsCimMethodRemoveSSLCertificateBindingsParameters - Invoke-RsCimMethod @invokeRsCimMethodParameters + if ( $removeSslCertficateBindingResult.HRESULT -ne 0 ) + { + $removeSslCertficateBindingErrorArguments = @( + $invokeRsCimMethodRemoveSSLCertificateBindingsParameterArguments.Application + $invokeRsCimMethodRemoveSSLCertificateBindingsParameterArguments.CertificateHash + $invokeRsCimMethodRemoveSSLCertificateBindingsParameterArguments.IPAddress + $invokeRsCimMethodRemoveSSLCertificateBindingsParameterArguments.Port + ) + New-InvalidResultException -Message ( $script:localizedData.RemoveSslCertficateBindingError -f $removeSslCertficateBindingErrorArguments ) -ErrorRecord $removeSslCertficateBindingResult.Error } + } + } - $ReportServerReservedUrl | ForEach-Object -Process { - Write-Verbose -Message "Adding report server URL reservation on $DatabaseServerName\$DatabaseInstanceName`: $_." - $invokeRsCimMethodParameters = @{ - CimInstance = $reportingServicesData.Configuration - MethodName = 'ReserveUrl' - Arguments = @{ - Application = 'ReportServerWebService' - UrlString = $_ - Lcid = $language - } + if ( $PSBoundParameters.ContainsKey('HttpsCertificateThumbprint') ) + { + $applicationNames = @( + 'ReportServerWebApp' + 'ReportServerWebService' + + # I thought I saw other app names being used, but I can't seem to find them + #'PowerBIWebApp' + #'OfficeWebApp' + ) + + $invokeRsCimMethodCreateSSLCertificateBindingParameters = @{ + CimInstance = $reportingServicesData.Configuration + MethodName = 'CreateSSLCertificateBinding' + Arguments = $null + } + + foreach ( $applicationName in $applicationNames ) + { + $sslCertificateBindingExists = $sslCertificateBindingObjects | + Where-Object -Property Application -EQ -Value $applicationName | + Where-Object -Property CertificateHash -EQ -Value $HttpsCertificateThumbprint | + Where-Object -Property IPAddress -EQ -Value $HttpsIPAddress | + Where-Object -Property Port -EQ -Value $HttpsPort + + if ( -not $sslCertificateBindingExists ) + { + $invokeRsCimMethodCreateSSLCertificateBindingParameterArguments = @{ + Application = $applicationName + CertificateHash = $HttpsCertificateThumbprint.ToLower() + IPAddress = $HttpsIPAddress + Port = $HttpsPort + Lcid = $language } + $invokeRsCimMethodCreateSSLCertificateBindingParameters.Arguments = $invokeRsCimMethodCreateSSLCertificateBindingParameterArguments - Invoke-RsCimMethod @invokeRsCimMethodParameters + Invoke-RsCimMethod @invokeRsCimMethodCreateSSLCertificateBindingParameters > $null } } - $compareParameters = @{ - ReferenceObject = $currentConfig.ReportsReservedUrl - DifferenceObject = $ReportsReservedUrl - } + # Get the current configuration + $currentConfig = Get-TargetResource @getTargetResourceParameters + Write-Verbose -Message ( $script:localizedData.ReportingServicesIsInitialized -f $DatabaseServerName, $DatabaseInstanceName, $currentConfig.IsInitialized ) -Verbose + } + #endregion SSL Certificate Bindings + + #region Initialize + Write-Verbose -Message ( $script:localizedData.InitializeReportingServices -f $DatabaseServerName, $DatabaseInstanceName ) -Verbose + + <# + When initializing SSRS 2019, the call to InitializeReportServer + always fails, even if IsInitialized flag is $false. + It also seems that simply restarting SSRS at this point initializes + it. + + This has since been change to always restart Reporting Services service + for all versions to initialize the Reporting Services. If still not + initialized after restart, the CIM method InitializeReportServer will + also run after. - if ( ($null -ne $ReportsReservedUrl) -and ($null -ne (Compare-Object @compareParameters)) ) + We will ignore $SuppressRestart here. + #> + Write-Verbose -Message $script:localizedData.RestartToFinishInitialization + + Restart-ReportingServicesService -ServiceName $currentConfig.ServiceName -WaitTime 30 + + $restartReportingService = $false + + # Get the current configuration + $reportingServicesData = Get-ReportingServicesData -InstanceName $InstanceName + $currentConfig = Get-TargetResource @getTargetResourceParameters + Write-Verbose -Message ( $script:localizedData.ReportingServicesIsInitialized -f $DatabaseServerName, $DatabaseInstanceName, $currentConfig.IsInitialized ) -Verbose + + <# + Only execute InitializeReportServer if SetDatabaseConnection hasn't + initialized Reporting Services already. Otherwise, executing + InitializeReportServer will fail on SQL Server Standard and + lower editions. + #> + if ( -not $currentConfig.IsInitialized ) + { + Write-Verbose -Message ( + $script:localizedData.RestartDidNotHelp -f @( + $DatabaseServerName + $DatabaseInstanceName + $reportingServicesData.Configuration.InstallationID + ) + ) + + $restartReportingService = $true + $restoreKey = $false + $reportingServicesInitialized = $currentConfig.IsInitialized + + do { - $restartReportingService = $true + if ( $restoreKey ) + { + Write-Verbose -Message ( $script:localizedData.EncryptionKeyBackupCredentialUserName -f $EncryptionKeyBackupCredential.UserName ) -Verbose - $currentConfig.ReportsReservedUrl | ForEach-Object -Process { - $invokeRsCimMethodParameters = @{ + try + { + $EncryptionKeyBackupCredential.GetNetworkCredential() + } + catch + { + throw 'Failed getting the network credential.' + } + + $invokeRsCimMethodRestoreEncryptionKeyParameters = @{ CimInstance = $reportingServicesData.Configuration - MethodName = 'RemoveURL' + MethodName = 'RestoreEncryptionKey' Arguments = @{ - Application = $reportingServicesData.ReportsApplicationName - UrlString = $_ - Lcid = $language + KeyFile = $backupEncryptionKeyResult.KeyFile + Length = $backupEncryptionKeyResult.Length + Password = $EncryptionKeyBackupCredential.GetNetworkCredential().Password } } - - Invoke-RsCimMethod @invokeRsCimMethodParameters + Invoke-RsCimMethod @invokeRsCimMethodRestoreEncryptionKeyParameters > $null } - $ReportsReservedUrl | ForEach-Object -Process { - Write-Verbose -Message "Adding reports URL reservation on $DatabaseServerName\$DatabaseInstanceName`: $_." - - $invokeRsCimMethodParameters = @{ + try + { + $invokeRsCimMethodInitializeReportServerParameters = @{ CimInstance = $reportingServicesData.Configuration - MethodName = 'ReserveUrl' + MethodName = 'InitializeReportServer' Arguments = @{ - Application = $reportingServicesData.ReportsApplicationName - UrlString = $_ - Lcid = $language + InstallationId = $reportingServicesData.Configuration.InstallationID } } - Invoke-RsCimMethod @invokeRsCimMethodParameters + $initializeReportServerResult = Invoke-RsCimMethod @invokeRsCimMethodInitializeReportServerParameters + $reportingServicesInitialized = $initializeReportServerResult.ReturnValue + Write-Verbose -Message "Reporting Services Initialized: $reportingServicesInitialized" -Verbose + } + catch [System.Management.Automation.RuntimeException] + { + if ( $_.Exception -match 'The report server was unable to validate the integrity of encrypted data in the database' ) + { + Write-Verbose -Message $_.Exception -Verbose + + # Restore the encryption key before trying again + $restoreKey = $true + } + else + { + throw $_ + } } } + while ( -not $reportingServicesInitialized ) - if ( $PSBoundParameters.ContainsKey('UseSsl') -and $UseSsl -ne $currentConfig.UseSsl ) - { - Write-Verbose -Message "Changing value for using SSL to '$UseSsl'." + # Refresh the reportingServicesData + $reportingServicesData = Get-ReportingServicesData -InstanceName $InstanceName - $restartReportingService = $true + # Get the current configuration + $currentConfig = Get-TargetResource @getTargetResourceParameters + Write-Verbose -Message ( $script:localizedData.ReportingServicesIsInitialized -f $DatabaseServerName, $DatabaseInstanceName, $currentConfig.IsInitialized ) -Verbose + } + else + { + Write-Verbose -Message ( $script:localizedData.ReportingServicesIsInitialized -f $DatabaseServerName, $DatabaseInstanceName, $currentConfig.IsInitialized ) -Verbose + Write-Verbose -Message ( + $script:localizedData.ReportingServicesIsInitialized -f @( + $DatabaseServerName + $DatabaseInstanceName + $currentConfig.IsInitialized + ) + ) + } + #endregion Initialize - $invokeRsCimMethodParameters = @{ - CimInstance = $reportingServicesData.Configuration - MethodName = 'SetSecureConnectionLevel' - Arguments = @{ - Level = @(0, 1)[$UseSsl] - } - } + #region Use SSL + if ( $PSBoundParameters.ContainsKey('UseSsl') -and $UseSsl -ne $currentConfig.UseSsl ) + { + Write-Verbose -Message ( $script:localizedData.SetUseSsl -f $UseSsl ) -Verbose - Invoke-RsCimMethod @invokeRsCimMethodParameters + $restartReportingService = $true + + $invokeRsCimMethodParameters = @{ + CimInstance = $reportingServicesData.Configuration + MethodName = 'SetSecureConnectionLevel' + Arguments = @{ + Level = @(0, 1)[$UseSsl] + } } + + Invoke-RsCimMethod @invokeRsCimMethodParameters } + #endregion Use SSL + #region Restart if ( $restartReportingService -and $SuppressRestart ) { Write-Warning -Message $script:localizedData.SuppressRestart @@ -814,8 +1200,9 @@ function Set-TargetResource elseif ( $restartReportingService -and (-not $SuppressRestart) ) { Write-Verbose -Message $script:localizedData.Restart - Restart-ReportingServicesService -InstanceName $InstanceName -WaitTime 30 + Restart-ReportingServicesService -ServiceName $currentConfig.ServiceName -WaitTime 30 } + #endregion Restart } if ( -not (Test-TargetResource @PSBoundParameters) ) @@ -827,7 +1214,8 @@ function Set-TargetResource <# .SYNOPSIS - Tests the SQL Reporting Services initialization status. + Tests the SQL Server Reporting Services to determine if it is in the + desired state. .PARAMETER InstanceName Name of the SQL Server Reporting Services instance to be configured. @@ -838,6 +1226,18 @@ function Set-TargetResource .PARAMETER DatabaseInstanceName Name of the SQL Server instance to host the Reporting Service database. + .PARAMETER DatabaseName + Name of the the Reporting Services database. Default is "ReportServer". + + .PARAMETER LocalServiceAccountType + Name of the local account which the service will run as. This is + ignored if the _ServiceAccount_ parameter is supplied.. Default is + "VirtualAccount". + + .PARAMETER ServiceAccount + The service account that should be used when running the Windows + service. + .PARAMETER ReportServerVirtualDirectory Report Server Web Service virtual directory. Optional. @@ -846,12 +1246,23 @@ function Set-TargetResource .PARAMETER ReportServerReservedUrl Report Server URL reservations. Optional. If not specified, - http://+:80' URL reservation will be used. + 'http://+:80' URL reservation will be used. .PARAMETER ReportsReservedUrl Report Manager/Report Web App URL reservations. Optional. If not specified, 'http://+:80' URL reservation will be used. + .PARAMETER HttpsCertificateThumbprint + The thumbprint of the certificate used to secure SSL communication. + + .PARAMETER HttpsIPAddress + The IP address to bind the certificate specified in the + CertificateThumbprint parameter to. Default is `0.0.0.0` which binds to + all IP addresses. + + .PARAMETER HttpsPort + The port used for SSL communication. Default is `443`. + .PARAMETER UseSsl If connections to the Reporting Services must use SSL. If this parameter is not assigned a value, the default is that Reporting @@ -870,6 +1281,18 @@ function Set-TargetResource on the SqlConnection object of the Microsoft.Data.SqlClient driver. This parameter can only be used when the module SqlServer v22.x.x is installed. + + .PARAMETER EncryptionKeyBackupPath + The path where the encryption key will be backed up to. + + .PARAMETER EncryptionKeyBackupPathCredential + The credential which is used to access the path specified in + "EncryptionKeyBackupPath". + + .PARAMETER EncryptionKeyBackupCredential + The credential which should be used to backup the encryption key. If no + credential is supplied, a randomized value will be generated for + internal use during runtime. #> function Test-TargetResource { @@ -890,6 +1313,24 @@ function Test-TargetResource [System.String] $DatabaseInstanceName, + [Parameter()] + [System.String] + $DatabaseName = 'ReportServer', + + [Parameter()] + [ValidateSet( + 'LocalService', + 'NetworkService', + 'System', + 'VirtualAccount' + )] + [System.String] + $LocalServiceAccountType = 'VirtualAccount', + + [Parameter()] + [System.Management.Automation.PSCredential] + $ServiceAccount, + [Parameter()] [System.String] $ReportServerVirtualDirectory, @@ -900,11 +1341,23 @@ function Test-TargetResource [Parameter()] [System.String[]] - $ReportServerReservedUrl, + $ReportServerReservedUrl = @('http://+:80'), [Parameter()] [System.String[]] - $ReportsReservedUrl, + $ReportsReservedUrl = @('http://+:80'), + + [Parameter()] + [System.String] + $HttpsCertificateThumbprint, + + [Parameter()] + [System.String] + $HttpsIPAddress = '0.0.0.0', + + [Parameter()] + [System.Int32] + $HttpsPort = 443, [Parameter()] [System.Boolean] @@ -917,7 +1370,19 @@ function Test-TargetResource [Parameter()] [ValidateSet('Mandatory', 'Optional', 'Strict')] [System.String] - $Encrypt + $Encrypt, + + [Parameter()] + [System.String] + $EncryptionKeyBackupPath, + + [Parameter()] + [System.Management.Automation.PSCredential] + $EncryptionKeyBackupPathCredential, + + [Parameter()] + [System.Management.Automation.PSCredential] + $EncryptionKeyBackupCredential ) $result = $true @@ -933,23 +1398,53 @@ function Test-TargetResource $getTargetResourceParameters.Encrypt = $Encrypt } + if ( $PSBoundParameters.ContainsKey('EncryptionKeyBackupPath') ) + { + $getTargetResourceParameters.EncryptionKeyBackupPath = $EncryptionKeyBackupPath + } + + if ( $PSBoundParameters.ContainsKey('EncryptionKeyBackupPathCredential') ) + { + $getTargetResourceParameters.EncryptionKeyBackupPathCredential = $EncryptionKeyBackupPathCredential + } + $currentConfig = Get-TargetResource @getTargetResourceParameters if (-not $currentConfig.IsInitialized) { - Write-Verbose -Message "Reporting services $DatabaseServerName\$DatabaseInstanceName are not initialized." + Write-Verbose -Message ( $script:localizedData.TestNotInitialized -f $DatabaseServerName, $DatabaseInstanceName ) -Verbose + $result = $false + } + + if ( $DatabaseName -ne $currentConfig.DatabaseName ) + { + Write-Verbose -Message ( $script:localizedData.TestDatabaseName -f $currentConfig.DatabaseName, $DatabaseName ) -Verbose $result = $false } if (-not [System.String]::IsNullOrEmpty($ReportServerVirtualDirectory) -and ($ReportServerVirtualDirectory -ne $currentConfig.ReportServerVirtualDirectory)) { - Write-Verbose -Message "Report server virtual directory on $DatabaseServerName\$DatabaseInstanceName is $($currentConfig.ReportServerVirtualDir), should be $ReportServerVirtualDirectory." + Write-Verbose -Message ( + $script:localizedData.TestReportServerVirtualDirectory -f @( + $DatabaseServerName + $DatabaseInstanceName + $currentConfig.ReportServerVirtualDir + $ReportServerVirtualDirectory + ) + ) -Verbose $result = $false } if (-not [System.String]::IsNullOrEmpty($ReportsVirtualDirectory) -and ($ReportsVirtualDirectory -ne $currentConfig.ReportsVirtualDirectory)) { - Write-Verbose -Message "Reports virtual directory on $DatabaseServerName\$DatabaseInstanceName is $($currentConfig.ReportsVirtualDir), should be $ReportsVirtualDirectory." + Write-Verbose -Message ( + $script:localizedData.TestReportsVirtualDirectory -f @( + $DatabaseServerName + $DatabaseInstanceName + $currentConfig.ReportsVirtualDir + $ReportsVirtualDirectory + ) + ) -Verbose $result = $false } @@ -969,7 +1464,14 @@ function Test-TargetResource if ($null -ne (Compare-Object @compareParameters)) { - Write-Verbose -Message "Report server reserved URLs on $DatabaseServerName\$DatabaseInstanceName are $($currentConfig.ReportServerReservedUrl -join ', '), should be $($ReportServerReservedUrl -join ', ')." + Write-Verbose -Message ( + $script:localizedData.ReportServerReservedUrlNotInDesiredState -f @( + $DatabaseServerName, + $DatabaseInstanceName, + $($currentConfig.ReportServerReservedUrl -join ', '), + ($ReportServerReservedUrl -join ', ') + ) + ) $result = $false } } @@ -991,7 +1493,14 @@ function Test-TargetResource if ($null -ne (Compare-Object @compareParameters)) { - Write-Verbose -Message "Reports reserved URLs on $DatabaseServerName\$DatabaseInstanceName are $($currentConfig.ReportsReservedUrl -join ', ')), should be $($ReportsReservedUrl -join ', ')." + Write-Verbose -Message ( + $script:localizedData.ReportsReservedUrlNotInDesiredState -f @( + $DatabaseServerName, + $DatabaseInstanceName, + $($currentConfig.ReportsReservedUrl -join ', '), + ($ReportsReservedUrl -join ', ') + ) + ) $result = $false } } @@ -999,7 +1508,68 @@ function Test-TargetResource if ($PSBoundParameters.ContainsKey('UseSsl') -and $UseSsl -ne $currentConfig.UseSsl) { - Write-Verbose -Message "The value for using SSL are not in desired state. Should be '$UseSsl', but was '$($currentConfig.UseSsl)'." + Write-Verbose -Message ( + $script:localizedData.TestUseSsl -f @( + $UseSsl + $currentConfig.UseSsl + ) + ) -Verbose + $result = $false + } + + if ( $PSBoundParameters.ContainsKey('ServiceAccount') ) + { + if ( $ServiceAccount.UserName -ne $currentConfig.ServiceAccountName ) + { + Write-Verbose -Message ( + $script:localizedData.TestServiceAccount -f @( + $ServiceAccount.UserName + $currentConfig.ServiceAccountName + ) + ) -Verbose + $result = $false + } + } + else + { + $getLocalServiceAccountNameParameters = @{ + LocalServiceAccountType = $LocalServiceAccountType + ServiceName = $currentConfig.ServiceName + } + $localServiceAccountName = Get-LocalServiceAccountName @getLocalServiceAccountNameParameters + + if ( $localServiceAccountName -ne $currentConfig.ServiceAccountName ) + { + Write-Verbose -Message ( + $script:localizedData.TestServiceAccount -f @( + $localServiceAccountName + $currentConfig.ServiceAccountName + ) + ) -Verbose + $result = $false + } + } + + if ( $PSBoundParameters.ContainsKey('EncryptionKeyBackupPath') -and [System.String]::IsNullOrEmpty($currentConfig.EncryptionKeyBackupFile) ) + { + $result = $false + } + + if ( $PSBoundParameters.ContainsKey('HttpsCertificateThumbprint') -and $HttpsCertificateThumbprint -ne $currentConfig.HttpsCertificateThumbprint ) + { + Write-Verbose -Message ( $script:localizedData.HttpsCertificateThumbprintNotInDesiredState -f ( $currentConfig.HttpsCertificateThumbprint -join ', ' ), $HttpsCertificateThumbprint ) -Verbose + $result = $false + } + + if ( $PSBoundParameters.ContainsKey('HttpsCertificateThumbprint') -and $HttpsIPAddress -ne $currentConfig.HttpsIPAddress ) + { + Write-Verbose -Message ( $script:localizedData.HttpsIPAddressNotInDesiredState -f ( $currentConfig.HttpsIPAddress -join ', ' ), $HttpsIPAddress ) -Verbose + $result = $false + } + + if ( $PSBoundParameters.ContainsKey('HttpsCertificateThumbprint') -and $HttpsPort -ne $currentConfig.HttpsPort ) + { + Write-Verbose -Message ( $script:localizedData.HttpsPortNotInDesiredState -f ( $currentConfig.HttpsPort -join ', '), $HttpsPort ) -Verbose $result = $false } @@ -1033,18 +1603,17 @@ function Get-ReportingServicesData if (Test-Path -Path "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$instanceId\MSSQLServer\CurrentVersion") { - # SQL Server 2017 and 2019 SSRS stores current SQL Server version to a different Registry path. - $sqlVersion = [int]((Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$InstanceId\MSSQLServer\CurrentVersion" -Name 'CurrentVersion').CurrentVersion).Split('.')[0] + # Get the SQL Server 2017+ SSRS and PBIRS version + $sqlVersion = [System.Int32] ((Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$InstanceId\MSSQLServer\CurrentVersion" -Name 'CurrentVersion').CurrentVersion).Split('.')[0] } else { - $sqlVersion = [int]((Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$instanceId\Setup" -Name 'Version').Version).Split('.')[0] + # Get the SQL Server 2016 and older version + $sqlVersion = [System.Int32] ((Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\$instanceId\Setup" -Name 'Version').Version).Split('.')[0] } - $reportingServicesConfiguration = Get-CimInstance -ClassName MSReportServer_ConfigurationSetting -Namespace "root\Microsoft\SQLServer\ReportServer\RS_$InstanceName\v$sqlVersion\Admin" - $reportingServicesConfiguration = $reportingServicesConfiguration | Where-Object -FilterScript { - $_.InstanceName -eq $InstanceName - } + $reportingServicesConfiguration = Get-CimInstance -ClassName MSReportServer_ConfigurationSetting -Namespace "root\Microsoft\SQLServer\ReportServer\RS_$InstanceName\v$sqlVersion\Admin" | + Where-Object -Property InstanceName -EQ -Value $InstanceName <# SQL Server Reporting Services Web Portal application name changed @@ -1061,7 +1630,7 @@ function Get-ReportingServicesData } } - @{ + return @{ Configuration = $reportingServicesConfiguration ReportsApplicationName = $reportsApplicationName SqlVersion = $sqlVersion @@ -1079,7 +1648,7 @@ function Get-ReportingServicesData The method to call in the CIM Instance object. .PARAMETER Arguments - The arguments that should be + The arguments that should be supplied to the CIM Method. #> function Invoke-RsCimMethod { @@ -1133,12 +1702,216 @@ function Invoke-RsCimMethod $errorMessage = $invokeCimMethodResult.Error } - throw 'Method {0}() failed with an error. Error: {1} (HRESULT:{2})' -f @( - $MethodName - $errorMessage - $invokeCimMethodResult.HRESULT + throw ( + $script:localizedData.InvokeRsCimMethodError -f @( + $MethodName + $errorMessage + $invokeCimMethodResult.HRESULT + ) ) } return $invokeCimMethodResult } + +<# + .SYNOPSIS + Convert the local service account type to a local service account name. + + .PARAMETER LocalServiceAccountType + The type name of the local service account. + + .PARAMETER ServiceName + The name of the service that is running reporting services. +#> +function Get-LocalServiceAccountName +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [ValidateSet( + 'LocalService', + 'NetworkService', + 'System', + 'VirtualAccount' + )] + [System.String] + $LocalServiceAccountType, + + [Parameter()] + [System.String] + $ServiceName + ) + + if ( $LocalServiceAccountType -eq 'VirtualAccount' -and [System.String]::IsNullOrEmpty($ServiceName) ) + { + $newInvalidArgumentException = @{ + Message = $script:localizedData.GetLocalServiceAccountNameServiceNotSpecified -f $LocalServiceAccountType + ArgumentName = 'ServiceName' + } + New-InvalidArgumentException @newInvalidArgumentException + } + + $serviceAccountLookupTable = @{ + LocalService = 'NT AUTHORITY\LocalService' + NetworkService = 'NT AUTHORITY\NetworkService' + System = 'NT AUTHORITY\System' + VirtualAccount = "NT SERVICE\$ServiceName" + } + + $localServiceAccountName = $serviceAccountLookupTable.$LocalServiceAccountType + Write-Verbose -Message ( $script:localizedData.GetLocalServiceAccountName -f $localServiceAccountName, $LocalServiceAccountType ) -Verbose + return $localServiceAccountName +} + +<# + .SYNOPSIS + Create a new credential with a randomly generated password. + + .PARAMETER CharacterSet + The characters from which the password will be generated from. + + .PARAMETER PasswordLength + The length of the generated password. Default is 16. +#> +function New-EncryptionKeyBackupCredential +{ + [CmdletBinding()] + [OutputType([System.Management.Automation.PSCredential])] + param + ( + [Parameter()] + [System.String[]] + $CharacterSet = ( @(33..126) | Foreach-Object -Process { [System.Char][System.Byte]$_ } ), + + [Parameter()] + [System.Int32] + $PasswordLength = 16 + ) + + Write-Verbose -Message ( + $script:localizedData.CreateNewEncryptionKeyBackupCredential -f ( $CharacterSet -join ', ' ) + ) -Verbose + + $encryptionKeyBackupPassword = [System.Security.SecureString]::new() + for ( $loop=1; $loop -le $PasswordLength; $loop++ ) + { + $encryptionKeyBackupPassword.InsertAt(($loop - 1), ($CharacterSet | Get-Random)) + } + + $encryptionKeyBackupCredential = [System.Management.Automation.PSCredential]::new('BackupUser', $encryptionKeyBackupPassword) + return $encryptionKeyBackupCredential +} + +<# + .SYNOPSIS + Back up the report server encryption key. + + .PARAMETER IsInitialized + Is the report server initialized? + + .PARAMETER EncryptionKeyBackupCredential + The credential which should be used to backup the encryption key. If no + credential is supplied, a randomized value will be generated for + internal use during runtime. + + .PARAMETER EncryptionKeyBackupPath + The path where the encryption key will be backed up to. + + .PARAMETER EncryptionKeyBackupPathCredential + The credential which is used to access the path specified in + "EncryptionKeyBackupPath". + + .PARAMETER CimInstance + The CIM instance object that contains the method to call. +#> +function Backup-EncryptionKey +{ + [CmdletBinding()] + [OutputType([Microsoft.Management.Infrastructure.CimMethodResult])] + param + ( + [Parameter(Mandatory = $true)] + [System.Boolean] + $IsInitialized, + + [Parameter()] + [System.Management.Automation.PSCredential] + $EncryptionKeyBackupCredential, + + [Parameter()] + [System.String] + $EncryptionKeyBackupPath, + + [Parameter()] + [System.Management.Automation.PSCredential] + $EncryptionKeyBackupPathCredential, + + [Parameter(Mandatory = $true)] + [Microsoft.Management.Infrastructure.CimInstance] + $CimInstance + ) + + if ( $IsInitialized ) + { + Write-Verbose -Message ( $script:localizedData.EncryptionKeyBackupCredentialUserName -f $EncryptionKeyBackupCredential.UserName ) -Verbose + + $invokeRsCimMethodParameters = @{ + CimInstance = $CimInstance + MethodName = 'BackupEncryptionKey' + Arguments = @{ + Password = $EncryptionKeyBackupCredential.GetNetworkCredential().Password + } + } + $backupEncryptionKeyResult = Invoke-RsCimMethod @invokeRsCimMethodParameters + + if ( $PSBoundParameters.ContainsKey('EncryptionKeyBackupPath') ) + { + $EncryptionKeyBackupPath = [Environment]::ExpandEnvironmentVariables($EncryptionKeyBackupPath) + + $encryptionKeyBackupPathIsUnc = $false + if ( $EncryptionKeyBackupPath -match '^\\\\') + { + $encryptionKeyBackupPathIsUnc = $true + } + + if ( $encryptionKeyBackupPathIsUnc -and $PSBoundParameters.ContainsKey('EncryptionKeyBackupPathCredential') ) + { + Connect-UncPath -RemotePath $EncryptionKeyBackupPath -SourceCredential $EncryptionKeyBackupPathCredential + } + + if ( -not ( Test-Path -Path $EncryptionKeyBackupPath ) ) + { + New-Item -Path $EncryptionKeyBackupPath -ItemType Directory + } + + $encryptionKeyBackupFileName = "$(Get-ComputerName)-$($CimInstance.InstanceName).snk" + $encryptionKeyBackupFile = Join-Path -Path $EncryptionKeyBackupPath -ChildPath $encryptionKeyBackupFileName + Write-Verbose -Message ($script:localizedData.BackupEncryptionKey -f $encryptionKeyBackupFile) -Verbose + + $setContentParameters = @{ + Path = $encryptionKeyBackupFile + Value = $backupEncryptionKeyResult.KeyFile + } + + if ( $PSVersionTable.PSVersion.Major -gt 5 ) + { + $setContentParameters.AsByteStream = $true + } + else + { + $setContentParameters.Encoding = 'Byte' + } + + Set-Content @setContentParameters + + if ( $encryptionKeyBackupPathIsUnc -and $PSBoundParameters.ContainsKey('EncryptionKeyBackupCredential') ) + { + Disconnect-UncPath -RemotePath $EncryptionKeyBackupPath + } + } + + return $backupEncryptionKeyResult + } +} diff --git a/source/DSCResources/DSC_SqlRS/DSC_SqlRS.schema.mof b/source/DSCResources/DSC_SqlRS/DSC_SqlRS.schema.mof index c4d7fdc3a..15b2518a0 100644 --- a/source/DSCResources/DSC_SqlRS/DSC_SqlRS.schema.mof +++ b/source/DSCResources/DSC_SqlRS/DSC_SqlRS.schema.mof @@ -4,12 +4,24 @@ class DSC_SqlRS : OMI_BaseResource [Key, Description("Name of the _SQL Server Reporting Services_ instance to be configured.")] String InstanceName; [Required, Description("Name of the _SQL Server_ to host the _Reporting Services_ database.")] String DatabaseServerName; [Required, Description("Name of the _SQL Server_ instance to host the _Reporting Services_ database.")] String DatabaseInstanceName; + [Write, Description("Name of the the _Reporting Services_ database. Default is _ReportServer_.")] String DatabaseName; + [Write, Description("Name of the local account which the service will run as. This is ignored if the _ServiceAccount_ parameter is supplied.. Default is _VirtualAccount_."), ValueMap{"LocalService","NetworkService","System","VirtualAccount"}, Values{"LocalService","NetworkService","System","VirtualAccount"}] String LocalServiceAccountType; + [Write, EmbeddedInstance("MSFT_Credential"), Description("The service account that should be used when running the _Windows_ service.")] String ServiceAccount; [Write, Description("_Report Server Web Service_ virtual directory. Optional.")] String ReportServerVirtualDirectory; [Write, Description("_Report Manager_ or _Report Web App_ virtual directory name. Optional.")] String ReportsVirtualDirectory; [Write, Description("_Report Server_ URL reservations. Optional. If not specified, `'http://+:80'` URL reservation will be used.")] String ReportServerReservedUrl[]; [Write, Description("_Report Manager_ or _Report Web App_ URL reservations. Optional. If not specified, `'http://+:80'` URL reservation will be used.")] String ReportsReservedUrl[]; + [Write, Description("The thumbprint of the certificate used to secure SSL communication.")] String HttpsCertificateThumbprint; + [Write, Description("The IP address to bind the certificate specified in the _CertificateThumbprint_ parameter to. Default is `0.0.0.0` which binds to all IP addresses.")] String HttpsIPAddress; + [Write, Description("The port used for SSL communication. Default is `443`.")] SInt32 HttpsPort; [Write, Description("If connections to the _Reporting Services_ must use SSL. If this parameter is not assigned a value, the default is that _Reporting Services_ does not use SSL.")] Boolean UseSsl; [Write, Description("_Reporting Services_ need to be restarted after initialization or settings change. If this parameter is set to `$true`, _Reporting Services_ will not be restarted, even after initialization.")] Boolean SuppressRestart; [Write, Description("Specifies how encryption should be enforced when using command `Invoke-SqlCmd`. When not specified, the default value is `Mandatory`."), ValueMap{"Mandatory","Optional","Strict"}, Values{"Mandatory","Optional","Strict"}] String Encrypt; + [Write, Description("The path where the encryption key will be backed up to.")] String EncryptionKeyBackupPath; + [Write, EmbeddedInstance("MSFT_Credential"), Description("The credential which is used to access the path specified in `EncryptionKeyBackupPath`.")] String EncryptionKeyBackupPathCredential; + [Write, EmbeddedInstance("MSFT_Credential"), Description("The credential which should be used to backup the encryption key. If no credential is supplied, a randomized value will be generated for internal use during runtime.")] String EncryptionKeyBackupCredential; [Read, Description("Returns if the _Reporting Services_ instance initialized or not.")] Boolean IsInitialized; + [Read, Description("Returns the name of the _Reporting Services_ service.")] String ServiceName; + [Read, Description("Returns the name of the _Reporting Services_ service account.")] String ServiceAccountName; + [Read, Description("Returns the name of the encryption key backup file.")] String EncryptionKeyBackupFile; }; diff --git a/source/DSCResources/DSC_SqlRS/README.md b/source/DSCResources/DSC_SqlRS/README.md index bc0263081..bd8a36cb7 100644 --- a/source/DSCResources/DSC_SqlRS/README.md +++ b/source/DSCResources/DSC_SqlRS/README.md @@ -1,14 +1,14 @@ # Description The `SqlRS` DSC resource initializes and configures SQL Reporting Services -server. +server and Power BI Report Server. ## Requirements * Target machine must be running Windows Server 2012 or later. -* Target machine must be running SQL Server Reporting Services 20012 or later. +* Target machine must be running SQL Server Reporting Services 2017 or later. * To use parameter `UseSSL` target machine must be running SQL Server Reporting - Services 2012 or later. + Services 2017 or later. * 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 `DatabaseServerName` and `DatabaseInstanceName`, and have permission @@ -32,10 +32,6 @@ server. ## Known issues -* This resource does not currently have full SSL support, please see - [issue #587](https://github.com/dsccommunity/SqlServerDsc/issues/587) for more - information. - All issues are not listed here, see [here for all open issues](https://github.com/dsccommunity/SqlServerDsc/issues?q=is%3Aissue+is%3Aopen+in%3Atitle+SqlRS). ## Known error messages diff --git a/source/DSCResources/DSC_SqlRS/en-US/DSC_SqlRS.strings.psd1 b/source/DSCResources/DSC_SqlRS/en-US/DSC_SqlRS.strings.psd1 index 07dd9a79e..f74a1b44b 100644 --- a/source/DSCResources/DSC_SqlRS/en-US/DSC_SqlRS.strings.psd1 +++ b/source/DSCResources/DSC_SqlRS/en-US/DSC_SqlRS.strings.psd1 @@ -5,4 +5,37 @@ ConvertFrom-StringData @' ReportingServicesNotFound = SQL Reporting Services instance '{0}' does not exist. GetConfiguration = Get the current reporting services configuration for the instance '{0}'. RestartToFinishInitialization = Restarting Reporting Services to finish initialization. + SetServiceAccount = The service account should be '{0}' but is '{1}'. + TestDatabaseName = The database name is '{0}' but should be '{1}'. + ReportServerReservedUrlNotInDesiredState = Report Server reserved URLs on '{0}\\{1}' are '{2}', should be '{3}'. + ReportsReservedUrlNotInDesiredState = Reports URLs on '{0}\\{1}' are '{2}', should be '{3}'. + BackupEncryptionKey = Backing up the encryption key to '{0}'. + GetLocalServiceAccountName = The local service account name is '{0}' for the type '{1}'. + DatabaseServerIsRemote = The database server '{0}' is remote: {1} + LocalServiceAccountUnsupportedException = Cannot use '{0}' as the service account in reporting services version '{1}'. + HttpsCertificateThumbprintNotInDesiredState = The HTTPS certificate thumbprint is '{0}' but should be '{1}'. + HttpsIPAddressNotInDesiredState = The HTTPS IP address binding is '{0}' but should be '{1}'. + HttpsPortNotInDesiredState = The HTTPS port is '{0}' but should be '{1}'. + RemoveSslCertficateBindingError = Failed to remove the SSL certificate binding for the application '{0}', certificate thumbprint '{1}', IP Address '{2}', and port '{3}'. + GetOperatingSystemClassError = Unable to find WMI object Win32_OperatingSystem. + InvokeRsCimMethodError = Method {0}() failed with an error. Error: {1} (HRESULT:{2}) + GenerateDatabaseCreateScript = Generate database creation script on '{0}\\{1}' for database '{2}'. + GenerateDatabaseRightsScript = Generate database rights script on '{0}\\{1}' for database '{2}'. + SetDatabaseConnection = Set database connection on '{0}\\{1}' to database '{2}'. + SetReportServerVirtualDirectory = Setting report server virtual directory on '{0}\\{1}' to '{2}'. + SetReportsVirtualDirectory = Setting report server virtual directory on '{0}\\{1}' to '{2}'. + AddReportsUrlReservation = Adding reports URL reservation on '{0}\\{1}': '{2}'. + RestartDidNotHelp = Did not help restarting the Reporting Services service, running the CIM method to initialize report server on '{0}\\{1}' for instance ID '{2}'. + SetUseSsl = Changing value for using SSL to '{0}'. + TestNotInitialized = Reporting services '{0}\\{1}' is not initialized. + TestReportServerVirtualDirectory = Report server virtual directory on '{0}\\{1}' is '{2}', should be '{3}'. + TestReportsVirtualDirectory = Report server virtual directory on '{0}\\{1}' is '{2}', should be '{3}'. + TestUseSsl = The value for using SSL is not in desired state. Should be '{0}', but is '{1}'. + TestServiceAccount = The ServiceAccount should be '{0}' but is '{1}'. + GetLocalServiceAccountNameServiceNotSpecified = The 'ServiceName' parameter is required with the 'LocalServiceAccountType' is '{0}'. + InitializeReportingServices = Initializing Reporting Services on '{0}\{1}'. + ReportingServicesIsInitialized = Reporting Services on '{0}\{1}' is initialized: {2}. + EncryptionKeyBackupCredentialNotSpecified = An encryption key backup credential was not specified. Generating a random credential. + EncryptionKeyBackupCredentialUserName = The encryption key backup credential user name is '{0}'. + CreateNewEncryptionKeyBackupCredential = Creating a new encryption key backup credential from the character set: {0}. '@ diff --git a/source/Examples/Resources/SqlRS/2-CustomConfiguration.ps1 b/source/Examples/Resources/SqlRS/2-CustomConfiguration.ps1 index 46211f0a0..9ecc6b3fc 100644 --- a/source/Examples/Resources/SqlRS/2-CustomConfiguration.ps1 +++ b/source/Examples/Resources/SqlRS/2-CustomConfiguration.ps1 @@ -11,8 +11,6 @@ Report Manager: http://localhost:80/MyReports https://localhost:443/MyReports - - Note: this resource does not currently handle SSL bindings for HTTPS endpoints. #> Configuration Example { @@ -29,6 +27,7 @@ Configuration Example ReportsVirtualDirectory = 'MyReports' ReportServerReservedUrl = @('http://+:80', 'https://+:443') ReportsReservedUrl = @('http://+:80', 'https://+:443') + HttpsCertificateThumbprint = '9BA056B7A32842056D5B09C77F23EE0D13604D80' } } } diff --git a/source/Examples/Resources/SqlRS/3-CustomConfigurationUsingSsl.ps1 b/source/Examples/Resources/SqlRS/3-CustomConfigurationUsingSsl.ps1 index 0e8041cca..2ce7ca115 100644 --- a/source/Examples/Resources/SqlRS/3-CustomConfigurationUsingSsl.ps1 +++ b/source/Examples/Resources/SqlRS/3-CustomConfigurationUsingSsl.ps1 @@ -6,8 +6,6 @@ Report Server Web Service: https://localhost:443/MyReportServer () Report Manager: https://localhost:443/MyReports - - Note: this resource does not currently handle SSL bindings for HTTPS endpoints. #> Configuration Example { @@ -24,6 +22,7 @@ Configuration Example ReportsVirtualDirectory = 'MyReports' ReportServerReservedUrl = @('https://+:443') ReportsReservedUrl = @('https://+:443') + HttpsCertificateThumbprint = '9BA056B7A32842056D5B09C77F23EE0D13604D80' UseSsl = $true } } diff --git a/source/Examples/Resources/SqlRS/5-DefaultWithPowerBIReportServer.ps1 b/source/Examples/Resources/SqlRS/5-DefaultWithPowerBIReportServer.ps1 new file mode 100644 index 000000000..acdba0142 --- /dev/null +++ b/source/Examples/Resources/SqlRS/5-DefaultWithPowerBIReportServer.ps1 @@ -0,0 +1,23 @@ +<# + .DESCRIPTION + This example performs a default Power BI Report Server configuration. + It will initialize Power BI Report Server and register default Report + Server Web Service and Report Manager URLs. + + Report Server Web Service: http://localhost:80/ReportServer + Report Manager: http://localhost:80/Reports +#> +Configuration Example +{ + Import-DscResource -ModuleName 'SqlServerDsc' + + node localhost + { + SqlRS 'PowerBIReportServerDefaultConfiguration' + { + InstanceName = 'PBIRS' + DatabaseServerName = 'localhost' + DatabaseInstanceName = 'MSSQLSERVER' + } + } +} diff --git a/source/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 b/source/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 index d325cd886..8c2f4b8a3 100644 --- a/source/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 +++ b/source/Modules/SqlServerDsc.Common/SqlServerDsc.Common.psm1 @@ -1187,38 +1187,15 @@ function Restart-ReportingServicesService ( [Parameter()] [System.String] - $InstanceName = 'MSSQLSERVER', + $ServiceName = 'SQLServerReportingServices', [Parameter()] [System.UInt16] $WaitTime = 0 ) - if ($InstanceName -eq 'SSRS') - { - # Check if we're dealing with SSRS 2017 or SQL2019 - $ServiceName = 'SQLServerReportingServices' - - Write-Verbose -Message ($script:localizedData.GetServiceInformation -f $ServiceName) -Verbose - $reportingServicesService = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue - } - - if ($null -eq $reportingServicesService) - { - $ServiceName = 'ReportServer' - - <# - Pre-2017 SSRS support multiple instances, check if we're dealing - with a named instance. - #> - if (-not ($InstanceName -eq 'MSSQLSERVER')) - { - $ServiceName += '${0}' -f $InstanceName - } - - Write-Verbose -Message ($script:localizedData.GetServiceInformation -f $ServiceName) -Verbose - $reportingServicesService = Get-Service -Name $ServiceName - } + Write-Verbose -Message ($script:localizedData.GetServiceInformation -f $ServiceName) -Verbose + $reportingServicesService = Get-Service -Name $ServiceName <# Get all dependent services that are running. diff --git a/tests/Unit/DSC_SqlRS.Tests.ps1 b/tests/Unit/DSC_SqlRS.Tests.ps1 index 81c270cdd..3142a47b9 100644 --- a/tests/Unit/DSC_SqlRS.Tests.ps1 +++ b/tests/Unit/DSC_SqlRS.Tests.ps1 @@ -77,7 +77,8 @@ Describe 'SqlRS\Get-TargetResource' -Tag 'Get' { $mockReportingServicesDatabaseServerName = 'SERVER' $mockReportingServicesDatabaseNamedInstanceName = $mockNamedInstanceName $mockReportingServicesDatabaseDefaultInstanceName = $mockDefaultInstanceName - + $mockReportingServicesDatabaseName = 'ReportServer' + $mockServiceAccountName = 'Contoso\ServiceAccount' $mockReportsApplicationName = 'ReportServerWebApp' $mockReportServerApplicationName = 'ReportServerWebService' $mockReportsApplicationUrl = 'http://+:80' @@ -85,6 +86,14 @@ Describe 'SqlRS\Get-TargetResource' -Tag 'Get' { $mockVirtualDirectoryReportManagerName = 'Reports_SQL2016' $mockVirtualDirectoryReportServerName = 'ReportServer_SQL2016' + $mockEncryptionKeyBackupFileName = "$($env:ComputerName)-$($mockNamedInstanceName).snk" + $mockEncryptionKeyBackupPathUnc = '\\share\backup\path' + $mockEncryptionKeyBackupFileUnc = Join-Path -Path $mockEncryptionKeyBackupPathUnc -ChildPath $mockEncryptionKeyBackupFileName + $mockEncryptionKeyBackupPathLocal = 'C:\backup\path' + $mockEncryptionKeyBackupFileLocal = Join-Path -Path $mockEncryptionKeyBackupPathLocal -ChildPath $mockEncryptionKeyBackupFileName + $mockEncryptionKeyBackupPathCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @('CONTOSO\User', ( ConvertTo-SecureString 'P@$$w0rd1' -AsPlainText -Force ) ) + $mockEncryptionKeyBackupCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @('AnyName', ( ConvertTo-SecureString 'P@$$w0rd1' -AsPlainText -Force ) ) + $mockInvokeRsCimMethod_ListReservedUrls = { return New-Object -TypeName Object | Add-Member -MemberType ScriptProperty -Name 'Application' -Value { @@ -108,11 +117,13 @@ Describe 'SqlRS\Get-TargetResource' -Tag 'Get' { 'MSReportServer_ConfigurationSetting' 'root/Microsoft/SQLServer/ReportServer/RS_SQL2016/v13/Admin' ) | Add-Member -MemberType NoteProperty -Name 'DatabaseServerName' -Value "$mockReportingServicesDatabaseServerName\$mockReportingServicesDatabaseNamedInstanceName" -PassThru | + Add-Member -MemberType NoteProperty -Name 'DatabaseName' -Value $mockReportingServicesDatabaseName -PassThru | Add-Member -MemberType NoteProperty -Name 'IsInitialized' -Value $mockDynamicIsInitialized -PassThru | Add-Member -MemberType NoteProperty -Name 'InstanceName' -Value $mockNamedInstanceName -PassThru | Add-Member -MemberType NoteProperty -Name 'VirtualDirectoryReportServer' -Value $mockVirtualDirectoryReportServerName -PassThru | Add-Member -MemberType NoteProperty -Name 'VirtualDirectoryReportManager' -Value $mockVirtualDirectoryReportManagerName -PassThru | - Add-Member -MemberType NoteProperty -Name 'SecureConnectionLevel' -Value $mockDynamicSecureConnectionLevel -PassThru -Force + Add-Member -MemberType NoteProperty -Name 'SecureConnectionLevel' -Value $mockDynamicSecureConnectionLevel -PassThru | + Add-Member -MemberType NoteProperty -Name 'WindowsServiceIdentityActual' -Value $mockServiceAccountName -PassThru -Force ), ( # Array is a regression test for issue #819. @@ -129,17 +140,41 @@ Describe 'SqlRS\Get-TargetResource' -Tag 'Get' { 'MSReportServer_ConfigurationSetting' 'root/Microsoft/SQLServer/ReportServer/RS_SQL2016/v13/Admin' ) | Add-Member -MemberType NoteProperty -Name 'DatabaseServerName' -Value "$mockReportingServicesDatabaseServerName" -PassThru | + Add-Member -MemberType NoteProperty -Name 'DatabaseName' -Value $mockReportingServicesDatabaseName -PassThru | Add-Member -MemberType NoteProperty -Name 'IsInitialized' -Value $false -PassThru | Add-Member -MemberType NoteProperty -Name 'InstanceName' -Value $mockDefaultInstanceName -PassThru | Add-Member -MemberType NoteProperty -Name 'VirtualDirectoryReportServer' -Value '' -PassThru | - Add-Member -MemberType NoteProperty -Name 'VirtualDirectoryReportManager' -Value '' -PassThru -Force | - Add-Member -MemberType NoteProperty -Name 'SecureConnectionLevel' -Value $mockDynamicSecureConnectionLevel -PassThru -Force + Add-Member -MemberType NoteProperty -Name 'VirtualDirectoryReportManager' -Value '' -PassThru | + Add-Member -MemberType NoteProperty -Name 'SecureConnectionLevel' -Value $mockDynamicSecureConnectionLevel -PassThru | + Add-Member -MemberType NoteProperty -Name 'WindowsServiceIdentityActual' -Value $mockServiceAccountName -PassThru | + Add-Member -MemberType NoteProperty -Name 'ServiceName' -Value 'ReportServer' -PassThru -Force + } + + $mockGetItem_EncryptionKeyBackupFile = { + return @{ + Name = $mockEncryptionKeyBackupFileName + } } Mock -CommandName Invoke-RsCimMethod -MockWith $mockInvokeRsCimMethod_ListReservedUrls -ParameterFilter { $MethodName -eq 'ListReservedUrls' } + Mock -CommandName Invoke-RsCimMethod -MockWith $mockInvokeRsCimMethod_ListReservedUrls -ParameterFilter { + $MethodName -eq 'ListSSLCertificateBindings' + } + + Mock -CommandName Get-Item -MockWith $mockGetItem_EncryptionKeyBackupFile -ParameterFilter { + $Path -eq $mockEncryptionKeyBackupFileUnc + } + + Mock -CommandName Get-Item -MockWith $mockGetItem_EncryptionKeyBackupFile -ParameterFilter { + $Path -eq $mockEncryptionKeyBackupFileLocal + } + + Mock -CommandName Connect-UncPath + Mock -CommandName Disconnect-UncPath + InModuleScope -ScriptBlock { $script:mockNamedInstanceName = 'INSTANCE' $script:mockReportingServicesDatabaseServerName = 'SERVER' @@ -195,6 +230,7 @@ Describe 'SqlRS\Get-TargetResource' -Tag 'Get' { InModuleScope -ScriptBlock { Set-StrictMode -Version 1.0 + $mockServiceAccountName = 'Contoso\ServiceAccount' $mockReportsApplicationUrl = 'http://+:80' $mockReportServerApplicationUrl = 'http://+:80' $mockVirtualDirectoryReportManagerName = 'Reports_SQL2016' @@ -208,6 +244,7 @@ Describe 'SqlRS\Get-TargetResource' -Tag 'Get' { $resultGetTargetResource.ReportServerReservedUrl | Should -Be $mockReportServerApplicationUrl $resultGetTargetResource.ReportsReservedUrl | Should -Be $mockReportsApplicationUrl $resultGetTargetResource.UseSsl | Should -BeFalse + $resultGetTargetResource.ServiceAccountName | Should -Be $mockServiceAccountName } Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { @@ -246,6 +283,74 @@ Describe 'SqlRS\Get-TargetResource' -Tag 'Get' { } } } + + Context 'When an encryption key backup path is supplied' { + It 'Should return the correct file name when the path type is UNC' { + $inModuleScopeParameters = @{ + MockEncryptionKeyBackupPath = $mockEncryptionKeyBackupPathUnc + MockEncryptionKeyBackupPathCredential = $mockEncryptionKeyBackupPathCredential + MockEncryptionKeyBackupFileName = $mockEncryptionKeyBackupFileName + } + + InModuleScope -Parameters $inModuleScopeParameters -ScriptBlock { + param + ( + $MockEncryptionKeyBackupPath, + $MockEncryptionKeyBackupPathCredential, + $MockEncryptionKeyBackupFileName + ) + + Set-StrictMode -Version 1.0 + + $mockEncryptionKeyBackupParameters = $mockDefaultParameters.Clone() + $mockEncryptionKeyBackupParameters.EncryptionKeyBackupPath = $MockEncryptionKeyBackupPath + $mockEncryptionKeyBackupParameters.EncryptionKeyBackupPathCredential = $MockEncryptionKeyBackupPathCredential + + $resultGetTargetResource = Get-TargetResource @mockEncryptionKeyBackupParameters + + $resultGetTargetResource.EncryptionKeyBackupFile | Should -Be $MockEncryptionKeyBackupFileName + } + + Should -Invoke -CommandName Connect-UncPath -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Disconnect-UncPath -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Get-Item -ParameterFilter { + $Path -eq $mockEncryptionKeyBackupFileUnc + } -Exactly -Times 1 -Scope It + } + + It 'Should return the correct file name when the path type is Local' { + $inModuleScopeParameters = @{ + MockEncryptionKeyBackupPath = $mockEncryptionKeyBackupPathLocal + MockEncryptionKeyBackupPathCredential = $mockEncryptionKeyBackupPathCredential + MockEncryptionKeyBackupFileName = $mockEncryptionKeyBackupFileName + } + + InModuleScope -Parameters $inModuleScopeParameters -ScriptBlock { + param + ( + $MockEncryptionKeyBackupPath, + $MockEncryptionKeyBackupPathCredential, + $MockEncryptionKeyBackupFileName + ) + + Set-StrictMode -Version 1.0 + + $mockEncryptionKeyBackupParameters = $mockDefaultParameters.Clone() + $mockEncryptionKeyBackupParameters.EncryptionKeyBackupPath = $MockEncryptionKeyBackupPath + $mockEncryptionKeyBackupParameters.EncryptionKeyBackupPathCredential = $MockEncryptionKeyBackupPathCredential + + $resultGetTargetResource = Get-TargetResource @mockEncryptionKeyBackupParameters + + $resultGetTargetResource.EncryptionKeyBackupFile | Should -Be $MockEncryptionKeyBackupFileName + } + + Should -Invoke -CommandName Connect-UncPath -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Disconnect-UncPath -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Get-Item -ParameterFilter { + $Path -eq $mockEncryptionKeyBackupFileLocal + }-Exactly -Times 1 -Scope It + } + } } Context 'When the system is not in the desired state' { @@ -299,7 +404,7 @@ Describe 'SqlRS\Get-TargetResource' -Tag 'Get' { Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { $MethodName -eq 'ListReservedUrls' - } -Exactly -Times 0 -Scope It + } -Exactly -Times 1 -Scope It } # Regression test for issue #822. @@ -362,14 +467,17 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { @{ TestCaseVersionName = "SQL Server Reporting Services 2016" TestCaseVersion = 13 + IsInitialized = $false } @{ TestCaseVersionName = "SQL Server Reporting Services 2017" TestCaseVersion = 14 + IsInitialized = $true } @{ TestCaseVersionName = "SQL Server Reporting Services 2019" TestCaseVersion = 15 + IsInitialized = $true } ) } @@ -407,6 +515,7 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { $mockReportingServicesDatabaseServerName = 'SERVER' $mockReportingServicesDatabaseNamedInstanceName = $mockNamedInstanceName $mockReportingServicesDatabaseDefaultInstanceName = $mockDefaultInstanceName + $mockReportingServicesDatabaseName = 'ReportServer' $mockReportsApplicationName = 'ReportServerWebApp' $mockReportsApplicationNameLegacy = 'ReportManager' @@ -416,6 +525,23 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { $mockVirtualDirectoryReportManagerName = 'Reports_SQL2016' $mockVirtualDirectoryReportServerName = 'ReportServer_SQL2016' + $mockServiceNamePowerBiReportServer = 'PowerBIReportServer' + + $mockServiceAccountName = 'CONTOSO\ServiceAccount' + $mockPassword = [System.Security.SecureString]::new() + $mockPassword.AppendChar(' ') + $mockServiceAccountCredential = [System.Management.Automation.PSCredential]::new($mockServiceAccountName, $mockPassword) + + $mockEncryptionKeyBackupFileName = "$($env:ComputerName)-$($mockNamedInstanceName).snk" + $mockEncryptionKeyBackupPathUnc = '\\share\backup\path' + $mockEncryptionKeyBackupFileUnc = Join-Path -Path $mockEncryptionKeyBackupPathUnc -ChildPath $mockEncryptionKeyBackupFileName + $mockEncryptionKeyBackupPathLocal = 'C:\backup\path' + $mockEncryptionKeyBackupFileLocal = Join-Path -Path $mockEncryptionKeyBackupPathLocal -ChildPath $mockEncryptionKeyBackupFileName + $mockEncryptionKeyBackupPathCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @('Contoso\User', ( ConvertTo-SecureString 'P@$$w0rd1' -AsPlainText -Force ) ) + $mockEncryptionKeyBackupCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @('AnyName', ( ConvertTo-SecureString 'P@$$w0rd1' -AsPlainText -Force ) ) + + $mockCertificateThumbprint = '0000000000000000000000000000000000000000' + $mockInvokeCimMethod = { throw 'Should not call Invoke-CimMethod directly, should call the wrapper Invoke-RsCimMethod.' } @@ -448,6 +574,59 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { } } + $mockInvokeRsCimMethod_CreateSSLCertificateBinding = { + return @{ + HRESULT = 0 + } + } + + $mockInvokeRsCimMethod_ListSSLCertificateBindings = { + return New-Object -TypeName Object | + Add-Member -MemberType ScriptProperty -Name 'Application' -Value { + return @( + $mockDynamicReportServerApplicationName + $mockDynamicReportsApplicationName + ) + } -PassThru | + Add-Member -MemberType ScriptProperty -Name 'CertificateHash' -Value { + return @( + $mockCertificateThumbprint + $mockCertificateThumbprint + ) + } -PassThru | + Add-Member -MemberType ScriptProperty -Name 'IPAddress' -Value { + return @( + '1.1.1.1' + '0.0.0.0' + ) + } -PassThru | + Add-Member -MemberType ScriptProperty -Name 'Port' -Value { + return @( + 443 + 1234 + ) + } -PassThru -Force + } + + $mockInvokeRsCimMethod_RemoveSSLCertificateBindings = { + return @{ + HRESULT = 0 + } + } + + $mockInvokeRsCimMethod_RestoreEncryptionKey = { + return @{ + HRESULT = 0 + } + } + + $mockInvokeRsCimMethod_InitializeReportServer = { + return @{ + HRESULT = 0 + ReturnValue = $true + } + } + $mockGetCimInstance_ConfigurationSetting_NamedInstance = { return @( ( @@ -459,7 +638,9 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { Add-Member -MemberType NoteProperty -Name 'InstanceName' -Value $mockNamedInstanceName -PassThru | Add-Member -MemberType NoteProperty -Name 'VirtualDirectoryReportServer' -Value $mockVirtualDirectoryReportServerName -PassThru | Add-Member -MemberType NoteProperty -Name 'VirtualDirectoryReportManager' -Value $mockVirtualDirectoryReportManagerName -PassThru | - Add-Member -MemberType NoteProperty -Name 'SecureConnectionLevel' -Value $mockDynamicSecureConnectionLevel -PassThru -Force + Add-Member -MemberType NoteProperty -Name 'SecureConnectionLevel' -Value $mockDynamicSecureConnectionLevel -PassThru | + Add-Member -MemberType NoteProperty -Name 'WindowsServiceIdentityActual' -Value $mockServiceAccountName -PassThru | + Add-Member -MemberType NoteProperty -Name 'ServiceName' -Value "ReportServer`$$mockNamedInstanceName" -PassThru -Force ), ( # Array is a regression test for issue #819. @@ -479,8 +660,37 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { Add-Member -MemberType NoteProperty -Name 'IsInitialized' -Value $false -PassThru | Add-Member -MemberType NoteProperty -Name 'InstanceName' -Value $mockDefaultInstanceName -PassThru | Add-Member -MemberType NoteProperty -Name 'VirtualDirectoryReportServer' -Value '' -PassThru | - Add-Member -MemberType NoteProperty -Name 'VirtualDirectoryReportManager' -Value '' -PassThru -Force | - Add-Member -MemberType NoteProperty -Name 'SecureConnectionLevel' -Value $mockDynamicSecureConnectionLevel -PassThru -Force + Add-Member -MemberType NoteProperty -Name 'VirtualDirectoryReportManager' -Value '' -PassThru | + Add-Member -MemberType NoteProperty -Name 'SecureConnectionLevel' -Value $mockDynamicSecureConnectionLevel -PassThru | + Add-Member -MemberType NoteProperty -Name 'WindowsServiceIdentityActual' -Value $mockServiceAccountName -PassThru | + Add-Member -MemberType NoteProperty -Name 'ServiceName' -Value 'ReportServer' -PassThru -Force + } + + $mockGetCimInstance_ConfigurationSetting_PowerBIReportServer = { + return @( + ( + New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList @( + 'MSReportServer_ConfigurationSetting' + 'root/Microsoft/SQLServer/ReportServer/RS_PBIRS/v15/Admin' + ) | Add-Member -MemberType NoteProperty -Name 'DatabaseServerName' -Value "$mockReportingServicesDatabaseServerName\$mockReportingServicesDatabaseNamedInstanceName" -PassThru | + Add-Member -MemberType NoteProperty -Name 'IsInitialized' -Value $mockDynamicIsInitialized -PassThru | + Add-Member -MemberType NoteProperty -Name 'InstanceName' -Value $mockNamedInstanceName -PassThru | + Add-Member -MemberType NoteProperty -Name 'VirtualDirectoryReportServer' -Value $mockVirtualDirectoryReportServerName -PassThru | + Add-Member -MemberType NoteProperty -Name 'VirtualDirectoryReportManager' -Value $mockVirtualDirectoryReportManagerName -PassThru | + Add-Member -MemberType NoteProperty -Name 'SecureConnectionLevel' -Value $mockDynamicSecureConnectionLevel -PassThru | + Add-Member -MemberType NoteProperty -Name 'ServiceName' -Value $mockServiceNamePowerBiReportServer -PassThru | + Add-Member -MemberType NoteProperty -Name 'WindowsServiceIdentityActual' -Value 'NT AUTHORITY\SYSTEM' -PassThru | + Add-Member -MemberType NoteProperty -Name 'ServiceName' -Value 'PowerBIReportServer' -PassThru -Force + ), + ( + # Array is a regression test for issue #819. + New-Object -TypeName Object | + Add-Member -MemberType NoteProperty -Name 'DatabaseServerName' -Value "$mockReportingServicesDatabaseServerName\$mockReportingServicesDatabaseNamedInstanceName" -PassThru | + Add-Member -MemberType NoteProperty -Name 'IsInitialized' -Value $true -PassThru | + Add-Member -MemberType NoteProperty -Name 'InstanceName' -Value 'DummyInstance' -PassThru | + Add-Member -MemberType NoteProperty -Name 'ServiceName' -Value 'ReportServer' -PassThru -Force + ) + ) } $mockGetCimInstance_ConfigurationSetting_ParameterFilter = { @@ -497,14 +707,13 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { $ClassName -eq 'Win32_OperatingSystem' } - Mock -CommandName Invoke-RsCimMethod -MockWith $mockInvokeRsCimMethod_ListReservedUrls -ParameterFilter { - $MethodName -eq 'ListReservedUrls' + $mockGetCimInstance_ServiceAccountUserName = { + return @{ + Name = $mockServiceAccountName + } } - <# - This is mocked here so that no calls are made to it directly, - or if any mock of Invoke-RsCimMethod are wrong. - #> + #This is mocked here so that no calls are made to it directly, or if any mock of Invoke-RsCimMethod are wrong. Mock -CommandName Invoke-CimMethod -MockWith $mockInvokeCimMethod Mock -CommandName Import-SqlDscPreferredModule @@ -519,10 +728,34 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { $MethodName -eq 'GenerateDatabaseRightsScript' } - <# - This is mocked here so that no calls are made to it directly, - or if any mock of Invoke-RsCimMethod are wrong. - #> + Mock -CommandName Invoke-RsCimMethod -MockWith $mockInvokeRsCimMethod_CreateSSLCertificateBinding -ParameterFilter { + $MethodName -eq 'CreateSSLCertificateBinding' + } + + Mock -CommandName Invoke-RsCimMethod -MockWith $mockInvokeRsCimMethod_RestoreEncryptionKey -ParameterFilter { + $MethodName -eq 'RestoreEncryptionKey' + } + + Mock -CommandName Invoke-RsCimMethod -MockWith $mockInvokeRsCimMethod_InitializeReportServer -ParameterFilter { + $MethodName -eq 'InitializeReportServer' + } + + Mock -CommandName Invoke-RsCimMethod -MockWith $mockInvokeRsCimMethod_RemoveSSLCertificateBindings -ParameterFilter { + $MethodName -eq 'RemoveSSLCertificateBindings' + } + + Mock -CommandName Backup-EncryptionKey + Mock -CommandName Connect-UncPath + Mock -CommandName Disconnect-UncPath + Mock -CommandName Import-SqlDscPreferredModule + Mock -CommandName Invoke-Sqlcmd + Mock -CommandName New-Item + Mock -CommandName Restart-ReportingServicesService + Mock -CommandName Set-Content + Mock -CommandName Invoke-RsCimMethod + + + # This is mocked here so that no calls are made to it directly, or if any mock of Invoke-RsCimMethod are wrong. Mock -CommandName Invoke-CimMethod -MockWith $mockInvokeCimMethod $mockDynamicReportServerApplicationName = $mockReportServerApplicationName @@ -551,7 +784,7 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { } Context "When the system is not in the desired state ()" -ForEach $sqlVersions { - Context "When configuring a named instance that are not initialized ()" { + Context "When configuring a named instance that is not initialized ()" { BeforeAll { $mockDynamicIsInitialized = $false @@ -567,16 +800,8 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { return $true } - BeforeAll { - Mock -CommandName Get-Command -ParameterFilter { - $Name -eq 'Invoke-SqlCmd' - } -MockWith { - return @{ - Parameters = @{ - Keys = @() - } - } - } + Mock -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'ListReservedUrls' } } @@ -584,10 +809,23 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { Mock -CommandName Get-CimInstance ` -MockWith $mockGetCimInstance_Language ` -ParameterFilter $mockGetCimInstance_OperatingSystem_ParameterFilter + + Mock -CommandName Get-CimInstance ` + -MockWith $mockGetCimInstance_ServiceAccountUserName ` + -ParameterFilter $mockGetCimInstance_Service_ParameterFilter } It 'Should configure Reporting Service without throwing an error' { - InModuleScope -ScriptBlock { + InModuleScope -Parameters @{ + TestCaseVersion = $TestCaseVersion + } -ScriptBlock { + param + ( + [Parameter(Mandatory = $true)] + [System.Int32] + $TestCaseVersion + ) + Set-StrictMode -Version 1.0 $mockDefaultParameters = @{ @@ -597,52 +835,65 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { UseSsl = $true } - { Set-TargetResource @mockDefaultParameters } | Should -Not -Throw + if ( $TestCaseVersion -lt 14 ) + { + { Set-TargetResource @mockDefaultParameters } | Should -Throw ($script:localizedData.VersionNotSupported) + } + else + { + { Set-TargetResource @mockDefaultParameters } | Should -Not -Throw + } } - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'SetSecureConnectionLevel' - } -Exactly -Times 1 -Scope It + if ( $TestCaseVersion -ge 14 ) + { + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'SetSecureConnectionLevel' + } -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'RemoveURL' - } -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'RemoveURL' + } -Exactly -Times 0 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'InitializeReportServer' - } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'InitializeReportServer' + } -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'SetDatabaseConnection' - } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'SetDatabaseConnection' + } -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'GenerateDatabaseRightsScript' - } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'GenerateDatabaseRightsScript' + } -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'GenerateDatabaseCreationScript' - } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'GenerateDatabaseCreationScript' + } -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'SetVirtualDirectory' -and $Arguments.Application -eq $mockReportServerApplicationName - } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'SetVirtualDirectory' -and $Arguments.Application -eq $mockReportServerApplicationName + } -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'SetVirtualDirectory' -and $Arguments.Application -eq $mockReportsApplicationName - } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'SetVirtualDirectory' -and $Arguments.Application -eq $mockReportsApplicationName + } -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'ReserveUrl' -and $Arguments.Application -eq $mockReportServerApplicationName - } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'ReserveUrl' -and $Arguments.Application -eq $mockReportServerApplicationName + } -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'ReserveUrl' -and $Arguments.Application -eq $mockReportsApplicationName - } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'ReserveUrl' -and $Arguments.Application -eq $mockReportsApplicationName + } -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Get-CimInstance -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-SqlCmd -Exactly -Times 2 -Scope It - Should -Invoke -CommandName Restart-ReportingServicesService -Exactly -Times 2 -Scope It + Should -Invoke -CommandName Get-CimInstance -ParameterFilter { + $ClassName -eq 'Win32_OperatingSystem' + } -Exactly -Times 10 -Scope It + + Should -Invoke -CommandName Invoke-SqlCmd -Exactly -Times 2 -Scope It + Should -Invoke -CommandName Restart-ReportingServicesService -Exactly -Times 2 -Scope It + } } Context 'When there is no Reporting Services instance after Set-TargetResource has been called' { @@ -652,7 +903,10 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { } It 'Should throw the correct error message' { - InModuleScope -ScriptBlock { + InModuleScope -Parameters @{ + TestCaseVersion = $TestCaseVersion + } -ScriptBlock { + Set-StrictMode -Version 1.0 # This tests should not pass the parameter Encrypt to test that det default value works. @@ -668,6 +922,15 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { Should -Invoke -CommandName Invoke-SqlCmd -ParameterFilter { $PesterBoundParameters.Keys -notcontains 'Encrypt' } -Times 2 -Exactly -Scope It + + if ( $TestCaseVersion -lt 14 ) + { + { Set-TargetResource @mockDefaultParameters } | Should -Throw -ExpectedMessage ($script:localizedData.VersionNotSupported) + } + else + { + { Set-TargetResource @mockDefaultParameters } | Should -Throw -ExpectedMessage ('*' + $script:localizedData.TestFailedAfterSet) + } } } } @@ -675,12 +938,14 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { Context 'When it is not possible to evaluate OSLanguage' { BeforeEach { Mock -CommandName Get-CimInstance -MockWith { - return $null + throw } -ParameterFilter $mockGetCimInstance_OperatingSystem_ParameterFilter } It 'Should throw the correct error message' { - InModuleScope -ScriptBlock { + InModuleScope -Parameters @{ + TestCaseVersion = $TestCaseVersion + } -ScriptBlock { Set-StrictMode -Version 1.0 $mockDefaultParameters = @{ @@ -690,13 +955,20 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { UseSsl = $true } - { Set-TargetResource @mockDefaultParameters } | Should -Throw ('*' + 'Unable to find WMI object Win32_OperatingSystem.') + if ( $TestCaseVersion -lt 14 ) + { + { Set-TargetResource @mockDefaultParameters } | Should -Throw -ExpectedMessage ($script:localizedData.VersionNotSupported) + } + else + { + { Set-TargetResource @mockDefaultParameters } | Should -Throw ('*Unable to find WMI object Win32_OperatingSystem.*') + } } } } } - Context "When configuring a named instance that are already initialized ()" { + Context "When configuring a named instance that is already initialized ()" { BeforeAll { $mockDynamicIsInitialized = $true @@ -710,8 +982,14 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { Mock -CommandName Get-TargetResource -MockWith { return @{ + DatabaseServerName = $mockReportingServicesDatabaseServerName + DatabaseInstanceName = $mockReportingServicesDatabaseNamedInstanceName + DatabaseName = $mockReportingServicesDatabaseName + IsInitialized = $mockDynamicIsInitialized ReportServerReservedUrl = $mockReportServerApplicationUrl ReportsReservedUrl = $mockReportsApplicationUrl + ServiceName = "ReportServer`$$mockNamedInstanceName" + ServiceAccountName = $mockServiceAccountName } } @@ -719,15 +997,28 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { return $true } + Mock -CommandName Invoke-RsCimMethod -MockWith $mockInvokeRsCimMethod_ListReservedUrls -ParameterFilter { + $MethodName -eq 'ListReservedUrls' + } + + Mock -CommandName Invoke-RsCimMethod -MockWith $mockInvokeRsCimMethod_ListSSLCertificateBindings -ParameterFilter { + $MethodName -eq 'ListSSLCertificateBindings' + } + $testParameters = @{ - InstanceName = $mockNamedInstanceName - DatabaseServerName = $mockReportingServicesDatabaseServerName - DatabaseInstanceName = $mockReportingServicesDatabaseNamedInstanceName - ReportServerVirtualDirectory = 'ReportServer_NewName' - ReportsVirtualDirectory = 'Reports_NewName' - ReportServerReservedUrl = 'https://+:4443' - ReportsReservedUrl = 'https://+:4443' - UseSsl = $true + InstanceName = $mockNamedInstanceName + DatabaseServerName = $mockReportingServicesDatabaseServerName + DatabaseInstanceName = $mockReportingServicesDatabaseNamedInstanceName + DatabaseName = 'NewDatabase' + EncryptionKeyBackupPath = $mockEncryptionKeyBackupPathUnc + EncryptionKeyBackupPathCredential = $mockEncryptionKeyBackupPathCredential + EncryptionKeyBackupCredential = $mockEncryptionKeyBackupCredential + HttpsCertificateThumbprint = $mockCertificateThumbprint + ReportServerVirtualDirectory = 'ReportServer_NewName' + ReportsVirtualDirectory = 'Reports_NewName' + ReportServerReservedUrl = 'https://+:4443' + ReportsReservedUrl = 'https://+:4443' + UseSsl = $true } } @@ -738,59 +1029,126 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { } It 'Should configure Reporting Service without throwing an error' { - { Set-TargetResource @testParameters } | Should -Not -Throw + if ( $TestCaseVersion -lt 14 ) + { + { Set-TargetResource @mockDefaultParameters } | Should -Throw -ExpectedMessage ($script:localizedData.VersionNotSupported) + } + else + { + { Set-TargetResource @testParameters } | Should -Not -Throw + } - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'SetSecureConnectionLevel' - } -Exactly -Times 1 -Scope It + if ( $TestCaseVersion -ge 14 ) + { + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'SetSecureConnectionLevel' + } -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'RemoveURL' -and $Arguments.Application -eq $mockReportServerApplicationName - } -Exactly -Times 2 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'RemoveURL' -and $Arguments.Application -eq $mockReportServerApplicationName + } -Exactly -Times 2 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'RemoveURL' -and $Arguments.Application -eq $mockReportsApplicationName - } -Exactly -Times 2 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'RemoveURL' -and $Arguments.Application -eq $mockReportsApplicationName + } -Exactly -Times 2 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'InitializeReportServer' - } -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'ListSSLCertificateBindings' + } -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'SetDatabaseConnection' - } -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'RemoveSSLCertificateBindings' + } -Exactly -Times 2 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'GenerateDatabaseRightsScript' - } -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'InitializeReportServer' + } -Exactly -Times 0 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'GenerateDatabaseCreationScript' - } -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'SetDatabaseConnection' + } -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'SetVirtualDirectory' -and $Arguments.Application -eq $mockReportServerApplicationName - } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'GenerateDatabaseRightsScript' + } -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'SetVirtualDirectory' -and $Arguments.Application -eq $mockReportsApplicationName - } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'GenerateDatabaseCreationScript' + } -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'ReserveUrl' -and $Arguments.Application -eq $mockReportServerApplicationName - } -Exactly -Times 2 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'SetVirtualDirectory' -and $Arguments.Application -eq $mockReportServerApplicationName + } -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'ReserveUrl' -and $Arguments.Application -eq $mockReportsApplicationName - } -Exactly -Times 2 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'SetVirtualDirectory' -and $Arguments.Application -eq $mockReportsApplicationName + } -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Get-CimInstance -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-SqlCmd -Exactly -Times 0 -Scope It - Should -Invoke -CommandName Restart-ReportingServicesService -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'ReserveUrl' -and $Arguments.Application -eq $mockReportServerApplicationName + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'ReserveUrl' -and $Arguments.Application -eq $mockReportsApplicationName + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Connect-UncPath -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Disconnect-UncPath -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Get-CimInstance -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Invoke-SqlCmd -Exactly -Times 2 -Scope It + Should -Invoke -CommandName Restart-ReportingServicesService -Exactly -Times 2 -Scope It + Should -Invoke -CommandName New-Item -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Set-Content -Exactly -Times 0 -Scope It + } + } + + It 'Should throw when the version is 14 or greater and the service account type is System' { + $setTargetResourceParameters = $testParameters.Clone() + $setTargetResourceParameters.LocalServiceAccountType = 'System' + + if ( $TestCaseVersion -ge 14 ) + { + $expectedError = "*Cannot use '$($setTargetResourceParameters.LocalServiceAccountType)' as the service account in reporting services version '$TestCaseVersion'.*" + { Set-TargetResource @setTargetResourceParameters } | Should -Throw $expectedError + } + else + { + { Set-TargetResource @setTargetResourceParameters } | Should -Throw ($script:localizedData.VersionNotSupported) + } + } + + Context 'When removing a SSL certificate binding fails' { + BeforeAll { + $mockRemoveSSLCertificateBindingsError = 'RemoveSSLCertificateBindings failed' + Mock -CommandName Invoke-RsCimMethod -MockWith { + throw $mockRemoveSSLCertificateBindingsError + } -ParameterFilter $mockInvokeRsCimMethod_RemoveSSLCertificateBindings + } + + It 'Should throw the correct error message' { + $mockDefaultParameters = @{ + InstanceName = $mockNamedInstanceName + DatabaseServerName = $mockReportingServicesDatabaseServerName + DatabaseInstanceName = $mockReportingServicesDatabaseNamedInstanceName + DatabaseName = $mockReportingServicesDatabaseName + HttpsCertificateThumbprint = 'ffffffffffffffffffffffffffffffffffffffff' + ServiceAccount = $mockServiceAccountCredential + UseSsl = $true + } + + if ( $TestCaseVersion -lt 14 ) + { + { Set-TargetResource @mockDefaultParameters } | Should -Throw ($script:localizedData.VersionNotSupported) + } + else + { + { Set-TargetResource @mockDefaultParameters } | Should -Throw $mockRemoveSSLCertificateBindingsError + } + } } } - Context "When configuring a named instance that are already initialized (), suppress restart" { + Context "When configuring a named instance that is already initialized (), suppress restart" { BeforeAll { $mockDynamicIsInitialized = $true @@ -804,8 +1162,11 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { Mock -CommandName Get-TargetResource -MockWith { return @{ + DatabaseName = $mockReportingServicesDatabaseName + IsInitialized = $IsInitialized ReportServerReservedUrl = $mockReportServerApplicationUrl ReportsReservedUrl = $mockReportsApplicationUrl + ServiceName = "ReportServer`$$mockNamedInstanceName" } } @@ -833,59 +1194,78 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { } It 'Should configure Reporting Service without throwing an error' { - { Set-TargetResource @testParameters } | Should -Not -Throw + if ( $TestCaseVersion -lt 14 ) + { + { Set-TargetResource @mockDefaultParameters } | Should -Throw ($script:localizedData.VersionNotSupported) + } + else + { + { Set-TargetResource @testParameters } | Should -Not -Throw + } - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'SetSecureConnectionLevel' - } -Exactly -Times 1 -Scope It + if ( $TestCaseVersion -ge 14 ) + { + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'SetSecureConnectionLevel' + } -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'RemoveURL' -and $Arguments.Application -eq $mockReportServerApplicationName - } -Exactly -Times 2 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'RemoveURL' -and $Arguments.Application -eq $mockReportServerApplicationName + } -Exactly -Times 2 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'RemoveURL' -and $Arguments.Application -eq $mockReportsApplicationName - } -Exactly -Times 2 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'RemoveURL' -and $Arguments.Application -eq $mockReportsApplicationName + } -Exactly -Times 2 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'InitializeReportServer' - } -Exactly -Times 0 -Scope It + if ( $TestCaseVersion -lt 14 ) + { + $shouldInvokeInitializeReportServerTime = 1 + } + else + { + $shouldInvokeInitializeReportServerTime = 0 + } - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'SetDatabaseConnection' - } -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'InitializeReportServer' + } -Exactly -Times $shouldInvokeInitializeReportServerTime -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'GenerateDatabaseRightsScript' - } -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'SetDatabaseConnection' + } -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'GenerateDatabaseCreationScript' - } -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'GenerateDatabaseRightsScript' + } -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'SetVirtualDirectory' -and $Arguments.Application -eq $mockReportServerApplicationName - } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'GenerateDatabaseCreationScript' + } -Exactly -Times 0 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'SetVirtualDirectory' -and $Arguments.Application -eq $mockReportsApplicationName - } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'SetVirtualDirectory' -and $Arguments.Application -eq $mockReportServerApplicationName + } -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'ReserveUrl' -and $Arguments.Application -eq $mockReportServerApplicationName - } -Exactly -Times 2 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'SetVirtualDirectory' -and $Arguments.Application -eq $mockReportsApplicationName + } -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'ReserveUrl' -and $Arguments.Application -eq $mockReportsApplicationName - } -Exactly -Times 2 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'ReserveUrl' -and $Arguments.Application -eq $mockReportServerApplicationName + } -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Get-CimInstance -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-SqlCmd -Exactly -Times 0 -Scope It - Should -Invoke -CommandName Restart-ReportingServicesService -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'ReserveUrl' -and $Arguments.Application -eq $mockReportsApplicationName + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Get-CimInstance -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Invoke-SqlCmd -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Restart-ReportingServicesService -Exactly -Times 1 -Scope It + } } } - Context "When configuring a default instance that are not initialized ()" { + Context "When configuring a default instance that is not initialized ()" { BeforeAll { $mockDynamicIsInitialized = $false @@ -927,9 +1307,102 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { -ParameterFilter $mockGetCimInstance_OperatingSystem_ParameterFilter } + It 'Should configure Reporting Service without throwing an error' { + if ( $TestCaseVersion -lt 14 ) + { + { Set-TargetResource @mockDefaultParameters } | Should -Throw ($script:localizedData.VersionNotSupported) + } + else + { + { Set-TargetResource @defaultParameters } | Should -Not -Throw + } + + if ( $TestCaseVersion -ge 14 ) + { + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'RemoveURL' + } -Exactly -Times 0 -Scope It + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'InitializeReportServer' + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'SetDatabaseConnection' + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'GenerateDatabaseRightsScript' + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'GenerateDatabaseCreationScript' + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'SetVirtualDirectory' -and $Arguments.Application -eq $mockReportServerApplicationName + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'SetVirtualDirectory' -and $Arguments.Application -eq $mockReportsApplicationNameLegacy + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'ReserveUrl' -and $Arguments.Application -eq $mockReportServerApplicationName + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'ReserveUrl' -and $Arguments.Application -eq $mockReportsApplicationNameLegacy + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Get-CimInstance -Exactly -Times 10 -Scope It + Should -Invoke -CommandName Invoke-SqlCmd -Exactly -Times 2 -Scope It + Should -Invoke -CommandName Restart-ReportingServicesService -Exactly -Times 2 -Scope It + + Should -Invoke -CommandName Invoke-SqlCmd -ParameterFilter { + $Encrypt -eq 'Optional' + } -Times 2 -Exactly -Scope It + } + } + } + } + + Context 'When Power BI Report Server is not in the desired state' { + Context "When configuring an instance that is not initialized" { + BeforeAll { + $mockDynamicIsInitialized = $false + + Mock -CommandName Test-TargetResource -MockWith { + return $true + } + + $defaultParameters = @{ + InstanceName = 'PBIRS' + DatabaseServerName = $mockReportingServicesDatabaseServerName + DatabaseInstanceName = $mockReportingServicesDatabaseDefaultInstanceName + ServiceAccount = $mockServiceAccountCredential + } + + Mock -CommandName Get-ReportingServicesData -MockWith { + return @{ + Configuration = (& $mockGetCimInstance_ConfigurationSetting_PowerBIReportServer)[0] + ReportsApplicationName = 'ReportServerWebApp' + SqlVersion = 15 + } + } + + Mock -CommandName Get-CimInstance ` + -MockWith $mockGetCimInstance_Language ` + -ParameterFilter $mockGetCimInstance_OperatingSystem_ParameterFilter + } + It 'Should configure Reporting Service without throwing an error' { { Set-TargetResource @defaultParameters } | Should -Not -Throw + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'SetWindowsServiceIdentity' + } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { $MethodName -eq 'RemoveURL' } -Exactly -Times 0 -Scope It @@ -955,7 +1428,7 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { } -Exactly -Times 1 -Scope It Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'SetVirtualDirectory' -and $Arguments.Application -eq $mockReportsApplicationNameLegacy + $MethodName -eq 'SetVirtualDirectory' -and $Arguments.Application -eq $mockReportsApplicationName } -Exactly -Times 1 -Scope It Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { @@ -963,16 +1436,114 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { } -Exactly -Times 1 -Scope It Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'ReserveUrl' -and $Arguments.Application -eq $mockReportsApplicationNameLegacy + $MethodName -eq 'ReserveUrl' -and $Arguments.Application -eq $mockReportsApplicationName } -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Get-CimInstance -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-SqlCmd -Exactly -Times 2 -Scope It + Should -Invoke -CommandName Get-CimInstance -Exactly -Times 10 -Scope It + Should -Invoke -CommandName Invoke-Sqlcmd -Exactly -Times 2 -Scope It Should -Invoke -CommandName Restart-ReportingServicesService -Exactly -Times 2 -Scope It + } + } + + Context "When configuring an instance that is already initialized" { + BeforeAll { + $mockDynamicIsInitialized = $true + + Mock -CommandName Get-ReportingServicesData -MockWith { + return @{ + Configuration = (& $mockGetCimInstance_ConfigurationSetting_PowerBIReportServer)[0] + ReportsApplicationName = 'ReportServerWebApp' + SqlVersion = 15 + } + } - Should -Invoke -CommandName Invoke-SqlCmd -ParameterFilter { - $Encrypt -eq 'Optional' - } -Times 2 -Exactly -Scope It + Mock -CommandName Get-TargetResource -MockWith { + return @{ + DatabaseName = $mockReportingServicesDatabaseName + IsInitialized = $true + ReportServerReservedUrl = $mockReportServerApplicationUrl + ReportsReservedUrl = $mockReportsApplicationUrl + ServiceName = 'SQLServerReportingServices' + } + } + + Mock -CommandName Test-TargetResource -MockWith { + return $true + } + + $testParameters = @{ + InstanceName = 'PBIRS' + DatabaseServerName = $mockReportingServicesDatabaseServerName + DatabaseInstanceName = $mockReportingServicesDatabaseNamedInstanceName + ReportServerVirtualDirectory = 'ReportServer_NewName' + ReportsVirtualDirectory = 'Reports_NewName' + ReportServerReservedUrl = 'https://+:4443' + ReportsReservedUrl = 'https://+:4443' + UseSsl = $true + ServiceAccount = $mockServiceAccountCredential + } + } + + BeforeEach { + Mock -CommandName Get-CimInstance ` + -MockWith $mockGetCimInstance_Language ` + -ParameterFilter $mockGetCimInstance_OperatingSystem_ParameterFilter + } + + It 'Should configure Reporting Service without throwing an error' { + { Set-TargetResource @testParameters } | Should -Not -Throw + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'SetWindowsServiceIdentity' + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'SetSecureConnectionLevel' + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'RemoveURL' -and $Arguments.Application -eq $mockReportServerApplicationName + } -Exactly -Times 2 -Scope It + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'RemoveURL' -and $Arguments.Application -eq $mockReportsApplicationName + } -Exactly -Times 2 -Scope It + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'InitializeReportServer' + } -Exactly -Times 0 -Scope It + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'SetDatabaseConnection' + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'GenerateDatabaseRightsScript' + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'GenerateDatabaseCreationScript' + } -Exactly -Times 0 -Scope It + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'SetVirtualDirectory' -and $Arguments.Application -eq $mockReportServerApplicationName + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'SetVirtualDirectory' -and $Arguments.Application -eq $mockReportsApplicationName + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'ReserveUrl' -and $Arguments.Application -eq $mockReportServerApplicationName + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'ReserveUrl' -and $Arguments.Application -eq $mockReportsApplicationName + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Get-CimInstance -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Invoke-Sqlcmd -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Restart-ReportingServicesService -Exactly -Times 2 -Scope It } } } @@ -1002,7 +1573,8 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { Add-Member -MemberType NoteProperty -Name 'InstanceName' -Value $mockNamedInstanceName -PassThru | Add-Member -MemberType NoteProperty -Name 'VirtualDirectoryReportServer' -Value $mockVirtualDirectoryReportServerName -PassThru | Add-Member -MemberType NoteProperty -Name 'VirtualDirectoryReportManager' -Value $mockVirtualDirectoryReportManagerName -PassThru | - Add-Member -MemberType NoteProperty -Name 'SecureConnectionLevel' -Value $script:mockDynamicSecureConnectionLevel -PassThru -Force + Add-Member -MemberType NoteProperty -Name 'SecureConnectionLevel' -Value $script:mockDynamicSecureConnectionLevel -PassThru | + Add-Member -MemberType NoteProperty -Name 'ServiceName' -Value 'SQLServerReportingServices' -PassThru -Force ReportsApplicationName = 'ReportServerWebApp' SqlVersion = $sqlVersion.Version } @@ -1027,75 +1599,327 @@ Describe 'SqlRS\Set-TargetResource' -Tag 'Set' { } It 'Should configure Reporting Service without throwing an error' { - { Set-TargetResource @defaultParameters } | Should -Not -Throw + if ( $TestCaseVersion -lt 14 ) + { + { Set-TargetResource @mockDefaultParameters } | Should -Throw ($script:localizedData.VersionNotSupported) + } + else + { + { Set-TargetResource @defaultParameters } | Should -Not -Throw + } - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'SetSecureConnectionLevel' - } -Exactly -Times 0 -Scope It + if ( $TestCaseVersion -ge 14 ) + { + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'SetSecureConnectionLevel' + } -Exactly -Times 0 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'RemoveURL' - } -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'RemoveURL' + } -Exactly -Times 0 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'InitializeReportServer' - } -Exactly -Times 0 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'InitializeReportServer' + } -Exactly -Times 0 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'SetDatabaseConnection' - } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'SetDatabaseConnection' + } -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'GenerateDatabaseRightsScript' - } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'GenerateDatabaseRightsScript' + } -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'GenerateDatabaseCreationScript' - } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'GenerateDatabaseCreationScript' + } -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'SetVirtualDirectory' -and $Arguments.Application -eq $mockReportServerApplicationName - } -Exactly -Times 1 -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'SetVirtualDirectory' -and $Arguments.Application -eq $mockReportServerApplicationName + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'SetVirtualDirectory' -and $Arguments.Application -eq $mockReportsApplicationName + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'ReserveUrl' -and $Arguments.Application -eq $mockReportServerApplicationName + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { + $MethodName -eq 'ReserveUrl' -and $Arguments.Application -eq $mockReportsApplicationName + } -Exactly -Times 1 -Scope It + + Should -Invoke -CommandName Get-CimInstance -Exactly -Times 9 -Scope It + Should -Invoke -CommandName Invoke-SqlCmd -Exactly -Times 2 -Scope It + Should -Invoke -CommandName Restart-ReportingServicesService -Exactly -Times 1 -Scope It + } + } + } +} + +Describe 'SqlRS\Test-TargetResource' -Tag 'Test' { + Context 'When the system is not in the desired state' { + Context 'When Reporting Services are not initialized' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + DatabaseName = 'ReportServer' + IsInitialized = $false + ReportServerReservedUrl = 'http://+:80' + ReportsReservedUrl = 'http://+:80' + ServiceName = 'ReportServer' + } + } + } + + It 'Should return state as not in desired state' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $testParameters = @{ + InstanceName = 'INSTANCE' + DatabaseServerName = 'DBSERVER' + DatabaseInstanceName = 'DBINSTANCE' + Encrypt = 'Optional' + } + + $resultTestTargetResource = Test-TargetResource @testParameters + + $resultTestTargetResource | Should -BeFalse + } + } + } + + Context 'When current Report Server reserved URL is $null' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + IsInitialized = $false + ReportServerReservedUrl = $null + } + } + } + + It 'Should return state as not in desired state' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $testParameters = @{ + InstanceName = 'INSTANCE' + DatabaseServerName = 'DBSERVER' + DatabaseInstanceName = 'DBINSTANCE' + ReportServerReservedUrl = 'ReportServer_SQL2016' + } + + $resultTestTargetResource = Test-TargetResource @testParameters + + $resultTestTargetResource | Should -BeFalse + } + } + } + + Context 'When current Reports reserved URL is $null' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + IsInitialized = $false + ReportsReservedUrl = $null + } + } + } + + It 'Should return state as not in desired state' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $testParameters = @{ + InstanceName = 'INSTANCE' + DatabaseServerName = 'DBSERVER' + DatabaseInstanceName = 'DBINSTANCE' + ReportsReservedUrl = 'Reports_SQL2016' + } + + $resultTestTargetResource = Test-TargetResource @testParameters + + $resultTestTargetResource | Should -BeFalse + } + } + } + + Context 'When Report Server database name is different' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + DatabaseName = 'ReportServer' + IsInitialized = $true + ReportServerReservedUrl = 'http://+:80' + ReportsReservedUrl = 'http://+:80' + ServiceName = 'ReportServer' + } + } + } + + It 'Should return state as not in desired state' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $testParameters = @{ + InstanceName = 'INSTANCE' + DatabaseServerName = 'DBSERVER' + DatabaseInstanceName = 'DBINSTANCE' + DatabaseName = 'NewDatabase' + } + + $resultTestTargetResource = Test-TargetResource @testParameters + $resultTestTargetResource | Should -BeFalse + } + } + } + + Context 'When Report Server virtual directory is different' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + DatabaseName = 'ReportServer' + IsInitialized = $true + ReportServerReservedUrl = 'http://+:80' + ReportsReservedUrl = 'http://+:80' + ReportServerVirtualDirectory = 'ReportServer_SQL2016' + ReportsVirtualDirectory = 'Reports_SQL2016' + ServiceName = 'ReportServer' + } + } + } + + It 'Should return state as not in desired state' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $testParameters = @{ + InstanceName = 'INSTANCE' + DatabaseServerName = 'DBSERVER' + DatabaseInstanceName = 'DBINSTANCE' + ReportsVirtualDirectory = 'Reports_SQL2016' + ReportServerVirtualDirectory = 'ReportServer_NewName' + } + + $resultTestTargetResource = Test-TargetResource @testParameters + $resultTestTargetResource | Should -BeFalse + } + } + } + + Context 'When Reports virtual directory is different' { + BeforeAll { + Mock -CommandName Get-TargetResource -MockWith { + return @{ + DatabaseName = 'ReportServer' + IsInitialized = $true + ReportServerReservedUrl = 'http://+:80' + ReportsReservedUrl = 'http://+:80' + ReportServerVirtualDirectory = 'ReportServer_SQL2016' + ReportsVirtualDirectory = 'Reports_SQL2016' + ServiceName = 'ReportServer' + } + } + } + + It 'Should return state as not in desired state' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $testParameters = @{ + InstanceName = 'INSTANCE' + DatabaseServerName = 'DBSERVER' + DatabaseInstanceName = 'DBINSTANCE' + ReportServerVirtualDirectory = 'ReportServer_SQL2016' + ReportsVirtualDirectory = 'Reports_NewName' + } + + $resultTestTargetResource = Test-TargetResource @testParameters + + $resultTestTargetResource | Should -BeFalse + } + } + } + + Context 'When Report Server Report Server reserved URLs is specified' { + It 'Should return state as not in desired state when the URL is different' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + Mock -CommandName Get-TargetResource -MockWith { + return @{ + DatabaseName = 'ReportServer' + IsInitialized = $true + ReportServerReservedUrl = 'http://+:80' + ReportsReservedUrl = 'http://+:80' + ServiceName = 'ReportServer' + } + } + + $testParameters = @{ + InstanceName = 'INSTANCE' + DatabaseServerName = 'DBSERVER' + DatabaseInstanceName = 'DBINSTANCE' + ReportServerReservedUrl = 'https://+:443' + } - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'SetVirtualDirectory' -and $Arguments.Application -eq $mockReportsApplicationName - } -Exactly -Times 1 -Scope It + $resultTestTargetResource = Test-TargetResource @testParameters - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'ReserveUrl' -and $Arguments.Application -eq $mockReportServerApplicationName - } -Exactly -Times 1 -Scope It + $resultTestTargetResource | Should -BeFalse + } + } - Should -Invoke -CommandName Invoke-RsCimMethod -ParameterFilter { - $MethodName -eq 'ReserveUrl' -and $Arguments.Application -eq $mockReportsApplicationName - } -Exactly -Times 1 -Scope It + It 'Should return state as not in desired state when the URL is null' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 - Should -Invoke -CommandName Get-CimInstance -Exactly -Times 1 -Scope It - Should -Invoke -CommandName Invoke-SqlCmd -Exactly -Times 2 -Scope It - Should -Invoke -CommandName Restart-ReportingServicesService -Exactly -Times 1 -Scope It - } - } -} + Mock -CommandName Get-TargetResource -MockWith { + return @{ + DatabaseName = 'ReportServer' + IsInitialized = $true + ReportServerReservedUrl = $null + ReportsReservedUrl = 'http://+:80' + ServiceName = 'ReportServer' + } + } -Describe 'SqlRS\Test-TargetResource' -Tag 'Test' { - Context 'When the system is not in the desired state' { - Context 'When Reporting Services are not initialized' { - BeforeAll { - Mock -CommandName Get-TargetResource -MockWith { - return @{ - IsInitialized = $false + $testParameters = @{ + InstanceName = 'INSTANCE' + DatabaseServerName = 'DBSERVER' + DatabaseInstanceName = 'DBINSTANCE' + ReportServerReservedUrl = 'https://+:443' } + + $resultTestTargetResource = Test-TargetResource @testParameters + + $resultTestTargetResource | Should -BeFalse } } + } - It 'Should return state as not in desired state' { + Context 'When Report Server Reports reserved URLs is specified' { + It 'Should return state as not in desired state when the URL is different' { InModuleScope -ScriptBlock { Set-StrictMode -Version 1.0 + Mock -CommandName Get-TargetResource -MockWith { + return @{ + DatabaseName = 'ReportServer' + IsInitialized = $true + ReportServerReservedUrl = 'http://+:80' + ReportsReservedUrl = 'http://+:80' + ServiceName = 'ReportServer' + } + } + $testParameters = @{ InstanceName = 'INSTANCE' DatabaseServerName = 'DBSERVER' DatabaseInstanceName = 'DBINSTANCE' - Encrypt = 'Optional' + ReportsReservedUrl = 'https://+:443' } $resultTestTargetResource = Test-TargetResource @testParameters @@ -1103,27 +1927,26 @@ Describe 'SqlRS\Test-TargetResource' -Tag 'Test' { $resultTestTargetResource | Should -BeFalse } } - } - - Context 'When current Report Server reserved URL is $null' { - BeforeAll { - Mock -CommandName Get-TargetResource -MockWith { - return @{ - IsInitialized = $false - ReportServerReservedUrl = $null - } - } - } - It 'Should return state as not in desired state' { + It 'Should return state as not in desired state when the URL is null' { InModuleScope -ScriptBlock { Set-StrictMode -Version 1.0 + Mock -CommandName Get-TargetResource -MockWith { + return @{ + DatabaseName = 'ReportServer' + IsInitialized = $true + ReportServerReservedUrl = 'http://+:80' + ReportsReservedUrl = $null + ServiceName = 'ReportServer' + } + } + $testParameters = @{ - InstanceName = 'INSTANCE' - DatabaseServerName = 'DBSERVER' - DatabaseInstanceName = 'DBINSTANCE' - ReportServerReservedUrl = 'ReportServer_SQL2016' + InstanceName = 'INSTANCE' + DatabaseServerName = 'DBSERVER' + DatabaseInstanceName = 'DBINSTANCE' + ReportsReservedUrl = 'https://+:443' } $resultTestTargetResource = Test-TargetResource @testParameters @@ -1133,12 +1956,16 @@ Describe 'SqlRS\Test-TargetResource' -Tag 'Test' { } } - Context 'When current Reports reserved URL is $null' { + Context 'When SSL is not used' { BeforeAll { Mock -CommandName Get-TargetResource -MockWith { return @{ - IsInitialized = $false - ReportsReservedUrl = $null + DatabaseName = 'ReportServer' + IsInitialized = $true + ReportServerReservedUrl = @('http://+:80') + ReportsReservedUrl = @('http://+:80') + ServiceName = 'ReportServer' + UseSsl = $false } } } @@ -1151,7 +1978,7 @@ Describe 'SqlRS\Test-TargetResource' -Tag 'Test' { InstanceName = 'INSTANCE' DatabaseServerName = 'DBSERVER' DatabaseInstanceName = 'DBINSTANCE' - ReportsReservedUrl = 'Reports_SQL2016' + UseSsl = $true } $resultTestTargetResource = Test-TargetResource @testParameters @@ -1161,56 +1988,101 @@ Describe 'SqlRS\Test-TargetResource' -Tag 'Test' { } } - Context 'When Report Server virtual directory is different' { - BeforeAll { - Mock -CommandName Get-TargetResource -MockWith { - return @{ - IsInitialized = $true - ReportServerVirtualDirectory = 'ReportServer_SQL2016' - ReportsVirtualDirectory = 'Reports_SQL2016' - } - } - } - - It 'Should return state as not in desired state' { + Context 'When a certificate thumprint is supplied' { + It 'Should return not in desired state when the thumprint is not correct' { InModuleScope -ScriptBlock { Set-StrictMode -Version 1.0 + Mock -CommandName Get-TargetResource -MockWith { + return @{ + DatabaseName = 'ReportServer' + HttpsCertificateThumbprint = 'ffffffffffffffffffffffffffffffffffffffff' + HttpsIPAddress = '0.0.0.0' + HttpsPort = 443 + IsInitialized = $true + ReportServerReservedUrl = @('http://+:80') + ReportsReservedUrl = @('http://+:80') + ServiceName = 'ReportServer' + UseSsl = $true + } + } + $testParameters = @{ - InstanceName = 'INSTANCE' - DatabaseServerName = 'DBSERVER' - DatabaseInstanceName = 'DBINSTANCE' - ReportsVirtualDirectory = 'Reports_SQL2016' - ReportServerVirtualDirectory = 'ReportServer_NewName' + InstanceName = 'INSTANCE' + DatabaseServerName = 'DBSERVER' + DatabaseInstanceName = 'DBINSTANCE' + HttpsCertificateThumbprint = '0000000000000000000000000000000000000000' + HttpsIPAddress = '0.0.0.0' + HttpsPort = 443 + UseSsl = $true } $resultTestTargetResource = Test-TargetResource @testParameters + $resultTestTargetResource | Should -BeFalse } } - } - Context 'When Reports virtual directory is different' { - BeforeAll { - Mock -CommandName Get-TargetResource -MockWith { - return @{ - IsInitialized = $true - ReportServerVirtualDirectory = 'ReportServer_SQL2016' - ReportsVirtualDirectory = 'Reports_SQL2016' + It 'Should return not in desired state when the IP address is not correct' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + Mock -CommandName Get-TargetResource -MockWith { + return @{ + DatabaseName = 'ReportServer' + HttpsCertificateThumbprint = '0000000000000000000000000000000000000000' + HttpsIPAddress = '192.168.1.1' + HttpsPort = 443 + IsInitialized = $true + ReportServerReservedUrl = @('http://+:80') + ReportsReservedUrl = @('http://+:80') + ServiceName = 'ReportServer' + UseSsl = $true + } + } + + $testParameters = @{ + InstanceName = 'INSTANCE' + DatabaseServerName = 'DBSERVER' + DatabaseInstanceName = 'DBINSTANCE' + HttpsCertificateThumbprint = '0000000000000000000000000000000000000000' + HttpsIPAddress = '0.0.0.0' + HttpsPort = 443 + UseSsl = $true } + + $resultTestTargetResource = Test-TargetResource @testParameters + + $resultTestTargetResource | Should -BeFalse } } - It 'Should return state as not in desired state' { + It 'Should return not in desired state when the port is not correct' { InModuleScope -ScriptBlock { Set-StrictMode -Version 1.0 + Mock -CommandName Get-TargetResource -MockWith { + return @{ + DatabaseName = 'ReportServer' + HttpsCertificateThumbprint = '0000000000000000000000000000000000000000' + HttpsIPAddress = '0.0.0.0' + HttpsPort = 1234 + IsInitialized = $true + ReportServerReservedUrl = @('http://+:80') + ReportsReservedUrl = @('http://+:80') + ServiceName = 'ReportServer' + UseSsl = $true + } + } + $testParameters = @{ - InstanceName = 'INSTANCE' - DatabaseServerName = 'DBSERVER' - DatabaseInstanceName = 'DBINSTANCE' - ReportServerVirtualDirectory = 'ReportServer_SQL2016' - ReportsVirtualDirectory = 'Reports_NewName' + InstanceName = 'INSTANCE' + DatabaseServerName = 'DBSERVER' + DatabaseInstanceName = 'DBINSTANCE' + HttpsCertificateThumbprint = '0000000000000000000000000000000000000000' + HttpsIPAddress = '0.0.0.0' + HttpsPort = 443 + UseSsl = $true } $resultTestTargetResource = Test-TargetResource @testParameters @@ -1220,45 +2092,42 @@ Describe 'SqlRS\Test-TargetResource' -Tag 'Test' { } } - Context 'When Report Server Report Server reserved URLs is different' { + Context 'When the service account is not to the desired state' { BeforeAll { Mock -CommandName Get-TargetResource -MockWith { return @{ + DatabaseName = 'ReportServer' IsInitialized = $true ReportServerReservedUrl = 'http://+:80' + ReportsReservedUrl = 'http://+:80' + ServiceAccountName = 'NT AUTHORITY\SYSTEM' + ServiceName = 'ReportServer' } } } - It 'Should return state as not in desired state' { + It 'Should return state as not in desired state when a service account is supplied' { InModuleScope -ScriptBlock { Set-StrictMode -Version 1.0 + $mockServiceAccount = 'CONTOSO\ServiceAccount' + $mockPassword = [System.Security.SecureString]::new() + $mockPassword.AppendChar(' ') + $mockServiceAccountCredential = [System.Management.Automation.PSCredential]::new($mockServiceAccount,$mockPassword) + $testParameters = @{ - InstanceName = 'INSTANCE' - DatabaseServerName = 'DBSERVER' - DatabaseInstanceName = 'DBINSTANCE' - ReportServerReservedUrl = 'https://+:443' + InstanceName = 'INSTANCE' + DatabaseServerName = 'DBSERVER' + DatabaseInstanceName = 'DBINSTANCE' + ServiceAccount = $mockServiceAccountCredential } $resultTestTargetResource = Test-TargetResource @testParameters - $resultTestTargetResource | Should -BeFalse } } - } - - Context 'When Report Server Reports reserved URLs is different' { - BeforeAll { - Mock -CommandName Get-TargetResource -MockWith { - return @{ - IsInitialized = $true - ReportsReservedUrl = 'http://+:80' - } - } - } - It 'Should return state as not in desired state' { + It 'Should return state as not in desired state when a service account is not supplied' { InModuleScope -ScriptBlock { Set-StrictMode -Version 1.0 @@ -1266,39 +2135,40 @@ Describe 'SqlRS\Test-TargetResource' -Tag 'Test' { InstanceName = 'INSTANCE' DatabaseServerName = 'DBSERVER' DatabaseInstanceName = 'DBINSTANCE' - ReportsReservedUrl = 'https://+:443' } $resultTestTargetResource = Test-TargetResource @testParameters - $resultTestTargetResource | Should -BeFalse } } } - Context 'When SSL is not used' { + Context 'When an encryption key backup path is supplied' { BeforeAll { Mock -CommandName Get-TargetResource -MockWith { return @{ - IsInitialized = $true - UseSsl = $false + DatabaseName = 'ReportServer' + EncryptionKeyBackupFile = $null + IsInitialized = $true + ReportServerReservedUrl = 'http://+:80' + ReportsReservedUrl = 'http://+:80' + ServiceName = 'ReportServer' } } } - It 'Should return state as not in desired state' { + It 'Should return as not in desired state when a file name is not returned' { InModuleScope -ScriptBlock { Set-StrictMode -Version 1.0 $testParameters = @{ - InstanceName = 'INSTANCE' - DatabaseServerName = 'DBSERVER' - DatabaseInstanceName = 'DBINSTANCE' - UseSsl = $true + InstanceName = 'INSTANCE' + DatabaseServerName = 'DBSERVER' + DatabaseInstanceName = 'DBINSTANCE' + EncryptionKeyBackupPath = '\\share\backup\path' } $resultTestTargetResource = Test-TargetResource @testParameters - $resultTestTargetResource | Should -BeFalse } } @@ -1309,7 +2179,12 @@ Describe 'SqlRS\Test-TargetResource' -Tag 'Test' { BeforeAll { Mock -CommandName Get-TargetResource -MockWith { return @{ - IsInitialized = $true + IsInitialized = $true + DatabaseName = 'ReportServer' + ReportServerReservedUrl = @('http://+:80') + ReportsReservedUrl = @('http://+:80') + ServiceAccountName = 'NT SERVICE\ReportServer' + ServiceName = 'ReportServer' } } } @@ -1494,6 +2369,12 @@ Describe 'SqlRS\Get-ReportingServicesData' -Tag 'Helper' { } } + $mockGetItemProperty_PBIRS = { + return @{ + CurrentVersion = '15.0.1108.297' + } + } + $mockGetItemProperty_InstanceNames_ParameterFilter = { $Path -eq 'HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\Instance Names\RS' } @@ -1502,7 +2383,7 @@ Describe 'SqlRS\Get-ReportingServicesData' -Tag 'Helper' { $Path -eq ('HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\{0}\Setup' -f $mockInstanceId) } - $mockGetItemProperty_Sql2017AndSql2019_ParameterFilter = { + $mockGetItemProperty_Sql2017AndLater_ParameterFilter = { $Path -eq ('HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server\{0}\MSSQLServer\CurrentVersion' -f $mockInstanceId) } @@ -1530,29 +2411,30 @@ Describe 'SqlRS\Get-ReportingServicesData' -Tag 'Helper' { } -ParameterFilter $mockGetItemProperty_InstanceNames_ParameterFilter Mock -CommandName Get-CimInstance -MockWith { - return @( - ( - New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList @( - 'MSReportServer_ConfigurationSetting' - 'root/Microsoft/SQLServer/ReportServer/RS_SQL2016/v13/Admin' - ) | Add-Member -MemberType NoteProperty -Name 'DatabaseServerName' -Value 'DBSERVER\DBINSTANCE' -PassThru | - Add-Member -MemberType NoteProperty -Name 'IsInitialized' -Value $false -PassThru | - Add-Member -MemberType NoteProperty -Name 'InstanceName' -Value 'INSTANCE' -PassThru | - Add-Member -MemberType NoteProperty -Name 'VirtualDirectoryReportServer' -Value 'ReportServer' -PassThru | - Add-Member -MemberType NoteProperty -Name 'VirtualDirectoryReportManager' -Value 'Reports' -PassThru | - Add-Member -MemberType NoteProperty -Name 'SecureConnectionLevel' -Value $false -PassThru -Force - ), - ( - # Array is a regression test for issue #819. - New-Object -TypeName Object | - Add-Member -MemberType NoteProperty -Name 'DatabaseServerName' -Value 'DBSERVER\DBINSTANCE' -PassThru | - Add-Member -MemberType NoteProperty -Name 'IsInitialized' -Value $true -PassThru | - Add-Member -MemberType NoteProperty -Name 'InstanceName' -Value 'DummyInstance' -PassThru -Force - ) + return @( + ( + New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList @( + 'MSReportServer_ConfigurationSetting' + 'root/Microsoft/SQLServer/ReportServer/RS_SQL2016/v13/Admin' + ) | Add-Member -MemberType NoteProperty -Name 'DatabaseServerName' -Value 'DBSERVER\DBINSTANCE' -PassThru | + Add-Member -MemberType NoteProperty -Name 'IsInitialized' -Value $false -PassThru | + Add-Member -MemberType NoteProperty -Name 'InstanceName' -Value 'INSTANCE' -PassThru | + Add-Member -MemberType NoteProperty -Name 'VirtualDirectoryReportServer' -Value 'ReportServer' -PassThru | + Add-Member -MemberType NoteProperty -Name 'VirtualDirectoryReportManager' -Value 'Reports' -PassThru | + Add-Member -MemberType NoteProperty -Name 'SecureConnectionLevel' -Value $false -PassThru | + Add-Member -MemberType NoteProperty -Name 'WindowsServiceIdentityActual' -Value 'Contoso\ssrsServiceAccount' -PassThru -Force + ), + ( + # Array is a regression test for issue #819. + New-Object -TypeName Object | + Add-Member -MemberType NoteProperty -Name 'DatabaseServerName' -Value 'DBSERVER\DBINSTANCE' -PassThru | + Add-Member -MemberType NoteProperty -Name 'IsInitialized' -Value $true -PassThru | + Add-Member -MemberType NoteProperty -Name 'InstanceName' -Value 'DummyInstance' -PassThru -Force ) - } -ParameterFilter { - $ClassName -eq 'MSReportServer_ConfigurationSetting' - } + ) + } -ParameterFilter { + $ClassName -eq 'MSReportServer_ConfigurationSetting' + } } Context 'When the instance is SQL Server Reporting Services 2014 or older' { @@ -1579,6 +2461,7 @@ Describe 'SqlRS\Get-ReportingServicesData' -Tag 'Helper' { $getReportingServicesDataResult.Configuration.VirtualDirectoryReportServer | Should -Be 'ReportServer' $getReportingServicesDataResult.Configuration.VirtualDirectoryReportManager | Should -Be 'Reports' $getReportingServicesDataResult.Configuration.SecureConnectionLevel | Should -Be 0 + $getReportingServicesDataResult.Configuration.WindowsServiceIdentityActual | Should -Be 'Contoso\ssrsServiceAccount' $getReportingServicesDataResult.ReportsApplicationName | Should -Be 'ReportManager' $getReportingServicesDataResult.SqlVersion | Should -Be '12' } @@ -1619,6 +2502,7 @@ Describe 'SqlRS\Get-ReportingServicesData' -Tag 'Helper' { $getReportingServicesDataResult.Configuration.VirtualDirectoryReportServer | Should -Be 'ReportServer' $getReportingServicesDataResult.Configuration.VirtualDirectoryReportManager | Should -Be 'Reports' $getReportingServicesDataResult.Configuration.SecureConnectionLevel | Should -Be 0 + $getReportingServicesDataResult.Configuration.WindowsServiceIdentityActual | Should -Be 'Contoso\ssrsServiceAccount' $getReportingServicesDataResult.ReportsApplicationName | Should -Be 'ReportServerWebApp' $getReportingServicesDataResult.SqlVersion | Should -Be '13' } @@ -1643,7 +2527,7 @@ Describe 'SqlRS\Get-ReportingServicesData' -Tag 'Helper' { Mock -CommandName Get-ItemProperty ` -MockWith $mockGetItemProperty_Sql2017 ` - -ParameterFilter $mockGetItemProperty_Sql2017AndSql2019_ParameterFilter + -ParameterFilter $mockGetItemProperty_Sql2017AndLater_ParameterFilter } It 'Should return the correct information' { @@ -1659,6 +2543,7 @@ Describe 'SqlRS\Get-ReportingServicesData' -Tag 'Helper' { $getReportingServicesDataResult.Configuration.VirtualDirectoryReportServer | Should -Be 'ReportServer' $getReportingServicesDataResult.Configuration.VirtualDirectoryReportManager | Should -Be 'Reports' $getReportingServicesDataResult.Configuration.SecureConnectionLevel | Should -Be 0 + $getReportingServicesDataResult.Configuration.WindowsServiceIdentityActual | Should -Be 'Contoso\ssrsServiceAccount' $getReportingServicesDataResult.ReportsApplicationName | Should -Be 'ReportServerWebApp' $getReportingServicesDataResult.SqlVersion | Should -Be '14' } @@ -1668,7 +2553,7 @@ Describe 'SqlRS\Get-ReportingServicesData' -Tag 'Helper' { -Exactly -Times 2 -Scope 'It' Should -Invoke -CommandName Get-ItemProperty ` - -ParameterFilter $mockGetItemProperty_Sql2017AndSql2019_ParameterFilter ` + -ParameterFilter $mockGetItemProperty_Sql2017AndLater_ParameterFilter ` -Exactly -Times 1 -Scope 'It' Should -Invoke -CommandName Get-CimInstance -Exactly -Times 1 -Scope 'It' @@ -1683,7 +2568,48 @@ Describe 'SqlRS\Get-ReportingServicesData' -Tag 'Helper' { Mock -CommandName Get-ItemProperty ` -MockWith $mockGetItemProperty_Sql2019 ` - -ParameterFilter $mockGetItemProperty_Sql2017AndSql2019_ParameterFilter + -ParameterFilter $mockGetItemProperty_Sql2017AndLater_ParameterFilter + } + + It 'Should return the correct information' { + InModuleScope -ScriptBlock { + Set-StrictMode -Version 1.0 + + $getReportingServicesDataResult = Get-ReportingServicesData -InstanceName 'INSTANCE' + + $getReportingServicesDataResult.Configuration | Should -BeOfType [Microsoft.Management.Infrastructure.CimInstance] + $getReportingServicesDataResult.Configuration.InstanceName | Should -Be 'INSTANCE' + $getReportingServicesDataResult.Configuration.DatabaseServerName | Should -Be 'DBSERVER\DBINSTANCE' + $getReportingServicesDataResult.Configuration.IsInitialized | Should -BeFalse + $getReportingServicesDataResult.Configuration.VirtualDirectoryReportServer | Should -Be 'ReportServer' + $getReportingServicesDataResult.Configuration.VirtualDirectoryReportManager | Should -Be 'Reports' + $getReportingServicesDataResult.Configuration.SecureConnectionLevel | Should -Be 0 + $getReportingServicesDataResult.Configuration.WindowsServiceIdentityActual | Should -Be 'Contoso\ssrsServiceAccount' + $getReportingServicesDataResult.ReportsApplicationName | Should -Be 'ReportServerWebApp' + $getReportingServicesDataResult.SqlVersion | Should -Be '15' + } + + Should -Invoke -CommandName Get-ItemProperty ` + -ParameterFilter $mockGetItemProperty_InstanceNames_ParameterFilter ` + -Exactly -Times 2 -Scope 'It' + + Should -Invoke -CommandName Get-ItemProperty ` + -ParameterFilter $mockGetItemProperty_Sql2017AndLater_ParameterFilter ` + -Exactly -Times 1 -Scope 'It' + + Should -Invoke -CommandName Get-CimInstance -Exactly -Times 1 -Scope 'It' + } + } + + Context 'When the instance is Power BI Reporting Services' { + BeforeAll { + Mock -CommandName Test-Path -MockWith { + return $true + } + + Mock -CommandName Get-ItemProperty ` + -MockWith $mockGetItemProperty_PBIRS ` + -ParameterFilter $mockGetItemProperty_Sql2017AndLater_ParameterFilter } It 'Should return the correct information' { @@ -1699,6 +2625,7 @@ Describe 'SqlRS\Get-ReportingServicesData' -Tag 'Helper' { $getReportingServicesDataResult.Configuration.VirtualDirectoryReportServer | Should -Be 'ReportServer' $getReportingServicesDataResult.Configuration.VirtualDirectoryReportManager | Should -Be 'Reports' $getReportingServicesDataResult.Configuration.SecureConnectionLevel | Should -Be 0 + $getReportingServicesDataResult.Configuration.WindowsServiceIdentityActual | Should -Be 'Contoso\ssrsServiceAccount' $getReportingServicesDataResult.ReportsApplicationName | Should -Be 'ReportServerWebApp' $getReportingServicesDataResult.SqlVersion | Should -Be '15' } @@ -1708,7 +2635,7 @@ Describe 'SqlRS\Get-ReportingServicesData' -Tag 'Helper' { -Exactly -Times 2 -Scope 'It' Should -Invoke -CommandName Get-ItemProperty ` - -ParameterFilter $mockGetItemProperty_Sql2017AndSql2019_ParameterFilter ` + -ParameterFilter $mockGetItemProperty_Sql2017AndLater_ParameterFilter ` -Exactly -Times 1 -Scope 'It' Should -Invoke -CommandName Get-CimInstance -Exactly -Times 1 -Scope 'It' @@ -1716,3 +2643,131 @@ Describe 'SqlRS\Get-ReportingServicesData' -Tag 'Helper' { } } } + +Describe 'SqlRS\Get-LocalServiceAccountName' -Tag 'Helper' { + BeforeDiscovery { + $builtinServiceAccountTestCases = @( + @{ + ServiceAccountTypeName = 'LocalService' + ServiceAccountName = 'NT AUTHORITY\LocalService' + } + @{ + ServiceAccountTypeName = 'NetworkService' + ServiceAccountName = 'NT AUTHORITY\NetworkService' + } + @{ + ServiceAccountTypeName = 'System' + ServiceAccountName = 'NT AUTHORITY\System' + } + ) + + $virtualAccountTestCases = @( + @{ + ServiceName = 'ReportServer' + } + @{ + ServiceName = 'SQLServerReportingServices' + } + @{ + ServiceName = 'PowerBIReportServer' + } + ) + } + + Context 'When a builtin account is specified' { + It 'Should be "" when "LocalServiceAccountType" is ""' -ForEach $builtinServiceAccountTestCases { + $localServiceAccountName = Get-LocalServiceAccountName -LocalServiceAccountType $ServiceAccountTypeName + $localServiceAccountName | Should -Be $ServiceAccountName + } + } + + Context 'When a virtual account is specified' { + It 'Should be "NT SERVICE\" when "LocalServiceAccountType" is "VirtualAccount" and the service name is ""' -ForEach $virtualAccountTestCases { + $localServiceAccountName = Get-LocalServiceAccountName -LocalServiceAccountType VirtualAccount -ServiceName $ServiceName + $localServiceAccountName | Should -Be "NT SERVICE\$ServiceName" + } + + It 'Should throw the correct error when when "LocalServiceAccountType" is "VirtualAccount" and the service name is not supplied' { + { Get-LocalServiceAccountName -LocalServiceAccountType VirtualAccount } | Should -Throw "The 'ServiceName' parameter is required with the 'LocalServiceAccountType' is 'VirtualAccount'.*" + } + } +} + +Describe 'SqlRS\Backup-EncryptionKey' -Tag 'Helper' { + BeforeDiscovery { + $mockEncryptionKeyValue = 'encryption key value' + $mockCimInstance = New-Object -TypeName Microsoft.Management.Infrastructure.CimInstance -ArgumentList @( + 'MSReportServer_ConfigurationSetting' + 'root/Microsoft/SQLServer/ReportServer/RS_SQL2016/v13/Admin' + ) + $mockEncryptionKeyBackupCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @('mockBackupUser', ( ConvertTo-SecureString 'P@$$w0rd1' -AsPlainText -Force ) ) + $mockEncryptionKeyBackupPathCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @('mockBackupPathUser', ( ConvertTo-SecureString 'P@$$w0rd1' -AsPlainText -Force ) ) + $mockBackupPath = '\\Remote\Backup\Path' + + $testCases = @( + @{ + Parameters = @{ + IsInitialized = $false + CimInstance = $mockCimInstance + } + ReturnValue = $null + ShouldInvokeConnectUncPath = 0 + ShouldInvokeDisconnectUncPath = 0 + ShouldInvokeInvokeRsCimMethod = 0 + ShouldInvokeNewItem = 0 + ShouldInvokeSetContent = 0 + } + @{ + Parameters = @{ + IsInitialized = $true + EncryptionKeyBackupCredential = $mockEncryptionKeyBackupCredential + EncryptionKeyBackupPath = $mockBackupPath + EncryptionKeyBackupPathCredential = $mockEncryptionKeyBackupPathCredential + CimInstance = $mockCimInstance + } + ReturnValue = $mockEncryptionKeyValue + ShouldInvokeConnectUncPath = 1 + ShouldInvokeDisconnectUncPath = 1 + ShouldInvokeInvokeRsCimMethod = 1 + ShouldInvokeNewItem = 1 + ShouldInvokeSetContent = 1 + } + ) + } + + BeforeAll { + $mockInvokeRsCimMethod_BackupEncryptionKey = { + return New-Object -TypeName PSObject | + Add-Member -MemberType NoteProperty -Name KeyFile -Value $ReturnValue -PassThru -Force + } + + Mock -CommandName Connect-UncPath + Mock -CommandName Disconnect-UncPath + Mock -CommandName Invoke-RsCimMethod -MockWith $mockInvokeRsCimMethod_BackupEncryptionKey + Mock -CommandName New-Item + Mock -CommandName Set-Content + } + + Context 'When IsInitialized is ""' -ForEach $testCases { + It 'Should return ""' { + $backupEncryptionKeyResult = Backup-EncryptionKey @Parameters + $backupEncryptionKeyResult.KeyFile | Should -Be $ReturnValue + + Should -Invoke -CommandName Connect-UncPath -Exactly -Times $ShouldInvokeConnectUncPath -Scope It + Should -Invoke -CommandName Disconnect-UncPath -Exactly -Times $ShouldInvokeDisconnectUncPath -Scope It + Should -Invoke -CommandName Invoke-RsCimMethod -Exactly -Times $ShouldInvokeInvokeRsCimMethod -Scope It + Should -Invoke -CommandName New-Item -Exactly -Times $ShouldInvokeNewItem -Scope It + Should -Invoke -CommandName Set-Content -Exactly -Times $ShouldInvokeSetContent -Scope It + } + } +} + +Describe 'SqlRS\New-EncryptionKeyBackupCredential' -Tag 'Helper' { + Context 'When the function is called' { + It 'Should create a new credential' { + $credential = New-EncryptionKeyBackupCredential + $credential | Should -BeOfType System.Management.Automation.PSCredential + $credential.GetNetworkCredential().Password.Length | Should -Be 16 + } + } +} diff --git a/tests/Unit/SqlServerDsc.Common.Tests.ps1 b/tests/Unit/SqlServerDsc.Common.Tests.ps1 index fb6378348..7b64e87d9 100644 --- a/tests/Unit/SqlServerDsc.Common.Tests.ps1 +++ b/tests/Unit/SqlServerDsc.Common.Tests.ps1 @@ -3065,7 +3065,7 @@ Describe 'SqlServerDsc.Common\Restart-ReportingServicesService' -Tag 'RestartRep } It 'Should restart the service and dependent service' { - { Restart-ReportingServicesService -InstanceName 'MSSQLSERVER' } | Should -Not -Throw + { Restart-ReportingServicesService -ServiceName $mockServiceName } | Should -Not -Throw Should -Invoke -CommandName Get-Service -ParameterFilter { $Name -eq $mockServiceName @@ -3090,7 +3090,7 @@ Describe 'SqlServerDsc.Common\Restart-ReportingServicesService' -Tag 'RestartRep } It 'Should restart the service and dependent service' { - { Restart-ReportingServicesService -InstanceName 'SSRS' } | Should -Not -Throw + { Restart-ReportingServicesService } | Should -Not -Throw Should -Invoke -CommandName Get-Service -ParameterFilter { $Name -eq $mockServiceName @@ -3115,7 +3115,32 @@ Describe 'SqlServerDsc.Common\Restart-ReportingServicesService' -Tag 'RestartRep } It 'Should restart the service and dependent service' { - { Restart-ReportingServicesService -InstanceName 'TEST' } | Should -Not -Throw + { Restart-ReportingServicesService -ServiceName $mockServiceName } | Should -Not -Throw + + Should -Invoke -CommandName Get-Service -ParameterFilter { + $Name -eq $mockServiceName + } -Scope It -Exactly -Times 1 + Should -Invoke -CommandName Stop-Service -Scope It -Exactly -Times 1 + Should -Invoke -CommandName Start-Service -Scope It -Exactly -Times 2 + } + } + + Context 'When restarting Power BI Report Server' { + BeforeAll { + $mockServiceName = 'PowerBIReportServer' + $mockDependedServiceName = 'DependentService' + + $mockDynamicServiceName = $mockServiceName + $mockDynamicDependedServiceName = $mockDependedServiceName + $mockDynamicServiceDisplayName = 'Reporting Services (TEST)' + + Mock -CommandName Stop-Service + Mock -CommandName Start-Service + Mock -CommandName Get-Service -MockWith $mockGetService + } + + It 'Should restart the service and dependent service' { + { Restart-ReportingServicesService -ServiceName $mockServiceName } | Should -Not -Throw Should -Invoke -CommandName Get-Service -ParameterFilter { $Name -eq $mockServiceName @@ -3141,7 +3166,7 @@ Describe 'SqlServerDsc.Common\Restart-ReportingServicesService' -Tag 'RestartRep } It 'Should restart the service and dependent service' { - { Restart-ReportingServicesService -InstanceName 'TEST' -WaitTime 1 } | Should -Not -Throw + { Restart-ReportingServicesService -ServiceName $mockServiceName -WaitTime 1 } | Should -Not -Throw Should -Invoke -CommandName Get-Service -ParameterFilter { $Name -eq $mockServiceName