diff --git a/CHANGELOG.md b/CHANGELOG.md index 3457a840c9..914c0fdabf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,20 @@ # UNRELEASED +* AADConditionalAccessPolicy + * Fixed bug where a null value was passed in the request for the + excludePlatforms parameter when just values were assigned to includePlatforms, which throws an error. + * Fixed bug where a null value was passed in the request for the + sessionControl parameter when there are no session controls, which throws an error. + * Fixed bug where a null value was passed in the request for the + applicationEnforcedRestrictions parameter when value was set to false, which throws an error. * AADRoleEligibilityScheduleRequest * Adds support for custom role assignments at app scope. +* IntuneDeviceConfigurationPolicyAndroidDeviceOwner + * Fixed issue when properties `DetailedHelpText`, + `DeviceOwnerLockScreenMessage` or `ShortHelpText` were defined but the + request was not being sent correctly + FIXES [#5411](https://github.com/microsoft/Microsoft365DSC/issues/5411) * IntuneDiskEncryptionPDEPolicyWindows10 * Initial release. * IntuneFirewallRulesHyperVPolicyWindows10 @@ -23,6 +35,8 @@ * AADRoleEligibilityScheduleRequest * FIXES [#3787](https://github.com/microsoft/Microsoft365DSC/issues/3787) * FIXES [#5089](https://github.com/microsoft/Microsoft365DSC/issues/5089) +* AzureBillingAccountPolicy + * Initial release. * EXOATPBuiltInProtectionRule, EXOEOPProtectionRule * Fixed issue where empty arrays were being compared incorrectly to null strings diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADConditionalAccessPolicy/MSFT_AADConditionalAccessPolicy.psm1 b/Modules/Microsoft365DSC/DSCResources/MSFT_AADConditionalAccessPolicy/MSFT_AADConditionalAccessPolicy.psm1 index efa3b783bb..af09ab623d 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADConditionalAccessPolicy/MSFT_AADConditionalAccessPolicy.psm1 +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADConditionalAccessPolicy/MSFT_AADConditionalAccessPolicy.psm1 @@ -1485,13 +1485,11 @@ function Set-TargetResource if (-not $conditions.Contains('platforms')) { $conditions.Add('platforms', @{ - excludePlatforms = @() includePlatforms = @() }) } else { - $conditions.platforms.Add('excludePlatforms', @()) $conditions.platforms.Add('includePlatforms', @()) } Write-Verbose -Message "Set-Targetresource: IncludePlatforms: $IncludePlatforms" @@ -1504,8 +1502,11 @@ function Set-TargetResource $conditions.platforms.includePlatforms = @() + $IncludePlatforms } #no translation or conversion needed - Write-Verbose -Message "Set-Targetresource: ExcludePlatforms: $ExcludePlatforms" - $conditions.platforms.excludePlatforms = @() + $ExcludePlatforms + if (([Array]$ExcludePlatforms).Length -ne 0) + { + $conditions.platforms.Add('excludePlatforms', @()) + $conditions.platforms.excludePlatforms = @() + $ExcludePlatforms + } #no translation or conversion needed } else @@ -1729,18 +1730,16 @@ function Set-TargetResource $NewParameters.Add('grantControls', $GrantControls) } - Write-Verbose -Message 'Set-Targetresource: process session controls' - - $sessioncontrols = $null if ($ApplicationEnforcedRestrictionsIsEnabled -or $CloudAppSecurityIsEnabled -or $SignInFrequencyIsEnabled -or $PersistentBrowserIsEnabled) { + Write-Verbose -Message 'Set-Targetresource: process session controls' + $sessioncontrols = $null Write-Verbose -Message 'Set-Targetresource: create provision Session Control object' - $sessioncontrols = @{ - applicationEnforcedRestrictions = @{} - } + $sessioncontrols = @{} if ($ApplicationEnforcedRestrictionsIsEnabled -eq $true) { + $sessioncontrols.Add('applicationEnforcedRestrictions', @{}) #create and provision ApplicationEnforcedRestrictions object if used $sessioncontrols.applicationEnforcedRestrictions.Add('IsEnabled', $true) } @@ -1798,9 +1797,9 @@ function Set-TargetResource $sessioncontrols.persistentBrowser.isEnabled = $true $sessioncontrols.persistentBrowser.mode = $PersistentBrowserMode } + $NewParameters.Add('sessionControls', $sessioncontrols) + #add SessionControls to the parameter list } - $NewParameters.Add('sessionControls', $sessioncontrols) - #add SessionControls to the parameter list } Write-Host "newparameters: $($NewParameters | ConvertTo-Json -Depth 5)" diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountPolicy/MSFT_AzureBillingAccountPolicy.psm1 b/Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountPolicy/MSFT_AzureBillingAccountPolicy.psm1 new file mode 100644 index 0000000000..8b9275b11f --- /dev/null +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountPolicy/MSFT_AzureBillingAccountPolicy.psm1 @@ -0,0 +1,478 @@ +function Get-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $BillingAccount, + + [Parameter()] + [Microsoft.Management.Infrastructure.CimInstance] + $EnterpriseAgreementPolicies, + + [Parameter()] + [System.String] + $MarketplacePurchases, + + [Parameter()] + [System.String] + $ReservationPurchases, + + [Parameter()] + [System.String] + $SavingsPlanPurchases, + + [Parameter()] + [System.String] + $Name, + + [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 + { + $uri = "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$($BillingAccount)/policies/default?api-version=2024-04-01" + $response = Invoke-AzRest -Uri $uri -Method GET + $instance = (ConvertFrom-Json ($response.Content)).value + + if ($null -eq $instance) + { + return $nullResult + } + + $EnterpriseAgreementPoliciesValue = $null + if ($null -ne $EnterpriseAgreementPolicies) + { + $EnterpriseAgreementPoliciesValue = @{ + accountOwnerViewCharges = $instance.properties.enterpriseAgreementPolicies.accountOwnerViewCharges + authenticationType = $instance.properties.enterpriseAgreementPolicies.authenticationType + departmentAdminViewCharges = $instance.properties.enterpriseAgreementPolicies.departmentAdminViewCharges + } + } + + $results = @{ + BillingAccount = $BillingAccount + Name = $instance.name + EnterpriseAgreementPolicies = $EnterpriseAgreementPoliciesValue + MarketplacePurchases = $instance.properties.marketplacePurchases + ReservationPurchases = $instance.properties.reservationPurchases + SavingsPlanPurchases = $instance.properties.savingsPlanPurchases + 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] + $BillingAccount, + + [Parameter()] + [Microsoft.Management.Infrastructure.CimInstance] + $EnterpriseAgreementPolicies, + + [Parameter()] + [System.String] + $MarketplacePurchases, + + [Parameter()] + [System.String] + $ReservationPurchases, + + [Parameter()] + [System.String] + $SavingsPlanPurchases, + + [Parameter()] + [System.String] + $Name, + + [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 + + $instanceParams = @{ + properties = @{ + enterpriseAgreementPolicies = @{ + accountOwnerViewCharges = $EnterpriseAgreementPolicies.accountOwnerViewCharges + authenticationType = $EnterpriseAgreementPolicies.authenticationType + departmentAdminViewCharges = $EnterpriseAgreementPolicies.departmentAdminViewCharges + } + marketplacePurchases = $MarketplacePurchases + reservationPurchases = $ReservationPurchases + savingsPlanPurchases = $SavingsPlanPurchases + } + } + $payload = ConvertTo-Json $instanceParams -Depth 5 -Compress + Write-Verbose -Message "Updating billing account policy for {$BillingAccount} with payload:`r`n$($payload)" + $uri = "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$($BillingAccount)/policies/default?api-version=2024-04-01" + $response = Invoke-AzRest -Uri $uri -Method "PUT" -Payload $payload + if (-not [System.String]::IsNullOrEmpty($response.Error)) + { + throw "Error: $($response.Error)" + } + Write-Verbose -Message "Response:`r`n$($response.Content)" +} + +function Test-TargetResource +{ + [CmdletBinding()] + [OutputType([System.Boolean])] + param + ( + [Parameter(Mandatory = $true)] + [System.String] + $BillingAccount, + + [Parameter()] + [Microsoft.Management.Infrastructure.CimInstance] + $EnterpriseAgreementPolicies, + + [Parameter()] + [System.String] + $MarketplacePurchases, + + [Parameter()] + [System.String] + $ReservationPurchases, + + [Parameter()] + [System.String] + $SavingsPlanPurchases, + + [Parameter()] + [System.String] + $Name, + + [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 = $true + + #Compare Cim instances + foreach ($key in $PSBoundParameters.Keys) + { + $source = $PSBoundParameters.$key + $target = $CurrentValues.$key + if ($source.getType().Name -like '*CimInstance*') + { + $testResult = Compare-M365DSCComplexObject ` + -Source ($source) ` + -Target ($target) + + if (-Not $testResult) + { + $testResult = $false + break + } + + $ValuesToCheck.Remove($key) | Out-Null + } + } + + if ($testResult) + { + $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 ($accounts.Length -eq 0) + { + Write-Host $Global:M365DSCEmojiGreenCheckMark + } + else + { + Write-Host "`r`n" -NoNewline + } + foreach ($account in $accounts.value) + { + $displayedKey = $account.properties.displayName + Write-Host " |---[$i/$($accounts.value.Length)] $displayedKey" -NoNewline + + if ($null -ne $Global:M365DSCExportResourceInstancesCount) + { + $Global:M365DSCExportResourceInstancesCount++ + } + $params = @{ + BillingAccount = $account.name + Credential = $Credential + ApplicationId = $ApplicationId + TenantId = $TenantId + CertificateThumbprint = $CertificateThumbprint + ManagedIdentity = $ManagedIdentity.IsPresent + AccessTokens = $AccessTokens + } + + $Results = Get-TargetResource @Params + $Results = Update-M365DSCExportAuthenticationResults -ConnectionMode $ConnectionMode ` + -Results $Results + + if ($Results.EnterpriseAgreementPolicies) + { + $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject $Results.EnterpriseAgreementPolicies -CIMInstanceName AzureBillingAccountPolicyEnterpriseAgreementPolicy + if ($complexTypeStringResult) + { + $Results.EnterpriseAgreementPolicies = $complexTypeStringResult + } + else + { + $Results.Remove('EnterpriseAgreementPolicies') | Out-Null + } + } + $currentDSCBlock = Get-M365DSCExportContentForResource -ResourceName $ResourceName ` + -ConnectionMode $ConnectionMode ` + -ModulePath $PSScriptRoot ` + -Results $Results ` + -Credential $Credential + + if ($Results.EnterpriseAgreementPolicies) + { + $isCIMArray = $false + if ($Results.EnterpriseAgreementPolicies.getType().Fullname -like '*[[\]]') + { + $isCIMArray = $true + } + $currentDSCBlock = Convert-DSCStringParamToVariable -DSCBlock $currentDSCBlock -ParameterName 'EnterpriseAgreementPolicies' -IsCIMArray:$isCIMArray + } + $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_AzureBillingAccountPolicy/MSFT_AzureBillingAccountPolicy.schema.mof b/Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountPolicy/MSFT_AzureBillingAccountPolicy.schema.mof new file mode 100644 index 0000000000..5b68ca919e --- /dev/null +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountPolicy/MSFT_AzureBillingAccountPolicy.schema.mof @@ -0,0 +1,25 @@ +[ClassVersion("1.0.0.0")] +class MSFT_AzureBillingAccountPolicyEnterpriseAgreementPolicy +{ + [Write, Description("The policy that controls whether account owner can view charges.")] String accountOwnerViewCharges; + [Write, Description("The state showing the enrollment auth level.")] String authenticationType; + [Write, Description("The policy that controls whether department admin can view charges.")] String departmentAdminViewCharges; +}; +[ClassVersion("1.0.0.0"), FriendlyName("AzureBillingAccountPolicy")] +class MSFT_AzureBillingAccountPolicy : OMI_BaseResource +{ + [Key, Description("Unique identifier of the associated billing account.")] String BillingAccount; + [Write, Description("Name of the policy.")] String Name; + [Write, Description("The policies for Enterprise Agreement enrollments."), EmbeddedInstance("MSFT_AzureBillingAccountPolicyEnterpriseAgreementPolicy")] String EnterpriseAgreementPolicies; + [Write, Description("The policy that controls whether Azure marketplace purchases are allowed.")] String MarketplacePurchases; + [Write, Description("The policy that controls whether Azure reservation purchases are allowed.")] String ReservationPurchases; + [Write, Description("The policy that controls whether users with Azure savings plan purchase are allowed.")] String SavingsPlanPurchases; + + [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_AzureBillingAccountPolicy/readme.md b/Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountPolicy/readme.md new file mode 100644 index 0000000000..8432d3040a --- /dev/null +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountPolicy/readme.md @@ -0,0 +1,6 @@ + +# AzureBillingAccountPolicy + +## Description + +Configures policies settings for an Azure billing account. diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountPolicy/settings.json b/Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountPolicy/settings.json new file mode 100644 index 0000000000..2be977460b --- /dev/null +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountPolicy/settings.json @@ -0,0 +1,20 @@ +{ + "resourceName": "AzureBillingAccountPolicy", + "description": "Configures policies settings for an Azure billing account.", + "roles": { + "read": [], + "update": [] + }, + "permissions": { + "graph": { + "delegated": { + "read": [], + "update": [] + }, + "application": { + "read": [], + "update": [] + } + } + } +} diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountScheduledAction/MSFT_AzureBillingAccountScheduledAction.psm1 b/Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountScheduledAction/MSFT_AzureBillingAccountScheduledAction.psm1 index e6b49c0e6f..5eff8056b5 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountScheduledAction/MSFT_AzureBillingAccountScheduledAction.psm1 +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AzureBillingAccountScheduledAction/MSFT_AzureBillingAccountScheduledAction.psm1 @@ -81,7 +81,7 @@ function Get-TargetResource $nullResult.Ensure = 'Absent' try { - $uri = "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$($account.name)/providers/Microsoft.CostManagement/scheduledActions?api-version=2023-11-01" + $uri = "https://management.azure.com/providers/Microsoft.Billing/billingAccounts/$($BillingAccount)/providers/Microsoft.CostManagement/scheduledActions?api-version=2023-11-01" $response = Invoke-AzRest -Uri $uri -Method GET $actions = (ConvertFrom-Json ($response.Content)).value diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_IntuneDeviceConfigurationPolicyAndroidDeviceOwner/MSFT_IntuneDeviceConfigurationPolicyAndroidDeviceOwner.psm1 b/Modules/Microsoft365DSC/DSCResources/MSFT_IntuneDeviceConfigurationPolicyAndroidDeviceOwner/MSFT_IntuneDeviceConfigurationPolicyAndroidDeviceOwner.psm1 index 20fd28f74c..19c44b7a7c 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_IntuneDeviceConfigurationPolicyAndroidDeviceOwner/MSFT_IntuneDeviceConfigurationPolicyAndroidDeviceOwner.psm1 +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_IntuneDeviceConfigurationPolicyAndroidDeviceOwner/MSFT_IntuneDeviceConfigurationPolicyAndroidDeviceOwner.psm1 @@ -667,7 +667,7 @@ function Get-TargetResource $complexAzureAdSharedDeviceDataClearApps = @() $currentValueArray = $getValue.AdditionalProperties.azureAdSharedDeviceDataClearApps - if ($null -ne $currentValueArray -and $currentValueArray.count -gt 0 ) + if ($null -ne $currentValueArray -and $currentValueArray.count -gt 0) { foreach($currentValue in $currentValueArray) { @@ -686,10 +686,10 @@ function Get-TargetResource $currentValue = $getValue.AdditionalProperties.detailedHelpText if ($null -ne $currentValue) { - $complexDetailedHelpText.Add('DefaultMessage',$currentValue.defaultMessage) + $complexDetailedHelpText.Add('DefaultMessage', $currentValue.defaultMessage) $complexLocalizedMessages = @() $currentValueArray = $currentValue.localizedMessages - if ($null -ne $currentValueArray -and $currentValueArray.count -gt 0 ) + if ($null -ne $currentValueArray -and $currentValueArray.count -gt 0) { foreach($currentChildValue in $currentValueArray) { @@ -700,17 +700,17 @@ function Get-TargetResource $complexLocalizedMessages += $currentHash } } - $complexDetailedHelpText.Add('LocalizedMessages',$complexLocalizedMessages) + $complexDetailedHelpText.Add('LocalizedMessages', $complexLocalizedMessages) } $complexDeviceOwnerLockScreenMessage = @{} $currentValue = $getValue.AdditionalProperties.deviceOwnerLockScreenMessage if ($null -ne $currentValue) { - $complexDeviceOwnerLockScreenMessage.Add('DefaultMessage',$currentValue.defaultMessage) + $complexDeviceOwnerLockScreenMessage.Add('DefaultMessage', $currentValue.defaultMessage) $complexLocalizedMessages = @() $currentValueArray = $currentValue.localizedMessages - if ($null -ne $currentValueArray -and $currentValueArray.count -gt 0 ) + if ($null -ne $currentValueArray -and $currentValueArray.count -gt 0) { foreach($currentChildValue in $currentValueArray) { @@ -721,48 +721,48 @@ function Get-TargetResource $complexLocalizedMessages += $currentHash } } - $complexDeviceOwnerLockScreenMessage.Add('LocalizedMessages',$complexLocalizedMessages) + $complexDeviceOwnerLockScreenMessage.Add('LocalizedMessages', $complexLocalizedMessages) } $complexGlobalProxy = @{} $currentValue = $getValue.AdditionalProperties.globalProxy if ($null -ne $currentValue) { - $complexGlobalProxy.Add('ProxyAutoConfigURL',$currentValue.proxyAutoConfigURL) - $complexGlobalProxy.Add('ExcludedHosts',$currentValue.excludedHosts) - $complexGlobalProxy.Add('Host',$currentValue.host) - $complexGlobalProxy.Add('Port',$currentValue.port) - $complexGlobalProxy.Add('oDataType',$currentValue.'@odata.type') + $complexGlobalProxy.Add('ProxyAutoConfigURL', $currentValue.proxyAutoConfigURL) + $complexGlobalProxy.Add('ExcludedHosts', $currentValue.excludedHosts) + $complexGlobalProxy.Add('Host', $currentValue.host) + $complexGlobalProxy.Add('Port', $currentValue.port) + $complexGlobalProxy.Add('oDataType', $currentValue.'@odata.type') } $complexKioskModeApps = @() $currentValueArray = $getValue.AdditionalProperties.kioskModeApps - if ($null -ne $currentValueArray -and $currentValueArray.count -gt 0 ) + if ($null -ne $currentValueArray -and $currentValueArray.count -gt 0) { foreach($currentValue in $currentValueArray) { $currentHash = @{} - $currentHash.add('AppId',$currentValue.appid) - $currentHash.add('Publisher',$currentValue.publisher) - $currentHash.add('AppStoreUrl',$currentValue.appStoreUrl) - $currentHash.add('Name',$currentValue.name) - $currentHash.add('oDataType',$currentValue.'@odata.type') + $currentHash.add('AppId', $currentValue.appid) + $currentHash.add('Publisher', $currentValue.publisher) + $currentHash.add('AppStoreUrl', $currentValue.appStoreUrl) + $currentHash.add('Name', $currentValue.name) + $currentHash.add('oDataType', $currentValue.'@odata.type') $complexKioskModeApps += $currentHash } } $complexPersonalProfilePersonalApplications = @() $currentValueArray = $getValue.AdditionalProperties.personalProfilePersonalApplications - if ($null -ne $currentValueArray -and $currentValueArray.count -gt 0 ) + if ($null -ne $currentValueArray -and $currentValueArray.count -gt 0) { foreach($currentValue in $currentValueArray) { $currentHash = @{} - $currentHash.add('AppId',$currentValue.appid) - $currentHash.add('Publisher',$currentValue.publisher) - $currentHash.add('AppStoreUrl',$currentValue.appStoreUrl) - $currentHash.add('Name',$currentValue.name) - $currentHash.add('oDataType',$currentValue.'@odata.type') + $currentHash.add('AppId', $currentValue.appid) + $currentHash.add('Publisher', $currentValue.publisher) + $currentHash.add('AppStoreUrl', $currentValue.appStoreUrl) + $currentHash.add('Name', $currentValue.name) + $currentHash.add('oDataType', $currentValue.'@odata.type') $complexPersonalProfilePersonalApplications += $currentHash } } @@ -771,10 +771,10 @@ function Get-TargetResource $currentValue = $getValue.AdditionalProperties.shortHelpText if ($null -ne $currentValue) { - $complexShortHelpText.Add('DefaultMessage',$currentValue.defaultMessage) + $complexShortHelpText.Add('DefaultMessage', $currentValue.defaultMessage) $complexLocalizedMessages = @() $currentValueArray = $currentValue.localizedMessages - if ($null -ne $currentValueArray -and $currentValueArray.count -gt 0 ) + if ($null -ne $currentValueArray -and $currentValueArray.count -gt 0) { foreach($currentChildValue in $currentValueArray) { @@ -785,20 +785,20 @@ function Get-TargetResource $complexLocalizedMessages += $currentHash } } - $complexShortHelpText.Add('LocalizedMessages',$complexLocalizedMessages) + $complexShortHelpText.Add('LocalizedMessages', $complexLocalizedMessages) } $complexSystemUpdateFreezePeriods = @() $currentValueArray = $getValue.AdditionalProperties.systemUpdateFreezePeriods - if ($null -ne $currentValueArray -and $currentValueArray.count -gt 0 ) + if ($null -ne $currentValueArray -and $currentValueArray.count -gt 0) { foreach($currentValue in $currentValueArray) { $currentHash = @{} - $currentHash.Add('StartDay',$currentValue.startDay) - $currentHash.Add('EndDay',$currentValue.endDay) - $currentHash.Add('StartMonth',$currentValue.startMonth) - $currentHash.Add('EndMonth',$currentValue.endMonth) + $currentHash.Add('StartDay', $currentValue.startDay) + $currentHash.Add('EndDay', $currentValue.endDay) + $currentHash.Add('StartMonth', $currentValue.startMonth) + $currentHash.Add('EndMonth', $currentValue.endMonth) $complexSystemUpdateFreezePeriods += $currentHash } } @@ -1644,6 +1644,19 @@ function Set-TargetResource foreach ($key in ($CreateParameters.clone()).Keys) { + if ($key -eq 'DetailedHelpText' -or $key -eq 'DeviceOwnerLockScreenMessage' -or $key -eq 'ShortHelpText') + { + if ($null -ne $CreateParameters.$key.DefaultMessage -or $null -ne $CreateParameters.$key.LocalizedMessages) + { + $CreateParameters.$key.Add('@odata.type', '#microsoft.graph.androidDeviceOwnerUserFacingMessage') + } + + if ($null -eq $CreateParameters.$key.LocalizedMessages) + { + $CreateParameters.$key.Add('localizedMessages', @()) + } + } + if ($CreateParameters[$key].getType().Fullname -like '*CimInstance*') { $CreateParameters[$key] = Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $CreateParameters[$key] @@ -1684,6 +1697,19 @@ function Set-TargetResource foreach ($key in (($UpdateParameters.clone()).Keys | Sort-Object)) { + if ($key -eq 'DetailedHelpText' -or $key -eq 'DeviceOwnerLockScreenMessage' -or $key -eq 'ShortHelpText') + { + if ($null -ne $UpdateParameters.$key.DefaultMessage -or $null -ne $UpdateParameters.$key.LocalizedMessages) + { + $UpdateParameters.$key.Add('@odata.type', '#microsoft.graph.androidDeviceOwnerUserFacingMessage') + } + + if ($null -eq $UpdateParameters.$key.LocalizedMessages) + { + $UpdateParameters.$key.Add('localizedMessages', @()) + } + } + if ($UpdateParameters.$key.getType().Fullname -like '*CimInstance*') { $UpdateParameters.$key = Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $UpdateParameters.$key diff --git a/Modules/Microsoft365DSC/Examples/Resources/AzureBillingAccountPolicy/2-Update.ps1 b/Modules/Microsoft365DSC/Examples/Resources/AzureBillingAccountPolicy/2-Update.ps1 new file mode 100644 index 0000000000..4bedeb545c --- /dev/null +++ b/Modules/Microsoft365DSC/Examples/Resources/AzureBillingAccountPolicy/2-Update.ps1 @@ -0,0 +1,39 @@ +<# +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 + { + AzureBillingAccountPolicy "MyBillingAccountPolicy" + { + BillingAccount = "1e5b9e50-a1ea-581e-fb3a-xxxxxxxxx:6487d5cf-0a7b-42e6-9549-xxxxxxx_2019-05-31"; + Name = "default" + ApplicationId = $ApplicationId + TenantId = $TenantId + CertificateThumbprint = $CertificateThumbprint + EnterpriseAgreementPolicies = MSFT_AzureBillingAccountPolicyEnterpriseAgreementPolicy { + authenticationType = "OrganizationalAccountOnly" + } + MarketplacePurchases = "AllAllowed" + ReservationPurchases = "Allowed" + SavingsPlanPurchases = "NotAllowed" + } + } +} diff --git a/Modules/Microsoft365DSC/SchemaDefinition.json b/Modules/Microsoft365DSC/SchemaDefinition.json index 2ebfcaead9..35f76dc9dd 100644 --- a/Modules/Microsoft365DSC/SchemaDefinition.json +++ b/Modules/Microsoft365DSC/SchemaDefinition.json @@ -9955,6 +9955,96 @@ } ] }, + { + "ClassName": "MSFT_AzureBillingAccountPolicyEnterpriseAgreementPolicy", + "Parameters": [ + { + "CIMType": "String", + "Name": "accountOwnerViewCharges", + "Option": "Write" + }, + { + "CIMType": "String", + "Name": "authenticationType", + "Option": "Write" + }, + { + "CIMType": "String", + "Name": "departmentAdminViewCharges", + "Option": "Write" + } + ] + }, + { + "ClassName": "MSFT_AzureBillingAccountPolicy", + "Parameters": [ + { + "CIMType": "String", + "Name": "BillingAccount", + "Option": "Key" + }, + { + "CIMType": "String", + "Name": "Name", + "Option": "Write" + }, + { + "CIMType": "MSFT_AzureBillingAccountPolicyEnterpriseAgreementPolicy", + "Name": "EnterpriseAgreementPolicies", + "Option": "Write" + }, + { + "CIMType": "String", + "Name": "MarketplacePurchases", + "Option": "Write" + }, + { + "CIMType": "String", + "Name": "ReservationPurchases", + "Option": "Write" + }, + { + "CIMType": "String", + "Name": "SavingsPlanPurchases", + "Option": "Write" + }, + { + "CIMType": "string", + "Name": "Ensure", + "Option": "Write" + }, + { + "CIMType": "MSFT_Credential", + "Name": "Credential", + "Option": "Write" + }, + { + "CIMType": "String", + "Name": "ApplicationId", + "Option": "Write" + }, + { + "CIMType": "String", + "Name": "TenantId", + "Option": "Write" + }, + { + "CIMType": "String", + "Name": "CertificateThumbprint", + "Option": "Write" + }, + { + "CIMType": "Boolean", + "Name": "ManagedIdentity", + "Option": "Write" + }, + { + "CIMType": "String[]", + "Name": "AccessTokens", + "Option": "Write" + } + ] + }, { "ClassName": "MSFT_AzureBillingAccountsAssociatedTenant", "Parameters": [ diff --git a/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AzureBillingAccountPolicy.Tests.ps1 b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AzureBillingAccountPolicy.Tests.ps1 new file mode 100644 index 0000000000..1aaf278223 --- /dev/null +++ b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AzureBillingAccountPolicy.Tests.ps1 @@ -0,0 +1,186 @@ +[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 Get-M365DSCAzureBillingAccount -MockWith { + return @{ + value = @{ + name = "1e5b9e50-a1ea-581e-fb3a-xxxxxxxxx:6487d5cf-0a7b-42e6-9549-xxxxxxx_2019-05-31" + 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 exists and values are already in the desired state" -Fixture { + BeforeAll { + $testParams = @{ + BillingAccount = "1e5b9e50-a1ea-581e-fb3a-xxxxxxxxx:6487d5cf-0a7b-42e6-9549-xxxxxxx_2019-05-31"; + Name = "default" + EnterpriseAgreementPolicies = (New-CimInstance -ClassName MSFT_AzureBillingAccountPolicyEnterpriseAgreementPolicy -Property @{ + authenticationType = "OrganizationalAccountOnly" + } -ClientOnly) + MarketplacePurchases = "AllAllowed" + ReservationPurchases = "Allowed" + SavingsPlanPurchases = "NotAllowed" + Ensure = 'Present' + Credential = $Credential; + } + + Mock -CommandName Invoke-AzRest -MockWith { + return @{ + Content = ConvertTo-Json @{ + value = @( + @{ + name = "default" + id = "12345-12345-12345-12345-12345" + properties = @{ + enterpriseAgreementPolicies = @{ + authenticationType = "OrganizationalAccountOnly" + } + marketplacePurchases = "AllAllowed" + reservationPurchases = "Allowed" + savingsPlanPurchases = "NotAllowed" + } + } + ) + } -Dept 10 -Compress + } + } + } + + 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 = @{ + BillingAccount = "1e5b9e50-a1ea-581e-fb3a-xxxxxxxxx:6487d5cf-0a7b-42e6-9549-xxxxxxx_2019-05-31"; + Name = "default" + EnterpriseAgreementPolicies = (New-CimInstance -ClassName MSFT_AzureBillingAccountPolicyEnterpriseAgreementPolicy -Property @{ + authenticationType = "OrganizationalAccountOnly" + } -ClientOnly) + MarketplacePurchases = "AllAllowed" + ReservationPurchases = "Allowed" + SavingsPlanPurchases = "Allowed" #Drift + Ensure = 'Present' + Credential = $Credential; + } + + Mock -CommandName Invoke-AzRest -MockWith { + return @{ + Content = ConvertTo-Json @{ + value = @( + @{ + name = "default" + id = "12345-12345-12345-12345-12345" + properties = @{ + enterpriseAgreementPolicies = @{ + authenticationType = "OrganizationalAccountOnly" + } + marketplacePurchases = "AllAllowed" + reservationPurchases = "Allowed" + savingsPlanPurchases = "NotAllowed" + } + } + ) + } -Dept 10 -Compress + } + } + } + + 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 Invoke-AzRest -Exactly 1 + } + } + + Context -Name 'ReverseDSC Tests' -Fixture { + BeforeAll { + $Global:CurrentModeIsExport = $true + $Global:PartialExportFileName = "$(New-Guid).partial.ps1" + $testParams = @{ + Credential = $Credential; + } + + Mock -CommandName Invoke-AzRest -MockWith { + return @{ + Content = ConvertTo-Json @{ + value = @( + @{ + name = "default" + id = "12345-12345-12345-12345-12345" + properties = @{ + enterpriseAgreementPolicies = @{ + authenticationType = "OrganizationalAccountOnly" + } + marketplacePurchases = "AllAllowed" + reservationPurchases = "Allowed" + savingsPlanPurchases = "NotAllowed" + } + } + ) + } -Dept 10 -Compress + } + } + } + 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/docs/docs/resources/azure/AzureBillingAccountPolicy.md b/docs/docs/resources/azure/AzureBillingAccountPolicy.md new file mode 100644 index 0000000000..c1a200a8a8 --- /dev/null +++ b/docs/docs/resources/azure/AzureBillingAccountPolicy.md @@ -0,0 +1,105 @@ +# AzureBillingAccountPolicy + +## Parameters + +| Parameter | Attribute | DataType | Description | Allowed Values | +| --- | --- | --- | --- | --- | +| **BillingAccount** | Key | String | Unique identifier of the associated billing account. | | +| **Name** | Write | String | Name of the policy. | | +| **EnterpriseAgreementPolicies** | Write | MSFT_AzureBillingAccountPolicyEnterpriseAgreementPolicy | The policies for Enterprise Agreement enrollments. | | +| **MarketplacePurchases** | Write | String | The policy that controls whether Azure marketplace purchases are allowed. | | +| **ReservationPurchases** | Write | String | The policy that controls whether Azure reservation purchases are allowed. | | +| **SavingsPlanPurchases** | Write | String | The policy that controls whether users with Azure savings plan purchase are allowed. | | +| **Ensure** | Write | String | Present ensures the instance exists, absent ensures it is removed. | `Absent`, `Present` | +| **Credential** | Write | PSCredential | Credentials of the workload's Admin | | +| **ApplicationId** | Write | String | Id of the Azure Active Directory application to authenticate with. | | +| **TenantId** | Write | String | Id of the Azure Active Directory tenant used for authentication. | | +| **CertificateThumbprint** | Write | String | Thumbprint of the Azure Active Directory application's authentication certificate to use for authentication. | | +| **ManagedIdentity** | Write | Boolean | Managed ID being used for authentication. | | +| **AccessTokens** | Write | StringArray[] | Access token used for authentication. | | + +### MSFT_AzureBillingAccountPolicyEnterpriseAgreementPolicy + +#### Parameters + +| Parameter | Attribute | DataType | Description | Allowed Values | +| --- | --- | --- | --- | --- | +| **accountOwnerViewCharges** | Write | String | The policy that controls whether account owner can view charges. | | +| **authenticationType** | Write | String | The state showing the enrollment auth level. | | +| **departmentAdminViewCharges** | Write | String | The policy that controls whether department admin can view charges. | | + + +## Description + +Configures policies settings for an Azure billing account. + +## Permissions + +### Microsoft Graph + +To authenticate with the Microsoft Graph API, this resource required the following permissions: + +#### Delegated permissions + +- **Read** + + - None + +- **Update** + + - None + +#### Application permissions + +- **Read** + + - None + +- **Update** + + - None + +## Examples + +### Example 1 + +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. + +```powershell +Configuration Example +{ + param( + [Parameter()] + [System.String] + $ApplicationId, + + [Parameter()] + [System.String] + $TenantId, + + [Parameter()] + [System.String] + $CertificateThumbprint + ) + Import-DscResource -ModuleName Microsoft365DSC + node localhost + { + AzureBillingAccountPolicy "MyBillingAccountPolicy" + { + BillingAccount = "1e5b9e50-a1ea-581e-fb3a-xxxxxxxxx:6487d5cf-0a7b-42e6-9549-xxxxxxx_2019-05-31"; + Name = "default" + ApplicationId = $ApplicationId + TenantId = $TenantId + CertificateThumbprint = $CertificateThumbprint + EnterpriseAgreementPolicies = MSFT_AzureBillingAccountPolicyEnterpriseAgreementPolicy { + authenticationType = "OrganizationalAccountOnly" + } + MarketplacePurchases = "AllAllowed" + ReservationPurchases = "Allowed" + SavingsPlanPurchases = "NotAllowed" + } + } +} +``` +