From 84bae9db4e14d6a0f727b5ef89a9e7a35404c34d Mon Sep 17 00:00:00 2001 From: Nik Charlebois Date: Fri, 25 Oct 2024 10:32:58 -0400 Subject: [PATCH] AzureBillingAccountsAssociatedTenant - Initial Release --- CHANGELOG.md | 2 + ..._AzureBillingAccountsAssociatedTenant.psm1 | 435 ++++++++++++++++++ ...BillingAccountsAssociatedTenant.schema.mof | 17 + .../readme.md | 6 + .../settings.json | 20 + .../1-Create.ps1 | 37 ++ .../2-Update.ps1 | 37 ++ .../3-Remove.ps1 | 37 ++ Modules/Microsoft365DSC/Microsoft365DSC.psd1 | 1 + .../WorkloadHelpers/M365DSCAzureHelper.psm1 | 71 +++ ...eBillingAccountsAssociatedTenant.Tests.ps1 | 240 ++++++++++ 11 files changed, 903 insertions(+) create mode 100644 Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountsAssociatedTenant/MSFT_AzureBillingAccountsAssociatedTenant.psm1 create mode 100644 Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountsAssociatedTenant/MSFT_AzureBillingAccountsAssociatedTenant.schema.mof create mode 100644 Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountsAssociatedTenant/readme.md create mode 100644 Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountsAssociatedTenant/settings.json create mode 100644 Modules/Microsoft365DSC/Examples/Resources/AzureBillingAccountsAssociatedTenant/1-Create.ps1 create mode 100644 Modules/Microsoft365DSC/Examples/Resources/AzureBillingAccountsAssociatedTenant/2-Update.ps1 create mode 100644 Modules/Microsoft365DSC/Examples/Resources/AzureBillingAccountsAssociatedTenant/3-Remove.ps1 create mode 100644 Modules/Microsoft365DSC/Modules/WorkloadHelpers/M365DSCAzureHelper.psm1 create mode 100644 Tests/Unit/Microsoft365DSC/Microsoft365DSC.AzureBillingAccountsAssociatedTenant.Tests.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 16a833aab1..2df5390baf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ * Initial release. * AADSocialIdentityProvider * Fixed missing permissions in settings.json +* AzureBillingAccountsAssociatedTenant + * Initial release. * EXOMailboxAuditBypassAssociation * Initial release. * EXOTenantAllowBlockListItems diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountsAssociatedTenant/MSFT_AzureBillingAccountsAssociatedTenant.psm1 b/Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountsAssociatedTenant/MSFT_AzureBillingAccountsAssociatedTenant.psm1 new file mode 100644 index 0000000000..7b74f0d20b --- /dev/null +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountsAssociatedTenant/MSFT_AzureBillingAccountsAssociatedTenant.psm1 @@ -0,0 +1,435 @@ +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DisplayName, + + [Parameter(Mandatory = $true)] + [System.String] + $BillingAccount, + + [Parameter(Mandatory = $true)] + [System.String] + $AssociatedTenantId, + + [Parameter()] + [System.String] + $BillingManagementState, + + [Parameter()] + [System.String] + $ProvisioningManagementState, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter()] + [System.String] + $ApplicationId, + + [Parameter()] + [System.String] + $TenantId, + + [Parameter()] + [System.String] + $CertificateThumbprint, + + [Parameter()] + [Switch] + $ManagedIdentity, + + [Parameter()] + [System.String[]] + $AccessTokens + ) + + New-M365DSCConnection -Workload 'Azure' ` + -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 + { + $accounts = Get-M365DSCAzureBillingAccount + $currentAccount = $accounts.value | Where-Object -FilterScript {$_.properties.displayName -eq $BillingAccount} + + if ($null -ne $currentAccount) + { + $instances = Get-M365DSCAzureBillingAccountsAssociatedTenant -BillingAccountId $currentAccount.Name -ErrorAction Stop + $instance = $instances.value | Where-Object -FilterScript {$_.properties.displayName -eq $DisplayName} + } + if ($null -eq $instance) + { + return $nullResult + } + + $results = @{ + BillingAccount = $BillingAccount + DisplayName = $DisplayName + AssociatedTenantId = $instance.properties.tenantId + BillingManagementState = $instance.properties.billingManagementState + ProvisioningManagementState = $instance.properties.provisioningManagementState + Ensure = 'Present' + Credential = $Credential + ApplicationId = $ApplicationId + TenantId = $TenantId + CertificateThumbprint = $CertificateThumbprint + ManagedIdentity = $ManagedIdentity.IsPresent + AccessTokens = $AccessTokens + } + return [System.Collections.Hashtable] $results + } + catch + { + Write-Verbose -Message $_ + 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] + $DisplayName, + + [Parameter(Mandatory = $true)] + [System.String] + $BillingAccount, + + [Parameter(Mandatory = $true)] + [System.String] + $AssociatedTenantId, + + [Parameter()] + [System.String] + $BillingManagementState, + + [Parameter()] + [System.String] + $ProvisioningManagementState, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter()] + [System.String] + $ApplicationId, + + [Parameter()] + [System.String] + $TenantId, + + [Parameter()] + [System.String] + $CertificateThumbprint, + + [Parameter()] + [Switch] + $ManagedIdentity, + + [Parameter()] + [System.String[]] + $AccessTokens + ) + + #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 + $billingAccounts = Get-M365DSCAzureBillingAccount + $account = $billingAccounts.value | Where-Object -FilterScript {$_.properties.displayName -eq $BillingAccount} + + $instanceParams = @{ + properties = @{ + displayName = $DisplayName + tenantId = $AssociatedTenantId + billingManagementState = $BillingManagementState + provisioningManagementState = $ProvisioningManagementState + } + } + # CREATE + if ($Ensure -eq 'Present' -and $currentInstance.Ensure -eq 'Absent') + { + Write-Verbose -Message "Adding associated tenant {$AssociatedTenantId}" + New-M365DSCAzureBillingAccountsAssociatedTenant -BillingAccountId $account.Name ` + -AssociatedTenantId $AssociatedTenantId ` + -Body $instanceParams + } + # UPDATE + elseif ($Ensure -eq 'Present' -and $currentInstance.Ensure -eq 'Present') + { + Write-Verbose -Message "Updating associated tenant {$AssociatedTenantId}" + New-M365DSCAzureBillingAccountsAssociatedTenant -BillingAccountId $account.Name ` + -AssociatedTenantId $AssociatedTenantId ` + -Body $instanceParams + } + # REMOVE + elseif ($Ensure -eq 'Absent' -and $currentInstance.Ensure -eq 'Present') + { + Write-Verbose -Message "Removing associated tenant {$AssociatedTenantId}" + Remove-M365DSCAzureBillingAccountsAssociatedTenant -BillingAccountId $account.Name ` + -AssociatedTenantId $AssociatedTenantId + } +} + +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $DisplayName, + + [Parameter(Mandatory = $true)] + [System.String] + $BillingAccount, + + [Parameter(Mandatory = $true)] + [System.String] + $AssociatedTenantId, + + [Parameter()] + [System.String] + $BillingManagementState, + + [Parameter()] + [System.String] + $ProvisioningManagementState, + + [Parameter()] + [ValidateSet('Present', 'Absent')] + [System.String] + $Ensure = 'Present', + + [Parameter()] + [System.Management.Automation.PSCredential] + $Credential, + + [Parameter()] + [System.String] + $ApplicationId, + + [Parameter()] + [System.String] + $TenantId, + + [Parameter()] + [System.String] + $CertificateThumbprint, + + [Parameter()] + [Switch] + $ManagedIdentity, + + [Parameter()] + [System.String[]] + $AccessTokens + ) + + #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 + + $CurrentValues = Get-TargetResource @PSBoundParameters + $ValuesToCheck = ([Hashtable]$PSBoundParameters).Clone() + + Write-Verbose -Message "Current Values: $(Convert-M365DscHashtableToString -Hashtable $CurrentValues)" + Write-Verbose -Message "Target Values: $(Convert-M365DscHashtableToString -Hashtable $ValuesToCheck)" + + $testResult = Test-M365DSCParameterState -CurrentValues $CurrentValues ` + -Source $($MyInvocation.MyCommand.Source) ` + -DesiredValues $PSBoundParameters ` + -ValuesToCheck $ValuesToCheck.Keys + + 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, + + [Parameter()] + [System.String[]] + $AccessTokens + ) + + $ConnectionMode = New-M365DSCConnection -Workload 'Azure' ` + -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 + { + $Script:ExportMode = $true + + #Get all billing account + $accounts = Get-M365DSCAzureBillingAccount + + $i = 1 + $dscContent = '' + if ($Script:exportedInstances.Length -eq 0) + { + Write-Host $Global:M365DSCEmojiGreenCheckMark + } + else + { + Write-Host "`r`n" -NoNewline + } + [array] $Script:exportedInstances = @() + foreach ($config in $accounts.value) + { + $displayedKey = $config.properties.displayName + Write-Host " |---[$i/$($accounts.Count)] $displayedKey" + + $associatedTenants += Get-M365DSCAzureBillingAccountsAssociatedTenant -BillingAccountId $config.name + + $j = 1 + foreach ($associatedTenant in $associatedTenants.value) + { + if ($null -ne $Global:M365DSCExportResourceInstancesCount) + { + $Global:M365DSCExportResourceInstancesCount++ + } + Write-Host " |---[$j/$($associatedTenants.value.Length)] $($associatedTenant.properties.DisplayName)" -NoNewline + $params = @{ + BillingAccount = $config.properties.displayName + DisplayName = $associatedTenant.properties.displayName + AssociatedTenantId = $associatedTenant.properties.tenantId + Credential = $Credential + ApplicationId = $ApplicationId + TenantId = $TenantId + CertificateThumbprint = $CertificateThumbprint + ManagedIdentity = $ManagedIdentity.IsPresent + AccessTokens = $AccessTokens + } + + $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 + $j++ + Write-Host $Global:M365DSCEmojiGreenCheckMark + } + $i++ + } + 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_AzureBillingAccountsAssociatedTenant/MSFT_AzureBillingAccountsAssociatedTenant.schema.mof b/Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountsAssociatedTenant/MSFT_AzureBillingAccountsAssociatedTenant.schema.mof new file mode 100644 index 0000000000..d5a700ae3c --- /dev/null +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountsAssociatedTenant/MSFT_AzureBillingAccountsAssociatedTenant.schema.mof @@ -0,0 +1,17 @@ +[ClassVersion("1.0.0.0"), FriendlyName("AzureBillingAccountsAssociatedTenant")] +class MSFT_AzureBillingAccountsAssociatedTenant : OMI_BaseResource +{ + [Key, Description("The ID that uniquely identifies a tenant.")] String AssociatedTenantId; + [Write, Description("The name of the associated tenant.")] String DisplayName; + [Write, Description("Name of the billing account.")] String BillingAccount; + [Write, Description("The state determines whether users from the associated tenant can be assigned roles for commerce activities like viewing and downloading invoices, managing payments, and making purchases.")] String BillingManagementState; + [Write, Description("The state determines whether subscriptions and licenses can be provisioned in the associated tenant. It can be set to 'Pending' to initiate a billing request.")] String ProvisioningManagementState; + + [Write, Description("Present ensures the instance exists, absent ensures it is removed."), ValueMap{"Absent","Present"}, Values{"Absent","Present"}] string Ensure; + [Write, Description("Credentials of the workload's 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("Managed ID being used for authentication.")] Boolean ManagedIdentity; + [Write, Description("Access token used for authentication.")] String AccessTokens[]; +}; diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountsAssociatedTenant/readme.md b/Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountsAssociatedTenant/readme.md new file mode 100644 index 0000000000..c1162d9567 --- /dev/null +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountsAssociatedTenant/readme.md @@ -0,0 +1,6 @@ + +# AzureBillingAccountsAssociatedTenant + +## Description + +Configures associated tenants to billing accounts in the Microsoft Admin Center. diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountsAssociatedTenant/settings.json b/Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountsAssociatedTenant/settings.json new file mode 100644 index 0000000000..0b91a4be2d --- /dev/null +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountsAssociatedTenant/settings.json @@ -0,0 +1,20 @@ +{ + "resourceName": "AzureBillingAccountsAssociatedTenant", + "description": "Configures associated tenants to billing accounts in the Microsoft Admin Center.", + "roles": { + "read": [], + "update": [] + }, + "permissions": { + "graph": { + "delegated": { + "read": [], + "update": [] + }, + "application": { + "read": [], + "update": [] + } + } + } +} diff --git a/Modules/Microsoft365DSC/Examples/Resources/AzureBillingAccountsAssociatedTenant/1-Create.ps1 b/Modules/Microsoft365DSC/Examples/Resources/AzureBillingAccountsAssociatedTenant/1-Create.ps1 new file mode 100644 index 0000000000..96f3448026 --- /dev/null +++ b/Modules/Microsoft365DSC/Examples/Resources/AzureBillingAccountsAssociatedTenant/1-Create.ps1 @@ -0,0 +1,37 @@ +<# +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 + { + AzureBillingAccountsAssociatedTenant "AzureBillingAccountsAssociatedTenantIntegration Tenant" + { + ApplicationId = $ApplicationId; + AssociatedTenantId = "7a575036-2dac-4713-8e23-2963cc2c5f37"; + BillingAccount = "My Test Account"; + BillingManagementState = "Active"; + CertificateThumbprint = $CertificateThumbprint; + DisplayName = "Integration Tenant"; + Ensure = "Present"; + ProvisioningManagementState = "Pending"; + TenantId = $TenantId; + } + } +} diff --git a/Modules/Microsoft365DSC/Examples/Resources/AzureBillingAccountsAssociatedTenant/2-Update.ps1 b/Modules/Microsoft365DSC/Examples/Resources/AzureBillingAccountsAssociatedTenant/2-Update.ps1 new file mode 100644 index 0000000000..eba2a4ebfb --- /dev/null +++ b/Modules/Microsoft365DSC/Examples/Resources/AzureBillingAccountsAssociatedTenant/2-Update.ps1 @@ -0,0 +1,37 @@ +<# +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 + { + AzureBillingAccountsAssociatedTenant "AzureBillingAccountsAssociatedTenantIntegration Tenant" + { + ApplicationId = $ApplicationId; + AssociatedTenantId = "7a575036-2dac-4713-8e23-2963cc2c5f37"; + BillingAccount = "My Test Account"; + BillingManagementState = "NotAllowed"; # Drift + CertificateThumbprint = $CertificateThumbprint; + DisplayName = "Integration Tenant"; + Ensure = "Present"; + ProvisioningManagementState = "Pending"; + TenantId = $TenantId; + } + } +} diff --git a/Modules/Microsoft365DSC/Examples/Resources/AzureBillingAccountsAssociatedTenant/3-Remove.ps1 b/Modules/Microsoft365DSC/Examples/Resources/AzureBillingAccountsAssociatedTenant/3-Remove.ps1 new file mode 100644 index 0000000000..cb1d041e05 --- /dev/null +++ b/Modules/Microsoft365DSC/Examples/Resources/AzureBillingAccountsAssociatedTenant/3-Remove.ps1 @@ -0,0 +1,37 @@ +<# +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 + { + AzureBillingAccountsAssociatedTenant "AzureBillingAccountsAssociatedTenantIntegration Tenant" + { + ApplicationId = $ApplicationId; + AssociatedTenantId = "7a575036-2dac-4713-8e23-2963cc2c5f37"; + BillingAccount = "My Test Account"; + BillingManagementState = "Active"; + CertificateThumbprint = $CertificateThumbprint; + DisplayName = "Integration Tenant"; + Ensure = "Absent"; + ProvisioningManagementState = "Pending"; + TenantId = $TenantId; + } + } +} diff --git a/Modules/Microsoft365DSC/Microsoft365DSC.psd1 b/Modules/Microsoft365DSC/Microsoft365DSC.psd1 index db41671f0b..c0ed86dc78 100644 --- a/Modules/Microsoft365DSC/Microsoft365DSC.psd1 +++ b/Modules/Microsoft365DSC/Microsoft365DSC.psd1 @@ -79,6 +79,7 @@ 'Modules/M365DSCDRGUtil.psm1', 'Modules/EncodingHelpers/M365DSCEmojis.psm1', 'Modules/EncodingHelpers/M365DSCStringEncoding.psm1', + 'Modules/WorkloadHelpers/M365DSCAzureHelper.psm1', 'Modules/WorkloadHelpers/M365DSCAzureDevOPSHelper.psm1', 'Modules/WorkloadHelpers/M365DSCFabricHelper.psm1', 'Modules/M365DSCConfigurationHelper.psm1' diff --git a/Modules/Microsoft365DSC/Modules/WorkloadHelpers/M365DSCAzureHelper.psm1 b/Modules/Microsoft365DSC/Modules/WorkloadHelpers/M365DSCAzureHelper.psm1 new file mode 100644 index 0000000000..a8afa9fa69 --- /dev/null +++ b/Modules/Microsoft365DSC/Modules/WorkloadHelpers/M365DSCAzureHelper.psm1 @@ -0,0 +1,71 @@ +function Get-M365DSCAzureBillingAccount +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param() + + $uri = 'https://management.azure.com/providers/Microsoft.Billing/billingAccounts?api-version=2022-10-01-privatepreview&?includeAll=true' + $response = Invoke-AzRest -Method GET -Uri $uri + $result = ConvertFrom-Json $response.Content + return $result +} + +function Get-M365DSCAzureBillingAccountsAssociatedTenant +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param( + [Parameter(Mandatory = $true)] + [System.String] + $BillingAccountId + ) + + $uri = "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$($BillingAccountId)/associatedTenants?api-version=2024-04-01" + $response = Invoke-AzRest -Method GET -Uri $uri + $result = ConvertFrom-Json $response.Content + return $result +} + +function Remove-M365DSCAzureBillingAccountsAssociatedTenant +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param( + [Parameter(Mandatory = $true)] + [System.String] + $BillingAccountId, + + [Parameter(Mandatory = $true)] + [System.String] + $AssociatedTenantId + ) + + $uri = "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$($BillingAccountId)/associatedTenants/$($AssociatedTenantId)?api-version=2024-04-01" + $response = Invoke-AzRest -Method DELETE -Uri $uri + $result = ConvertFrom-Json $response.Content + return $result +} +function New-M365DSCAzureBillingAccountsAssociatedTenant +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param( + [Parameter(Mandatory = $true)] + [System.String] + $BillingAccountId, + + [Parameter(Mandatory = $true)] + [System.String] + $AssociatedTenantId, + + [Parameter(Mandatory = $true)] + [System.Collections.Hashtable] + $Body + ) + + $uri = "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$($BillingAccountId)/associatedTenants/$($AssociatedTenantId)?api-version=2024-04-01" + $payload = ConvertTo-Json $body -Depth 10 -Compress + $response = Invoke-AzRest -Method PUT -Uri $uri -Payload $payload + $result = ConvertFrom-Json $response.Content + return $result +} diff --git a/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AzureBillingAccountsAssociatedTenant.Tests.ps1 b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AzureBillingAccountsAssociatedTenant.Tests.ps1 new file mode 100644 index 0000000000..22fc20623c --- /dev/null +++ b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AzureBillingAccountsAssociatedTenant.Tests.ps1 @@ -0,0 +1,240 @@ +[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) + +$CurrentScriptPath = $PSCommandPath.Split('\') +$CurrentScriptName = $CurrentScriptPath[$CurrentScriptPath.Length -1] +$ResourceName = $CurrentScriptName.Split('.')[1] +$Global:DscHelper = New-M365DscUnitTestHelper -StubModule $CmdletModule ` + -DscResource $ResourceName -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 New-M365DSCConnection -MockWith { + return "Credentials" + } + + Mock -CommandName New-M365DSCAzureBillingAccountsAssociatedTenant -MockWith { + } + + Mock -CommandName Remove-M365DSCAzureBillingAccountsAssociatedTenant -MockWith { + + } + + Mock -CommandName Get-M365DSCAzureBillingAccount -MockWith { + return @{ + value = @( + @{ + name = "12345-12345-12345-12345-12345" + properties = @{ + displayName = 'MyBillingAccount' + } + } + ) + } + } + + # 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 instance should exist but it DOES NOT" -Fixture { + BeforeAll { + $testParams = @{ + AssociatedTenantId = "7a575036-2dac-4713-8e23-2963cc2c5f37"; + BillingAccount = "MyBillingAccount"; + BillingManagementState = "Active"; + DisplayName = "Test Tenant"; + ProvisioningManagementState = "Pending"; + Ensure = 'Present' + Credential = $Credential; + } + + Mock -CommandName Get-M365DSCAzureBillingAccountsAssociatedTenant -MockWith { + return $null + } + } + 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 create a new instance from the Set method' { + Set-TargetResource @testParams + Should -Invoke -CommandName New-M365DSCAzureBillingAccountsAssociatedTenant -Exactly 1 + } + } + + Context -Name "The instance exists but it SHOULD NOT" -Fixture { + BeforeAll { + $testParams = @{ + AssociatedTenantId = "7a575036-2dac-4713-8e23-2963cc2c5f37"; + BillingAccount = "MyBillingAccount"; + BillingManagementState = "Active"; + DisplayName = "Test Tenant"; + ProvisioningManagementState = "Pending"; + Ensure = 'Absent' + Credential = $Credential; + } + + Mock -CommandName Get-M365DSCAzureBillingAccountsAssociatedTenant -MockWith { + return @{ + value = @( + @{ + properties = @{ + billingManagementState = 'Active' + tenantId = '7a575036-2dac-4713-8e23-2963cc2c5f37' + displayName = 'Test Tenant' + provisioningManagementState = 'Pending' + } + } + ) + } + } + } + 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 remove the instance from the Set method' { + Set-TargetResource @testParams + Should -Invoke -CommandName Remove-M365DSCAzureBillingAccountsAssociatedTenant -Exactly 1 + } + } + + Context -Name "The instance exists and values are already in the desired state" -Fixture { + BeforeAll { + $testParams = @{ + AssociatedTenantId = "7a575036-2dac-4713-8e23-2963cc2c5f37"; + BillingAccount = "MyBillingAccount"; + BillingManagementState = "Active"; + DisplayName = "Test Tenant"; + ProvisioningManagementState = "Pending"; + Ensure = 'Present' + Credential = $Credential; + } + + Mock -CommandName Get-M365DSCAzureBillingAccountsAssociatedTenant -MockWith { + return @{ + value = @( + @{ + properties = @{ + billingManagementState = 'Active' + tenantId = '7a575036-2dac-4713-8e23-2963cc2c5f37' + displayName = 'Test Tenant' + provisioningManagementState = 'Pending' + } + } + ) + } + } + } + + It 'Should return true from the Test method' { + Test-TargetResource @testParams | Should -Be $true + } + } + + Context -Name "The instance exists and values are NOT in the desired state" -Fixture { + BeforeAll { + $testParams = @{ + AssociatedTenantId = "7a575036-2dac-4713-8e23-2963cc2c5f37"; + BillingAccount = "MyBillingAccount"; + BillingManagementState = "Not Allowed"; #Drift + DisplayName = "Test Tenant"; + ProvisioningManagementState = "Pending"; + Ensure = 'Present' + Credential = $Credential; + } + + Mock -CommandName Get-M365DSCAzureBillingAccountsAssociatedTenant -MockWith { + return @{ + value = @( + @{ + properties = @{ + billingManagementState = 'Active' + tenantId = '7a575036-2dac-4713-8e23-2963cc2c5f37' + displayName = 'Test Tenant' + provisioningManagementState = 'Pending' + } + } + ) + } + } + } + + 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 call the Set method' { + Set-TargetResource @testParams + Should -Invoke -CommandName New-M365DSCAzureBillingAccountsAssociatedTenant -Exactly 1 + } + } + + Context -Name 'ReverseDSC Tests' -Fixture { + BeforeAll { + $Global:CurrentModeIsExport = $true + $Global:PartialExportFileName = "$(New-Guid).partial.ps1" + $testParams = @{ + Credential = $Credential; + } + + Mock -CommandName Get-M365DSCAzureBillingAccountsAssociatedTenant -MockWith { + return @{ + value = @( + @{ + properties = @{ + billingManagementState = 'Active' + tenantId = '7a575036-2dac-4713-8e23-2963cc2c5f37' + displayName = 'Test Tenant' + provisioningManagementState = 'Pending' + } + } + ) + } + } + } + It 'Should Reverse Engineer resource from the Export method' { + $result = Export-TargetResource @testParams + $result | Should -Not -BeNullOrEmpty + } + } + } +} + +Invoke-Command -ScriptBlock $Global:DscHelper.CleanupScript -NoNewScope