diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_EXOSmtpDaneInbound/MSFT_EXOSmtpDaneInbound.psm1 b/Modules/Microsoft365DSC/DSCResources/MSFT_EXOSmtpDaneInbound/MSFT_EXOSmtpDaneInbound.psm1 new file mode 100644 index 0000000000..4f9f1b7a28 --- /dev/null +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_EXOSmtpDaneInbound/MSFT_EXOSmtpDaneInbound.psm1 @@ -0,0 +1,343 @@ +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure, + + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter()] + [System.String] + $ApplicationId, + + [Parameter()] + [System.String] + $TenantId, + + [Parameter()] + [System.String] + $CertificateThumbprint, + + [Parameter()] + [System.Management.Automation.PSCredential] + $ApplicationSecret + ) + + New-M365DSCConnection -Workload 'ExchangeOnline' ` + -InboundParameters $PSBoundParameters | Out-Null + + #Ensure the proper dependencies are installed in the current environment. + Confirm-M365DSCDependencies + + #region Telemetry + $ResourceName = $MyInvocation.MyCommand.ModuleName.Replace('MSFT_', '') + $CommandName = $MyInvocation.MyCommand + $data = Format-M365DSCTelemetryParameters -ResourceName $ResourceName ` + -CommandName $CommandName ` + -Parameters $PSBoundParameters + Add-M365DSCTelemetryEvent -Data $data + #endregion + + $nullResult = $PSBoundParameters + $nullResult.Ensure = 'Absent' + try + { + $instance = Get-AcceptedDomain -Identity $DomainName -ErrorAction SilentlyContinue + if ($null -eq $instance -or $instance.SmtpDaneStatus -ne 'Enabled') + { + return $nullResult + } + + Write-Verbose -Message "Found an instance with DomainName {$DomainName}" + $results = @{ + DomainName = $instance.DomainName + Ensure = 'Present' + Credential = $Credential + ApplicationId = $ApplicationId + TenantId = $TenantId + CertificateThumbprint = $CertificateThumbprint + ApplicationSecret = $ApplicationSecret + } + return [System.Collections.Hashtable] $results + } + catch + { + New-M365DSCLogEntry -Message 'Error retrieving data:' ` + -Exception $_ ` + -Source $($MyInvocation.MyCommand.Source) ` + -TenantId $TenantId ` + -Credential $Credential + + return $nullResult + } +} + +function Set-TargetResource +{ + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure, + + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter()] + [System.String] + $ApplicationId, + + [Parameter()] + [System.String] + $TenantId, + + [Parameter()] + [System.String] + $CertificateThumbprint, + + [Parameter()] + [System.Management.Automation.PSCredential] + $ApplicationSecret + ) + + New-M365DSCConnection -Workload 'ExchangeOnline' ` + -InboundParameters $PSBoundParameters | Out-Null + + #Ensure the proper dependencies are installed in the current environment. + Confirm-M365DSCDependencies + + #region Telemetry + $ResourceName = $MyInvocation.MyCommand.ModuleName.Replace('MSFT_', '') + $CommandName = $MyInvocation.MyCommand + $data = Format-M365DSCTelemetryParameters -ResourceName $ResourceName ` + -CommandName $CommandName ` + -Parameters $PSBoundParameters + Add-M365DSCTelemetryEvent -Data $data + #endregion + + $currentInstance = Get-TargetResource @PSBoundParameters + + if ($Ensure -eq 'Present' -and $currentInstance.Ensure -eq 'Absent') + { + Write-Verbose -Message "Enabling SmtpDaneInbound for {$DomainName}" + try { + Enable-SmtpDaneInbound -DomainName $DomainName -ErrorAction Stop | Out-Null + } + catch { + write-verbose "Cannot enable SmtpDaneInbound for DomainName $DomainName - check that DNSSEC is enabled" + New-M365DSCLogEntry -Message "Error enabling SmtpDaneInbound for DomainName '$DomainName'" ` + -Exception $_ ` + -Source $($MyInvocation.MyCommand.Source) ` + -TenantId $TenantId ` + -Credential $Credential + } + } + elseif ($Ensure -eq 'Absent' -and $currentInstance.Ensure -eq 'Present') + { + Write-Verbose -Message "Disabling SmtpDaneInbound for {$DomainName}" + Disable-SmtpDaneInbound -DomainName $currentInstance.DomainName + } +} + +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DomainName, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure, + + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter()] + [System.String] + $ApplicationId, + + [Parameter()] + [System.String] + $TenantId, + + [Parameter()] + [System.String] + $CertificateThumbprint, + + [Parameter()] + [System.Management.Automation.PSCredential] + $ApplicationSecret + ) + + #Ensure the proper dependencies are installed in the current environment. + Confirm-M365DSCDependencies + + #region Telemetry + $ResourceName = $MyInvocation.MyCommand.ModuleName.Replace('MSFT_', '') + $CommandName = $MyInvocation.MyCommand + $data = Format-M365DSCTelemetryParameters -ResourceName $ResourceName ` + -CommandName $CommandName ` + -Parameters $PSBoundParameters + Add-M365DSCTelemetryEvent -Data $data + #endregion + + Write-Verbose -Message "Testing configuration of {$DomainName}" + + $CurrentValues = Get-TargetResource @PSBoundParameters + + if ($CurrentValues.Ensure -ne $Ensure) + { + Write-Verbose -Message "Test-TargetResource returned $false" + return $false + } + + Write-Verbose -Message "Current Values: DomainName=$($currentValue.DomainName), Ensure=$($currentValues.Ensure)" + Write-Verbose -Message "Target Values: DomainName=$DomainName, Ensure=$Ensure" + + $testResult = $true + + Write-Verbose -Message "Test-TargetResource returned $testResult" + + return $testResult +} + +function Export-TargetResource +{ + [CmdletBinding()] + [OutputType([System.String])] + param + ( + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter()] + [System.String] + $ApplicationId, + + [Parameter()] + [System.String] + $TenantId, + + [Parameter()] + [System.Management.Automation.PSCredential] + $ApplicationSecret, + + [Parameter()] + [System.String] + $CertificateThumbprint, + + [Parameter()] + [Switch] + $ManagedIdentity + ) + + $ConnectionMode = New-M365DSCConnection -Workload 'ExchangeOnline' ` + -InboundParameters $PSBoundParameters + + #Ensure the proper dependencies are installed in the current environment. + Confirm-M365DSCDependencies + + #region Telemetry + $ResourceName = $MyInvocation.MyCommand.ModuleName.Replace('MSFT_', '') + $CommandName = $MyInvocation.MyCommand + $data = Format-M365DSCTelemetryParameters -ResourceName $ResourceName ` + -CommandName $CommandName ` + -Parameters $PSBoundParameters + Add-M365DSCTelemetryEvent -Data $data + #endregion + + try + { + [array]$getValue = Get-AcceptedDomain -ResultSize Unlimited -ErrorAction Stop + + $i = 1 + $dscContent = '' + if ($getValue.Length -eq 0) + { + Write-Host $Global:M365DSCEmojiGreenCheckMark + } + else + { + Write-Host "`r`n" -NoNewline + } + foreach ($config in $getValue) + { + if ($null -ne $Global:M365DSCExportResourceInstancesCount) + { + $Global:M365DSCExportResourceInstancesCount++ + } + + $displayedKey = $config.DomainName + if (-not [String]::IsNullOrEmpty($config.displayName)) + { + $displayedKey = $config.displayName + } + Write-Host " |---[$i/$($getValue.Count)] $displayedKey" -NoNewline + $params = @{ + DomainName = $config.DomainName + Ensure = 'Present' + Credential = $Credential + ApplicationId = $ApplicationId + TenantId = $TenantId + CertificateThumbprint = $CertificateThumbprint + ApplicationSecret = $ApplicationSecret + + } + + $Results = Get-TargetResource @Params + $Results = Update-M365DSCExportAuthenticationResults -ConnectionMode $ConnectionMode ` + -Results $Results + + $currentDSCBlock = Get-M365DSCExportContentForResource -ResourceName $ResourceName ` + -ConnectionMode $ConnectionMode ` + -ModulePath $PSScriptRoot ` + -Results $Results ` + -Credential $Credential + $dscContent += $currentDSCBlock + Save-M365DSCPartialExport -Content $currentDSCBlock ` + -FileName $Global:PartialExportFileName + $i++ + Write-Host $Global:M365DSCEmojiGreenCheckMark + } + return $dscContent + } + catch + { + Write-Host $Global:M365DSCEmojiRedX + + New-M365DSCLogEntry -Message 'Error during Export:' ` + -Exception $_ ` + -Source $($MyInvocation.MyCommand.Source) ` + -TenantId $TenantId ` + -Credential $Credential + + return '' + } +} + +Export-ModuleMember -Function *-TargetResource diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_EXOSmtpDaneInbound/MSFT_EXOSmtpDaneInbound.schema.mof b/Modules/Microsoft365DSC/DSCResources/MSFT_EXOSmtpDaneInbound/MSFT_EXOSmtpDaneInbound.schema.mof new file mode 100644 index 0000000000..56c6a39e6c --- /dev/null +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_EXOSmtpDaneInbound/MSFT_EXOSmtpDaneInbound.schema.mof @@ -0,0 +1,14 @@ +[ClassVersion("1.0.0.0"), FriendlyName("EXOSmtpDaneInbound")] +class MSFT_EXOSmtpDaneInbound : OMI_BaseResource +{ + [Key, Description("Specifies the accepted domain in the Exchange Online organization where you want to enable SMTP DANE")] String DomainName; + [Write, Description("Present ensures SmtpDaneInbound is enabled, absent ensures it is disabled."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] string Ensure; + [Write, Description("Credentials of the Exchange Global Admin"), EmbeddedInstance("MSFT_Credential")] string Credential; + [Write, Description("Id of the Azure Active Directory application to authenticate with.")] String ApplicationId; + [Write, Description("Id of the Azure Active Directory tenant used for authentication.")] String TenantId; + [Write, Description("Thumbprint of the Azure Active Directory application's authentication certificate to use for authentication.")] String CertificateThumbprint; + [Write, Description("Username can be made up to anything but password will be used for CertificatePassword"), EmbeddedInstance("MSFT_Credential")] String CertificatePassword; + [Write, Description("Path to certificate used in service principal usually a PFX file.")] String CertificatePath; + [Write, Description("Managed ID being used for authentication.")] Boolean ManagedIdentity; + [Write, Description("Access token used for authentication.")] String AccessTokens[]; +}; diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_EXOSmtpDaneInbound/readme.md b/Modules/Microsoft365DSC/DSCResources/MSFT_EXOSmtpDaneInbound/readme.md new file mode 100644 index 0000000000..ee0ddbf38c --- /dev/null +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_EXOSmtpDaneInbound/readme.md @@ -0,0 +1,10 @@ + +# EXOSmtpDaneInbound + +## Description: + +This resource configures SmtpDaneInbound for an accepted domain in Exchange Online. +Reference: https://learn.microsoft.com/en-us/powershell/module/exchange/enable-smtpdaneinbound?view=exchange-ps + + + diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_EXOSmtpDaneInbound/settings.json b/Modules/Microsoft365DSC/DSCResources/MSFT_EXOSmtpDaneInbound/settings.json new file mode 100644 index 0000000000..4b748450f9 --- /dev/null +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_EXOSmtpDaneInbound/settings.json @@ -0,0 +1,33 @@ +{ + "resourceName": "EXOSmtpDaneInbound", + "description": "This resource configures an .", + "roles": { + "read": [ + "Security Reader" + ], + "update": [ + "Security Administrator" + ] + }, + "description": "", + "permissions": { + "graph": { + "delegated": { + "read": [], + "update": [] + }, + "application": { + "read": [], + "update": [] + } + }, + "exchange": { + "requiredroles": [ + "Security Admin", + "View-Only Configuration", + "Security Reader" + ], + "requiredrolegroups": "Organization Management" + } + } +} diff --git a/Modules/Microsoft365DSC/Examples/Resources/EXOSmtpDaneInbound/1-create.ps1 b/Modules/Microsoft365DSC/Examples/Resources/EXOSmtpDaneInbound/1-create.ps1 new file mode 100644 index 0000000000..fa72ae5729 --- /dev/null +++ b/Modules/Microsoft365DSC/Examples/Resources/EXOSmtpDaneInbound/1-create.ps1 @@ -0,0 +1,34 @@ +<# +This example is used to test new resources and showcase the usage of new resources being worked on. +It is not meant to use as a production baseline. +#> + +Configuration Example +{ + param( + [Parameter()] + [System.String] + $ApplicationId, + + [Parameter()] + [System.String] + $TenantId, + + [Parameter()] + [System.String] + $CertificateThumbprint + ) + Import-DscResource -ModuleName Microsoft365DSC + + node localhost + { + EXOSmtpDaneInbound 'SmtpDaneInbound-mydomain.com' + { + DomainName = "mydomain.com" + Ensure = "Present" # note: DNSSEC for the domain must be enabled. See resource EXODnssecForVerifiedDomain + ApplicationId = $ApplicationId + TenantId = $TenantId + CertificateThumbprint = $CertificateThumbprint + } + } +} diff --git a/Modules/Microsoft365DSC/Examples/Resources/EXOSmtpDaneInbound/2-update.ps1 b/Modules/Microsoft365DSC/Examples/Resources/EXOSmtpDaneInbound/2-update.ps1 new file mode 100644 index 0000000000..fa72ae5729 --- /dev/null +++ b/Modules/Microsoft365DSC/Examples/Resources/EXOSmtpDaneInbound/2-update.ps1 @@ -0,0 +1,34 @@ +<# +This example is used to test new resources and showcase the usage of new resources being worked on. +It is not meant to use as a production baseline. +#> + +Configuration Example +{ + param( + [Parameter()] + [System.String] + $ApplicationId, + + [Parameter()] + [System.String] + $TenantId, + + [Parameter()] + [System.String] + $CertificateThumbprint + ) + Import-DscResource -ModuleName Microsoft365DSC + + node localhost + { + EXOSmtpDaneInbound 'SmtpDaneInbound-mydomain.com' + { + DomainName = "mydomain.com" + Ensure = "Present" # note: DNSSEC for the domain must be enabled. See resource EXODnssecForVerifiedDomain + ApplicationId = $ApplicationId + TenantId = $TenantId + CertificateThumbprint = $CertificateThumbprint + } + } +} diff --git a/Modules/Microsoft365DSC/Examples/Resources/EXOSmtpDaneInbound/3-remove.ps1 b/Modules/Microsoft365DSC/Examples/Resources/EXOSmtpDaneInbound/3-remove.ps1 new file mode 100644 index 0000000000..0464db4d21 --- /dev/null +++ b/Modules/Microsoft365DSC/Examples/Resources/EXOSmtpDaneInbound/3-remove.ps1 @@ -0,0 +1,34 @@ +<# +This example is used to test new resources and showcase the usage of new resources being worked on. +It is not meant to use as a production baseline. +#> + +Configuration Example +{ + param( + [Parameter()] + [System.String] + $ApplicationId, + + [Parameter()] + [System.String] + $TenantId, + + [Parameter()] + [System.String] + $CertificateThumbprint + ) + Import-DscResource -ModuleName Microsoft365DSC + + node localhost + { + EXOSmtpDaneInbound 'SmtpDaneInbound-mydomain.com' + { + DomainName = "mydomain.com" + Ensure = "Absent" + ApplicationId = $ApplicationId + TenantId = $TenantId + CertificateThumbprint = $CertificateThumbprint + } + } +} diff --git a/Tests/Unit/Microsoft365DSC/Microsoft365DSC.EXOSmtpDaneInbound.Tests.ps1 b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.EXOSmtpDaneInbound.Tests.ps1 new file mode 100644 index 0000000000..3f7ba614df --- /dev/null +++ b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.EXOSmtpDaneInbound.Tests.ps1 @@ -0,0 +1,175 @@ +[CmdletBinding()] +param( +) +$M365DSCTestFolder = Join-Path -Path $PSScriptRoot ` + -ChildPath '..\..\Unit' ` + -Resolve +$CmdletModule = (Join-Path -Path $M365DSCTestFolder ` + -ChildPath '\Stubs\Microsoft365.psm1' ` + -Resolve) +$GenericStubPath = (Join-Path -Path $M365DSCTestFolder ` + -ChildPath '\Stubs\Generic.psm1' ` + -Resolve) +Import-Module -Name (Join-Path -Path $M365DSCTestFolder ` + -ChildPath '\UnitTestHelper.psm1' ` + -Resolve) + +$Global:DscHelper = New-M365DscUnitTestHelper -StubModule $CmdletModule ` + -DscResource "EXOSmtpDaneInbound" -GenericStubModule $GenericStubPath +Describe -Name $Global:DscHelper.DescribeHeader -Fixture { + InModuleScope -ModuleName $Global:DscHelper.ModuleName -ScriptBlock { + Invoke-Command -ScriptBlock $Global:DscHelper.InitializeScript -NoNewScope + BeforeAll { + + $secpasswd = ConvertTo-SecureString (New-Guid | Out-String) -AsPlainText -Force + $Credential = New-Object System.Management.Automation.PSCredential ('tenantadmin@mydomain.com', $secpasswd) + + Mock -CommandName Confirm-M365DSCDependencies -MockWith { + } + + Mock -CommandName Get-MSCloudLoginConnectionProfile -MockWith { + } + + Mock -CommandName New-M365DSCConnection -MockWith { + return 'Credentials' + } + + Mock -CommandName Reset-MSCloudLoginConnectionProfileContext -MockWith { + } + + Mock -CommandName Get-PSSession -MockWith { + } + + Mock -CommandName Remove-PSSession -MockWith { + } + + Mock -CommandName Get-AcceptedDomain -MockWith { + } + + Mock -CommandName Enable-SmtpDaneInbound -MockWith { + } + + Mock -CommandName Disable-SmtpDaneInbound -MockWith { + } + + Mock -CommandName New-M365DSCConnection -MockWith { + return "Credentials" + } + + # Mock Write-Host to hide output during the tests + Mock -CommandName Write-Host -MockWith { + } + $Script:exportedInstances =$null + $Script:ExportMode = $false + } + + # Test contexts + Context -Name "The EXOSmtpDaneInbound should exist but it DOES NOT" -Fixture { + BeforeAll { + $testParams = @{ + DomainName = "fakedomain.com" + Ensure = "Present" + Credential = $Credential; + } + + Mock -CommandName Get-AcceptedDomain -MockWith { + return @{ + DomainName = "fakedomain.com" + SmtpDaneStatus = "Disabled" + } + } + } + It 'Should return Values from the Get method' { + (Get-TargetResource @testParams).Ensure | Should -Be 'Absent' + } + It 'Should return false from the Test method' { + Test-TargetResource @testParams | Should -Be $false + } + It 'Should Enable SmtpDaneInbound from the Set method' { + Set-TargetResource @testParams + Should -Invoke -CommandName Enable-SmtpDaneInbound -Exactly 1 + } + } + + Context -Name "The EXOSmtpDaneInbound exists but it SHOULD NOT" -Fixture { + BeforeAll { + $testParams = @{ + DomainName = "fakedomain.com" + Ensure = "Absent" + Credential = $Credential; + } + + Mock -CommandName Get-AcceptedDomain -MockWith { + return @{ + DomainName = "fakedomain.com" + SmtpDaneStatus = "Enabled" + } + } + } + + It 'Should return Values from the Get method' { + (Get-TargetResource @testParams).Ensure | Should -Be 'Present' + } + + It 'Should return false from the Test method' { + Test-TargetResource @testParams | Should -Be $false + } + + It 'Should disable SmtpDaneInbound from the Set method' { + Set-TargetResource @testParams + Should -Invoke -CommandName Disable-SmtpDaneInbound -Exactly 1 + } + } + + Context -Name "The EXOSmtpDaneInbound Exists and Values are already in the desired state" -Fixture { + BeforeAll { + $testParams = @{ + DomainName = "fakedomain.com" + Ensure = "Present" + Credential = $Credential; + } + + Mock -CommandName Get-AcceptedDomain -MockWith { + return @{ + DomainName = "fakedomain.com" + SmtpDaneStatus = "Enabled" + } + } + } + + It 'Should return true from the Test method' { + Test-TargetResource @testParams | Should -Be $true + } + } + + Context -Name 'ReverseDSC Tests' -Fixture { + BeforeAll { + $Global:CurrentModeIsExport = $true + $Global:PartialExportFileName = "$(New-Guid).partial.ps1" + $testParams = @{ + Credential = $Credential + } + + Mock -CommandName Get-AcceptedDomain -MockWith { + return @( + @{ + DomainName = "fakedomain.com" + SmtpDaneStatus = 'Disabled' + }, + @{ + DomainName = "otherfakedomain.com" + SmtpDaneStatus = 'Enabled' + } + ) + } + } + + It 'Should Reverse Engineer resource from the Export method' { + $result = Export-TargetResource @testParams + $result | Should -Not -BeNullOrEmpty + } + } + } +} + +Invoke-Command -ScriptBlock $Global:DscHelper.CleanupScript -NoNewScope diff --git a/Tests/Unit/Stubs/Generic.psm1 b/Tests/Unit/Stubs/Generic.psm1 index eedf2c58dd..7d18926ab1 100644 --- a/Tests/Unit/Stubs/Generic.psm1 +++ b/Tests/Unit/Stubs/Generic.psm1 @@ -1274,6 +1274,25 @@ function Disable-EOPProtectionPolicyRule ) } +function Enable-SmtpDaneInbound +{ + [CmdletBinding()] + Param( + [Parameter()] + [System.String] + $DomainName + ) +} + +function Disable-SmtpDaneInbound +{ + [CmdletBinding()] + Param( + [Parameter()] + [System.String] + $DomainName + ) +} #region MSCloudLoginAssistant function Get-MSCloudLoginConnectionProfile{