From 617e80122829ab009180c4b67dacb958a1592288 Mon Sep 17 00:00:00 2001 From: RWebster-Noble Date: Sun, 7 Jul 2024 17:36:37 +0100 Subject: [PATCH] Add UNC Path support to WebVirtualDirectory (#634) - WebVirtualDirectory - Added Credential paramater - Fixed error when using UNC PhysicalPath (issue #94). --- CHANGELOG.md | 4 + .../DSC_WebVirtualDirectory.psm1 | 118 +++++++++-- .../DSC_WebVirtualDirectory.schema.mof | 1 + .../DSC_WebVirtualDirectory.strings.psd1 | 6 +- ...ectory_NewVirtualDirectory_WithUncPath.ps1 | 79 ++++++++ tests/Unit/DSC_WebVirtualDirectory.Tests.ps1 | 190 +++++++++++++++++- 6 files changed, 372 insertions(+), 26 deletions(-) create mode 100644 source/Examples/Resources/WebVirtualDirectory/Sample_WebVirtualDirectory_NewVirtualDirectory_WithUncPath.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 57ba453cb..c7462522e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ For older change log history see the [historic changelog](HISTORIC_CHANGELOG.md) - Website - Add Ensure to LogCustomFieldInformation. ([issue #571](https://github.com/dsccommunity/WebAdministrationDsc/issues/571)) - Added code to ensure certificate selected has longest time until expiration when multiple matching certificates are found ([issue #578](https://github.com/dsccommunity/WebAdministrationDsc/issues/578)) +- WebVirtualDirectory + - Added Credential paramater ### Fixed @@ -27,6 +29,8 @@ For older change log history see the [historic changelog](HISTORIC_CHANGELOG.md) - WebSite - Added code to ensure certificate has private key. ([issue #578](https://github.com/dsccommunity/WebAdministrationDsc/issues/578)) - Removed duplicated resource descriptions in README.md +- WebVirtualDirectory + - Fixed error when using UNC PhysicalPath. ([issue #94](https://github.com/dsccommunity/WebAdministrationDsc/issues/94)) ## [4.1.0] - 2023-01-03 diff --git a/source/DSCResources/DSC_WebVirtualDirectory/DSC_WebVirtualDirectory.psm1 b/source/DSCResources/DSC_WebVirtualDirectory/DSC_WebVirtualDirectory.psm1 index ef47858c4..c72c9d4f1 100644 --- a/source/DSCResources/DSC_WebVirtualDirectory/DSC_WebVirtualDirectory.psm1 +++ b/source/DSCResources/DSC_WebVirtualDirectory/DSC_WebVirtualDirectory.psm1 @@ -35,7 +35,11 @@ function Get-TargetResource [Parameter(Mandatory = $true)] [System.String] - $PhysicalPath + $PhysicalPath, + + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential ) Assert-Module -ModuleName WebAdministration @@ -46,11 +50,29 @@ function Get-TargetResource $PhysicalPath = '' $Ensure = 'Absent' + $Credential = $null if ($virtualDirectory.Count -eq 1) { $PhysicalPath = $virtualDirectory.PhysicalPath $Ensure = 'Present' + + if ([System.String]::IsNullOrEmpty($WebApplication)) + { + $itemPath = "IIS:Sites\$Website\$Name" + } + else + { + $itemPath = "IIS:Sites\$Website\$WebApplication\$Name" + } + + $userName = (Get-ItemProperty $itemPath -Name UserName).Value + if (-not [System.String]::IsNullOrEmpty($userName)) + { + $password = New-Object System.Security.SecureString # Blank Password + $secStringPassword = $password | ConvertTo-SecureString -AsPlainText -Force + $Credential = New-Object System.Management.Automation.PSCredential ($userName, $secStringPassword) + } } Write-Verbose -Message ($script:localizedData.VerboseGetTargetResource) @@ -60,6 +82,7 @@ function Get-TargetResource Website = $Website WebApplication = $WebApplication PhysicalPath = $PhysicalPath + Credential = $Credential Ensure = $Ensure } @@ -95,7 +118,11 @@ function Set-TargetResource [Parameter(Mandatory = $true)] [System.String] - $PhysicalPath + $PhysicalPath, + + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential ) Assert-Module -ModuleName WebAdministration @@ -115,34 +142,57 @@ function Set-TargetResource $WebApplication = '' } + if ([System.String]::IsNullOrEmpty($WebApplication)) + { + $itemPath = "IIS:Sites\$Website\$Name" + } + else + { + $itemPath = "IIS:Sites\$Website\$WebApplication\$Name" + } + $virtualDirectory = Get-WebVirtualDirectory -Site $Website ` -Name $Name ` -Application $WebApplication if ($virtualDirectory.count -eq 0) { Write-Verbose -Message ($script:localizedData.VerboseSetTargetCreateVirtualDirectory -f $Name) - New-WebVirtualDirectory -Site $Website ` - -Application $WebApplication ` - -Name $Name ` - -PhysicalPath $PhysicalPath - } - else - { - Write-Verbose -Message ($script:localizedData.VerboseSetTargetPhysicalPath -f $Name) - - if ($WebApplication.Length -gt 0) + if (([System.Uri] $PhysicalPath).IsUnc) { - $ItemPath = "IIS:Sites\$Website\$WebApplication\$Name" + # If physical path is provided using Unc syntax run New-WebVirtualDirectory with -Force flag + New-WebVirtualDirectory -Site $Website ` + -Application $WebApplication ` + -Name $Name ` + -PhysicalPath $PhysicalPath ` + -ErrorAction Stop ` + -Force } else { - $ItemPath = "IIS:Sites\$Website\$Name" + # Run New-WebVirtualDirectory without -Force flag to verify that the path exists + New-WebVirtualDirectory -Site $Website ` + -Application $WebApplication ` + -Name $Name ` + -PhysicalPath $PhysicalPath ` + -ErrorAction Stop } + } + else + { + Write-Verbose -Message ($script:localizedData.VerboseSetTargetPhysicalPath -f $Name) - Set-ItemProperty -Path $ItemPath ` + Set-ItemProperty -Path $itemPath ` -Name physicalPath ` -Value $PhysicalPath } + + if ($Credential) + { + Write-Verbose -Message ($script:localizedData.VerboseSetTargetCredential -f $Name) + + Set-ItemProperty $itemPath -Name UserName -Value $Credential.UserName + Set-ItemProperty $itemPath -Name Password -Value $Credential.GetNetworkCredential().Password + } } if ($Ensure -eq 'Absent') @@ -197,7 +247,11 @@ function Test-TargetResource [Parameter(Mandatory = $true)] [System.String] - $PhysicalPath + $PhysicalPath, + + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential ) Assert-Module -ModuleName WebAdministration @@ -210,12 +264,38 @@ function Test-TargetResource { if ($virtualDirectory.PhysicalPath -eq $PhysicalPath) { - Write-Verbose -Message ($script:localizedData.VerboseTestTargetTrue) - return $true + if (-not $Credential) + { + Write-Verbose -Message ($script:localizedData.VerboseTestTargetTrue) + return $true + } + + if ([System.String]::IsNullOrEmpty($WebApplication)) + { + $itemPath = "IIS:Sites\$Website\$Name" + } + else + { + $itemPath = "IIS:Sites\$Website\$WebApplication\$Name" + } + + $userName = (Get-ItemProperty $itemPath -Name UserName).Value + $password = (Get-ItemProperty $itemPath -Name Password).Value + + if (($Credential.UserName -eq $userName -and $Credential.GetNetworkCredential().Password -eq $password)) + { + Write-Verbose -Message ($script:localizedData.VerboseTestTargetTrue) + return $true + } + else + { + Write-Verbose -Message ($script:localizedData.VerboseTestTargetCredentialFalse -f $PhysicalPath, $Name) + return $false + } } else { - Write-Verbose -Message ($script:localizedData.VerboseTestTargetFalse -f $PhysicalPath, $Name) + Write-Verbose -Message ($script:localizedData.VerboseTestTargetPhysicalPathFalse -f $Credential.UserName, $Name) return $false } } diff --git a/source/DSCResources/DSC_WebVirtualDirectory/DSC_WebVirtualDirectory.schema.mof b/source/DSCResources/DSC_WebVirtualDirectory/DSC_WebVirtualDirectory.schema.mof index 1e03e9d4c..caf64bb99 100644 --- a/source/DSCResources/DSC_WebVirtualDirectory/DSC_WebVirtualDirectory.schema.mof +++ b/source/DSCResources/DSC_WebVirtualDirectory/DSC_WebVirtualDirectory.schema.mof @@ -6,5 +6,6 @@ class DSC_WebVirtualDirectory : OMI_BaseResource [Key, Description("Web application name for the virtual directory")] string WebApplication; [Key, Description("Name of virtual directory")] string Name; [Required, Description("Physical path for the virtual directory")] string PhysicalPath; + [Write, Description("Credential to use for accessing the virtual directory"), EmbeddedInstance("MSFT_Credential")] String Credential; [Write, Description("Whether virtual directory should be present or absent"), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] string Ensure; }; diff --git a/source/DSCResources/DSC_WebVirtualDirectory/en-US/DSC_WebVirtualDirectory.strings.psd1 b/source/DSCResources/DSC_WebVirtualDirectory/en-US/DSC_WebVirtualDirectory.strings.psd1 index b8e6fba88..83370b231 100644 --- a/source/DSCResources/DSC_WebVirtualDirectory/en-US/DSC_WebVirtualDirectory.strings.psd1 +++ b/source/DSCResources/DSC_WebVirtualDirectory/en-US/DSC_WebVirtualDirectory.strings.psd1 @@ -1,10 +1,12 @@ # culture ="en-US" ConvertFrom-StringData -StringData @' VerboseGetTargetResource = Get-TargetResource has been run. - VerboseSetTargetPhysicalPath = Updating physical path for Web Virtual Directory '{0}'. + VerboseSetTargetPhysicalPath = Updating PhysicalPath for Web Virtual Directory '{0}'. + VerboseSetTargetCredential = Updating Credential for Web Virtual Directory '{0}'. VerboseSetTargetCreateVirtualDirectory = Creating new Web Virtual Directory '{0}'. VerboseSetTargetRemoveVirtualDirectory = Removing existing Virtual Directory '{0}'. - VerboseTestTargetFalse = Physical path '{0}' for Web Virtual Directory '{1}' is not in desired state. + VerboseTestTargetPhysicalPathFalse = Physical path '{0}' for Web Virtual Directory '{1}' is not in desired state. + VerboseTestTargetCredentialFalse = Credential {0} for Web Virtual Directory '{1}' is not in desired state. VerboseTestTargetTrue = Web Virtual Directory is in desired state. VerboseTestTargetAbsentTrue = Web Virtual Directory '{0}' should be Absent and is Absent. '@ diff --git a/source/Examples/Resources/WebVirtualDirectory/Sample_WebVirtualDirectory_NewVirtualDirectory_WithUncPath.ps1 b/source/Examples/Resources/WebVirtualDirectory/Sample_WebVirtualDirectory_NewVirtualDirectory_WithUncPath.ps1 new file mode 100644 index 000000000..acba7cb4e --- /dev/null +++ b/source/Examples/Resources/WebVirtualDirectory/Sample_WebVirtualDirectory_NewVirtualDirectory_WithUncPath.ps1 @@ -0,0 +1,79 @@ +<# + .SYNOPSIS + Create a new web virtual directory on the Default Web Site + .DESCRIPTION + This example shows how to use the WebVirtualDirectory DSC resource to create a new virtual + directory on the Default Web Site with a UNC path that requires credentials. +#> +configuration Sample_WebVirtualDirectory_NewVirtualDirectory_WithUncPath +{ + param + ( + # Target nodes to apply the configuration + [System.String[]] + $NodeName = 'localhost', + + # Name of virtual directory to create + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [System.String] + $VirtualDirectoryName, + + # Physical path of the virtual directory + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [System.String] + $PhysicalPath, + + # Credential to use for the virtual directory + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [PSCredential] + $Credential + ) + + # Import the module that defines custom resources + Import-DscResource -Module PSDesiredStateConfiguration + Import-DscResource -Module WebAdministrationDsc + + Node $NodeName + { + # Install the IIS role + WindowsFeature IIS + { + Ensure = 'Present' + Name = 'Web-Server' + } + + # Start the default website + WebSite DefaultSite + { + Ensure = 'Present' + Name = 'Default Web Site' + State = 'Started' + PhysicalPath = 'C:\inetpub\wwwroot' + DependsOn = '[WindowsFeature]IIS' + } + + # Copy the virtual directory content + File VirtualDirectoryContent + { + Ensure = 'Present' + DestinationPath = $PhysicalPath + Type = 'Directory' + DependsOn = '[WindowsFeature]IIS' + } + + # Create the new virtual directory + WebVirtualDirectory NewVirtualDirectory + { + Ensure = 'Present' + Website = "Default Web Site" + WebApplication = '' + Name = $VirtualDirectoryName + PhysicalPath = $PhysicalPath + DependsOn = '[File]VirtualDirectoryContent' + Credential = $Credential + } + } +} diff --git a/tests/Unit/DSC_WebVirtualDirectory.Tests.ps1 b/tests/Unit/DSC_WebVirtualDirectory.Tests.ps1 index c785843fe..5e707780a 100644 --- a/tests/Unit/DSC_WebVirtualDirectory.Tests.ps1 +++ b/tests/Unit/DSC_WebVirtualDirectory.Tests.ps1 @@ -63,6 +63,35 @@ try } } + Context 'Directory is Present and PhysicalPath and Credential is Correct with no WebApplication' { + It 'Should return true' { + $mockUsername = "SomeUsername" + $mockPassword = "SomePassword" + $passwordSecureString = $mockPassword | ConvertTo-SecureString -AsPlainText -Force + $mockCred = New-Object System.Management.Automation.PSCredential($mockUsername, $passwordSecureString) + + $returnUsername = @{ + 'Value' = $mockUsername + } + $returnPassword = @{ + 'Value' = $mockPassword + } + Mock -CommandName Get-ItemProperty -ParameterFilter { $Name -eq "userName" } -MockWith { return $returnUsername } + Mock -CommandName Get-ItemProperty -ParameterFilter { $Name -eq "password" } -MockWith { return $returnPassword } + + Mock -CommandName Get-WebVirtualDirectory -MockWith { return $virtualDir } + + $result = Test-TargetResource -Website $MockSite.Website ` + -WebApplication '' ` + -Name $MockSite.Name ` + -PhysicalPath $MockSite.PhysicalPath ` + -Credential $mockCred ` + -Ensure $MockSite.Ensure + + $result | Should Be $true + } + } + Context 'Directory is Present and PhysicalPath is incorrect' { It 'Should return false' { $virtualDir = @{ @@ -83,19 +112,29 @@ try } } - Context 'Directory is Present and PhysicalPath is incorrect' { + Context 'Directory is Present and Credential is incorrect' { It 'Should return false' { - $virtualDir = @{ - Name = 'shared_directory' - PhysicalPath = 'C:\inetpub\wwwroot\shared_wrong' - Count = 1 + $mockUsername = "SomeUsername" + $mockPassword = "SomePassword" + $passwordSecureString = $mockPassword | ConvertTo-SecureString -AsPlainText -Force + $mockCred = New-Object System.Management.Automation.PSCredential($mockUsername, $passwordSecureString) + + $returnUsername = @{ + 'Value' = 'SomeIncorrectUsername' } + $returnPassword = @{ + 'Value' = 'SomeIncorrectPassword' + } + Mock -CommandName Get-ItemProperty -ParameterFilter { $Name -eq "userName" } -MockWith { return $returnUsername } + Mock -CommandName Get-ItemProperty -ParameterFilter { $Name -eq "password" } -MockWith { return $returnPassword } Mock -CommandName Get-WebVirtualDirectory -MockWith { return $virtualDir } + $result = Test-TargetResource -Website $MockSite.Website ` -WebApplication $MockSite.WebApplication ` -Name $MockSite.Name ` -PhysicalPath $MockSite.PhysicalPath ` + -Credential $mockCred ` -Ensure $MockSite.Ensure $result | Should Be $false @@ -146,7 +185,12 @@ try 'Count' = 1 } + $returnEmpty = @{ + 'Value' = '' + } + Mock -CommandName Get-WebVirtualDirectory -MockWith { return $returnObj } + Mock -CommandName Get-ItemProperty -MockWith { return $returnEmpty } $result = Get-TargetResource -Website $returnSite.Website ` -WebApplication $returnSite.WebApplication ` @@ -159,6 +203,51 @@ try $result.PhysicalPath | Should Be $returnSite.PhysicalPath $result.Ensure | Should Be $returnSite.Ensure } + + Context 'Ensure = Present and Physical Path and Credential Exists with no WebApplication' { + $returnSite = @{ + Name = 'SomeName' + Website = 'Website' + WebApplication = '' + PhysicalPath = 'PhysicalPath' + Ensure = 'Present' + } + + $returnObj = @{ + 'Name' = $returnSite.Name + 'PhysicalPath' = $returnSite.PhysicalPath + 'Count' = 1 + } + + $mockUsername = "SomeUsername" + $mockPassword = "SomePassword" + $passwordSecureString = $mockPassword | ConvertTo-SecureString -AsPlainText -Force + $mockCred = New-Object System.Management.Automation.PSCredential($mockUsername, $passwordSecureString) + + $returnUsername = @{ + 'Value' = $mockUsername + } + $returnPassword = @{ + 'Value' = $mockPassword + } + Mock -CommandName Get-ItemProperty -ParameterFilter { $Name -eq "userName" } -MockWith { return $returnUsername } + Mock -CommandName Get-ItemProperty -ParameterFilter { $Name -eq "password" } -MockWith { return $returnPassword } + + Mock -CommandName Get-WebVirtualDirectory -MockWith { return $returnObj } + + $result = Get-TargetResource -Website $returnSite.Website ` + -WebApplication $returnSite.WebApplication ` + -Name $returnSite.Name ` + -PhysicalPath $returnSite.PhysicalPath ` + -Credential $mockCred + + $result.Name | Should Be $returnSite.Name + $result.Website | Should Be $returnSite.Website + $result.WebApplication | Should Be $returnSite.WebApplication + $result.PhysicalPath | Should Be $returnSite.PhysicalPath + $result.Ensure | Should Be $returnSite.Ensure + $result.Credential.UserName | Should Be $mockUsername + } } Describe "$script:dscResourceName\Set-TargetResource" { @@ -188,6 +277,39 @@ try } } + Context 'Ensure = Present and virtual directory does not exist and Credential provided with no WebApplication and UncPhysicalPath' { + It 'Should call New-WebVirtualDirectory' { + $mockSite = @{ + Name = 'SomeName' + Website = 'Website' + WebApplication = '' + PhysicalPath = '\\UncPhysicalPath' + } + $mockUsername = "SomeUsername" + $mockPassword = "SomePassword" + $passwordSecureString = $mockPassword | ConvertTo-SecureString -AsPlainText -Force + $mockCred = New-Object System.Management.Automation.PSCredential($mockUsername, $passwordSecureString) + + + Mock -CommandName Set-ItemProperty + + Mock -CommandName New-WebVirtualDirectory + + Mock -CommandName Get-WebVirtualDirectory + + + Set-TargetResource -Website $mockSite.Website ` + -WebApplication $mockSite.WebApplication ` + -Name $mockSite.Name ` + -PhysicalPath $mockSite.PhysicalPath ` + -Credential $mockCred ` + -Ensure 'Present' + + Assert-MockCalled -CommandName New-WebVirtualDirectory -Exactly 1 + Assert-MockCalled -CommandName Set-ItemProperty -Exactly 2 + } + } + Context 'Ensure = Present and WebApplication = ''/''' { # Issue #366 It 'Should change WebApplication to ''''' { @@ -237,6 +359,64 @@ try } } + Context 'Ensure = Present and virtual directory exists and Credential provided' { + It 'Should call Set-ItemProperty' { + $mockSite = @{ + Name = 'SomeName' + Website = 'Website' + WebApplication = 'Application' + PhysicalPath = 'PhysicalPath' + Count = 1 + } + + $mockUsername = "SomeUsername" + $mockPassword = "SomePassword" + $passwordSecureString = $mockPassword | ConvertTo-SecureString -AsPlainText -Force + $mockCred = New-Object System.Management.Automation.PSCredential($mockUsername, $passwordSecureString) + + Mock -CommandName Get-WebVirtualDirectory -MockWith { return $mockSite } + Mock -CommandName Set-ItemProperty + + Set-TargetResource -Website $mockSite.Website ` + -WebApplication $mockSite.WebApplication ` + -Name $mockSite.Name ` + -PhysicalPath $mockSite.PhysicalPath ` + -Credential $mockCred ` + -Ensure 'Present' + + Assert-MockCalled -CommandName Set-ItemProperty -Exactly 3 + } + } + + Context 'Ensure = Present and virtual directory exists with Credential provided and no WebApplication and UncPhysicalPath' { + It 'Should call Set-ItemProperty' { + $mockSite = @{ + Name = 'SomeName' + Website = 'Website' + WebApplication = '' + PhysicalPath = '\\UncPhysicalPath' + Count = 1 + } + + $mockUsername = "SomeUsername" + $mockPassword = "SomePassword" + $passwordSecureString = $mockPassword | ConvertTo-SecureString -AsPlainText -Force + $mockCred = New-Object System.Management.Automation.PSCredential($mockUsername, $passwordSecureString) + + Mock -CommandName Get-WebVirtualDirectory -MockWith { return $mockSite } + Mock -CommandName Set-ItemProperty + + Set-TargetResource -Website $mockSite.Website ` + -WebApplication $mockSite.WebApplication ` + -Name $mockSite.Name ` + -PhysicalPath $mockSite.PhysicalPath ` + -Credential $mockCred ` + -Ensure 'Present' + + Assert-MockCalled -CommandName Set-ItemProperty -Exactly 3 + } + } + Context 'Ensure = Absent' { It 'Should call Remove-WebVirtualDirectory' { $mockSite = @{