From 33a53d198c131dd9deaa3fae175836530e8daa72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Mon, 9 Jan 2023 15:09:17 +0100 Subject: [PATCH 01/29] Fixed invalild usage of beta Graph cmdlets --- .../MSFT_AADAdministrativeUnit.psm1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 index b257ad9cc0..bfcd1fc740 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 @@ -116,12 +116,12 @@ function Get-TargetResource #region resource generator code if (-Not [string]::IsNullOrEmpty($Id)) { - $getValue = Get-MgAdministrativeUnit -AdministrativeUnitId $Id -ErrorAction Stop + $getValue = Get-MgDirectoryAdministrativeUnit -AdministrativeUnitId $Id -ErrorAction Stop } if (-not $getValue -and -Not [string]::IsNullOrEmpty($DisplayName)) { - $getValue = Get-MgAdministrativeUnit -Filter "DisplayName eq '$DisplayName'" -ErrorAction Stop + $getValue = Get-MgDirectoryAdministrativeUnit -Filter "DisplayName eq '$DisplayName'" -ErrorAction Stop } #endregion @@ -565,7 +565,7 @@ function Set-TargetResource } #region resource generator code - $policy = New-MgAdministrativeUnit @CreateParameters + $policy = New-MgDirectoryAdministrativeUnit @CreateParameters #endregion @@ -622,7 +622,7 @@ function Set-TargetResource # The AU MembershipType may have changed from Dynamic to Static and that change has to be implemented before explicitly adding members #region resource generator code - Update-MgAdministrativeUnit @UpdateParameters ` + Update-MgDirectoryAdministrativeUnit @UpdateParameters ` -AdministrativeUnitId $currentInstance.Id #endregion From 992d17d657ef5fd880ac59c9b4b0f9da46d8290a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Fri, 13 Jan 2023 12:14:04 +0100 Subject: [PATCH 02/29] Changed AADAdministrativeUnit to use Graph v1.0 --- .../MSFT_AADAdministrativeUnit.psm1 | 51 ++++++++----- .../Microsoft365DSC/Modules/M365DSCUtil.psm1 | 7 +- ...soft365DSC.AADAdministrativeUnit.Tests.ps1 | 73 ++++--------------- 3 files changed, 53 insertions(+), 78 deletions(-) diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 index bfcd1fc740..85e84c874d 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 @@ -79,16 +79,11 @@ function Get-TargetResource $ManagedIdentity ) - # Note: Graph v1.0 names basic cmdlets xxx-MgDirectoryAdministrativeUnit(xxx) - # but the beta profile names latest cmdlets xxx-MgAdministrativeUnit(xxx) - # only the beta cmdlets support the preview enabling MembershipType and MembershipRuleProcessingState - # NB: Usage of these params require that the corresponding AAD preview feature is enabled - try { $ConnectionMode = New-M365DSCConnection -Workload 'MicrosoftGraph' ` -InboundParameters $PSBoundParameters ` - -ProfileName 'beta' + -ProfileName 'v1.0' } catch { @@ -365,7 +360,7 @@ function Set-TargetResource { $ConnectionMode = New-M365DSCConnection -Workload 'MicrosoftGraph' ` -InboundParameters $PSBoundParameters ` - -ProfileName 'beta' + -ProfileName 'v1.0' } catch { @@ -410,6 +405,7 @@ function Set-TargetResource { $CreateParameters = $currentParameters.Clone() + <# When using -BodyParameter it is not necessary to use AdditionalProperties for Dynamic AU params if ($CreateParameters.Containskey('MembershipType') -or $CreateParameters.Containskey('MembershipRule') -or $CreateParameters.Containskey('MembershipRuleProcessingState')) { $CreateParameters.Remove('MembershipType') | Out-Null @@ -429,6 +425,7 @@ function Set-TargetResource $CreateParameters.AdditionalProperties.Add('MembershipRuleProcessingState', $MembershipRuleProcessingState) } } + #> foreach ($key in ($CreateParameters.clone()).Keys) { @@ -449,7 +446,7 @@ function Set-TargetResource $memberIdentity = Get-MgUser -Filter "UserPrincipalName eq '$($Member.Identity)'" -ErrorAction Stop if ($memberIdentity) { - $memberSpecification += @{Id = $memberIdentity.Id } + $memberSpecification += @{Type="$($Member.Type)s";Id = $memberIdentity.Id } } else { @@ -461,7 +458,7 @@ function Set-TargetResource $memberIdentity = Get-MgGroup -Filter "DisplayName eq '$($Member.Identity)'" -ErrorAction Stop if ($memberIdentity) { - $memberSpecification += @{Id = $memberIdentity.Id } + $memberSpecification += @{Type="$($Member.Type)s";Id = $memberIdentity.Id } } else { @@ -473,7 +470,7 @@ function Set-TargetResource $memberIdentity = Get-MgDevice -Filter "DisplayName eq '$($Member.Identity)'" -ErrorAction Stop if ($memberIdentity) { - $memberSpecification += @{Id = $memberIdentity.Id } + $memberSpecification += @{Type="$($Member.Type)s";Id = $memberIdentity.Id } } else { @@ -485,7 +482,8 @@ function Set-TargetResource throw "AU {$($DisplayName)}: Member {$($Member.Identity)} has invalid type {$($Member.Type)}" } } - $CreateParameters.Members = $memberSpecification + # Members are added to the AU *after* it has been created + $CreateParameters.Remove('Members') | Out-Null } else { @@ -565,9 +563,17 @@ function Set-TargetResource } #region resource generator code - $policy = New-MgDirectoryAdministrativeUnit @CreateParameters + $policy = New-MgDirectoryAdministrativeUnit -BodyParameter $CreateParameters #endregion + foreach ($member in $memberSpecificationpecification) + { + $memberBodyParam = @{ + '@odata.id' = "https://graph.microsoft.com/v1.0/$($member.Type)/{$($member.Id)}" + } + + New-MgDirectoryAdministrativeUnitMemberByRef -AdministrativeUnitId $policy.Id -BodyParameter $memberBodyParam + } foreach ($scopedRoleMember in $scopedRoleMemberSpecification) { @@ -596,8 +602,9 @@ function Set-TargetResource #$UpdateParameters.Remove('Extensions') | Out-Null $UpdateParameters.Remove('Members') | Out-Null $UpdateParameters.Remove('ScopedRoleMembers') | Out-Null - $UpdateParameters.Remove('Visibility') | Out-Null + #$UpdateParameters.Remove('Visibility') | Out-Null + <# When using -BodyParameter it is not necessary to use AdditionalProperties for Dynamic AU params if ($UpdateParameters.Containskey('MembershipType') -or $UpdateParameters.Containskey('MembershipRule') -or $UpdateParameters.Containskey('MembershipRuleProcessingState')) { $UpdateParameters.Remove('MembershipType') | Out-Null @@ -617,12 +624,13 @@ function Set-TargetResource $UpdateParameters.AdditionalProperties.Add('MembershipRuleProcessingState', $MembershipRuleProcessingState) } } + #> # when updating the resource, update the AU first and its members (if any) afterwards. # The AU MembershipType may have changed from Dynamic to Static and that change has to be implemented before explicitly adding members #region resource generator code - Update-MgDirectoryAdministrativeUnit @UpdateParameters ` + Update-MgDirectoryAdministrativeUnit -BodyParameter $UpdateParameters ` -AdministrativeUnitId $currentInstance.Id #endregion @@ -660,6 +668,9 @@ function Set-TargetResource { $memberObject = Get-MgDevice -Filter "DisplayName eq '$($diff.Identity)'" $membertype = 'devices' + } else { + # throw if a *new* member has been specified with invalid type + throw "AU {$($DisplayName)}: Member {$($diff.Identity)} has invalid type {$($diff.Type)}" } if ($null -eq $memberObject) { @@ -667,15 +678,14 @@ function Set-TargetResource } if ($memberObject.Count -gt 1) { - throw "AU member {$($diff.Identity)} is not a unique $($diff.Type) (Count=$($memberObject.Count))" + throw "AU member {$($diff.Identity)} is not a unique $($diff.Type.ToLower()) (Count=$($memberObject.Count))" } if ($diff.SideIndicator -eq '=>') { Write-Verbose -Message "Adding new member {$($diff.Identity)}, type {$($diff.Type)} to Administrative Unit {$($currentInstance.DisplayName)}" $memberBodyParam = @{ - '@odata.id' = "https://graph.microsoft.com/v1.0b038744 Objectivism2o2! - b/$memberType/{$($memberObject.Id)}" + '@odata.id' = "https://graph.microsoft.com/v1.0/$memberType/{$($memberObject.Id)}" } New-MgDirectoryAdministrativeUnitMemberByRef -AdministrativeUnitId ($currentInstance.Id) -BodyParameter $memberBodyParam | Out-Null } @@ -948,6 +958,7 @@ function Test-TargetResource Write-Verbose -Message "Test-TargetResource returned $false" return $false } + $testResult = $true foreach ($key in $PSBoundParameters.Keys) @@ -1001,6 +1012,12 @@ function Test-TargetResource # Removing the visibility parameter from the check since this is always being returned as null currently by the Microsoft Graph. $ValuesToCheck.Remove('Visibility') | Out-Null + if ($MembershipType -ne 'Dynamic' -and $CurrentValues.MembershipType -ne 'Dynamic') + { + # Removing the MembershipType parameter from the check if it isn't Dynamic. If so, it can be aither Assigned or null + $ValuesToCheck.Remove('MembershipType') | Out-Null + } + Write-Verbose -Message "Current Values: $(Convert-M365DscHashtableToString -Hashtable $CurrentValues)" Write-Verbose -Message "Target Values: $(Convert-M365DscHashtableToString -Hashtable $ValuesToCheck)" diff --git a/Modules/Microsoft365DSC/Modules/M365DSCUtil.psm1 b/Modules/Microsoft365DSC/Modules/M365DSCUtil.psm1 index dc1f07373d..308b852686 100644 --- a/Modules/Microsoft365DSC/Modules/M365DSCUtil.psm1 +++ b/Modules/Microsoft365DSC/Modules/M365DSCUtil.psm1 @@ -1283,7 +1283,10 @@ Public function Import-M365DSCDependencies { [CmdletBinding()] - param() + param( + [parameter()] + [switch]$Global + ) $currentPath = Join-Path -Path $PSScriptRoot -ChildPath '..\' -Resolve $manifest = Import-PowerShellDataFile "$currentPath/Dependencies/Manifest.psd1" @@ -1291,7 +1294,7 @@ function Import-M365DSCDependencies foreach ($dependency in $dependencies) { - Import-Module $dependency.ModuleName -RequiredVersion $dependency.RequiredVersion -Force + Import-Module $dependency.ModuleName -RequiredVersion $dependency.RequiredVersion -Force -Global:$Global } } diff --git a/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 index a7f9568215..a231426f12 100644 --- a/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 +++ b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 @@ -42,13 +42,13 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } - Mock -CommandName Update-MgAdministrativeUnit -MockWith { + Mock -CommandName Update-MgDirectoryAdministrativeUnit -MockWith { } Mock -CommandName Remove-MgDirectoryAdministrativeUnit -MockWith { } - Mock -CommandName New-MgAdministrativeUnit -MockWith { + Mock -CommandName New-MgDirectoryAdministrativeUnit -MockWith { } Mock -CommandName New-MgDirectoryAdministrativeUnitMemberByRef -MockWith { @@ -66,7 +66,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Mock -CommandName Remove-MgDirectoryAdministrativeUnitScopedRoleMember -MockWith { } Mock -CommandName New-M365DSCConnection -MockWith { - Select-MgProfile beta + # Select-MgProfile beta # not anymore return 'Credential' } @@ -96,7 +96,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } } - Mock -CommandName Get-MgAdministrativeUnit -MockWith { + Mock -CommandName Get-MgDirectoryAdministrativeUnit -MockWith { return $null } } @@ -108,7 +108,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } It 'Should Create the AU from the Set method' { Set-TargetResource @testParams - Should -Invoke -CommandName New-MgAdministrativeUnit -Exactly 1 + Should -Invoke -CommandName New-MgDirectoryAdministrativeUnit -Exactly 1 } } @@ -126,7 +126,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Credential = $Credential } - Mock -CommandName Get-MgAdministrativeUnit -MockWith { + Mock -CommandName Get-MgDirectoryAdministrativeUnit -MockWith { return [pscustomobject]@{ Description = 'FakeStringValue2' DisplayName = 'FakeStringValue2' @@ -161,7 +161,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { DisplayName = 'DSCAU' Id = 'DSCAU' Members = @( - (New-CimInstance -ClassName MSFT_MicrosoftGraphdirectoryobject -Property @{ + (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ Identity = 'John.Doe@mytenant.com' Type = 'User' } -ClientOnly) @@ -187,13 +187,14 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { #> Visibility = 'Public' MembershipType = 'Assigned' + # MembershipRule and -ProcessingState params are only used when MembershipType is Dynamic MembershipRule = 'Canada' MembershipRuleProcessingState = 'On' Ensure = 'Present' Credential = $Credential } - Mock -CommandName Get-MgAdministrativeUnit -MockWith { + Mock -CommandName Get-MgDirectoryAdministrativeUnit -MockWith { return [pscustomobject]@{ Description = 'DSCAU' DisplayName = 'DSCAU' @@ -206,20 +207,6 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } ) #> - Members = @( - [pscustomobject]@{Id = '1234567890' } - ) - ScopedRoleMembers = @( - [pscustomobject]@{ - RoleId = '12345-67890' - RoleMemberInfo = @( - [pscustomobject]@{ - DisplayName = 'John Doe' - Id = '1234567890' - } - ) - } - ) Visibility = 'Public' AdditionalProperties = @{ membershipType = 'Assigned' @@ -289,7 +276,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { #> Id = 'DSCAU2' Members = @( - (New-CimInstance -ClassName MSFT_MicrosoftGraphdirectoryobject -Property @{ + (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ Identity = 'John.Doe@mytenant.com' Type = 'User' } -ClientOnly) @@ -309,24 +296,12 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Credential = $Credential } - Mock -CommandName Get-MgAdministrativeUnit -MockWith { + Mock -CommandName Get-MgDirectoryAdministrativeUnit -MockWith { return [pscustomobject]@{ Description = 'DSCAU2' DisplayName = 'DSCAU2' Id = 'DSCAU2' Visibility = 'Public' - Members = $null - ScopedRoleMembers = @( - [pscustomobject]@{ - RoleId = '12345-67890' - RoleMemberInfo = [pscustomobject]@( - @{ - DisplayName = 'John Doe' - Id = '1234567890' - } - ) - } - ) <# Extensions =@( [pscustomobject]@{ @@ -335,8 +310,6 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } ) #> - '@odata.type' = '#microsoft.graph.administrativeunit' - } } @@ -410,7 +383,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { ) #> Members = @( - (New-CimInstance -ClassName MSFT_MicrosoftGraphdirectoryobject -Property @{ + (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ Identity = 'John.Doe@mytenant.com' Type = 'User' } -ClientOnly) @@ -429,16 +402,12 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Credential = $Credential } - Mock -CommandName Get-MgAdministrativeUnit -MockWith { + Mock -CommandName Get-MgDirectoryAdministrativeUnit -MockWith { return [pscustomobject]@{ Description = 'DSCAU' DisplayName = 'DSCAU' Id = 'DSCAU' Visibility = 'Public' - Members = @( - [pscustomobject]@{Id = '1234567890' } - ) - ScopedRoleMembers = $null <# Extensions =@( [pscustomobject]@{ @@ -447,7 +416,6 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } ) #> - '@odata.type' = '#microsoft.graph.' } } @@ -514,7 +482,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Credential = $Credential } - Mock -CommandName Get-MgAdministrativeUnit -MockWith { + Mock -CommandName Get-MgDirectoryAdministrativeUnit -MockWith { return [pscustomobject]@{ Description = 'ExportDSCAU' DisplayName = 'ExportDSCAU' @@ -529,20 +497,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { ) #> Id = 'ExportDSCAU' - Members = @( - [pscustomobject]@{Id = '1234567890' } - ) - ScopedRoleMembers = @( - [pscustomobject]@{ - RoleId = '12345-67890' - RoleMemberInfo = [pscustomobject]@{ - DisplayName = 'John Doe' - Id = '1234567890' - } - } - ) Visibility = 'Public' - } } From 887677205e681df5d47daa3d1d9e3f88c89ca6ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Tue, 17 Jan 2023 12:41:02 +0100 Subject: [PATCH 03/29] Fixed issues with AADAdministrativeUnit --- .../MSFT_AADAdministrativeUnit.psm1 | 338 +++++++++++++----- .../MSFT_AADAdministrativeUnit.schema.mof | 12 +- ...soft365DSC.AADAdministrativeUnit.Tests.ps1 | 62 ++-- 3 files changed, 277 insertions(+), 135 deletions(-) diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 index 85e84c874d..a23359a6bc 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 @@ -159,37 +159,37 @@ function Get-TargetResource if ($auMembers) { $memberSpec = @() - } - foreach ($getMember in $auMembers) - { - # get object regardless of type - $memberObject = Invoke-MgGraphRequest -Method Get -Uri "https://graph.microsoft.com/v1.0/directoryObjects/$($getMember.Id)" - switch -regex ([regex]::Escape($memberObject.'@odata.type')) + foreach ($getMember in $auMembers) { - 'group' + # get object regardless of type + $memberObject = Invoke-MgGraphRequest -Method Get -Uri "https://graph.microsoft.com/v1.0/directoryObjects/$($getMember.Id)" + switch -regex ([regex]::Escape($memberObject.'@odata.type')) { - $memberSpec += @{ - Identity = $memberObject.DisplayName; - Type = 'Group' + 'group' + { + $memberSpec += [pscustomobject]@{ + Identity = $memberObject.DisplayName; + Type = 'Group' + } } - } - 'user' - { - $memberSpec += @{ - Identity = $memberObject.UserPrincipalName; - Type = 'User' + 'user' + { + $memberSpec += [pscustomobject]@{ + Identity = $memberObject.UserPrincipalName; + Type = 'User' + } } - } - 'device' - { - $memberSpec += @{ - Identity = $memberObject.DisplayName; - Type = 'Device' + 'device' + { + $memberSpec += [pscustomobject]@{ + Identity = $memberObject.DisplayName; + Type = 'Device' + } } } } + $results.Add('Members', $memberSpec) } - $results.Add('Members', $memberSpec) } $scopedRoleMemberSpec = $null @@ -219,17 +219,17 @@ function Get-TargetResource } $memberIdentity = $roleMemberObject.DisplayName } - $scopedRoleMemberInfo = @{ + $scopedRoleMemberInfo = [pscustomobject]@{ RoleName = $roleObject.DisplayName; - RoleMemberInfo = @{ - Identity = $memberIdentity - Type = $memberType - } + #RoleMemberInfo = @{ # avoid nested hashtable, can't handle it in Export + Identity = $memberIdentity + Type = $memberType + #} } $scopedRoleMemberSpec += $scopedRoleMemberInfo } + $results.Add('ScopedRoleMembers', $scopedRoleMemberSpec) } - $results.Add('ScopedRoleMembers', $scopedRoleMemberSpec) <# # Extensions are still too unwieldy @@ -356,16 +356,9 @@ function Set-TargetResource # ONLY the beta cmdlets support the preview enabling MembershipType and MembershipRuleProcessingState # NB: Usage of these params require that the corresponding AAD preview feature is enabled - try - { - $ConnectionMode = New-M365DSCConnection -Workload 'MicrosoftGraph' ` + $ConnectionMode = New-M365DSCConnection -Workload 'MicrosoftGraph' ` -InboundParameters $PSBoundParameters ` -ProfileName 'v1.0' - } - catch - { - Write-Verbose -Message $_ - } #Ensure the proper dependencies are installed in the current environment. Confirm-M365DSCDependencies @@ -427,6 +420,7 @@ function Set-TargetResource } #> + <# skipped - no use for hashtables foreach ($key in ($CreateParameters.clone()).Keys) { if ($CreateParameters[$key].getType().Fullname -like '*CimInstance*') @@ -434,19 +428,20 @@ function Set-TargetResource $CreateParameters[$key] = Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $CreateParameters[$key] } } + #> - # Resolve Members Type/Identity to user or group id - if ($currentParameters.Members) + $memberSpecification = @() + if ($MembershipType -ne 'Dynamic') { - $memberSpecification = @() - foreach ($Member in $Members) + # Resolve Members Type/Identity to user or group id + foreach ($Member in $currentParameters.Members) { if ($Member.Type -eq 'User') { $memberIdentity = Get-MgUser -Filter "UserPrincipalName eq '$($Member.Identity)'" -ErrorAction Stop if ($memberIdentity) { - $memberSpecification += @{Type="$($Member.Type)s";Id = $memberIdentity.Id } + $memberSpecification += [pscustomobject]@{Type="$($Member.Type)s";Id = $memberIdentity.Id } } else { @@ -458,7 +453,7 @@ function Set-TargetResource $memberIdentity = Get-MgGroup -Filter "DisplayName eq '$($Member.Identity)'" -ErrorAction Stop if ($memberIdentity) { - $memberSpecification += @{Type="$($Member.Type)s";Id = $memberIdentity.Id } + $memberSpecification += [pscustomobject]@{Type="$($Member.Type)s";Id = $memberIdentity.Id } } else { @@ -470,7 +465,7 @@ function Set-TargetResource $memberIdentity = Get-MgDevice -Filter "DisplayName eq '$($Member.Identity)'" -ErrorAction Stop if ($memberIdentity) { - $memberSpecification += @{Type="$($Member.Type)s";Id = $memberIdentity.Id } + $memberSpecification += [pscustomobject]@{Type="$($Member.Type)s";Id = $memberIdentity.Id } } else { @@ -483,51 +478,82 @@ function Set-TargetResource } } # Members are added to the AU *after* it has been created - $CreateParameters.Remove('Members') | Out-Null - } - else - { - $CreateParameters.Remove('Members') | Out-Null } + $CreateParameters.Remove('Members') | Out-Null # Resolve ScopedRoleMembers Type/Identity to user, group or service principal if ($currentParameters.ScopedRoleMembers) { + #write-verbose "AU {$DisplayName}: Set-TargetResource, Convert ScopedRoleMembers to Hashtable(s)" + $testScopedRoleMembers = Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $ScopedRoleMembers <#@() + foreach ($scopedRoleMember in $testScopedRoleMembers) + { + $testScopedRoleMember = @{} + foreach ($key in $scopedRoleMember.clone().Keys) + { + if ($scopedRoleMember[$key].GetType().FullName -like "*CimInstance*") + { + $testScopedRoleMember.Add($key, (Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $scopedRoleMember.$key)) + } + else + { + $testScopedRoleMember.Add($key, $scopedRoleMember.$key) + } + } + $testScopedRoleMembers += $testScopedRoleMember + } + #> $scopedRoleMemberSpecification = @() - foreach ($roleMember in $ScopedRoleMembers) + foreach ($roleMember in $testScopedRoleMembers) { + if ($roleMember.Contains('RoleMemberInfo')) + { + # flatten object if old schema-def with nested hashtable was used as input + if (string]::IsNullOrEmpty($roleMember.Identity) -and $roleMember.RoleMemberInfo.Identity) + { + $roleMember.Add('Identity', $roleMember.RoleMemberInfo.Identity) + } + if (string]::IsNullOrEmpty($roleMember.Type) -and $roleMember.RoleMemberInfo.Type) + { + $roleMember.Add('Type', $roleMember.RoleMemberInfo.Type) + } + } $roleObject = Get-MgDirectoryRole -Filter "DisplayName eq '$($roleMember.RoleName)'" -ErrorAction stop if ($null -eq $roleObject) { throw "AU {$($DisplayName)}: RoleName {$($roleMember.RoleName)} does not exist or is not enabled" } - if ($roleMember.RoleMemberInfo.Type -eq 'User') + if ($roleMember.Type -eq 'User') { - $roleMemberIdentity = Get-MgUser -Filter "UserPrincipalName eq '$($roleMember.RoleMemberInfo.Identity)'" -ErrorAction Stop + $roleMemberIdentity = Get-MgUser -Filter "UserPrincipalName eq '$($roleMember.Identity)'" -ErrorAction Stop if ($null -eq $roleMemberIdentity) { - throw "AU {$($DisplayName)}: Scoped Role User {$($roleMember.RoleMemberInfo.Identity)} does not exist" + throw "AU {$($DisplayName)}: Scoped Role User {$($roleMember.Identity)} does not exist" } } - elseif ($roleMember.RoleMemberInfo.Type -eq 'Group') + elseif ($roleMember.Type -eq 'Group') { - $roleMemberIdentity = Get-MgGroup -Filter "displayName eq '$($roleMember.RoleMemberInfo.Identity)'" -ErrorAction Stop + $roleMemberIdentity = Get-MgGroup -Filter "displayName eq '$($roleMember.Identity)'" -ErrorAction Stop if ($null -eq $roleMemberIdentity) { - throw "AU {$($DisplayName)}: Scoped Role Group {$($roleMember.RoleMemberInfo.Identity)} does not exist" + throw "AU {$($DisplayName)}: Scoped Role Group {$($roleMember.Identity)} does not exist" } } - elseif ($roleMember.RoleMemberInfo.Type -eq 'ServicePrincipal') + elseif ($roleMember.Type -eq 'ServicePrincipal') { - $roleMemberIdentity = Get-MgServicePrincipal -Filter "displayName eq '$($roleMember.RoleMemberInfo.Identity)'" -ErrorAction Stop + $roleMemberIdentity = Get-MgServicePrincipal -Filter "displayName eq '$($roleMember.Identity)'" -ErrorAction Stop if ($null -eq $roleMemberIdentity) { - throw "AU {$($DisplayName)}: Scoped Role ServicePrincipal {$($roleMember.RoleMemberInfo.Identity)} does not exist" + throw "AU {$($DisplayName)}: Scoped Role ServicePrincipal {$($roleMember.Identity)} does not exist" } } else { - throw "AU {$($DisplayName)}: Invalid ScopedRoleMember.RoleMemberInfo.Type {$($roleMember.RolememberInfo.Type)}" + throw "AU {$($DisplayName)}: Invalid ScopedRoleMember.Type {$($roleMember.Type)}" + } + if ($roleMemberIdentity.Count -gt 1) + { + throw "AU {$($DisplayName)}: ScopedRoleMember {$($roleMember.RoleName)}, Identity {$($roleMember.Identity)} of type {$($roleMember.Type)} is not unique" } $scopedRoleMemberSpecification += @{ RoleId = $roleObject.Id @@ -536,24 +562,21 @@ function Set-TargetResource } } } - #$CreateParameters.ScopedRoleMembers = $scopedRoleMemberSpecification # ScopedRoleMember-info is added after the AU is created } - else - { - $CreateParameters.Remove('ScopedRoleMembers') | Out-Null - } + $CreateParameters.Remove('ScopedRoleMembers') | Out-Null } if ($Ensure -eq 'Present' -and $currentInstance.Ensure -eq 'Absent') { Write-Verbose -Message "Creating AU {$DisplayName}" - $CreateParameters = Rename-M365DSCCimInstanceODataParameter -Properties $CreateParameters + #$CreateParameters = Rename-M365DSCCimInstanceODataParameter -Properties $CreateParameters $CreateParameters.Remove('Id') | Out-Null $CreateParameters.Remove('Verbose') | Out-Null + <# foreach ($key in ($CreateParameters.clone()).Keys) { if ($CreateParameters[$key].getType().Fullname -like '*CimInstance*') @@ -561,15 +584,16 @@ function Set-TargetResource $CreateParameters[$key] = Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $CreateParameters[$key] } } + #> #region resource generator code $policy = New-MgDirectoryAdministrativeUnit -BodyParameter $CreateParameters #endregion - foreach ($member in $memberSpecificationpecification) + foreach ($member in $memberSpecification) { $memberBodyParam = @{ - '@odata.id' = "https://graph.microsoft.com/v1.0/$($member.Type)/{$($member.Id)}" + '@odata.id' = "https://graph.microsoft.com/v1.0/$($member.Type)/$($member.Id)" } New-MgDirectoryAdministrativeUnitMemberByRef -AdministrativeUnitId $policy.Id -BodyParameter $memberBodyParam @@ -586,11 +610,12 @@ function Set-TargetResource Write-Verbose -Message "Updating AU {$DisplayName}" $UpdateParameters = $currentParameters.Clone() - $UpdateParameters = Rename-M365DSCCimInstanceODataParameter -Properties $UpdateParameters + #$UpdateParameters = Rename-M365DSCCimInstanceODataParameter -Properties $UpdateParameters $UpdateParameters.Remove('Id') | Out-Null $UpdateParameters.Remove('Verbose') | Out-Null + <# foreach ($key in ($UpdateParameters.clone()).Keys) { if ($UpdateParameters[$key].getType().Fullname -like '*CimInstance*') @@ -598,6 +623,7 @@ function Set-TargetResource $UpdateParameters[$key] = Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $UpdateParameters[$key] } } + #> #$UpdateParameters.Remove('Extensions') | Out-Null $UpdateParameters.Remove('Members') | Out-Null @@ -638,7 +664,7 @@ function Set-TargetResource if ($MembershipType -ne 'Dynamic' -and ($Members -or $backCurrentMembers)) { $currentMembersValue = @() - if ($currentInstance.Members.Length -ne 0) + if ($backCurrentMembers.Length -ne 0) { $currentMembersValue = $backCurrentMembers } @@ -651,7 +677,10 @@ function Set-TargetResource { $desiredMembersValue = @() } - $membersDiff = Compare-Object -ReferenceObject $currentMembersValue -DifferenceObject $desiredMembersValue -Property Identity, Type + write-verbose "AU {$DisplayName} Update Members: Current members: $($currentMembersValue.Identity -join ', ')" + write-verbose " Desired members: $($desiredMembersValue.Identity -join ', ')" + $membersDiff = Compare-Object -ReferenceObject $currentMembersValue -DifferenceObject $desiredMembersValue -Property Identity, Type -IncludeEqual + write-verbose " # compare results : $($membersDiff.Count -gt 0)" foreach ($diff in $membersDiff) { if ($diff.Type -eq 'User') @@ -668,9 +697,19 @@ function Set-TargetResource { $memberObject = Get-MgDevice -Filter "DisplayName eq '$($diff.Identity)'" $membertype = 'devices' - } else { - # throw if a *new* member has been specified with invalid type - throw "AU {$($DisplayName)}: Member {$($diff.Identity)} has invalid type {$($diff.Type)}" + } + else + { + if ($diff.Identity) + { + # throw if a *new* member has been specified with invalid type + throw "AU {$($DisplayName)}: Member {$($diff.Identity)} has invalid type {$($diff.Type)}" + } + else + { + write-verbose "Compare Members - skip processing blank Identity" + continue # don't process, continue to next diff if any + } } if ($null -eq $memberObject) { @@ -682,18 +721,22 @@ function Set-TargetResource } if ($diff.SideIndicator -eq '=>') { - Write-Verbose -Message "Adding new member {$($diff.Identity)}, type {$($diff.Type)} to Administrative Unit {$($currentInstance.DisplayName)}" + Write-Verbose -Message "Adding new member {$($diff.Identity)}, type {$($diff.Type)} to Administrative Unit {$DisplayName}" $memberBodyParam = @{ - '@odata.id' = "https://graph.microsoft.com/v1.0/$memberType/{$($memberObject.Id)}" + '@odata.id' = "https://graph.microsoft.com/v1.0/$memberType/$($memberObject.Id)" } New-MgDirectoryAdministrativeUnitMemberByRef -AdministrativeUnitId ($currentInstance.Id) -BodyParameter $memberBodyParam | Out-Null } elseif ($diff.SideIndicator -eq '<=') { - Write-Verbose -Message "Removing member {$($diff.Identity)}, type {$($diff.Type)} from Administrative UNit {$($currentInstance.DisplayName)}" + Write-Verbose -Message "Removing member {$($diff.Identity)}, type {$($diff.Type)} from Administrative Unit {$DisplayName}" Remove-MgDirectoryAdministrativeUnitMemberByRef -AdministrativeUnitId ($currentInstance.Id) -DirectoryObjectId ($memberObject.Id) | Out-Null } + else + { + Write-Verbose -Message "Unchanged member {$($diff.Identity)}, type {$($diff.Type)} of Administrative Unit {$DisplayName}" + } } } @@ -756,8 +799,7 @@ function Set-TargetResource if ($ScopedRoleMembers -or $backCurrentScopedRoleMembers) { - $currentScopedRoleMembersValue = @() - if ($currentInstance.ScopedRoleMembers.Length -ne 0) + if ($backCurrentScopedRoleMembers.Length -ne 0) { $currentScopedRoleMembersValue = $backCurrentScopedRoleMembers } @@ -770,7 +812,8 @@ function Set-TargetResource { $desiredScopedRoleMembersValue = @() } - # flatten objects to compare: + + <# # not necessary to flatten objects to compare when nested CimInstances are avoided in Schema/Get-TargetResource: $compareCurrentScopedRoleMembersValue = @() foreach ($roleMember in $currentScopedRoleMembersValue) { @@ -790,29 +833,52 @@ function Set-TargetResource } } $scopedRoleMembersDiff = Compare-Object -ReferenceObject $compareCurrentScopedRoleMembersValue -DifferenceObject $compareDesiredScopedRoleMembersValue -Property RoleName, Identity, Type + #> + write-verbose "AU {$DisplayName} Update ScopedRoleMembers: Current members: $($currentScopedRoleMembersValue.Identity -join ', ')" + write-verbose " Desired members: $($desiredScopedRoleMembersValue.Identity -join ', ')" + $scopedRoleMembersDiff = Compare-Object -ReferenceObject $CurrentScopedRoleMembersValue -DifferenceObject $DesiredScopedRoleMembersValue -Property RoleName, Identity, Type -IncludeEqual + write-verbose " # compare results : $($scopedRoleMembersDiff.Count -gt 0)" + foreach ($diff in $scopedRoleMembersDiff) { if ($diff.Type -eq 'User') { $memberObject = Get-MgUser -Filter "UserPrincipalName eq '$($diff.Identity)'" - $memberType = 'users' + #$memberType = 'users' } - else + elseif ($diff.Type -eq 'Group') { $memberObject = Get-MgGroup -Filter "DisplayName eq '$($diff.Identity)'" - $membertype = 'groups' + #$membertype = 'groups' + } + elseif ($diff.Type -eq 'ServicePrincipal') + { + $memberObject = Get-MgServicePrincipal -Filter "DisplayName eq '$($diff.Identity)'" + #$memberType = "servicePrincipals" + } + else + { + if ($diff.RoleName) + { + throw "AU {$DisplayName} scoped role {$($diff.RoleName)} member {$($diff.Identity)} has invalid type $($diff.Type)" + } + else + { + write-verbose "Compare ScopedRoleMembers - skip processing blank RoleName" + continue # don't process, + } } - if ($null -eq $memberobject) + if ($null -eq $memberObject) { - throw "AU scoped role member {$($diff.Identity)} does not exist as a $(diff.Type)" + throw "AU scoped role member {$($diff.Identity)} does not exist as a $($diff.Type)" } - if ($memberobject.Count -gt 1) + if ($memberObject.Count -gt 1) { - throw "AU scoped role member {$($diff.Identity)} is not a unique $($diff.Type)" + throw "AU scoped role member {$($diff.Identity)} is not a unique $($diff.Type) (Count=$($memberObject.Count))" } if ($diff.SideIndicator -ne '==') { - $roleObject = Get-MgDirectoryRole -Filter "DisplayName -eq '$($diff.RoleName)" + $roleObject = Get-MgDirectoryRole -Filter "DisplayName eq '$($diff.RoleName)'" if ($null -eq $roleObject) { throw "AU Scoped Role {$($diff.RoleName)} does not exist as an Azure AD role" @@ -820,7 +886,7 @@ function Set-TargetResource } if ($diff.SideIndicator -eq '=>') { - Write-Verbose -Message "Adding new scoped role {$($diff.RoleName)} member {$($diff.Identity)}, type {$($diff.Type)} to Administrative Unit {$($currentInstance.DisplayName)}" + Write-Verbose -Message "Adding new scoped role {$($diff.RoleName)} member {$($diff.Identity)}, type {$($diff.Type)} to Administrative Unit {$DisplayName}" $scopedRoleMemberParam = @{ RoleId = $roleObject.Id @@ -833,9 +899,16 @@ function Set-TargetResource } elseif ($diff.SideIndicator -eq '<=') { - Write-Verbose -Message "Removing scoped role {$($diff.RoleName)} member {$($diff.Identity)}, type {$($diff.Type)} from Administrative Unit {$($currentInstance.DisplayName)}" - $scopedRoleMemberObject = Get-MgDirectoryAdministrativeUnitScopedRoleMember -AdministrativeUnitId ($currentInstance.Id) -All | Where-Object -FilterScript { $_.RoleId -eq $roleObject.Id -and $_.RoleMemberInfo.Id -eq $memberObject.Id } - Remove-MgDirectoryAdministrativeUnitScopedRoleMember -AdministrativeUnitId ($currentInstance.Id) -ScopedRoleMembershipId $scopedRoleMemberObject.Id | Out-Null + if (-not [string]::IsNullOrEmpty($diff.Rolename)) + { + Write-Verbose -Message "Removing scoped role {$($diff.RoleName)} member {$($diff.Identity)}, type {$($diff.Type)} from Administrative Unit {$DisplayName}" + $scopedRoleMemberObject = Get-MgDirectoryAdministrativeUnitScopedRoleMember -AdministrativeUnitId ($currentInstance.Id) -All | Where-Object -FilterScript { $_.RoleId -eq $roleObject.Id -and $_.RoleMemberInfo.Id -eq $memberObject.Id } + Remove-MgDirectoryAdministrativeUnitScopedRoleMember -AdministrativeUnitId ($currentInstance.Id) -ScopedRoleMembershipId $scopedRoleMemberObject.Id | Out-Null + } + } + else + { + Write-Verbose -Message "Unchanged scoped role {$($diff.RoleName)} member {$($diff.Identity)}, type {$($diff.Type)} in Administrative Unit {$DisplayName}" } } } @@ -953,7 +1026,7 @@ function Test-TargetResource $CurrentValues = Get-TargetResource @PSBoundParameters $ValuesToCheck = ([Hashtable]$PSBoundParameters).clone() - if ($CurrentValues.Ensure -eq 'Absent') + if ($CurrentValues.Ensure -eq 'Absent' -and $Ensure -eq 'Present') { Write-Verbose -Message "Test-TargetResource returned $false" return $false @@ -961,13 +1034,61 @@ function Test-TargetResource $testResult = $true + if ($Members.Count -gt 0 -or $currentValues.Members.Count -gt 0) + { + if ($Members.Count -ne $currentValues.Members.Count) + { + $testresult = $false + } + $testMembers = $Members + if ($null -eq $testMembers) + { + $testMembers = @() + } + $testCurrentValuesMembers = $currentValues.Members + if ($null -eq $testCurrentValuesMembers) + { + $testCurrentValuesMembers = @() + } + if ((Compare-Object -ReferenceObject $testMembers -DifferenceObject $testCurrentValuesMembers -Property Identity, Type).Count -gt 0) + { + $testresult = $false + } + $ValuesToCheck.Remove('Members') | Out-Null + } + if ($true -eq $testResult -and ($ScopedRoleMembers.Count -gt 0 -or $currentValues.ScopedRoleMembers.Count -gt 0)) + { + if ($ScopedRoleMembers.Count -ne $currentValues.ScopedRoleMembers.Count) + { + $testresult = $false + } + $testScopedRoleMembers = $ScopedRoleMembers + if ($null -eq $testScopedRoleMembers) + { + $testScopedRoleMembers = @() + } + $testCurrentValuesScopedRoleMembers = $currentValues.ScopedRoleMembers + if ($null -eq $testCurrentValuesScopedRoleMembers) + { + $testCurrentValuesScopedRoleMembers = @() + } + if ((Compare-Object -ReferenceObject $testScopedRoleMembers -DifferenceObject $testCurrentValuesScopedRoleMembers -Property RoleName, Identity, Type).Count -gt 0) + { + $testresult = $false + } + $ValuesToCheck.Remove('ScopedRoleMembers') | Out-Null + } + <# foreach ($key in $PSBoundParameters.Keys) { if ($PSBoundParameters[$key].getType().Name -like '*CimInstance*') { $CIMArraySource = @() $CIMArrayTarget = @() - $CIMArraySource += $PSBoundParameters[$key] + if ($PSBoundParameters[$key]) + { + $CIMArraySource += $PSBoundParameters[$key] + } if ($CurrentValues.$key) { $CIMArrayTarget += $CurrentValues.$key @@ -1001,6 +1122,9 @@ function Test-TargetResource $ValuesToCheck.Remove($key) | Out-Null } } + #> + + Write-Verbose -Message "Test-TargetResource - testresult after comparing CimInstance(s): $testResult" $ValuesToCheck.Remove('Credential') | Out-Null $ValuesToCheck.Remove('ApplicationId') | Out-Null @@ -1014,7 +1138,7 @@ function Test-TargetResource if ($MembershipType -ne 'Dynamic' -and $CurrentValues.MembershipType -ne 'Dynamic') { - # Removing the MembershipType parameter from the check if it isn't Dynamic. If so, it can be aither Assigned or null + # Removing the MembershipType parameter from the check if it isn't Dynamic. If it isn't, it can be Assigned or null $ValuesToCheck.Remove('MembershipType') | Out-Null } @@ -1135,7 +1259,7 @@ function Export-TargetResource if ($Results.Members) { - $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject $Results.Members -CIMInstanceName MicrosoftGraphIdentity + $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject $Results.Members -CIMInstanceName MicrosoftGraphMember if ($complexTypeStringResult) { $Results.Members = $complexTypeStringResult @@ -1147,7 +1271,7 @@ function Export-TargetResource } if ($Results.ScopedRoleMembers) { - $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject $Results.ScopedRoleMembers -CIMInstanceName MicrosoftGraphscopedrolemembership + $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject $Results.ScopedRoleMembers -CIMInstanceName MicrosoftGraphScopedRoleMembership if ($complexTypeStringResult) { $Results.ScopedRoleMembers = $complexTypeStringResult @@ -1306,9 +1430,11 @@ function Get-M365DSCDRGComplexTypeToString #If ComplexObject is an Array if ($ComplexObject.GetType().FullName -like '*[[\]]') { + write-verbose "object is an array" $currentProperty = @() foreach ($item in $ComplexObject) { + write-verbose "Item=$($item -join '|')" $currentProperty += Get-M365DSCDRGComplexTypeToString ` -ComplexObject $item ` -isArray:$true ` @@ -1338,6 +1464,7 @@ function Get-M365DSCDRGComplexTypeToString $keyNotNull = 0 foreach ($key in $ComplexObject.Keys) { + write-verbose "" if ($ComplexObject[$key]) { $keyNotNull++ @@ -1606,6 +1733,7 @@ function Compare-M365DSCComplexObject if ($Source[$skey].getType().FullName -like '*CimInstance*' -or $Target[$skey].getType().FullName -like '*CimInstance*') { #Recursive call for complex object + write-verbose "Compare values for $key" if ($Source[$skey].getType().FullName -like '*CimInstance*') { $complexSourceValue = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $Source[$skey] @@ -1622,6 +1750,22 @@ function Compare-M365DSCComplexObject { $complexTargetValue = $Target[$key] } + if ($complexSourceValue.GetType().FullName -ne 'System.Collections.Hashtable') + { + $tempSourceValue = @{} + $complexSourceValue.psobject.properties | Foreach-Object { + $tempSourceValue[$_.Name] = $_.Value + } + $complexSourceValue = $tempSourceValue + } + if ($complexTargetValue.GetType().FullName -ne 'System.Collections.Hashtable') + { + $tempTargetValue = @{} + $complexTargetValue.psobject.properties | Foreach-Object { + $tempTargetValue[$_.Name] = $_.Value + } + $complexTargetValue = $tempTargetValue + } $compareResult = Compare-M365DSCComplexObject ` -Source $complexSourceValue ` -Target $complexTargetValue diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.schema.mof b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.schema.mof index f3d6bd5c60..2759ea880e 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.schema.mof +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.schema.mof @@ -1,14 +1,16 @@ [ClassVersion("1.0.0")] -class MSFT_MicrosoftGraphIdentity +class MSFT_MicrosoftGraphMember { - [Write, Description("Identity of direcory-object. For users, specify a UserPrincipalName. For Groups and SPNs, specify DisplayName")] String Identity; - [Write, Description("Specify User, Group or ServicePrincipal to interpret the Identity")] String Type; + [Write, Description("Identity of member. For users, specify a UserPrincipalName. For groups and devices, specify DisplayName")] String Identity; + [Write, Description("Specify User, Group or Device to interpret the Identity")] String Type; }; [ClassVersion("1.0.0")] class MSFT_MicrosoftGraphScopedRoleMembership { [Write, Description("Name of the Azure AD Role that is assigned")] String RoleName; - [Write, Description("Member that is assigned the scoped role"), EmbeddedInstance("MSFT_MicrosoftGraphIdentity")] String RoleMemberInfo; + //[Write, Description("Member that is assigned the scoped role"), EmbeddedInstance("MSFT_MicrosoftGraphIdentity")] String RoleMemberInfo; + [Write, Description("Identity of member. For users, specify a UserPrincipalName. For groups and SPNs, specify DisplayName")] String Identity; + [Write, Description("Specify User, Group or ServicePrincipal to interpret the Identity")] String Type; }; /* Extensions not incorporated in initial version [ClassVersion("1.0.0")] @@ -29,7 +31,7 @@ class MSFT_AADAdministrativeUnit : OMI_BaseResource [Write, Description("Specify membership type. Possible values are Assigned and Dynamic if the AU-preview has been activated. Otherwise do not use")] String MembershipType; [Write, Description("Specify membership rule. Requires that MembershipType is set to Dynamic AND the AU-preview has been activated. Otherwise, do not use")] String MembershipRule; [Write, Description("Specify dynamic membership-rule processing-state. Valid values are 'On' and 'Paused'. Requires that MembershipType is set to Dynamic AND the AU-preview has been activated. Otherwise, do not use")] String MembershipRuleProcessingState; - [Write, Description("Specify members. Only specify if MembershipType is set to Static"), EmbeddedInstance("MSFT_MicrosoftGraphIdentity")] String Members[]; + [Write, Description("Specify members. Only specify if MembershipType is set to Static"), EmbeddedInstance("MSFT_MicrosoftGraphMember")] String Members[]; [Write, Description(""), EmbeddedInstance("MSFT_MicrosoftGraphScopedRoleMembership")] String ScopedRoleMembers[]; // [Write, Description("Extensions. See https://docs.microsoft.com/en-us/graph/extensibility-overview#open-extensions"), EmbeddedInstance("MSFT_MicrosoftGraphOpenExtension")] String Extensions[]; [Write, Description("Present ensures the Administrative Unit exists, absent ensures it is removed."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] string Ensure; diff --git a/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 index a231426f12..34a8673173 100644 --- a/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 +++ b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 @@ -81,10 +81,12 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Description = 'FakeStringValue1' DisplayName = 'FakeStringValue1' Id = 'FakeStringValue1' - Members = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ - Type = 'User' - Identity = 'john.smith@contoso.com' - } -ClientOnly) + Members = @( + (New-CimInstance -ClassName MSFT_MicrosoftGraphMember -Property @{ + Type = 'User' + Identity = 'john.smith@contoso.com' + } -ClientOnly) + ) Visibility = 'Public' Ensure = 'Present' Credential = $Credential @@ -118,10 +120,12 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Description = 'FakeStringValue2' DisplayName = 'FakeStringValue2' Id = 'FakeStringValue2' - Members = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ - Type = 'User' - Identity = 'john.smith@contoso.com' - } -ClientOnly) + Members = @( + (New-CimInstance -ClassName MSFT_MicrosoftGraphMember -Property @{ + Type = 'User' + Identity = 'john.smith@contoso.com' + } -ClientOnly) + ) Ensure = 'Absent' Credential = $Credential } @@ -161,7 +165,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { DisplayName = 'DSCAU' Id = 'DSCAU' Members = @( - (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ + (New-CimInstance -ClassName MSFT_MicrosoftGraphMember -Property @{ Identity = 'John.Doe@mytenant.com' Type = 'User' } -ClientOnly) @@ -169,10 +173,10 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { ScopedRoleMembers = @( (New-CimInstance -ClassName MSFT_MicrosoftGraphScopedRoleMembership -Property @{ RoleName = 'User Administrator' - RoleMemberInfo = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ - Identity = 'John.Doe@mytenant.com' - Type = 'User' - } -ClientOnly) + #RoleMemberInfo = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ + Identity = 'John.Doe@mytenant.com' + Type = 'User' + # } -ClientOnly) } -ClientOnly) ) <# @@ -194,6 +198,8 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Credential = $Credential } + # Note: It is in fact possible to update the AU MembershipRule with any invalid value, but in the AAD-portal, updates are not possible unless the rule is valid. + Mock -CommandName Get-MgDirectoryAdministrativeUnit -MockWith { return [pscustomobject]@{ Description = 'DSCAU' @@ -276,7 +282,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { #> Id = 'DSCAU2' Members = @( - (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ + (New-CimInstance -ClassName MSFT_MicrosoftGraphMember -Property @{ Identity = 'John.Doe@mytenant.com' Type = 'User' } -ClientOnly) @@ -284,10 +290,10 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { ScopedRoleMembers = @( (New-CimInstance -ClassName MSFT_MicrosoftGraphScopedRoleMembership -Property @{ RoleName = 'User Administrator' - RoleMemberInfo = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ - Identity = 'John.Doe@mytenant.com' - Type = 'User' - } -ClientOnly) + #RoleMemberInfo = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ + Identity = 'John.Doe@mytenant.com' + Type = 'User' + # } -ClientOnly) } -ClientOnly) ) Visibility = 'Public' @@ -383,7 +389,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { ) #> Members = @( - (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ + (New-CimInstance -ClassName MSFT_MicrosoftGraphMember -Property @{ Identity = 'John.Doe@mytenant.com' Type = 'User' } -ClientOnly) @@ -391,10 +397,10 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { ScopedRoleMembers = @( (New-CimInstance -ClassName MSFT_MicrosoftGraphScopedRoleMembership -Property @{ RoleName = 'User Administrator' - RoleMemberInfo = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ - Identity = 'John.Doe@mytenant.com' - Type = 'User' - } -ClientOnly) + #RoleMemberInfo = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ + Identity = 'John.Doe@mytenant.com' + Type = 'User' + # } -ClientOnly) } -ClientOnly) ) Visibility = 'Public' @@ -419,15 +425,6 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } } - Mock -CommandName Invoke-MgGraphRequest -MockWith { - return [pscustomobject]@{ - '@odata.type' = '#microsoft.graph.user' - DisplayName = 'John Doe' - UserPrincipalName = 'John.Doe@mytenant.com' - Id = '1234567890' - } - } - Mock -CommandName Get-MgDirectoryAdministrativeUnitMember -MockWith { return [pscustomobject]@{ Id = '1234567890' @@ -445,7 +442,6 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Mock -CommandName Get-MgDirectoryAdministrativeUnitScopedRoleMember -MockWith { } - Mock -CommandName Invoke-MgGraphRequest -MockWith { return [pscustomobject]@{ '@odata.type' = '#microsoft.graph.user' From 4e3411022e50af5b3569b3ad9877734783c244e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Tue, 17 Jan 2023 15:02:08 +0100 Subject: [PATCH 04/29] Improved Admin Units, but still not 'there' --- .../MSFT_AADAdministrativeUnit.psm1 | 51 ++++++++++++++----- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 index a23359a6bc..295349f166 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 @@ -1259,7 +1259,8 @@ function Export-TargetResource if ($Results.Members) { - $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject $Results.Members -CIMInstanceName MicrosoftGraphMember + $hashResults = Get-M365DSCDRGObjectArrayToHashtable -ArrayObject $Results.Members + $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject $hashResults -CIMInstanceName MicrosoftGraphMember if ($complexTypeStringResult) { $Results.Members = $complexTypeStringResult @@ -1271,7 +1272,8 @@ function Export-TargetResource } if ($Results.ScopedRoleMembers) { - $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject $Results.ScopedRoleMembers -CIMInstanceName MicrosoftGraphScopedRoleMembership + $hashResults = Get-M365DSCDRGObjectArrayToHashtable -ArrayObject $Results.ScopedRoleMembers + $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject $hashResults -CIMInstanceName MicrosoftGraphScopedRoleMembership if ($complexTypeStringResult) { $Results.ScopedRoleMembers = $complexTypeStringResult @@ -1300,6 +1302,7 @@ function Export-TargetResource -Results $Results ` -Credential $Credential + <# if ($Results.Members) { $isCIMArray = $false @@ -1319,6 +1322,8 @@ function Export-TargetResource } $currentDSCBlock = Convert-DSCStringParamToVariable -DSCBlock $currentDSCBlock -ParameterName 'ScopedRoleMembers' -IsCIMArray:$isCIMArray } + #> + <# if ($Results.Extensions) { $isCIMArray = $false @@ -1328,6 +1333,7 @@ function Export-TargetResource } $currentDSCBlock = Convert-DSCStringParamToVariable -DSCBlock $currentDSCBlock -ParameterName 'Extensions' -IsCIMArray:$isCIMArray } + #> $dscContent += $currentDSCBlock Save-M365DSCPartialExport -Content $currentDSCBlock ` @@ -1402,6 +1408,27 @@ function Get-M365DSCDRGComplexTypeToHashtable return $results } +function Get-M365DSCDRGObjectArrayToHashtable +{ + [cmdletbinding()] + [outputtype([system.object[]])] + param( + [Parameter()] + [system.object[]] + $ArrayObject + ) + $returnValue = @() + foreach ($element in $ArrayObject) + { + $hashTable = @{} + foreach ($property in $element.PSObject.Properties.Name) + { + $hashtable.Add($property, $element.$property) | Out-Null + } + $returnValue += $hashTable + } + return $returnValue +} function Get-M365DSCDRGComplexTypeToString { [CmdletBinding()] @@ -1465,14 +1492,14 @@ function Get-M365DSCDRGComplexTypeToString foreach ($key in $ComplexObject.Keys) { write-verbose "" - if ($ComplexObject[$key]) + if ($ComplexObject.$key) { $keyNotNull++ - if ($ComplexObject[$key].GetType().FullName -like 'Microsoft.Graph.PowerShell.Models.*') + if ($ComplexObject.$key.GetType().FullName -like 'Microsoft.Graph.PowerShell.Models.*') { - $hashPropertyType = $ComplexObject[$key].GetType().Name.tolower() - $hashProperty = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $ComplexObject[$key] + $hashPropertyType = $ComplexObject[$key].GetType().Name + $hashProperty = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $ComplexObject.$key if (Test-M365DSCComplexObjectHasValues -ComplexObject $hashProperty) { @@ -1493,7 +1520,7 @@ function Get-M365DSCDRGComplexTypeToString { $Whitespace = ' ' } - $currentProperty += Get-M365DSCDRGSimpleObjectTypeToString -Key $key -Value $ComplexObject[$key] -Space ($Whitespace + ' ') + $currentProperty += Get-M365DSCDRGSimpleObjectTypeToString -Key $key -Value $ComplexObject.$key -Space ($Whitespace + ' ') } } } @@ -1512,16 +1539,16 @@ function Test-M365DSCComplexObjectHasValues [OutputType([System.Boolean])] param( [Parameter(Mandatory = $true)] - [System.Collections.Hashtable] + [system.object] $ComplexObject ) - $keys = $ComplexObject.keys + $keys = $ComplexObject.psobject.properties.name $hasValue = $false foreach ($key in $keys) { - if ($ComplexObject[$key]) + if ($ComplexObject.$key) { - if ($ComplexObject[$key].GetType().FullName -like 'Microsoft.Graph.PowerShell.Models.*') + if ($ComplexObject.$key.GetType().FullName -like 'Microsoft.Graph.PowerShell.Models.*') { $hash = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $ComplexObject[$key] if (-Not $hash) @@ -1533,7 +1560,7 @@ function Test-M365DSCComplexObjectHasValues else { $hasValue = $true - return $hasValue + break } } } From cb8ec9e2edd3fbc3569140b9e4b43ba8ef0f46a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Thu, 19 Jan 2023 14:26:45 +0100 Subject: [PATCH 05/29] Fixed AADAdministrativeUnit - NB update is a BR --- .../MSFT_AADAdministrativeUnit.psm1 | 1709 ++++++++--------- .../MSFT_AADAdministrativeUnit.schema.mof | 22 +- ...soft365DSC.AADAdministrativeUnit.Tests.ps1 | 137 +- 3 files changed, 834 insertions(+), 1034 deletions(-) diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 index 295349f166..6622563012 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 @@ -4,33 +4,37 @@ function Get-TargetResource [OutputType([System.Collections.Hashtable])] param ( - [Parameter(Mandatory = $true)] - [System.String] - $DisplayName, - + #region resource generator code [Parameter()] [System.String] - $Id, + $Description, - [Parameter()] + [Parameter(Mandatory=$true)] [System.String] - $Description, + $DisplayName, [Parameter()] [validateset('Public', 'HiddenMembership')] [System.String] $Visibility, + [Parameter()] + [System.String] + $Id, + [Parameter()] [validateset('Assigned', 'Dynamic')] - [System.String]$MembershipType, + [System.String] + $MembershipType, [Parameter()] - [System.String]$MembershipRule, + [System.String] + $MembershipRule, [Parameter()] [validateset('Paused', 'On')] - [System.String]$MembershipRuleProcessingState, + [System.String] + $MembershipRuleProcessingState, [Parameter()] [Microsoft.Management.Infrastructure.CimInstance[]] @@ -40,19 +44,12 @@ function Get-TargetResource [Microsoft.Management.Infrastructure.CimInstance[]] $ScopedRoleMembers, - <# - [Parameter()] - [Microsoft.Management.Infrastructure.CimInstance[]] - $Extensions, - #> - - #endregion [Parameter(Mandatory = $true)] [System.String] [ValidateSet('Absent', 'Present')] - $Ensure = 'Present', + $Ensure = $true, [Parameter()] [System.Management.Automation.PSCredential] @@ -84,180 +81,156 @@ function Get-TargetResource $ConnectionMode = New-M365DSCConnection -Workload 'MicrosoftGraph' ` -InboundParameters $PSBoundParameters ` -ProfileName 'v1.0' - } - catch - { - Write-Verbose -Message ($_) - } - #Ensure the proper dependencies are installed in the current environment. - Confirm-M365DSCDependencies + #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 + #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 - { - $getValue = $null + $nullResult = $PSBoundParameters + $nullResult.Ensure = 'Absent' + $getValue = $null #region resource generator code - if (-Not [string]::IsNullOrEmpty($Id)) + if (-not [string]::IsNullOrEmpty($Id)) { - $getValue = Get-MgDirectoryAdministrativeUnit -AdministrativeUnitId $Id -ErrorAction Stop + $getValue = Get-MgDirectoryAdministrativeUnit -AdministrativeUnitId $Id -ErrorAction SilentlyContinue } - if (-not $getValue -and -Not [string]::IsNullOrEmpty($DisplayName)) + if ($null -eq $getValue -and -not [string]::IsNullOrEmpty($DisplayName)) { - $getValue = Get-MgDirectoryAdministrativeUnit -Filter "DisplayName eq '$DisplayName'" -ErrorAction Stop + Write-Verbose -Message "Could not find an Azure AD Administrative Unit with Id {$Id}" + + if(-Not [string]::IsNullOrEmpty($DisplayName)) + { + $getValue = Get-MgDirectoryAdministrativeUnit -Filter "DisplayName eq '$DisplayName'" -ErrorAction Stop + } } #endregion - - if ($null -eq $getValue) { - Write-Verbose -Message "Nothing with id {$id} was found" + Write-Verbose -Message "Could not find an Azure AD Administrative Unit with DisplayName {$DisplayName}" return $nullResult } - - Write-Verbose -Message "Found AU with id {$($getValue.id)}, DisplayName {$($getValue.DisplayName)}" - + $Id = $getValue.Id + Write-Verbose -Message "An Azure AD Administrative Unit with Id {$Id} and DisplayName {$DisplayName} was found." $results = @{ - #region resource generator code - Id = $getValue.Id Description = $getValue.Description DisplayName = $getValue.DisplayName Visibility = $getValue.Visibility + Id = $getValue.Id Ensure = 'Present' Credential = $Credential ApplicationId = $ApplicationId TenantId = $TenantId ApplicationSecret = $ApplicationSecret CertificateThumbprint = $CertificateThumbprint - ManagedIdentity = $ManagedIdentity.IsPresent + Managedidentity = $ManagedIdentity.IsPresent + #endregion + } + + if (-not [string]::IsNullOrEmpty($getValue.AdditionalProperties.MembershipType)) + { + $results.Add('MembershipType', $getValue.AdditionalProperties.MembershipType) + } + if (-not [string]::IsNullOrEmpty($getValue.AdditionalProperties.MembershipRule)) + { + $results.Add('MembershipRule', $getValue.AdditionalProperties.MembershipRule) } - if (-not [string]::IsNullOrEmpty($getValue.AdditionalProperties.membershipType)) + if (-not [string]::IsNullOrEmpty($getValue.AdditionalProperties.MembershipRuleProcessingState)) { - # only include details about membership if values are present - $results.Add('MembershipType', $getValue.AdditionalProperties.membershipType) - $results.Add('MembershipRule', $getValue.AdditionalProperties.membershipRule) - $results.Add('MembershipRuleProcessingState', $getValue.AdditionalProperties.membershipRuleProcessingState) + $results.Add('MembershipRuleProcessingState', $getValue.AdditionalProperties.MembershipRuleProcessingState) } - $memberSpec = $null - if ($getValue.AdditionalProperties.MembershipType -ne 'Dynamic') + write-verbose "AU {$DisplayName} MembershipType {$($results.MembershipType)}" + if ($results.MembershipType -ne 'Dynamic') { - $auMembers = Get-MgDirectoryAdministrativeUnitMember -AdministrativeUnitId $getValue.Id -All - if ($auMembers) + write-verbose "AU {$DisplayName} get Members" + [array]$auMembers = Get-MgDirectoryAdministrativeUnitMember -AdministrativeUnitId $getValue.Id -All + if ($auMembers.Count -gt 0) { + write-verbose "AU {$DisplayName} process $($auMembers.Count) members" $memberSpec = @() - foreach ($getMember in $auMembers) + foreach ($auMember in $auMembers) { - # get object regardless of type - $memberObject = Invoke-MgGraphRequest -Method Get -Uri "https://graph.microsoft.com/v1.0/directoryObjects/$($getMember.Id)" - switch -regex ([regex]::Escape($memberObject.'@odata.type')) + $member = @{} + $memberObject = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/directoryobjects/$($auMember.Id)" + if ($memberObject.'@odata.type' -match 'user') { - 'group' - { - $memberSpec += [pscustomobject]@{ - Identity = $memberObject.DisplayName; - Type = 'Group' - } - } - 'user' - { - $memberSpec += [pscustomobject]@{ - Identity = $memberObject.UserPrincipalName; - Type = 'User' - } - } - 'device' - { - $memberSpec += [pscustomobject]@{ - Identity = $memberObject.DisplayName; - Type = 'Device' - } - } + $member.Add('Identity', $memberObject.UserPrincipalName) + $member.Add('Type', 'User') } - } - $results.Add('Members', $memberSpec) - } - } - - $scopedRoleMemberSpec = $null - $auScopedRoleMembers = Get-MgDirectoryAdministrativeUnitScopedRoleMember -AdministrativeUnitId $getValue.Id -All - if ($auScopedRoleMembers) - { - $scopedRoleMemberSpec = @() - foreach ($getMember in $auScopedRoleMembers) - { - $roleObject = Get-MgDirectoryRole -DirectoryRoleId $getMember.RoleId - # get object regardless of type - $roleMemberObject = Invoke-MgGraphRequest -Method Get -Uri "https://graph.microsoft.com/v1.0/directoryObjects/$($getMember.RoleMemberInfo.Id)" - if ([regex]::Escape($roleMemberObject.'@odata.type') -match 'user') - { - $memberType = 'User' - $memberIdentity = $roleMemberObject.UserPrincipalName - } - else - { - if ([regex]::Escape($roleMemberObject.'@odata.type') -match 'group') + elseif ($memberObject.'@odata.type' -match 'group') { - $memberType = 'Group'; + $member.Add('Identity', $memberObject.DisplayName) + $member.Add('Type', 'Group') } else { - $memberType = 'ServicePrincipal'; + $member.Add('Identity', $memberObject.DisplayName) + $member.Add('Type', 'Device') } - $memberIdentity = $roleMemberObject.DisplayName - } - $scopedRoleMemberInfo = [pscustomobject]@{ - RoleName = $roleObject.DisplayName; - #RoleMemberInfo = @{ # avoid nested hashtable, can't handle it in Export - Identity = $memberIdentity - Type = $memberType - #} + write-verbose "AU {$DisplayName} member found: Type '$($member.Type)' identity '$($member.Identity)'" + $memberSpec += $member } - $scopedRoleMemberSpec += $scopedRoleMemberInfo + write-verbose "AU {$DisplayName} add Members to results" + $results.Add('Members', $memberSpec) } - $results.Add('ScopedRoleMembers', $scopedRoleMemberSpec) } - <# - # Extensions are still too unwieldy - $auExtensions = Get-MgDirectoryAdministrativeUnitExtension -AdministrativeUnitId $getValue.Id -All - $extensionsSpec = $null - if ($auExtensions) + write-verbose "AU {$DisplayName} get Scoped Role Members" + $ErrorActionPreference = "Stop" + [array]$auScopedRoleMembers = Get-MgDirectoryAdministrativeUnitScopedRoleMember -AdministrativeUnitId $getValue.Id -All + if ($auScopedRoleMembers.Count -gt 0) { - $extensionsSpec = @() - $extensionDef = @{Id = $auExtensions.Id} - foreach ($auExtension in $auExtensions) + write-verbose "AU {$DisplayName} process $($auScopedRoleMembers.Count) scoped role members" + $scopedRoleMemberSpec = @() + foreach ($auScopedRoleMember in $auScopedRoleMembers) { - if ($auExtension.Properties -and $auExtension.Properties.Count % 2 -ne 0) - { - throw "AU {$($getValue.DisplayName)] has extension {$($auExtension.Id)} with properties without values" + write-verbose "AU {$DisplayName} verify RoleId {$($auScopedRoleMember.RoleId)}" + $roleObject = Get-MgDirectoryRole -DirectoryRoleId $auScopedRoleMember.RoleId -ErrorAction Stop + $scopedRoleMember = @{ + RoleName = $roleObject.DisplayName + Type = $null + Identity = $null + } + write-verbose "AU {$DisplayName} verify RoleMemberInfo.Id {$($auScopedRoleMember.RoleMemberInfo.Id)}" + $memberObject = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/directoryobjects/$($auScopedRoleMember.RoleMemberInfo.Id)" + write-verbose "AU {$DisplayName} @odata.Type={$($memberObject.'@odata.type')}" + if (($memberObject.'@odata.type') -match 'user') + { + write-verbose "AU {$DisplayName} UPN = {$($memberObject.UserPrincipalName)}" + $scopedRoleMember.Identity = $memberObject.UserPrincipalName + $scopedRoleMember.Type = 'User' + } + elseif (($memberObject.'@odata.type') -match 'group') + { + write-verbose "AU {$DisplayName} Group = {$($memberObject.DisplayName)}" + $scopedRoleMember.Identity = $memberObject.DisplayName + $scopedRoleMember.Type = 'Group' } - # address MSFT_KeyValuePair as an array containing key1,value1,key2,value2 etc - # see https://forums.powershell.org/t/hashtable-as-resource-parameter/2962/3 - # and https://learn.microsoft.com/en-us/answers/questions/440415/passing-a-key-value-pair-list-to-a-powershell-scri.html - for ($p = 0; $p -lt $auExtension.Properties.Count; $p = $p + 2) + else { - $extensionDef.Add($auExtension.Properties[$p], $auExtension.Properties[$p+1]) + write-verbose "AU {$DisplayName} SPN = {$($memberObject.DisplayName)}" + $scopedRoleMember.Identity = $memberObject.DisplayName + $scopedRoleMember.Type = 'ServicePrincipal' } + write-verbose "AU {$DisplayName} scoped role member: RoleName '$($scopedRoleMember.RoleName)' Type '$($scopedRoleMember.Type)' Identity '$($scopedRoleMember.Identity)'" + $scopedRoleMemberSpec += $scopedRoleMember } - $extensionsSpec += $extensionDef + write-verbose "AU {$DisplayName} add $($scopedRoleMemberSpec.Count) ScopedRoleMembers to results" + $results.Add('ScopedRoleMembers', $scopedRoleMemberSpec) } - $results.Add("Extensions", $extensionsSpec) - #> - return [System.Collections.Hashtable]$results + write-verbose "AU {$DisplayName} return results" + return [System.Collections.Hashtable] $results } catch { @@ -276,33 +249,37 @@ function Set-TargetResource [CmdletBinding()] param ( - [Parameter(Mandatory = $true)] - [System.String] - $DisplayName, - + #region resource generator code [Parameter()] [System.String] - $Id, + $Description, - [Parameter()] + [Parameter(Mandatory=$true)] [System.String] - $Description, + $DisplayName, [Parameter()] [validateset('Public', 'HiddenMembership')] [System.String] $Visibility, + [Parameter()] + [System.String] + $Id, + [Parameter()] [validateset('Assigned', 'Dynamic')] - [System.String]$MembershipType, + [System.String] + $MembershipType, [Parameter()] - [System.String]$MembershipRule, + [System.String] + $MembershipRule, [Parameter()] [validateset('Paused', 'On')] - [System.String]$MembershipRuleProcessingState, + [System.String] + $MembershipRuleProcessingState, [Parameter()] [Microsoft.Management.Infrastructure.CimInstance[]] @@ -312,19 +289,11 @@ function Set-TargetResource [Microsoft.Management.Infrastructure.CimInstance[]] $ScopedRoleMembers, - <# - [Parameter()] - [Microsoft.Management.Infrastructure.CimInstance[]] - $Extensions, - #> - - #endregion - [Parameter(Mandatory = $true)] [System.String] [ValidateSet('Absent', 'Present')] - $Ensure = 'Present', + $Ensure = $true, [Parameter()] [System.Management.Automation.PSCredential] @@ -351,15 +320,6 @@ function Set-TargetResource $ManagedIdentity ) - # Note: Graph names basic cmdlets xxx-MgDirectoryAdministrativeUnit(xxx) - # but the beta profile names latest cmdlets xxx-MgDirectoryAdministrativeUnit(xxx) - # ONLY the beta cmdlets support the preview enabling MembershipType and MembershipRuleProcessingState - # NB: Usage of these params require that the corresponding AAD preview feature is enabled - - $ConnectionMode = New-M365DSCConnection -Workload 'MicrosoftGraph' ` - -InboundParameters $PSBoundParameters ` - -ProfileName 'v1.0' - #Ensure the proper dependencies are installed in the current environment. Confirm-M365DSCDependencies @@ -374,98 +334,82 @@ function Set-TargetResource $currentInstance = Get-TargetResource @PSBoundParameters - $currentParameters = ([hashtable]$PSBoundParameters).Clone() - $currentParameters.Remove('Ensure') | Out-Null - $currentParameters.Remove('Credential') | Out-Null - $currentParameters.Remove('ApplicationId') | Out-Null - $currentParameters.Remove('ApplicationSecret') | Out-Null - $currentParameters.Remove('TenantId') | Out-Null - $currentParameters.Remove('CertificateThumbprint') | Out-Null - $currentParameters.Remove('ManagedIdentity') | Out-Null + $PSBoundParameters.Remove('Ensure') | Out-Null + $PSBoundParameters.Remove('Credential') | Out-Null + $PSBoundParameters.Remove('ApplicationId') | Out-Null + $PSBoundParameters.Remove('ApplicationSecret') | Out-Null + $PSBoundParameters.Remove('TenantId') | Out-Null + $PSBoundParameters.Remove('CertificateThumbprint') | Out-Null + $PSBoundParameters.Remove('ManagedIdentity') | Out-Null + $PSBoundParameters.Remove('Verbose') | Out-Null $backCurrentMembers = $currentInstance.Members $backCurrentScopedRoleMembers = $currentInstance.ScopedRoleMembers - #$backCurrentExtensions = $currentInstance.Extensions - $currentInstance.Remove('Members') | Out-Null - $currentInstance.Remove('ScopedRoleMembers') | Out-Null - - if ($MembershipType -eq 'Dynamic' -and $Members) - { - throw "AU {$($DisplayName)}: Members is not allowed when MembershipType is Dynamic" - } if ($Ensure -eq 'Present') { - $CreateParameters = $currentParameters.Clone() - - <# When using -BodyParameter it is not necessary to use AdditionalProperties for Dynamic AU params - if ($CreateParameters.Containskey('MembershipType') -or $CreateParameters.Containskey('MembershipRule') -or $CreateParameters.Containskey('MembershipRuleProcessingState')) + if ($MembershipType -eq 'Dynamic' -and $Members.Count -gt 0) { - $CreateParameters.Remove('MembershipType') | Out-Null - $CreateParameters.Remove('MembershipRule') | Out-Null - $CreateParameters.Remove('MembershipRuleProcessingState') | Out-Null - $CreateParameters.Add('AdditionalProperties', @{}) - if (-not [System.String]::IsNullOrEmpty($MembershipType)) - { - $CreateParameters.AdditionalProperties.Add('MembershipType', $MembershipType) - } - if (-not [System.String]::IsNullOrEmpty($MembershipType)) - { - $CreateParameters.AdditionalProperties.Add('MembershipRule', $MembershipRule) - } - if (-not [System.String]::IsNullOrEmpty($MembershipRuleProcessingState)) - { - $CreateParameters.AdditionalProperties.Add('MembershipRuleProcessingState', $MembershipRuleProcessingState) - } + throw "AU {$($DisplayName)}: Members is not allowed when MembershipType is Dynamic" } - #> + $CreateParameters = ([Hashtable]$PSBoundParameters).clone() + $CreateParameters = Rename-M365DSCCimInstanceParameter -Properties $CreateParameters + $CreateParameters.Remove('Id') | Out-Null - <# skipped - no use for hashtables - foreach ($key in ($CreateParameters.clone()).Keys) + $keys=(([Hashtable]$CreateParameters).clone()).Keys + foreach($key in $keys) { - if ($CreateParameters[$key].getType().Fullname -like '*CimInstance*') + if($null -ne $CreateParameters.$key -and $CreateParameters.$key.getType().Name -like "*cimInstance*") { - $CreateParameters[$key] = Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $CreateParameters[$key] + $CreateParameters.$key= Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $CreateParameters.$key } } - #> - - $memberSpecification = @() - if ($MembershipType -ne 'Dynamic') + $memberSpecification = $null + if ($CreateParameters.MembershipType -ne 'Dynamic' -and $CreateParameters.Members.Count -gt 0) { - # Resolve Members Type/Identity to user or group id - foreach ($Member in $currentParameters.Members) + $memberSpecification = @() + write-verbose "AU {$DisplayName} process $($CreateParameters.Members.Count) Members" + foreach ($member in $CreateParameters.Members) { - if ($Member.Type -eq 'User') + write-verbose "AU {$DisplayName} member Type '$($member.Type)' Identity '$($member.Identity)'" + if ($member.Type -eq 'User') { - $memberIdentity = Get-MgUser -Filter "UserPrincipalName eq '$($Member.Identity)'" -ErrorAction Stop + $memberIdentity = Get-MgUser -Filter "UserPrincipalName eq '$($member.Identity)'" -ErrorAction Stop if ($memberIdentity) { - $memberSpecification += [pscustomobject]@{Type="$($Member.Type)s";Id = $memberIdentity.Id } + $memberSpecification += [pscustomobject]@{Type="$($member.Type)s";Id = $memberIdentity.Id } } else { - throw "AU {$($DisplayName)}: User {$($Member.Identity)} does not exist" + throw "AU {$($DisplayName)}: User {$($member.Identity)} does not exist" } } - elseif ($Member.Type -eq 'Group') + elseif ($member.Type -eq 'Group') { - $memberIdentity = Get-MgGroup -Filter "DisplayName eq '$($Member.Identity)'" -ErrorAction Stop + $memberIdentity = Get-MgGroup -Filter "DisplayName eq '$($member.Identity)'" -ErrorAction Stop if ($memberIdentity) { - $memberSpecification += [pscustomobject]@{Type="$($Member.Type)s";Id = $memberIdentity.Id } + if ($memberIdentity.Count -gt 1) + { + throw "AU {$($DisplayName)}: Group displayname {$($member.Identity)} is not unique" + } + $memberSpecification += [pscustomobject]@{Type="$($member.Type)s";Id = $memberIdentity.Id } } else { - throw "AU {$($DisplayName)}: Group {$($Member.Identity)} does not exist" + throw "AU {$($DisplayName)}: Group {$($member.Identity)} does not exist" } } - elseif ($Member.Type -eq 'Device') + elseif ($member.Type -eq 'Device') { - $memberIdentity = Get-MgDevice -Filter "DisplayName eq '$($Member.Identity)'" -ErrorAction Stop + $memberIdentity = Get-MgDevice -Filter "DisplayName eq '$($member.Identity)'" -ErrorAction Stop if ($memberIdentity) { - $memberSpecification += [pscustomobject]@{Type="$($Member.Type)s";Id = $memberIdentity.Id } + if ($memberIdentity.Count -gt 1) + { + throw "AU {$($DisplayName)}: Device displayname {$($member.Identity)} is not unique" + } + $memberSpecification += [pscustomobject]@{Type="$($member.Type)s";Id = $memberIdentity.Id } } else { @@ -482,53 +426,36 @@ function Set-TargetResource $CreateParameters.Remove('Members') | Out-Null # Resolve ScopedRoleMembers Type/Identity to user, group or service principal - if ($currentParameters.ScopedRoleMembers) + if ($CreateParameters.ScopedRoleMembers) { - #write-verbose "AU {$DisplayName}: Set-TargetResource, Convert ScopedRoleMembers to Hashtable(s)" - $testScopedRoleMembers = Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $ScopedRoleMembers <#@() - foreach ($scopedRoleMember in $testScopedRoleMembers) - { - $testScopedRoleMember = @{} - foreach ($key in $scopedRoleMember.clone().Keys) - { - if ($scopedRoleMember[$key].GetType().FullName -like "*CimInstance*") - { - $testScopedRoleMember.Add($key, (Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $scopedRoleMember.$key)) - } - else - { - $testScopedRoleMember.Add($key, $scopedRoleMember.$key) - } - } - $testScopedRoleMembers += $testScopedRoleMember - } - #> + write-verbose "AU {$DisplayName} process $($CreateParameters.ScopedRoleMembers.Count) ScopedRoleMembers" $scopedRoleMemberSpecification = @() - foreach ($roleMember in $testScopedRoleMembers) + foreach ($roleMember in $CreateParameters.ScopedRoleMembers) { - if ($roleMember.Contains('RoleMemberInfo')) - { - # flatten object if old schema-def with nested hashtable was used as input - if (string]::IsNullOrEmpty($roleMember.Identity) -and $roleMember.RoleMemberInfo.Identity) - { - $roleMember.Add('Identity', $roleMember.RoleMemberInfo.Identity) - } - if (string]::IsNullOrEmpty($roleMember.Type) -and $roleMember.RoleMemberInfo.Type) + write-verbose "AU {$DisplayName} member: role '$($roleMember.RoleName)' type '$($roleMember.Type)' identity $($roleMember.Identity)" + try { + $roleObject = Get-MgDirectoryRole -Filter "DisplayName eq '$($roleMember.RoleName)'" -ErrorAction stop + write-verbose "AU {$DisplayName} role is enabled" + } + catch { + write-verbose -Message "Azure AD role {$($rolemember.RoleName)} is not enabled" + $roleTemplate = Get-MgdirectoryRoleTemplate -Filter "DisplayName eq '$($roleMember.RoleName)'" -ErrorAction Stop + if ($null -ne $roleTemplate) { - $roleMember.Add('Type', $roleMember.RoleMemberInfo.Type) + write-verbose -Message "Enable Azure AD role {$($rolemember.RoleName)} with id {$($roleTemplate.Id)}" + $roleObject = New-MgDirectoryRole -RoleTemplateId $roleTemplate.Id -ErrorAction Stop } } - $roleObject = Get-MgDirectoryRole -Filter "DisplayName eq '$($roleMember.RoleName)'" -ErrorAction stop if ($null -eq $roleObject) { - throw "AU {$($DisplayName)}: RoleName {$($roleMember.RoleName)} does not exist or is not enabled" + throw "AU {$($DisplayName)}: RoleName {$($roleMember.RoleName)} does not exist" } if ($roleMember.Type -eq 'User') { $roleMemberIdentity = Get-MgUser -Filter "UserPrincipalName eq '$($roleMember.Identity)'" -ErrorAction Stop if ($null -eq $roleMemberIdentity) { - throw "AU {$($DisplayName)}: Scoped Role User {$($roleMember.Identity)} does not exist" + throw "AU {$($DisplayName)}: Scoped Role User {$($roleMember.Identity)} for role {$($roleMember.RoleName)} does not exist" } } elseif ($roleMember.Type -eq 'Group') @@ -536,7 +463,7 @@ function Set-TargetResource $roleMemberIdentity = Get-MgGroup -Filter "displayName eq '$($roleMember.Identity)'" -ErrorAction Stop if ($null -eq $roleMemberIdentity) { - throw "AU {$($DisplayName)}: Scoped Role Group {$($roleMember.Identity)} does not exist" + throw "AU {$($DisplayName)}: Scoped Role Group {$($roleMember.Identity)} for role {$($roleMember.RoleName)} does not exist" } } elseif ($roleMember.Type -eq 'ServicePrincipal') @@ -544,7 +471,7 @@ function Set-TargetResource $roleMemberIdentity = Get-MgServicePrincipal -Filter "displayName eq '$($roleMember.Identity)'" -ErrorAction Stop if ($null -eq $roleMemberIdentity) { - throw "AU {$($DisplayName)}: Scoped Role ServicePrincipal {$($roleMember.Identity)} does not exist" + throw "AU {$($DisplayName)}: Scoped Role ServicePrincipal {$($roleMember.Identity)} for role {$($roleMember.RoleName)} does not exist" } } else @@ -553,7 +480,7 @@ function Set-TargetResource } if ($roleMemberIdentity.Count -gt 1) { - throw "AU {$($DisplayName)}: ScopedRoleMember {$($roleMember.RoleName)}, Identity {$($roleMember.Identity)} of type {$($roleMember.Type)} is not unique" + throw "AU {$($DisplayName)}: ScopedRoleMember for role {$($roleMember.RoleName)}: $($roleMember.Type) {$($roleMember.Identity)} is not unique" } $scopedRoleMemberSpecification += @{ RoleId = $roleObject.Id @@ -565,38 +492,26 @@ function Set-TargetResource # ScopedRoleMember-info is added after the AU is created } $CreateParameters.Remove('ScopedRoleMembers') | Out-Null + } if ($Ensure -eq 'Present' -and $currentInstance.Ensure -eq 'Absent') { - Write-Verbose -Message "Creating AU {$DisplayName}" - - #$CreateParameters = Rename-M365DSCCimInstanceODataParameter -Properties $CreateParameters + Write-Verbose -Message "Creating an Azure AD Administrative Unit with DisplayName {$DisplayName}" - $CreateParameters.Remove('Id') | Out-Null - $CreateParameters.Remove('Verbose') | Out-Null + #region resource generator code + $policy=New-MgDirectoryAdministrativeUnit -BodyParameter $CreateParameters - <# - foreach ($key in ($CreateParameters.clone()).Keys) + if ($MembershipType -ne 'Dynamic') { - if ($CreateParameters[$key].getType().Fullname -like '*CimInstance*') + foreach ($member in $memberSpecification) { - $CreateParameters[$key] = Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $CreateParameters[$key] - } - } - #> - - #region resource generator code - $policy = New-MgDirectoryAdministrativeUnit -BodyParameter $CreateParameters + $memberBodyParam = @{ + '@odata.id' = "https://graph.microsoft.com/v1.0/$($member.Type)/$($member.Id)" + } - #endregion - foreach ($member in $memberSpecification) - { - $memberBodyParam = @{ - '@odata.id' = "https://graph.microsoft.com/v1.0/$($member.Type)/$($member.Id)" + New-MgDirectoryAdministrativeUnitMemberByRef -AdministrativeUnitId $policy.Id -BodyParameter $memberBodyParam } - - New-MgDirectoryAdministrativeUnitMemberByRef -AdministrativeUnitId $policy.Id -BodyParameter $memberBodyParam } foreach ($scopedRoleMember in $scopedRoleMemberSpecification) @@ -604,200 +519,100 @@ function Set-TargetResource New-MgDirectoryAdministrativeUnitScopedRoleMember -AdministrativeUnitId $policy.Id -BodyParameter $scopedRoleMember } + + #endregion } elseif ($Ensure -eq 'Present' -and $currentInstance.Ensure -eq 'Present') { - Write-Verbose -Message "Updating AU {$DisplayName}" + Write-Verbose -Message "Updating the Azure AD Administrative Unit with Id {$($currentInstance.Id)}" - $UpdateParameters = $currentParameters.Clone() - #$UpdateParameters = Rename-M365DSCCimInstanceODataParameter -Properties $UpdateParameters + $UpdateParameters = ([Hashtable]$PSBoundParameters).clone() + $UpdateParameters = Rename-M365DSCCimInstanceParameter -Properties $UpdateParameters $UpdateParameters.Remove('Id') | Out-Null - $UpdateParameters.Remove('Verbose') | Out-Null - <# - foreach ($key in ($UpdateParameters.clone()).Keys) + $keys=(([Hashtable]$UpdateParameters).clone()).Keys + foreach($key in $keys) { - if ($UpdateParameters[$key].getType().Fullname -like '*CimInstance*') + if($null -ne $UpdateParameters.$key -and $UpdateParameters.$key.getType().Name -like "*cimInstance*") { - $UpdateParameters[$key] = Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $UpdateParameters[$key] + $UpdateParameters.$key = Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $UpdateParameters.$key } } - #> - #$UpdateParameters.Remove('Extensions') | Out-Null + $requestedMembers = $UpdateParameters.Members $UpdateParameters.Remove('Members') | Out-Null + $requestedScopedRoleMembers = $UpdateParameters.ScopedRoleMembers $UpdateParameters.Remove('ScopedRoleMembers') | Out-Null - #$UpdateParameters.Remove('Visibility') | Out-Null - - <# When using -BodyParameter it is not necessary to use AdditionalProperties for Dynamic AU params - if ($UpdateParameters.Containskey('MembershipType') -or $UpdateParameters.Containskey('MembershipRule') -or $UpdateParameters.Containskey('MembershipRuleProcessingState')) - { - $UpdateParameters.Remove('MembershipType') | Out-Null - $UpdateParameters.Remove('MembershipRule') | Out-Null - $UpdateParameters.Remove('MembershipRuleProcessingState') | Out-Null - $UpdateParameters.Add('AdditionalProperties', @{}) - if (-not [System.String]::IsNullOrEmpty($MembershipType)) - { - $UpdateParameters.AdditionalProperties.Add('MembershipType', $MembershipType) - } - if (-not [System.String]::IsNullOrEmpty($MembershipType)) - { - $UpdateParameters.AdditionalProperties.Add('MembershipRule', $MembershipRule) - } - if (-not [System.String]::IsNullOrEmpty($MembershipRuleProcessingState)) - { - $UpdateParameters.AdditionalProperties.Add('MembershipRuleProcessingState', $MembershipRuleProcessingState) - } - } - #> - - # when updating the resource, update the AU first and its members (if any) afterwards. - # The AU MembershipType may have changed from Dynamic to Static and that change has to be implemented before explicitly adding members #region resource generator code - Update-MgDirectoryAdministrativeUnit -BodyParameter $UpdateParameters ` - -AdministrativeUnitId $currentInstance.Id - + Update-MgDirectoryAdministrativeUnit -AdministrativeUnitId $currentInstance.Id -BodyParameter $UpdateParameters #endregion - if ($MembershipType -ne 'Dynamic' -and ($Members -or $backCurrentMembers)) + if ($MembershipType -ne 'Dynamic') { - $currentMembersValue = @() - if ($backCurrentMembers.Length -ne 0) - { - $currentMembersValue = $backCurrentMembers - } - if ($null -eq $currentMembersValue) - { - $currentMembersValue = @() - } - $desiredMembersValue = $Members - if ($null -eq $desiredMembersValue) - { - $desiredMembersValue = @() - } - write-verbose "AU {$DisplayName} Update Members: Current members: $($currentMembersValue.Identity -join ', ')" - write-verbose " Desired members: $($desiredMembersValue.Identity -join ', ')" - $membersDiff = Compare-Object -ReferenceObject $currentMembersValue -DifferenceObject $desiredMembersValue -Property Identity, Type -IncludeEqual - write-verbose " # compare results : $($membersDiff.Count -gt 0)" - foreach ($diff in $membersDiff) + if ($backCurrentMembers.Count -gt 0 -or $requestedMembers.Count -gt 0) { - if ($diff.Type -eq 'User') - { - $memberObject = Get-MgUser -Filter "UserPrincipalName eq '$($diff.Identity)'" - $memberType = 'users' - } - elseif ($diff.Type -eq 'Group') + $currentMembers = @() + foreach ($member in $backCurrentMembers) { - $memberObject = Get-MgGroup -Filter "DisplayName eq '$($diff.Identity)'" - $membertype = 'groups' + $currentMembers += [pscustomobject]@{Type=$member.Type; Identity = $member.Identity} } - elseif ($diff.Type -eq 'Device') + $desiredMembers = @() + foreach ($member in $requestedMembers) { - $memberObject = Get-MgDevice -Filter "DisplayName eq '$($diff.Identity)'" - $membertype = 'devices' + $desiredMembers += [pscustomobject]@{Type=$member.Type; Identity = $member.Identity} } - else + $membersDiff = Compare-Object -ReferenceObject $currentMembers -DifferenceObject $desiredMembers -Property Identity, Type + foreach ($diff in $membersDiff) { - if ($diff.Identity) + if ($diff.Type -eq 'User') { - # throw if a *new* member has been specified with invalid type - throw "AU {$($DisplayName)}: Member {$($diff.Identity)} has invalid type {$($diff.Type)}" + $memberObject = Get-MgUser -Filter "UserPrincipalName eq '$($diff.Identity)'" + $memberType = 'users' + } + elseif ($diff.Type -eq 'Group') + { + $memberObject = Get-MgGroup -Filter "DisplayName eq '$($diff.Identity)'" + $membertype = 'groups' + } + elseif ($diff.Type -eq 'Device') + { + $memberObject = Get-MgDevice -Filter "DisplayName eq '$($diff.Identity)'" + $membertype = 'devices' } else { - write-verbose "Compare Members - skip processing blank Identity" - continue # don't process, continue to next diff if any + # a *new* member has been specified with invalid type + throw "AU {$($DisplayName)}: Member {$($diff.Identity)} has invalid type {$($diff.Type)}" } - } - if ($null -eq $memberObject) - { - throw "AU member {$($diff.Identity)} does not exist as a $($diff.Type)" - } - if ($memberObject.Count -gt 1) - { - throw "AU member {$($diff.Identity)} is not a unique $($diff.Type.ToLower()) (Count=$($memberObject.Count))" - } - if ($diff.SideIndicator -eq '=>') - { - Write-Verbose -Message "Adding new member {$($diff.Identity)}, type {$($diff.Type)} to Administrative Unit {$DisplayName}" - - $memberBodyParam = @{ - '@odata.id' = "https://graph.microsoft.com/v1.0/$memberType/$($memberObject.Id)" + if ($null -eq $memberObject) + { + throw "AU member {$($diff.Identity)} does not exist as a $($diff.Type)" } - New-MgDirectoryAdministrativeUnitMemberByRef -AdministrativeUnitId ($currentInstance.Id) -BodyParameter $memberBodyParam | Out-Null - } - elseif ($diff.SideIndicator -eq '<=') - { - Write-Verbose -Message "Removing member {$($diff.Identity)}, type {$($diff.Type)} from Administrative Unit {$DisplayName}" - Remove-MgDirectoryAdministrativeUnitMemberByRef -AdministrativeUnitId ($currentInstance.Id) -DirectoryObjectId ($memberObject.Id) | Out-Null - } - else - { - Write-Verbose -Message "Unchanged member {$($diff.Identity)}, type {$($diff.Type)} of Administrative Unit {$DisplayName}" - } - } - } - - <# - if ($Extensions -or $backCurrentExtensions) - { - $currentExtensionsValue = @() - if ($currentInstance.Extensions.Length -ne 0) - { - $currentExtensionsValue = $backCurrentExtensions - } - if ($null -eq $currentExtensionsValue) - { - $currentExtensionsValue = @() - } - $desiredExtensionsValue = $Extensions - if ($null -eq $desiredExtensionsValue) - { - $desiredExtensionsValue = @() - } - $membersDiff = Compare-Object -ReferenceObject $currentExtensionsValue -DifferenceObject $desiredExtensionsValue -Property Id - foreach ($diff in $membersDiff) - { - if ($diff.SideIndicator -eq '=>') - { - Write-Verbose -Message "Adding new extension {$($diff.InputObject.Id)} to Administrative Unit {$($currentInstance.DisplayName)}" - $additionalPropertiesArg = @{} - if ($diff.InputObject.Properties.Count -gt 0) + if ($memberObject.Count -gt 1) { - $additionalPropertiesArg.AdditionalProperties = @{} - for ($p = 0; $p -lt $diff.InputObject.Properties.Count; $p = $p + 1) - { - $additionalPropertiesArg.AdditionalProperties.Add($diff.InputObject.Properties[$p], $diff.InputObject.Properties[$p+1]) - } + throw "AU member {$($diff.Identity)} is not a unique $($diff.Type.ToLower()) (Count=$($memberObject.Count))" } - New-MgDirectoryAdministrativeUnitExtension -AdministrativeUnitId ($currentInstance.Id) @additionalPropertiesArg -BodyParameter @{ExtensionId = $diff.InputObject.Id} | Out-Null - } - elseif ($diff.SideIndicator -eq '<=') - { - Write-Verbose -Message "Removing extension {$($diff.InputObject.Id)} from Administrative Unit {$($currentInstance.DisplayName)}" - Remove-MgDirectoryAdministrativeUnitExtension -AdministrativeUnitId ($currentInstance.Id) -ExtensionId ($diff.InputObject.Id) | Out-Null - } - else - { - #update AU Extension to use specified Properties only - $additionalPropertiesArg = @{} - if ($diff.InputObject.Properties.Count -gt 0) + if ($diff.SideIndicator -eq '=>') { - $additionalPropertiesArg.AdditionalProperties = @{} - for ($p = 0; $p -lt $diff.InputObject.Properties.Count; $p = $p + 1) - { - $additionalPropertiesArg.AdditionalProperties.Add($diff.InputObject.Properties[$p], $diff.InputObject.Properties[$p+1]) + Write-Verbose "AdministrativeUnit {$DisplayName} Adding member {$($diff.Identity)}, type {$($diff.Type)}" + + $memberBodyParam = @{ + '@odata.id' = "https://graph.microsoft.com/v1.0/$memberType/$($memberObject.Id)" } + New-MgDirectoryAdministrativeUnitMemberByRef -AdministrativeUnitId ($currentInstance.Id) -BodyParameter $memberBodyParam | Out-Null + } + else + { + Write-Verbose "Administrative Unit {$DisplayName} Removing member {$($diff.Identity)}, type {$($diff.Type)}" + Remove-MgDirectoryAdministrativeUnitMemberByRef -AdministrativeUnitId ($currentInstance.Id) -DirectoryObjectId ($memberObject.Id) | Out-Null } - Update-MgDirectoryAdministrativeUnitExtension -AdministrativeUnitId ($currentInstance.Id) -ExtensionId $diff.InputObject.Id @additionalPropertiesArg } } } - #> - if ($ScopedRoleMembers -or $backCurrentScopedRoleMembers) + if ($backCurrentScopedRoleMembers.Count -gt 0 -or $requestedScopedRoleMembers.Count -gt 0) { if ($backCurrentScopedRoleMembers.Length -ne 0) { @@ -807,20 +622,19 @@ function Set-TargetResource { $currentScopedRoleMembersValue = @() } - $desiredScopedRoleMembersValue = $ScopedRoleMembers + $desiredScopedRoleMembersValue = $requestedScopedRoleMembers if ($null -eq $desiredScopedRoleMembersValue) { $desiredScopedRoleMembersValue = @() } - <# # not necessary to flatten objects to compare when nested CimInstances are avoided in Schema/Get-TargetResource: $compareCurrentScopedRoleMembersValue = @() foreach ($roleMember in $currentScopedRoleMembersValue) { $compareCurrentScopedRoleMembersValue += [pscustomobject]@{ RoleName = $roleMember.RoleName - Identity = $roleMember.RoleMemberInfo.Identity - Type = $roleMember.RoleMemberInfo.Type + Identity = $roleMember.Identity + Type = $roleMember.Type } } $compareDesiredScopedRoleMembersValue = @() @@ -828,15 +642,14 @@ function Set-TargetResource { $compareDesiredScopedRoleMembersValue += [pscustomobject]@{ RoleName = $roleMember.RoleName - Identity = $roleMember.RoleMemberInfo.Identity - Type = $roleMember.RoleMemberInfo.Type + Identity = $roleMember.Identity + Type = $roleMember.Type } } + write-verbose "AU {$DisplayName} Update ScopedRoleMembers: Current members: $($compareCurrentScopedRoleMembersValue.Identity -join ', ')" + write-verbose " Desired members: $($compareDesiredScopedRoleMembersValue.Identity -join ', ')" $scopedRoleMembersDiff = Compare-Object -ReferenceObject $compareCurrentScopedRoleMembersValue -DifferenceObject $compareDesiredScopedRoleMembersValue -Property RoleName, Identity, Type - #> - write-verbose "AU {$DisplayName} Update ScopedRoleMembers: Current members: $($currentScopedRoleMembersValue.Identity -join ', ')" - write-verbose " Desired members: $($desiredScopedRoleMembersValue.Identity -join ', ')" - $scopedRoleMembersDiff = Compare-Object -ReferenceObject $CurrentScopedRoleMembersValue -DifferenceObject $DesiredScopedRoleMembersValue -Property RoleName, Identity, Type -IncludeEqual + # $scopedRoleMembersDiff = Compare-Object -ReferenceObject $CurrentScopedRoleMembersValue -DifferenceObject $DesiredScopedRoleMembersValue -Property RoleName, Identity, Type write-verbose " # compare results : $($scopedRoleMembersDiff.Count -gt 0)" foreach ($diff in $scopedRoleMembersDiff) @@ -881,7 +694,7 @@ function Set-TargetResource $roleObject = Get-MgDirectoryRole -Filter "DisplayName eq '$($diff.RoleName)'" if ($null -eq $roleObject) { - throw "AU Scoped Role {$($diff.RoleName)} does not exist as an Azure AD role" + throw "AU {$DisplayName} Scoped Role {$($diff.RoleName)} does not exist as an Azure AD role" } } if ($diff.SideIndicator -eq '=>') @@ -897,37 +710,24 @@ function Set-TargetResource # addition of scoped rolemember may throw if role is not supported as a scoped role New-MgDirectoryAdministrativeUnitScopedRoleMember -AdministrativeUnitId ($currentInstance.Id) -BodyParameter $scopedRoleMemberParam -ErrorAction Stop | Out-Null } - elseif ($diff.SideIndicator -eq '<=') + else { if (-not [string]::IsNullOrEmpty($diff.Rolename)) { Write-Verbose -Message "Removing scoped role {$($diff.RoleName)} member {$($diff.Identity)}, type {$($diff.Type)} from Administrative Unit {$DisplayName}" $scopedRoleMemberObject = Get-MgDirectoryAdministrativeUnitScopedRoleMember -AdministrativeUnitId ($currentInstance.Id) -All | Where-Object -FilterScript { $_.RoleId -eq $roleObject.Id -and $_.RoleMemberInfo.Id -eq $memberObject.Id } - Remove-MgDirectoryAdministrativeUnitScopedRoleMember -AdministrativeUnitId ($currentInstance.Id) -ScopedRoleMembershipId $scopedRoleMemberObject.Id | Out-Null + Remove-MgDirectoryAdministrativeUnitScopedRoleMember -AdministrativeUnitId ($currentInstance.Id) -ScopedRoleMembershipId $scopedRoleMemberObject.Id -ErrorAction Stop | Out-Null } } - else - { - Write-Verbose -Message "Unchanged scoped role {$($diff.RoleName)} member {$($diff.Identity)}, type {$($diff.Type)} in Administrative Unit {$DisplayName}" - } } } - } elseif ($Ensure -eq 'Absent' -and $currentInstance.Ensure -eq 'Present') { - Write-Verbose -Message "Removing AU {$DisplayName}" - - - #region resource generator code - #endregion - - - + Write-Verbose -Message "Removing the Azure AD Administrative Unit with Id {$($currentInstance.Id)}" #region resource generator code Remove-MgDirectoryAdministrativeUnit -AdministrativeUnitId $currentInstance.Id #endregion - } } @@ -937,33 +737,36 @@ function Test-TargetResource [OutputType([System.Boolean])] param ( - [Parameter(Mandatory = $true)] - [System.String] - $DisplayName, - - [Parameter()] - [System.String] - $Id, - + #region resource generator code [Parameter()] [System.String] $Description, + [Parameter(Mandatory=$true)] + [System.String] + $DisplayName, [Parameter()] [validateset('Public', 'HiddenMembership')] [System.String] $Visibility, + [Parameter()] + [System.String] + $Id, + [Parameter()] [validateset('Assigned', 'Dynamic')] - [System.String]$MembershipType, + [System.String] + $MembershipType, [Parameter()] - [System.String]$MembershipRule, + [System.String] + $MembershipRule, [Parameter()] [validateset('Paused', 'On')] - [System.String]$MembershipRuleProcessingState, + [System.String] + $MembershipRuleProcessingState, [Parameter()] [Microsoft.Management.Infrastructure.CimInstance[]] @@ -973,16 +776,12 @@ function Test-TargetResource [Microsoft.Management.Infrastructure.CimInstance[]] $ScopedRoleMembers, - <# - [Parameter()] - [Microsoft.Management.Infrastructure.CimInstance[]] - $Extensions, - #> + #endregion [Parameter(Mandatory = $true)] [System.String] [ValidateSet('Absent', 'Present')] - $Ensure = 'Present', + $Ensure = $true, [Parameter()] [System.Management.Automation.PSCredential] @@ -1021,140 +820,56 @@ function Test-TargetResource Add-M365DSCTelemetryEvent -Data $data #endregion - Write-Verbose -Message "Testing configuration of {$id}" + Write-Verbose -Message "Testing configuration of the Azure AD Administrative Unit with Id {$Id} and DisplayName {$DisplayName}" $CurrentValues = Get-TargetResource @PSBoundParameters $ValuesToCheck = ([Hashtable]$PSBoundParameters).clone() - if ($CurrentValues.Ensure -eq 'Absent' -and $Ensure -eq 'Present') + if ($CurrentValues.Ensure -ne $PSBoundParameters.Ensure) { - Write-Verbose -Message "Test-TargetResource returned $false" + Write-Verbose -Message "Test-TargetResource returned $false - Ensure not the same" return $false } - $testResult = $true - if ($Members.Count -gt 0 -or $currentValues.Members.Count -gt 0) - { - if ($Members.Count -ne $currentValues.Members.Count) - { - $testresult = $false - } - $testMembers = $Members - if ($null -eq $testMembers) - { - $testMembers = @() - } - $testCurrentValuesMembers = $currentValues.Members - if ($null -eq $testCurrentValuesMembers) - { - $testCurrentValuesMembers = @() - } - if ((Compare-Object -ReferenceObject $testMembers -DifferenceObject $testCurrentValuesMembers -Property Identity, Type).Count -gt 0) - { - $testresult = $false - } - $ValuesToCheck.Remove('Members') | Out-Null - } - if ($true -eq $testResult -and ($ScopedRoleMembers.Count -gt 0 -or $currentValues.ScopedRoleMembers.Count -gt 0)) - { - if ($ScopedRoleMembers.Count -ne $currentValues.ScopedRoleMembers.Count) - { - $testresult = $false - } - $testScopedRoleMembers = $ScopedRoleMembers - if ($null -eq $testScopedRoleMembers) - { - $testScopedRoleMembers = @() - } - $testCurrentValuesScopedRoleMembers = $currentValues.ScopedRoleMembers - if ($null -eq $testCurrentValuesScopedRoleMembers) - { - $testCurrentValuesScopedRoleMembers = @() - } - if ((Compare-Object -ReferenceObject $testScopedRoleMembers -DifferenceObject $testCurrentValuesScopedRoleMembers -Property RoleName, Identity, Type).Count -gt 0) - { - $testresult = $false - } - $ValuesToCheck.Remove('ScopedRoleMembers') | Out-Null - } <# + #Compare Cim instances foreach ($key in $PSBoundParameters.Keys) { - if ($PSBoundParameters[$key].getType().Name -like '*CimInstance*') + $source = $PSBoundParameters.$key + $target = $CurrentValues.$key + if ($source.getType().Name -like '*CimInstance*') { - $CIMArraySource = @() - $CIMArrayTarget = @() - if ($PSBoundParameters[$key]) - { - $CIMArraySource += $PSBoundParameters[$key] - } - if ($CurrentValues.$key) - { - $CIMArrayTarget += $CurrentValues.$key - } - if ($CIMArraySource.count -ne $CIMArrayTarget.count) - { - Write-Verbose -Message "Configuration drift:Number of items does not match: Source=$($CIMArraySource.count) Target=$($CIMArrayTarget.count)" - $testResult = $false - break - } - $i = 0 - foreach ($item in $CIMArraySource ) - { - $testResult = Compare-M365DSCComplexObject ` - -Source (Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $CIMArraySource[$i]) ` - -Target ($CIMArrayTarget[$i]) + $source = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $source + + $testResult = Compare-M365DSCComplexObject ` + -Source ($source) ` + -Target ($target) - $i++ - if (-Not $testResult) - { - $testResult = $false - break; - } - } if (-Not $testResult) { + Write-Verbose -Message "Difference found for $key" $testResult = $false break; } $ValuesToCheck.Remove($key) | Out-Null + } } #> - Write-Verbose -Message "Test-TargetResource - testresult after comparing CimInstance(s): $testResult" - $ValuesToCheck.Remove('Credential') | Out-Null $ValuesToCheck.Remove('ApplicationId') | Out-Null $ValuesToCheck.Remove('TenantId') | Out-Null $ValuesToCheck.Remove('ApplicationSecret') | Out-Null - $ValuesToCheck.Remove('CertificateThumbprint') | Out-Null - $ValuesToCheck.Remove('ManagedIdentity') | Out-Null - # Removing the visibility parameter from the check since this is always being returned as null currently by the Microsoft Graph. + # Visibility is currently not returned by Get-TargetResource $ValuesToCheck.Remove('Visibility') | Out-Null - if ($MembershipType -ne 'Dynamic' -and $CurrentValues.MembershipType -ne 'Dynamic') - { - # Removing the MembershipType parameter from the check if it isn't Dynamic. If it isn't, it can be Assigned or null - $ValuesToCheck.Remove('MembershipType') | Out-Null - } - Write-Verbose -Message "Current Values: $(Convert-M365DscHashtableToString -Hashtable $CurrentValues)" Write-Verbose -Message "Target Values: $(Convert-M365DscHashtableToString -Hashtable $ValuesToCheck)" - #Convert any DateTime to String - foreach ($key in $ValuesToCheck.Keys) - { - if (($null -ne $CurrentValues[$key]) ` - -and ($CurrentValues[$key].getType().Name -eq 'DateTime')) - { - $CurrentValues[$key] = $CurrentValues[$key].toString() - } - } - if ($testResult) { $testResult = Test-M365DSCParameterState -CurrentValues $CurrentValues ` @@ -1201,13 +916,7 @@ function Export-TargetResource $ConnectionMode = New-M365DSCConnection -Workload 'MicrosoftGraph' ` -InboundParameters $PSBoundParameters ` - -ProfileName 'beta' - $context = Get-MgContext - if ($null -eq $context) - { - $ConnectionMode = New-M365DSCConnection -Workload 'MicrosoftGraph' ` - -InboundParameters $PSBoundParameters -ProfileName 'beta' - } + -ProfileName 'v1.0' #Ensure the proper dependencies are installed in the current environment. Confirm-M365DSCDependencies @@ -1226,7 +935,6 @@ function Export-TargetResource #region resource generator code [array]$getValue = Get-MgDirectoryAdministrativeUnit -All ` -ErrorAction Stop - #endregion $i = 1 @@ -1241,100 +949,71 @@ function Export-TargetResource } foreach ($config in $getValue) { - Write-Host " |---[$i/$($getValue.Count)] $($config.DisplayName)" -NoNewline + $displayedKey = $config.Id + if (-not [String]::IsNullOrEmpty($config.displayName)) + { + $displayedKey = $config.displayName + } + Write-Host " |---[$i/$($getValue.Count)] $displayedKey" -NoNewline $params = @{ DisplayName = $config.DisplayName + Id = $config.Id Ensure = 'Present' Credential = $Credential ApplicationId = $ApplicationId TenantId = $TenantId ApplicationSecret = $ApplicationSecret CertificateThumbprint = $CertificateThumbprint - ManagedIdentity = $ManagedIdentity + ManagedIdentity = $ManagedIdentity.IsPresent } $Results = Get-TargetResource @Params - $Results = Update-M365DSCExportAuthenticationResults -ConnectionMode $ConnectionMode ` - -Results $Results - if ($Results.Members) + if ($null -ne $Results.ScopedRoleMembers) { - $hashResults = Get-M365DSCDRGObjectArrayToHashtable -ArrayObject $Results.Members - $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject $hashResults -CIMInstanceName MicrosoftGraphMember - if ($complexTypeStringResult) - { - $Results.Members = $complexTypeStringResult - } - else - { - $Results.Remove('Members') | Out-Null - } - } - if ($Results.ScopedRoleMembers) - { - $hashResults = Get-M365DSCDRGObjectArrayToHashtable -ArrayObject $Results.ScopedRoleMembers - $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject $hashResults -CIMInstanceName MicrosoftGraphScopedRoleMembership - if ($complexTypeStringResult) - { - $Results.ScopedRoleMembers = $complexTypeStringResult - } - else + $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject ([Array]$Results.ScopedRoleMembers) ` + -CIMInstanceName MicrosoftGraphScopedRoleMembership + + write-verbose "ScopedRoleMembers on next line:`r`n$complexTypeStringResult" + + $Results.ScopedRoleMembers = $complexTypeStringResult + + if ([String]::IsNullOrEmpty($complexTypeStringResult)) { $Results.Remove('ScopedRoleMembers') | Out-Null } } - <# - if ($Results.Extensions) + if ($null -ne $Results.Members) { - $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject $Results.Extensions -CIMInstanceName MicrosoftGraphextension - if ($complexTypeStringResult) - { - $Results.Extensions = $complexTypeStringResult } - else + $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject ([Array]$Results.Members) ` + -CIMInstanceName MicrosoftGraphMember + write-verbose "Members on next line:`r`n$complexTypeStringResult" + $Results.Members = $complexTypeStringResult + + if ([String]::IsNullOrEmpty($complexTypeStringResult)) { - $Results.Remove('Extensions') | Out-Null + $Results.Remove('Members') | Out-Null } } - #> + + $Results = Update-M365DSCExportAuthenticationResults -ConnectionMode $ConnectionMode ` + -Results $Results + $currentDSCBlock = Get-M365DSCExportContentForResource -ResourceName $ResourceName ` -ConnectionMode $ConnectionMode ` -ModulePath $PSScriptRoot ` -Results $Results ` -Credential $Credential - <# - if ($Results.Members) - { - $isCIMArray = $false - if ($Results.Members.getType().Fullname -like '*[[\]]') - { - $isCIMArray = $true - } - $currentDSCBlock = Convert-DSCStringParamToVariable -DSCBlock $currentDSCBlock -ParameterName 'Members' -IsCIMArray:$isCIMArray - $currentDSCBlock = $currentDSCBlock.Replace('}");', '});') - } - if ($Results.ScopedRoleMembers) + if ($null -ne $Results.ScopedRoleMembers) { - $isCIMArray = $false - if ($Results.ScopedRoleMembers.getType().Fullname -like '*[[\]]') - { - $isCIMArray = $true - } - $currentDSCBlock = Convert-DSCStringParamToVariable -DSCBlock $currentDSCBlock -ParameterName 'ScopedRoleMembers' -IsCIMArray:$isCIMArray + $currentDSCBlock = Convert-DSCStringParamToVariable -DSCBlock $currentDSCBlock -ParameterName "ScopedRoleMembers" -isCIMArray $true } - #> - <# - if ($Results.Extensions) + if ($null -ne $Results.Members) { - $isCIMArray = $false - if ($Results.Extensions.getType().Fullname -like '*[[\]]') - { - $isCIMArray = $true - } - $currentDSCBlock = Convert-DSCStringParamToVariable -DSCBlock $currentDSCBlock -ParameterName 'Extensions' -IsCIMArray:$isCIMArray + $currentDSCBlock = Convert-DSCStringParamToVariable -DSCBlock $currentDSCBlock -ParameterName "Members" -isCIMArray $true } - #> - + write-verbose "currentDSCBlock on next line:`r`n$($currentDSCBlock -join "`r`n")" $dscContent += $currentDSCBlock Save-M365DSCPartialExport -Content $currentDSCBlock ` -FileName $Global:PartialExportFileName @@ -1356,83 +1035,195 @@ function Export-TargetResource return '' } } +function Rename-M365DSCCimInstanceParameter +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable],[System.Collections.Hashtable[]])] + param( + [Parameter(Mandatory = 'true')] + $Properties + ) + + $keyToRename=@{ + "odataType"="@odata.type" + } + + $result=$Properties + + $type=$Properties.getType().FullName + + #region Array + if ($type -like '*[[\]]') + { + $values = @() + foreach ($item in $Properties) + { + $values += Rename-M365DSCCimInstanceParameter $item + } + $result=$values + + return ,$result + } + #endregion + + #region Single + if($type -like "*Hashtable") + { + $result=([Hashtable]$Properties).clone() + } + if($type -like '*CimInstance*' -or $type -like '*Hashtable*'-or $type -like '*Object*') + { + $hashProperties = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $result + $keys=($hashProperties.clone()).keys + foreach($key in $keys) + { + $keyName=$key.substring(0,1).tolower()+$key.substring(1,$key.length-1) + if ($key -in $keyToRename.Keys) + { + $keyName=$keyToRename.$key + } + $property=$hashProperties.$key + if($null -ne $property) + { + $hashProperties.Remove($key) + $hashProperties.add($keyName,(Rename-M365DSCCimInstanceParameter $property)) + } + } + $result = $hashProperties + } + return $result + #endregion +} function Get-M365DSCDRGComplexTypeToHashtable { [CmdletBinding()] - [OutputType([System.Collections.Hashtable])] + [OutputType([hashtable],[hashtable[]])] param( [Parameter()] $ComplexObject ) - if ($null -eq $ComplexObject) + if($null -eq $ComplexObject) { return $null } - if ($ComplexObject.GetType().FullName -like '*[[\]]') + if($ComplexObject.gettype().fullname -like "*[[\]]") { - $results = @() + $results=@() - foreach ($item in $ComplexObject) + foreach($item in $ComplexObject) { - if ($item) + if($item) { $hash = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $item - $results += $hash + $results+=$hash } } - if ($results.Count -eq 0) + + # PowerShell returns all non-captured stream output, not just the argument of the return statement. + #An empty array is mangled into $null in the process. + #However, an array can be preserved on return by prepending it with the array construction operator (,) + return ,[hashtable[]]$results + } + + if($ComplexObject.getType().fullname -like '*Dictionary*') + { + $results = @{} + + $ComplexObject=[hashtable]::new($ComplexObject) + $keys=$ComplexObject.Keys + foreach ($key in $keys) { - return $null + if($null -ne $ComplexObject.$key) + { + $keyName = $key + + $keyType=$ComplexObject.$key.gettype().fullname + + if($keyType -like "*CimInstance*" -or $keyType -like "*Dictionary*" -or $keyType -like "Microsoft.Graph.PowerShell.Models.*" -or $keyType -like "*[[\]]") + { + $hash = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $ComplexObject.$key + + $results.Add($keyName, $hash) + } + else + { + $results.Add($keyName, $ComplexObject.$key) + } + } } - return $results + return [hashtable]$results } $results = @{} - $keys = $ComplexObject | Get-Member | Where-Object -FilterScript { $_.MemberType -eq 'Property' -and $_.Name -ne 'AdditionalProperties' } - foreach ($key in $keys) + if($ComplexObject.getType().Fullname -like "*hashtable") { - if ($ComplexObject.$($key.Name)) - { - $results.Add($key.Name, $ComplexObject.$($key.Name)) - } + $keys = $ComplexObject.keys } - if ($results.count -eq 0) + else { - return $null + $keys = $ComplexObject | Get-Member | Where-Object -FilterScript {$_.MemberType -eq 'Property'} } - return $results -} -function Get-M365DSCDRGObjectArrayToHashtable -{ - [cmdletbinding()] - [outputtype([system.object[]])] - param( - [Parameter()] - [system.object[]] - $ArrayObject - ) - $returnValue = @() - foreach ($element in $ArrayObject) + foreach ($key in $keys) { - $hashTable = @{} - foreach ($property in $element.PSObject.Properties.Name) + $keyName=$key + if($ComplexObject.getType().Fullname -notlike "*hashtable") { - $hashtable.Add($property, $element.$property) | Out-Null + $keyName=$key.Name + } + + if($null -ne $ComplexObject.$keyName) + { + $keyType=$ComplexObject.$keyName.gettype().fullname + if($keyType -like "*CimInstance*" -or $keyType -like "*Dictionary*" -or $keyType -like "Microsoft.Graph.PowerShell.Models.*" ) + { + $hash = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $ComplexObject.$keyName + + $results.Add($keyName, $hash) + } + else + { + $results.Add($keyName, $ComplexObject.$keyName) + } } - $returnValue += $hashTable } - return $returnValue + + return [hashtable]$results } + +<# + Use ComplexTypeMapping to overwrite the type of nested CIM + Example + $complexMapping=@( + @{ + Name="ApprovalStages" + CimInstanceName="MSFT_MicrosoftGraphapprovalstage1" + IsRequired=$false + } + @{ + Name="PrimaryApprovers" + CimInstanceName="MicrosoftGraphuserset" + IsRequired=$false + } + @{ + Name="EscalationApprovers" + CimInstanceName="MicrosoftGraphuserset" + IsRequired=$false + } + ) + With + Name: the name of the parameter to be overwritten + CimInstanceName: The type of the CIM instance (can include or not the prefix MSFT_) + IsRequired: If isRequired equals true, an empty hashtable or array will be returned. Some of the Graph parameters are required even though they are empty +#> function Get-M365DSCDRGComplexTypeToString { [CmdletBinding()] - #[OutputType([System.String])] param( [Parameter()] $ComplexObject, @@ -1441,131 +1232,210 @@ function Get-M365DSCDRGComplexTypeToString [System.String] $CIMInstanceName, + [Parameter()] + [Array] + $ComplexTypeMapping, + [Parameter()] [System.String] - $Whitespace = '', + $Whitespace='', + + [Parameter()] + [System.uint32] + $IndentLevel=3, [Parameter()] [switch] - $isArray = $false + $isArray=$false ) + if ($null -eq $ComplexObject) { return $null } + write-verbose "Get-M365DSCDRGComplexTypeToString $CIMInstanceName isArray=$isArray" + + $indent='' + for ($i = 0; $i -lt $IndentLevel ; $i++) + { + $indent+=' ' + } #If ComplexObject is an Array - if ($ComplexObject.GetType().FullName -like '*[[\]]') + if ($ComplexObject.GetType().FullName -like "*[[\]]") { - write-verbose "object is an array" - $currentProperty = @() + $currentProperty=@() + $IndentLevel++ foreach ($item in $ComplexObject) { - write-verbose "Item=$($item -join '|')" - $currentProperty += Get-M365DSCDRGComplexTypeToString ` - -ComplexObject $item ` - -isArray:$true ` - -CIMInstanceName $CIMInstanceName ` - -Whitespace ' ' + $splat=@{ + 'ComplexObject'=$item + 'CIMInstanceName'=$CIMInstanceName + 'IndentLevel'=$IndentLevel + } + if ($ComplexTypeMapping) + { + $splat.add('ComplexTypeMapping',$ComplexTypeMapping) + } + $currentProperty += Get-M365DSCDRGComplexTypeToString -isArray:$true @splat } - if ([string]::IsNullOrEmpty($currentProperty)) - { - return $null - } - return $currentProperty + write-verbose "return array currentProperty on next line:`r`n $($currentProperty -join "`r`n")" + + # PowerShell returns all non-captured stream output, not just the argument of the return statement. + #An empty array is mangled into $null in the process. + #However, an array can be preserved on return by prepending it with the array construction operator (,) + return ,$currentProperty } - #If ComplexObject is a single CIM Instance - if (-Not (Test-M365DSCComplexObjectHasValues -ComplexObject $ComplexObject)) + $currentProperty='' + if($isArray) { - return $null + $currentProperty += "`r`n" + $currentProperty += $indent } - $currentProperty = '' - if ($isArray) + + $CIMInstanceName=$CIMInstanceName.replace("MSFT_","") + $currentProperty += "MSFT_$CIMInstanceName{`r`n" + $IndentLevel++ + $indent='' + for ($i = 0; $i -lt $IndentLevel ; $i++) { - $currentProperty += "`r`n" + $indent+=' ' } - $currentProperty += "$whitespace`MSFT_$CIMInstanceName{`r`n" $keyNotNull = 0 + + if ($ComplexObject.Keys.count -eq 0) + { + return $null + } + foreach ($key in $ComplexObject.Keys) { - write-verbose "" - if ($ComplexObject.$key) + write-verbose "ComplexObject key=$key" + if ($null -ne $ComplexObject.$key) { + write-verbose "`tnot null" $keyNotNull++ - - if ($ComplexObject.$key.GetType().FullName -like 'Microsoft.Graph.PowerShell.Models.*') + if ($ComplexObject.$key.GetType().FullName -like "Microsoft.Graph.PowerShell.Models.*" -or $key -in $ComplexTypeMapping.Name) { - $hashPropertyType = $ComplexObject[$key].GetType().Name - $hashProperty = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $ComplexObject.$key + $hashPropertyType=$ComplexObject[$key].GetType().Name.tolower() + + $isArray=$false + if($ComplexObject[$key].GetType().FullName -like "*[[\]]") + { + $isArray=$true + } + #overwrite type if object defined in mapping complextypemapping + if($key -in $ComplexTypeMapping.Name) + { + $hashPropertyType=($ComplexTypeMapping|Where-Object -FilterScript {$_.Name -eq $key}).CimInstanceName + $hashProperty=$ComplexObject[$key] + } + else + { + $hashProperty=Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $ComplexObject[$key] + } + + if(-not $isArray) + { + $currentProperty += $indent + $key + ' = ' + } - if (Test-M365DSCComplexObjectHasValues -ComplexObject $hashProperty) + if($isArray -and $key -in $ComplexTypeMapping.Name ) { - $Whitespace += ' ' - if (-not $isArray) + if($ComplexObject.$key.count -gt 0) { - $currentProperty += ' ' + $key + ' = ' + $currentProperty += $indent + $key + ' = ' + $currentProperty += "@(" } + } + + if ($isArray) + { + $IndentLevel++ + foreach ($item in $ComplexObject[$key]) + { + if ($ComplexObject.$key.GetType().FullName -like "Microsoft.Graph.PowerShell.Models.*") + { + $item=Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $item + } + $currentProperty += Get-M365DSCDRGComplexTypeToString ` + -ComplexObject $item ` + -CIMInstanceName $hashPropertyType ` + -IndentLevel $IndentLevel ` + -ComplexTypeMapping $ComplexTypeMapping ` + -IsArray:$true + } + $IndentLevel-- + } + else + { $currentProperty += Get-M365DSCDRGComplexTypeToString ` - -ComplexObject $hashProperty ` - -CIMInstanceName $hashPropertyType ` - -Whitespace $Whitespace + -ComplexObject $hashProperty ` + -CIMInstanceName $hashPropertyType ` + -IndentLevel $IndentLevel ` + -ComplexTypeMapping $ComplexTypeMapping } + if($isArray) + { + if($ComplexObject.$key.count -gt 0) + { + $currentProperty += $indent + $currentProperty += ')' + $currentProperty += "`r`n" + } + } + $isArray=$PSBoundParameters.IsArray } else { - if (-not $isArray) + $currentProperty += Get-M365DSCDRGSimpleObjectTypeToString -Key $key -Value $ComplexObject[$key] -Space ($indent) + } + } + else + { + $mappedKey=$ComplexTypeMapping|where-object -filterscript {$_.name -eq $key} + + if($mappedKey -and $mappedKey.isRequired) + { + if($mappedKey.isArray) { - $Whitespace = ' ' + $currentProperty += "$indent$key = @()`r`n" + } + else + { + $currentProperty += "$indent$key = `$null`r`n" } - $currentProperty += Get-M365DSCDRGSimpleObjectTypeToString -Key $key -Value $ComplexObject.$key -Space ($Whitespace + ' ') } } } - $currentProperty += ' }' - - if ($keyNotNull -eq 0) + $indent='' + for ($i = 0; $i -lt $IndentLevel-1 ; $i++) { - $currentProperty = $null + $indent+=' ' + } + $currentProperty += "$indent}" + if($isArray -or $IndentLevel -gt 4) + { + $currentProperty += "`r`n" } - return $currentProperty -} -function Test-M365DSCComplexObjectHasValues -{ - [CmdletBinding()] - [OutputType([System.Boolean])] - param( - [Parameter(Mandatory = $true)] - [system.object] - $ComplexObject - ) - $keys = $ComplexObject.psobject.properties.name - $hasValue = $false - foreach ($key in $keys) + #Indenting last parenthese when the cim instance is an array + if($IndentLevel -eq 5) { - if ($ComplexObject.$key) + $indent='' + for ($i = 0; $i -lt $IndentLevel-2 ; $i++) { - if ($ComplexObject.$key.GetType().FullName -like 'Microsoft.Graph.PowerShell.Models.*') - { - $hash = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $ComplexObject[$key] - if (-Not $hash) - { - return $false - } - $hasValue = Test-M365DSCComplexObjectHasValues -ComplexObject ($hash) - } - else - { - $hasValue = $true - break - } + $indent+=' ' } + $currentProperty += $indent } - return $hasValue + write-verbose "return item currentProperty on next line:`r`n$currentProperty" + return $currentProperty } + Function Get-M365DSCDRGSimpleObjectTypeToString { [CmdletBinding()] @@ -1580,49 +1450,50 @@ Function Get-M365DSCDRGSimpleObjectTypeToString [Parameter()] [System.String] - $Space = ' ' + $Space=" " ) - $returnValue = '' + write-verbose "Get-M365DSCDRGSimpleObjectTypeToString key='$Key', value='$Value'. Type=$($value.gettype().fullname)" + $returnValue="" switch -Wildcard ($Value.GetType().Fullname ) { - '*.Boolean' + "*.Boolean" { - $returnValue = $Space + $Key + " = `$" + $Value.ToString() + "`r`n" + $returnValue= $Space + $Key + " = `$" + $Value.ToString() + "`r`n" } - '*.String' + "*.String" { - if ($key -eq '@odata.type') + if($key -eq '@odata.type') { - $key = 'odataType' + $key='odataType' } - $returnValue = $Space + $Key + " = '" + $Value + "'`r`n" + $returnValue= $Space + $Key + " = '" + $Value + "'`r`n" } - '*.DateTime' + "*.DateTime" { - $returnValue = $Space + $Key + " = '" + $Value + "'`r`n" + $returnValue= $Space + $Key + " = '" + $Value + "'`r`n" } - '*[[\]]' + "*[[\]]" { - $returnValue = $Space + $key + ' = @(' - $whitespace = '' - $newline = '' - if ($Value.Count -gt 1) + $returnValue= $Space + $key + " = @(" + $whitespace="" + $newline="" + if($Value.count -gt 1) { $returnValue += "`r`n" - $whitespace = $Space + ' ' - $newline = "`r`n" + $whitespace=$Space+" " + $newline="`r`n" } foreach ($item in $Value) { switch -Wildcard ($item.GetType().Fullname ) { - '*.String' + "*.String" { $returnValue += "$whitespace'$item'$newline" } - '*.DateTime' + "*.DateTime" { $returnValue += "$whitespace'$item'$newline" } @@ -1632,7 +1503,7 @@ Function Get-M365DSCDRGSimpleObjectTypeToString } } } - if ($Value.Count -gt 1) + if($Value.count -gt 1) { $returnValue += "$Space)`r`n" } @@ -1644,232 +1515,212 @@ Function Get-M365DSCDRGSimpleObjectTypeToString } Default { - $returnValue = $Space + $Key + ' = ' + $Value + "`r`n" + $returnValue= $Space + $Key + " = " + $Value + "`r`n" } } + write-verbose "return '$returnValue'" return $returnValue } -function Rename-M365DSCCimInstanceODataParameter -{ - [CmdletBinding()] - [OutputType([System.Collections.Hashtable])] - param( - [Parameter(Mandatory = 'true')] - [System.Collections.Hashtable] - $Properties - ) - $CIMparameters = $Properties.GetEnumerator() | Where-Object -FilterScript { $_.Value.GetType().Fullname -like '*CimInstance*' } - foreach ($CIMParam in $CIMparameters) - { - if ($CIMParam.Value.GetType().Fullname -like '*[[\]]') - { - $CIMvalues = @() - foreach ($item in $CIMParam.Value) - { - $CIMHash = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $item - $keys = ($CIMHash.Clone()).Keys - if ($keys -contains 'odataType') - { - $CIMHash.Add('@odata.type', $CIMHash.odataType) - $CIMHash.Remove('odataType') - } - $CIMvalues += $CIMHash - } - $Properties.($CIMParam.Key) = $CIMvalues - } - else - { - $CIMHash = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $CIMParam.value - $keys = ($CIMHash.Clone()).Keys - if ($keys -contains 'odataType') - { - $CIMHash.Add('@odata.type', $CIMHash.odataType) - $CIMHash.Remove('odataType') - $Properties.($CIMParam.Key) = $CIMHash - } - } - } - return $Properties -} function Compare-M365DSCComplexObject { [CmdletBinding()] [OutputType([System.Boolean])] param( [Parameter()] - [System.Collections.Hashtable] $Source, [Parameter()] - [System.Collections.Hashtable] $Target ) - $keys = $Source.Keys | Where-Object -FilterScript { $_ -ne 'PSComputerName' } - foreach ($key in $keys) + #Comparing full objects + if($null -eq $Source -and $null -eq $Target) + { + return $true + } + + $sourceValue="" + $targetValue="" + if (($null -eq $Source) -xor ($null -eq $Target)) + { + if($null -eq $Source) + { + $sourceValue="Source is null" + } + + if($null -eq $Target) + { + $targetValue="Target is null" + } + Write-Verbose -Message "Configuration drift - Complex object: {$sourceValue$targetValue}" + return $false + } + + if($Source.getType().FullName -like "*CimInstance[[\]]" -or $Source.getType().FullName -like "*Hashtable[[\]]") { - Write-Verbose -Message "Comparing Source-key: {$key}" - $skey = $key - if ($key -eq 'odataType') + if($source.count -ne $target.count) { - $skey = '@odata.type' + Write-Verbose -Message "Configuration drift - The complex array have different number of items: Source {$($source.count)} Target {$($target.count)}" + return $false + } + if($source.count -eq 0) + { + return $true } - #Marking Target[key] to null if empty complex object or array - if ($null -ne $Target[$key]) + foreach($item in $Source) { - switch -Wildcard ($Target[$key].getType().Fullname ) + + $hashSource=Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $item + foreach($targetItem in $Target) { - 'Microsoft.Graph.PowerShell.Models.*' - { - $hashProperty = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $Target[$key] - if (-not (Test-M365DSCComplexObjectHasValues -ComplexObject $hashProperty)) - { - $Target[$key] = $null - } - } - '*[[\]]' + $compareResult= Compare-M365DSCComplexObject ` + -Source $hashSource ` + -Target $targetItem + + if ($compareResult) { - if ($Target[$key].count -eq 0) - { - $Target[$key] = $null - } + write-verbose "Compare-M365DSCComplexObject: Diff found" + break } } + + if(-not $compareResult) + { + Write-Verbose -Message "Configuration drift - The complex array items are not identical" + return $false + } } - $sourceValue = $Source[$key] - $targetValue = $Target[$key] - #One of the item is null - if (($null -eq $Source[$skey]) -xor ($null -eq $Target[$key])) + return $true + } + + $keys= $Source.Keys|Where-Object -FilterScript {$_ -ne "PSComputerName"} + foreach ($key in $keys) + { + #Matching possible key names between Source and Target + $skey=$key + $tkey=$key + + $sourceValue=$Source.$key + $targetValue=$Target.$tkey + #One of the item is null and not the other + if (($null -eq $Source.$key) -xor ($null -eq $Target.$tkey)) { - if ($null -eq $Source[$skey]) + + if($null -eq $Source.$key) { - $sourceValue = 'null' + $sourceValue="null" } - if ($null -eq $Target[$key]) + if($null -eq $Target.$tkey) { - $targetValue = 'null' + $targetValue="null" } - Write-Verbose -Message "Configuration drift - key: $key Source{$sourceValue} Target{$targetValue}" + + #Write-Verbose -Message "Configuration drift - key: $key Source {$sourceValue} Target {$targetValue}" return $false } - #Both source and target aren't null or empty - if (($null -ne $Source[$skey]) -and ($null -ne $Target[$key])) + + #Both keys aren't null or empty + if(($null -ne $Source.$key) -and ($null -ne $Target.$tkey)) { - if ($Source[$skey].getType().FullName -like '*CimInstance*' -or $Target[$skey].getType().FullName -like '*CimInstance*') + if($Source.$key.getType().FullName -like "*CimInstance*" -or $Source.$key.getType().FullName -like "*hashtable*" ) { #Recursive call for complex object - write-verbose "Compare values for $key" - if ($Source[$skey].getType().FullName -like '*CimInstance*') - { - $complexSourceValue = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $Source[$skey] - } - else - { - $complexSourceValue = $Source[$key] - } - if ($Target[$skey].getType().FullName -like '*CimInstance*') - { - $complexTargetValue = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $Target[$key] - } - else - { - $complexTargetValue = $Target[$key] - } - if ($complexSourceValue.GetType().FullName -ne 'System.Collections.Hashtable') - { - $tempSourceValue = @{} - $complexSourceValue.psobject.properties | Foreach-Object { - $tempSourceValue[$_.Name] = $_.Value - } - $complexSourceValue = $tempSourceValue - } - if ($complexTargetValue.GetType().FullName -ne 'System.Collections.Hashtable') - { - $tempTargetValue = @{} - $complexTargetValue.psobject.properties | Foreach-Object { - $tempTargetValue[$_.Name] = $_.Value - } - $complexTargetValue = $tempTargetValue - } - $compareResult = Compare-M365DSCComplexObject ` - -Source $complexSourceValue ` - -Target $complexTargetValue + $compareResult= Compare-M365DSCComplexObject ` + -Source (Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $Source.$key) ` + -Target $Target.$tkey - if (-not $compareResult) + if(-not $compareResult) { - Write-Verbose -Message "Configuration drift - key: $key Source{$sourceValue} Target{$targetValue}" + + #Write-Verbose -Message "Configuration drift - complex object key: $key Source {$sourceValue} Target {$targetValue}" return $false } } else { #Simple object comparison - $referenceObject = $Target[$key] - $differenceObject = $Source[$skey] + $referenceObject=$Target.$tkey + $differenceObject=$Source.$key - $compareResult = Compare-Object ` - -ReferenceObject ($referenceObject) ` - -DifferenceObject ($differenceObject) + #Identifying date from the current values + $targetType=($Target.$tkey.getType()).Name + if($targetType -like "*Date*") + { + $compareResult=$true + $sourceDate= [DateTime]$Source.$key + if($sourceDate -ne $targetType) + { + $compareResult=$null + } + } + else + { + $compareResult = Compare-Object ` + -ReferenceObject ($referenceObject) ` + -DifferenceObject ($differenceObject) + } if ($null -ne $compareResult) { - Write-Verbose -Message "Configuration drift - key: $key Source{$sourceValue} Target{$targetValue}" + #Write-Verbose -Message "Configuration drift - simple object key: $key Source {$sourceValue} Target {$targetValue}" return $false } - } } } return $true } - function Convert-M365DSCDRGComplexTypeToHashtable { [CmdletBinding()] + [OutputType([hashtable],[hashtable[]])] param( [Parameter(Mandatory = 'true')] $ComplexObject ) - if ($ComplexObject.getType().Fullname -like '*[[\]]') + + if($ComplexObject.getType().Fullname -like "*[[\]]") { - $results = @() - foreach ($item in $ComplexObject) - { - $hash = Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $item - if (Test-M365DSCComplexObjectHasValues -ComplexObject $hash) - { - $results += $hash - } - } - if ($results.count -eq 0) + $results=@() + foreach($item in $ComplexObject) { - return $null + $hash=Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $item + $results+=$hash } - return $Results + + #Write-Verbose -Message ("Convert-M365DSCDRGComplexTypeToHashtable >>> results: "+(convertTo-JSON $results -Depth 20)) + # PowerShell returns all non-captured stream output, not just the argument of the return statement. + #An empty array is mangled into $null in the process. + #However, an array can be preserved on return by prepending it with the array construction operator (,) + return ,[hashtable[]]$results } $hashComplexObject = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $ComplexObject - if ($hashComplexObject) + + if($null -ne $hashComplexObject) { - $results = $hashComplexObject.clone() - $keys = $hashComplexObject.Keys | Where-Object -FilterScript { $_ -ne 'PSComputerName' } + + $results=$hashComplexObject.clone() + $keys=$hashComplexObject.Keys|Where-Object -FilterScript {$_ -ne 'PSComputerName'} foreach ($key in $keys) { - if (($null -ne $hashComplexObject[$key]) -and ($hashComplexObject[$key].getType().Fullname -like '*CimInstance*')) + if($hashComplexObject[$key] -and $hashComplexObject[$key].getType().Fullname -like "*CimInstance*") { - $results[$key] = Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $hashComplexObject[$key] + $results[$key]=Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $hashComplexObject[$key] } - if ($null -eq $results[$key]) + else { - $results.remove($key) | Out-Null + $propertyName = $key[0].ToString().ToLower() + $key.Substring(1, $key.Length - 1) + $propertyValue=$results[$key] + $results.remove($key)|out-null + $results.add($propertyName,$propertyValue) } - } } - return $results + return [hashtable]$results } Export-ModuleMember -Function *-TargetResource diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.schema.mof b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.schema.mof index 2759ea880e..908e8f85ac 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.schema.mof +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.schema.mof @@ -8,18 +8,9 @@ class MSFT_MicrosoftGraphMember class MSFT_MicrosoftGraphScopedRoleMembership { [Write, Description("Name of the Azure AD Role that is assigned")] String RoleName; - //[Write, Description("Member that is assigned the scoped role"), EmbeddedInstance("MSFT_MicrosoftGraphIdentity")] String RoleMemberInfo; - [Write, Description("Identity of member. For users, specify a UserPrincipalName. For groups and SPNs, specify DisplayName")] String Identity; + [Write, Description("Identity of member. For users, specify a UserPrincipalName. For groups and SPNs, specify the DisplayName")] String Identity; [Write, Description("Specify User, Group or ServicePrincipal to interpret the Identity")] String Type; }; -/* Extensions not incorporated in initial version -[ClassVersion("1.0.0")] -class MSFT_MicrosoftGraphOpenExtension -{ - [Write, Description("See https://docs.microsoft.com/en-us/graph/extensibility-overview#open-extensions")] String Id; - [Write, Description("Optional list of properties and their values"), EmbeddedInstance("MSFT_KeyValuePair")] String Properties[]; -}; -*/ [ClassVersion("1.0.0.0"), FriendlyName("AADAdministrativeUnit")] class MSFT_AADAdministrativeUnit : OMI_BaseResource @@ -28,12 +19,11 @@ class MSFT_AADAdministrativeUnit : OMI_BaseResource [Write, Description("Object-Id of the Administrative Unit")] String Id; [Write, Description("Description of the Administrative Unit")] String Description; [Write, Description("Visibility of the Administrative Unit. Specify HiddenMembership if members of the AU are hidden")] String Visibility; - [Write, Description("Specify membership type. Possible values are Assigned and Dynamic if the AU-preview has been activated. Otherwise do not use")] String MembershipType; - [Write, Description("Specify membership rule. Requires that MembershipType is set to Dynamic AND the AU-preview has been activated. Otherwise, do not use")] String MembershipRule; - [Write, Description("Specify dynamic membership-rule processing-state. Valid values are 'On' and 'Paused'. Requires that MembershipType is set to Dynamic AND the AU-preview has been activated. Otherwise, do not use")] String MembershipRuleProcessingState; - [Write, Description("Specify members. Only specify if MembershipType is set to Static"), EmbeddedInstance("MSFT_MicrosoftGraphMember")] String Members[]; - [Write, Description(""), EmbeddedInstance("MSFT_MicrosoftGraphScopedRoleMembership")] String ScopedRoleMembers[]; -// [Write, Description("Extensions. See https://docs.microsoft.com/en-us/graph/extensibility-overview#open-extensions"), EmbeddedInstance("MSFT_MicrosoftGraphOpenExtension")] String Extensions[]; + [Write, Description("Specify membership type. Possible values are Assigned and Dynamic. Note that the functionality is currently in preview.")] String MembershipType; + [Write, Description("Specify membership rule. Requires that MembershipType is set to Dynamic. Note that the functionality is currently in preview.")] String MembershipRule; + [Write, Description("Specify dynamic membership-rule processing-state. Valid values are 'On' and 'Paused'. Requires that MembershipType is set to Dynamic. Note that the functionality is currently in preview.")] String MembershipRuleProcessingState; + [Write, Description("Specify members. Only specify if MembershipType is NOT set to Dynamic"), EmbeddedInstance("MSFT_MicrosoftGraphMember")] String Members[]; + [Write, Description("Specify Scoped Role Membership."), EmbeddedInstance("MSFT_MicrosoftGraphScopedRoleMembership")] String ScopedRoleMembers[]; [Write, Description("Present ensures the Administrative Unit exists, absent ensures it is removed."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] string Ensure; [Write, Description("Credentials of the Intune Admin"), EmbeddedInstance("MSFT_Credential")] string Credential; [Write, Description("Id of the Azure Active Directory application to authenticate with.")] String ApplicationId; diff --git a/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 index 34a8673173..b6a2ebea97 100644 --- a/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 +++ b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 @@ -101,6 +101,12 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Mock -CommandName Get-MgDirectoryAdministrativeUnit -MockWith { return $null } + Mock -CommandName Get-MgDirectoryAdministrativeUnitMember -MockWith { + return $null + } + Mock -CommandName Get-MgDirectoryAdministrativeUnitScopedRoleMember -MockWith { + return $null + } } It 'Should return Values from the Get method' { (Get-TargetResource @testParams).Ensure | Should -Be 'Absent' @@ -131,7 +137,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } Mock -CommandName Get-MgDirectoryAdministrativeUnit -MockWith { - return [pscustomobject]@{ + return @{ Description = 'FakeStringValue2' DisplayName = 'FakeStringValue2' Id = 'FakeStringValue2' @@ -172,23 +178,11 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { ) ScopedRoleMembers = @( (New-CimInstance -ClassName MSFT_MicrosoftGraphScopedRoleMembership -Property @{ - RoleName = 'User Administrator' - #RoleMemberInfo = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ + RoleName = 'User Administrator' Identity = 'John.Doe@mytenant.com' Type = 'User' - # } -ClientOnly) } -ClientOnly) ) - <# - Extensions =@( - (New-CimInstance -ClassName MSFT_MicrosoftGraphExtension -Property @{ - Id = '0123456789' - Properties = (New-CimInstance -ClassName MSFT_KeyValuePair -Property @{ - SomeAttribute = "somevalue" - } -ClientOnly) - } -ClientOnly) - ) - #> Visibility = 'Public' MembershipType = 'Assigned' # MembershipRule and -ProcessingState params are only used when MembershipType is Dynamic @@ -201,18 +195,10 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { # Note: It is in fact possible to update the AU MembershipRule with any invalid value, but in the AAD-portal, updates are not possible unless the rule is valid. Mock -CommandName Get-MgDirectoryAdministrativeUnit -MockWith { - return [pscustomobject]@{ + return @{ Description = 'DSCAU' DisplayName = 'DSCAU' Id = 'DSCAU' - <# - Extensions =@( - [pscustomobject]@{ - Id = '0123456789' - SomeAttribute = "somevalue" - } - ) - #> Visibility = 'Public' AdditionalProperties = @{ membershipType = 'Assigned' @@ -223,7 +209,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } Mock -CommandName Invoke-MgGraphRequest -MockWith { - return [pscustomobject]@{ + return @{ '@odata.type' = '#microsoft.graph.user' DisplayName = 'John Doe' UserPrincipalName = 'John.Doe@mytenant.com' @@ -232,23 +218,23 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } Mock -CommandName Get-MgDirectoryAdministrativeUnitMember -MockWith { - return [pscustomobject] { + return @(@{ Id = '1234567890' - } + }) } Mock -CommandName Get-MgDirectoryAdministrativeUnitScopedRoleMember -MockWith { - return [pscustomobject]@{ + return @(@{ RoleId = '12345-67890' - RoleMemberINfo = [pscustomobject]@{ + RoleMemberInfo = @{ DisplayName = 'John Doe' Id = '1234567890' } - } + }) } Mock -CommandName Get-MgDirectoryRole -MockWith { - return [pscustomobject]@{ + return @{ Id = '12345-67890' DisplayName = 'User Administrator' } @@ -270,16 +256,6 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { $testParams = @{ Description = 'DSCAU2' DisplayName = 'DSCAU2' - <# - Extensions =@( - (New-CimInstance -ClassName MSFT_MicrosoftGraphextension -Property @{ - Id = '0123456789' - Properties = (New-CimInstance -ClassName MSFT_KeyValuePair -Property @{ - SomeAttribute = "somevalue" - } -ClientOnly) - } -ClientOnly) - ) - #> Id = 'DSCAU2' Members = @( (New-CimInstance -ClassName MSFT_MicrosoftGraphMember -Property @{ @@ -290,10 +266,8 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { ScopedRoleMembers = @( (New-CimInstance -ClassName MSFT_MicrosoftGraphScopedRoleMembership -Property @{ RoleName = 'User Administrator' - #RoleMemberInfo = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ Identity = 'John.Doe@mytenant.com' Type = 'User' - # } -ClientOnly) } -ClientOnly) ) Visibility = 'Public' @@ -308,14 +282,6 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { DisplayName = 'DSCAU2' Id = 'DSCAU2' Visibility = 'Public' - <# - Extensions =@( - [pscustomobject]@{ - Id = "FakeExtensionIdentity" - SomeAttribute = "SomeValue" - } - ) - #> } } @@ -334,7 +300,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Mock -CommandName Get-MgDirectoryAdministrativeUnitScopedRoleMember -MockWith { return [pscustomobject]@{ RoleId = '12345-67890' - RoleMemberInfo = [pscustomobject]@{ + RoleMemberInfo = @{ DisplayName = 'John Doe' Id = '1234567890' } @@ -378,16 +344,6 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Description = 'DSCAU' DisplayName = 'DSCAU' Id = 'DSCAU' - <# - Extensions =@( - (New-CimInstance -ClassName MSFT_MicrosoftGraphextension -Property @{ - Id = '0123456789' - Properties = (New-CimInstance -ClassName MSFT_KeyValuePair -Property @{ - SomeAttribute = "somevalue" - } -ClientOnly) - } -ClientOnly) - ) - #> Members = @( (New-CimInstance -ClassName MSFT_MicrosoftGraphMember -Property @{ Identity = 'John.Doe@mytenant.com' @@ -397,10 +353,8 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { ScopedRoleMembers = @( (New-CimInstance -ClassName MSFT_MicrosoftGraphScopedRoleMembership -Property @{ RoleName = 'User Administrator' - #RoleMemberInfo = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ Identity = 'John.Doe@mytenant.com' Type = 'User' - # } -ClientOnly) } -ClientOnly) ) Visibility = 'Public' @@ -414,14 +368,6 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { DisplayName = 'DSCAU' Id = 'DSCAU' Visibility = 'Public' - <# - Extensions =@( - [pscustomobject]@{ - Id = "FakeExtensionId" - SomeAttribute = "SomeValue" - } - ) - #> } } @@ -440,6 +386,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } Mock -CommandName Get-MgDirectoryAdministrativeUnitScopedRoleMember -MockWith { + return $null } Mock -CommandName Invoke-MgGraphRequest -MockWith { @@ -479,19 +426,9 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } Mock -CommandName Get-MgDirectoryAdministrativeUnit -MockWith { - return [pscustomobject]@{ + return @{ Description = 'ExportDSCAU' DisplayName = 'ExportDSCAU' - <# - Extensions =@( - (New-CimInstance -ClassName MSFT_MicrosoftGraphextension -Property @{ - CIMType = "MSFT_MicrosoftGraphextension" - Name = "Extensions" - isArray = $True - - } -ClientOnly) - ) - #> Id = 'ExportDSCAU' Visibility = 'Public' } @@ -512,16 +449,23 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } Mock -CommandName Get-MgDirectoryAdministrativeUnitScopedRoleMember -MockWith { - return [pscustomobject]@{ + return @([pscustomobject]@{ RoleId = '12345-67890' - RoleMemberInfo = [pscustomobject]@{ + RoleMemberInfo = @{ DisplayName = 'John Doe' Id = '1234567890' } - } + }, + [pscustomobject]@{ + RoleId = '09876-54321' + RoleMemberInfo = @{ + DisplayName = 'Group' + Id = '0987654321' + } + }) } - Mock -CommandName Invoke-MgGraphRequest -MockWith { + Mock -CommandName Invoke-MgGraphRequest -ParameterFilter {$Uri -match '1234567890$'} -MockWith { return [pscustomobject]@{ '@odata.type' = '#microsoft.graph.user' DisplayName = 'John Doe' @@ -530,15 +474,30 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } } - Mock -CommandName Get-MgDirectoryRole -MockWith { + Mock -CommandName Invoke-MgGraphRequest -ParameterFilter {$Uri -match '0987654321$'} -MockWith { + return [pscustomobject]@{ + '@odata.type' = '#microsoft.graph.group' + DisplayName = 'FakeRoleGroup' + Id = '0987654321' + } + } + + Mock -CommandName Get-MgDirectoryRole -ParameterFilter {$Id -eq '12345-67890'} -MockWith { return [pscustomobject]@{ Id = '12345-67890' - DisplayName = 'User Administrator' + DisplayName = 'DSC User Administrator' + } + } + + Mock -CommandName Get-MgDirectoryRole -ParameterFilter {$Id -eq '09876-54321'} -MockWith { + return [pscustomobject]@{ + Id = '09876-54321' + DisplayName = 'DSC Groups Administrator' } } } It 'Should Reverse Engineer resource from the Export method' { - Export-TargetResource @testParams + Export-TargetResource @testParams -verbose } } } From 5edddad4c906c3acd7346b9968e43b6c12ac72df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Thu, 19 Jan 2023 14:46:42 +0100 Subject: [PATCH 06/29] No test of MembershipType if Dynamic isn't present --- .../MSFT_AADAdministrativeUnit.psm1 | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 index 6622563012..62fbeb0f6f 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 @@ -867,6 +867,12 @@ function Test-TargetResource # Visibility is currently not returned by Get-TargetResource $ValuesToCheck.Remove('Visibility') | Out-Null + if ($MembershipType -ne 'Dynamic' -and $CurrentValues.MembershipType -ne 'Dynamic') + { + # MembershipType may be returned as null or Assigned with same effect. Only compare if Dynamic is specified or returned + $ValuesToCheck.Remove('MembershipType') | Out-Null + } + Write-Verbose -Message "Current Values: $(Convert-M365DscHashtableToString -Hashtable $CurrentValues)" Write-Verbose -Message "Target Values: $(Convert-M365DscHashtableToString -Hashtable $ValuesToCheck)" From deae89ac85cbe232163ded2cee5f4ce8482d767f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Fri, 20 Jan 2023 08:35:09 +0100 Subject: [PATCH 07/29] added example using Members and SCopedRoleMembers --- .../azure-ad/AADAdministrativeUnit.md | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/docs/docs/resources/azure-ad/AADAdministrativeUnit.md b/docs/docs/resources/azure-ad/AADAdministrativeUnit.md index f8300e6355..dbc9be8564 100644 --- a/docs/docs/resources/azure-ad/AADAdministrativeUnit.md +++ b/docs/docs/resources/azure-ad/AADAdministrativeUnit.md @@ -21,13 +21,13 @@ | **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. | | -### MSFT_MicrosoftGraphIdentity +### MSFT_MicrosoftGraphMember #### Parameters | Parameter | Attribute | DataType | Description | Allowed Values | | --- | --- | --- | --- | --- | -| **Identity** | Write | String | Identity of direcory-object. For users, specify a UserPrincipalName. For Groups and SPNs, specify DisplayName | | +| **Identity** | Write | String | Identity of directory-object. For users, specify a UserPrincipalName. For Groups and SPNs, specify DisplayName | | | **Type** | Write | String | Specify User, Group or ServicePrincipal to interpret the Identity | | ### MSFT_MicrosoftGraphScopedRoleMembership @@ -37,7 +37,8 @@ | Parameter | Attribute | DataType | Description | Allowed Values | | --- | --- | --- | --- | --- | | **RoleName** | Write | String | Name of the Azure AD Role that is assigned | | -| **RoleMemberInfo** | Write | MSFT_MicrosoftGraphIdentity | Member that is assigned the scoped role | | +| **Identity** | Write | String | Identity of directory-object to be assigned the role. For users, specify a UserPrincipalName. For Groups and SPNs, specify DisplayName | | +| **Type** | Write | String | Specify User, Group or ServicePrincipal to interpret the Identity | | ## Description @@ -101,4 +102,47 @@ Configuration Example } } ``` +### Example 2 + +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(Mandatory = $true)] + [PSCredential] + $credsCredential + ) + Import-DscResource -ModuleName Microsoft365DSC + + node localhost + { + AADAdministrativeUnit 'TestUnit' + { + Credential = $credsCredential; + DisplayName = "Test-Unit"; + Ensure = "Present"; + Members = @( + MSFT_MicrosoftGraphMember{ + Identity = "jane.doe@mytenant.com" + Type = "User" + } + MSFT_MicrosoftGraphMember{ + Identity = "Sales" + Type = "Group" + } + ) + ScopedRoleMembers = @( + MSFT_MicrosoftGraphScopedRoleMembership{ + RoleName = "User Administrator" + Type = "User" + Identity = "john.doe@mytenant.com" + } + ) + } + } +} +``` From e176bba22d478a09bc540a71790c45875751d50a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Wed, 25 Jan 2023 13:49:32 +0100 Subject: [PATCH 08/29] Updated unit-test --- .../Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 index b6a2ebea97..7024623852 100644 --- a/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 +++ b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 @@ -497,7 +497,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } } It 'Should Reverse Engineer resource from the Export method' { - Export-TargetResource @testParams -verbose + Export-TargetResource @testParams } } } From 281cccda7761375b4863750807ad66f07978d682 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Thu, 26 Jan 2023 09:59:30 +0100 Subject: [PATCH 09/29] Refactored so it's not a BR --- .../MSFT_AADAdministrativeUnit.psm1 | 60 +++++++++++-------- .../MSFT_AADAdministrativeUnit.schema.mof | 9 +-- ...soft365DSC.AADAdministrativeUnit.Tests.ps1 | 40 ++++++++----- 3 files changed, 65 insertions(+), 44 deletions(-) diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 index 62fbeb0f6f..02dd5e06a5 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 @@ -197,10 +197,13 @@ function Get-TargetResource { write-verbose "AU {$DisplayName} verify RoleId {$($auScopedRoleMember.RoleId)}" $roleObject = Get-MgDirectoryRole -DirectoryRoleId $auScopedRoleMember.RoleId -ErrorAction Stop + write-verbose "Found DirectoryRole '$($roleObject.DisplayName)' with id $($roleObject.Id)" $scopedRoleMember = @{ RoleName = $roleObject.DisplayName - Type = $null - Identity = $null + RoleMemberInfo = @{ + Type = $null + Identity = $null + } } write-verbose "AU {$DisplayName} verify RoleMemberInfo.Id {$($auScopedRoleMember.RoleMemberInfo.Id)}" $memberObject = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/directoryobjects/$($auScopedRoleMember.RoleMemberInfo.Id)" @@ -208,22 +211,22 @@ function Get-TargetResource if (($memberObject.'@odata.type') -match 'user') { write-verbose "AU {$DisplayName} UPN = {$($memberObject.UserPrincipalName)}" - $scopedRoleMember.Identity = $memberObject.UserPrincipalName - $scopedRoleMember.Type = 'User' + $scopedRoleMember.RoleMemberInfo.Identity = $memberObject.UserPrincipalName + $scopedRoleMember.RoleMemberInfo.Type = 'User' } elseif (($memberObject.'@odata.type') -match 'group') { write-verbose "AU {$DisplayName} Group = {$($memberObject.DisplayName)}" - $scopedRoleMember.Identity = $memberObject.DisplayName - $scopedRoleMember.Type = 'Group' + $scopedRoleMember.RoleMemberInfo.Identity = $memberObject.DisplayName + $scopedRoleMember.RoleMemberInfo.Type = 'Group' } else { write-verbose "AU {$DisplayName} SPN = {$($memberObject.DisplayName)}" - $scopedRoleMember.Identity = $memberObject.DisplayName - $scopedRoleMember.Type = 'ServicePrincipal' + $scopedRoleMember.RoleMemberInfo.Identity = $memberObject.DisplayName + $scopedRoleMember.RoleMemberInfo.Type = 'ServicePrincipal' } - write-verbose "AU {$DisplayName} scoped role member: RoleName '$($scopedRoleMember.RoleName)' Type '$($scopedRoleMember.Type)' Identity '$($scopedRoleMember.Identity)'" + write-verbose "AU {$DisplayName} scoped role member: RoleName '$($scopedRoleMember.RoleName)' Type '$($scopedRoleMember.RoleMemberInfo.Type)' Identity '$($scopedRoleMember.RoleMemberInfo.Identity)'" $scopedRoleMemberSpec += $scopedRoleMember } write-verbose "AU {$DisplayName} add $($scopedRoleMemberSpec.Count) ScopedRoleMembers to results" @@ -450,37 +453,37 @@ function Set-TargetResource { throw "AU {$($DisplayName)}: RoleName {$($roleMember.RoleName)} does not exist" } - if ($roleMember.Type -eq 'User') + if ($roleMember.RoleMemberInfo.Type -eq 'User') { - $roleMemberIdentity = Get-MgUser -Filter "UserPrincipalName eq '$($roleMember.Identity)'" -ErrorAction Stop + $roleMemberIdentity = Get-MgUser -Filter "UserPrincipalName eq '$($roleMember.RoleMemberInfo.Identity)'" -ErrorAction Stop if ($null -eq $roleMemberIdentity) { - throw "AU {$($DisplayName)}: Scoped Role User {$($roleMember.Identity)} for role {$($roleMember.RoleName)} does not exist" + throw "AU {$($DisplayName)}: Scoped Role User {$($roleMember.RoleMemberInfo.Identity)} for role {$($roleMember.RoleName)} does not exist" } } elseif ($roleMember.Type -eq 'Group') { - $roleMemberIdentity = Get-MgGroup -Filter "displayName eq '$($roleMember.Identity)'" -ErrorAction Stop + $roleMemberIdentity = Get-MgGroup -Filter "displayName eq '$($roleMember.RoleMemberInfo.Identity)'" -ErrorAction Stop if ($null -eq $roleMemberIdentity) { - throw "AU {$($DisplayName)}: Scoped Role Group {$($roleMember.Identity)} for role {$($roleMember.RoleName)} does not exist" + throw "AU {$($DisplayName)}: Scoped Role Group {$($roleMember.RoleMemberInfo.Identity)} for role {$($roleMember.RoleName)} does not exist" } } elseif ($roleMember.Type -eq 'ServicePrincipal') { - $roleMemberIdentity = Get-MgServicePrincipal -Filter "displayName eq '$($roleMember.Identity)'" -ErrorAction Stop + $roleMemberIdentity = Get-MgServicePrincipal -Filter "displayName eq '$($roleMember.RoleMemberInfo.Identity)'" -ErrorAction Stop if ($null -eq $roleMemberIdentity) { - throw "AU {$($DisplayName)}: Scoped Role ServicePrincipal {$($roleMember.Identity)} for role {$($roleMember.RoleName)} does not exist" + throw "AU {$($DisplayName)}: Scoped Role ServicePrincipal {$($roleMember.RoleMemberInfo.Identity)} for role {$($roleMember.RoleName)} does not exist" } } else { - throw "AU {$($DisplayName)}: Invalid ScopedRoleMember.Type {$($roleMember.Type)}" + throw "AU {$($DisplayName)}: Invalid ScopedRoleMember.RoleMemberInfo.Type {$($roleMember.RoleMemberInfo.Type)}" } if ($roleMemberIdentity.Count -gt 1) { - throw "AU {$($DisplayName)}: ScopedRoleMember for role {$($roleMember.RoleName)}: $($roleMember.Type) {$($roleMember.Identity)} is not unique" + throw "AU {$($DisplayName)}: ScopedRoleMember for role {$($roleMember.RoleName)}: $($roleMember.RoleMemberInfo.Type) {$($roleMember.RoleMemberInfo.Identity)} is not unique" } $scopedRoleMemberSpecification += @{ RoleId = $roleObject.Id @@ -628,13 +631,14 @@ function Set-TargetResource $desiredScopedRoleMembersValue = @() } + # flatten hashtabls for compare $compareCurrentScopedRoleMembersValue = @() foreach ($roleMember in $currentScopedRoleMembersValue) { $compareCurrentScopedRoleMembersValue += [pscustomobject]@{ RoleName = $roleMember.RoleName - Identity = $roleMember.Identity - Type = $roleMember.Type + Identity = $roleMember.RoleMemberInfo.Identity + Type = $roleMember.RoleMemberInfo.Type } } $compareDesiredScopedRoleMembersValue = @() @@ -642,8 +646,8 @@ function Set-TargetResource { $compareDesiredScopedRoleMembersValue += [pscustomobject]@{ RoleName = $roleMember.RoleName - Identity = $roleMember.Identity - Type = $roleMember.Type + Identity = $roleMember.RoleMemberInfo.Identity + Type = $roleMember.RoleMemberInfo.Type } } write-verbose "AU {$DisplayName} Update ScopedRoleMembers: Current members: $($compareCurrentScopedRoleMembersValue.Identity -join ', ')" @@ -832,7 +836,6 @@ function Test-TargetResource } $testResult = $true - <# #Compare Cim instances foreach ($key in $PSBoundParameters.Keys) { @@ -857,7 +860,6 @@ function Test-TargetResource } } - #> $ValuesToCheck.Remove('Credential') | Out-Null $ValuesToCheck.Remove('ApplicationId') | Out-Null @@ -977,8 +979,14 @@ function Export-TargetResource if ($null -ne $Results.ScopedRoleMembers) { + $complexMapping = @( + @{ + Name = 'RoleMemberInfo' + CimInstanceName = 'MicrosoftGraphIdentity' + } + ) $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject ([Array]$Results.ScopedRoleMembers) ` - -CIMInstanceName MicrosoftGraphScopedRoleMembership + -CIMInstanceName MicrosoftGraphScopedRoleMembership -ComplexTypeMapping $complexMapping write-verbose "ScopedRoleMembers on next line:`r`n$complexTypeStringResult" @@ -992,7 +1000,7 @@ function Export-TargetResource if ($null -ne $Results.Members) { $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject ([Array]$Results.Members) ` - -CIMInstanceName MicrosoftGraphMember + -CIMInstanceName MicrosoftGraphIdentity write-verbose "Members on next line:`r`n$complexTypeStringResult" $Results.Members = $complexTypeStringResult diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.schema.mof b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.schema.mof index 908e8f85ac..d6581911c1 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.schema.mof +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.schema.mof @@ -1,15 +1,16 @@ [ClassVersion("1.0.0")] -class MSFT_MicrosoftGraphMember +class MSFT_MicrosoftGraphIdentity { [Write, Description("Identity of member. For users, specify a UserPrincipalName. For groups and devices, specify DisplayName")] String Identity; - [Write, Description("Specify User, Group or Device to interpret the Identity")] String Type; + [Write, Description("Specify User, Group or Device to interpret the identity. Can be Principal in ScopedRoleMembers")] String Type; }; [ClassVersion("1.0.0")] class MSFT_MicrosoftGraphScopedRoleMembership { [Write, Description("Name of the Azure AD Role that is assigned")] String RoleName; - [Write, Description("Identity of member. For users, specify a UserPrincipalName. For groups and SPNs, specify the DisplayName")] String Identity; - [Write, Description("Specify User, Group or ServicePrincipal to interpret the Identity")] String Type; + [Write, Description("Member that is assigned the scoped role"), EmbeddedInstance("MSFT_MicrosoftGraphIdentity")] String RoleMemberInfo; + // [Write, Description("Identity of member. For users, specify a UserPrincipalName. For groups and SPNs, specify the DisplayName")] String Identity; + // [Write, Description("Specify User, Group or ServicePrincipal to interpret the Identity")] String Type; }; [ClassVersion("1.0.0.0"), FriendlyName("AADAdministrativeUnit")] diff --git a/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 index 7024623852..f3f89ac64c 100644 --- a/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 +++ b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 @@ -82,7 +82,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { DisplayName = 'FakeStringValue1' Id = 'FakeStringValue1' Members = @( - (New-CimInstance -ClassName MSFT_MicrosoftGraphMember -Property @{ + (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ Type = 'User' Identity = 'john.smith@contoso.com' } -ClientOnly) @@ -127,7 +127,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { DisplayName = 'FakeStringValue2' Id = 'FakeStringValue2' Members = @( - (New-CimInstance -ClassName MSFT_MicrosoftGraphMember -Property @{ + (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ Type = 'User' Identity = 'john.smith@contoso.com' } -ClientOnly) @@ -171,7 +171,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { DisplayName = 'DSCAU' Id = 'DSCAU' Members = @( - (New-CimInstance -ClassName MSFT_MicrosoftGraphMember -Property @{ + (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ Identity = 'John.Doe@mytenant.com' Type = 'User' } -ClientOnly) @@ -179,8 +179,12 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { ScopedRoleMembers = @( (New-CimInstance -ClassName MSFT_MicrosoftGraphScopedRoleMembership -Property @{ RoleName = 'User Administrator' - Identity = 'John.Doe@mytenant.com' - Type = 'User' + RoleMemberInfo = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ + Identity = 'John.Doe@mytenant.com' + Type = 'User' + } -ClientOnly) + #Identity = 'John.Doe@mytenant.com' + #Type = 'User' } -ClientOnly) ) Visibility = 'Public' @@ -258,7 +262,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { DisplayName = 'DSCAU2' Id = 'DSCAU2' Members = @( - (New-CimInstance -ClassName MSFT_MicrosoftGraphMember -Property @{ + (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ Identity = 'John.Doe@mytenant.com' Type = 'User' } -ClientOnly) @@ -266,8 +270,12 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { ScopedRoleMembers = @( (New-CimInstance -ClassName MSFT_MicrosoftGraphScopedRoleMembership -Property @{ RoleName = 'User Administrator' - Identity = 'John.Doe@mytenant.com' - Type = 'User' + RoleMemberInfo = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ + Identity = 'John.Doe@mytenant.com' + Type = 'User' + } -ClientOnly) + #Identity = 'John.Doe@mytenant.com' + #Type = 'User' } -ClientOnly) ) Visibility = 'Public' @@ -345,7 +353,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { DisplayName = 'DSCAU' Id = 'DSCAU' Members = @( - (New-CimInstance -ClassName MSFT_MicrosoftGraphMember -Property @{ + (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ Identity = 'John.Doe@mytenant.com' Type = 'User' } -ClientOnly) @@ -353,8 +361,12 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { ScopedRoleMembers = @( (New-CimInstance -ClassName MSFT_MicrosoftGraphScopedRoleMembership -Property @{ RoleName = 'User Administrator' - Identity = 'John.Doe@mytenant.com' - Type = 'User' + RoleMemberInfo = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ + Identity = 'John.Doe@mytenant.com' + Type = 'User' + } -ClientOnly) + #Identity = 'John.Doe@mytenant.com' + #Type = 'User' } -ClientOnly) ) Visibility = 'Public' @@ -459,7 +471,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { [pscustomobject]@{ RoleId = '09876-54321' RoleMemberInfo = @{ - DisplayName = 'Group' + DisplayName = 'FakeRoleGroup' Id = '0987654321' } }) @@ -482,14 +494,14 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } } - Mock -CommandName Get-MgDirectoryRole -ParameterFilter {$Id -eq '12345-67890'} -MockWith { + Mock -CommandName Get-MgDirectoryRole -ParameterFilter {$DirectoryRoleId -eq '12345-67890'} -MockWith { return [pscustomobject]@{ Id = '12345-67890' DisplayName = 'DSC User Administrator' } } - Mock -CommandName Get-MgDirectoryRole -ParameterFilter {$Id -eq '09876-54321'} -MockWith { + Mock -CommandName Get-MgDirectoryRole -ParameterFilter {$DirectoryRoleId -eq '09876-54321'} -MockWith { return [pscustomobject]@{ Id = '09876-54321' DisplayName = 'DSC Groups Administrator' From d5b883a11a128a66e855acfc38668ec0992797ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Thu, 26 Jan 2023 15:00:09 +0100 Subject: [PATCH 10/29] fix order of props for export of ScopedRoleMembers --- .../MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 index 02dd5e06a5..6757cee4ea 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 @@ -198,7 +198,7 @@ function Get-TargetResource write-verbose "AU {$DisplayName} verify RoleId {$($auScopedRoleMember.RoleId)}" $roleObject = Get-MgDirectoryRole -DirectoryRoleId $auScopedRoleMember.RoleId -ErrorAction Stop write-verbose "Found DirectoryRole '$($roleObject.DisplayName)' with id $($roleObject.Id)" - $scopedRoleMember = @{ + $scopedRoleMember = [ordered]@{ RoleName = $roleObject.DisplayName RoleMemberInfo = @{ Type = $null From 7f2047693fe3edde1540354749b6685076d6f651 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Thu, 16 Feb 2023 11:25:27 +0100 Subject: [PATCH 11/29] AADAdministrativeUNit: added PR to changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9c3ea36b3..ddd33d1579 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ # UNRELEASED +* AADAdministrativeUnit + * Fixed general issues caused by improper handling of nested CIMInstances + Fixes #2775, #2776, #2786 * MISC * Updated Tasks.Read and Tasks.ReadWrite Permissions for Planner Plans and Planner Buckets FIXES [#2866](https://github.com/microsoft/Microsoft365DSC/issues/2866) From 7f93a5b910e564d1d557521398788a52a479b8ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Thu, 23 Feb 2023 08:30:00 +0100 Subject: [PATCH 12/29] Remove Ensure default value --- .../MSFT_AADAdministrativeUnit.psm1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 index 6757cee4ea..6f9b08d55e 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 @@ -49,7 +49,7 @@ function Get-TargetResource [Parameter(Mandatory = $true)] [System.String] [ValidateSet('Absent', 'Present')] - $Ensure = $true, + $Ensure, [Parameter()] [System.Management.Automation.PSCredential] @@ -296,7 +296,7 @@ function Set-TargetResource [Parameter(Mandatory = $true)] [System.String] [ValidateSet('Absent', 'Present')] - $Ensure = $true, + $Ensure, [Parameter()] [System.Management.Automation.PSCredential] From 01de6a3b83d1cd1a55ff08bbb5dfcb76a23c1132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Thu, 23 Feb 2023 08:38:28 +0100 Subject: [PATCH 13/29] updated changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cf7b7f458..ecdda12bdd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ * AADAdministrativeUnit * Fixed general issues caused by improper handling of nested CIMInstances Fixes #2775, #2776, #2786 +* TeamsOnlineVoiceUser + * Fix issue where the cmdlet Get-CsOnlineVoiceUser is now deprecated. + +# 1.23.222.1 + * AADEntitlementManagementAccessPackageAssignmentPolicy * Initial release * IntuneDeviceEnrollmentConfigurationWindows10 From c24200b1ca529b37d0ee5fbd5475155515b00e54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Wed, 1 Mar 2023 11:00:28 +0100 Subject: [PATCH 14/29] AADAdministrativeUnit: Fixed Ensure default value --- .../MSFT_AADAdministrativeUnit.psm1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 index 6f9b08d55e..02b33d58c4 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 @@ -49,7 +49,7 @@ function Get-TargetResource [Parameter(Mandatory = $true)] [System.String] [ValidateSet('Absent', 'Present')] - $Ensure, + $Ensure = 'Present', [Parameter()] [System.Management.Automation.PSCredential] @@ -296,7 +296,7 @@ function Set-TargetResource [Parameter(Mandatory = $true)] [System.String] [ValidateSet('Absent', 'Present')] - $Ensure, + $Ensure = 'Present', [Parameter()] [System.Management.Automation.PSCredential] @@ -785,7 +785,7 @@ function Test-TargetResource [Parameter(Mandatory = $true)] [System.String] [ValidateSet('Absent', 'Present')] - $Ensure = $true, + $Ensure = 'Present', [Parameter()] [System.Management.Automation.PSCredential] From 606752280950e5ddd89000a9745cb5bbdec2a731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Mon, 9 Jan 2023 15:09:17 +0100 Subject: [PATCH 15/29] Fixed invalild usage of beta Graph cmdlets --- .../MSFT_AADAdministrativeUnit.psm1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 index e424a8b8b1..0402c1d400 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 @@ -116,12 +116,12 @@ function Get-TargetResource #region resource generator code if (-Not [string]::IsNullOrEmpty($Id)) { - $getValue = Get-MgAdministrativeUnit -AdministrativeUnitId $Id -ErrorAction Stop + $getValue = Get-MgDirectoryAdministrativeUnit -AdministrativeUnitId $Id -ErrorAction Stop } if (-not $getValue -and -Not [string]::IsNullOrEmpty($DisplayName)) { - $getValue = Get-MgAdministrativeUnit -Filter "DisplayName eq '$DisplayName'" -ErrorAction Stop + $getValue = Get-MgDirectoryAdministrativeUnit -Filter "DisplayName eq '$DisplayName'" -ErrorAction Stop } #endregion @@ -565,7 +565,7 @@ function Set-TargetResource } #region resource generator code - $policy = New-MgAdministrativeUnit @CreateParameters + $policy = New-MgDirectoryAdministrativeUnit @CreateParameters #endregion @@ -622,7 +622,7 @@ function Set-TargetResource # The AU MembershipType may have changed from Dynamic to Static and that change has to be implemented before explicitly adding members #region resource generator code - Update-MgAdministrativeUnit @UpdateParameters ` + Update-MgDirectoryAdministrativeUnit @UpdateParameters ` -AdministrativeUnitId $currentInstance.Id #endregion From 92d4a16ee1d419bcb5adf69caab686f148aa75bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Fri, 13 Jan 2023 12:14:04 +0100 Subject: [PATCH 16/29] Changed AADAdministrativeUnit to use Graph v1.0 --- .../MSFT_AADAdministrativeUnit.psm1 | 51 +++++++---- .../Microsoft365DSC/Modules/M365DSCUtil.psm1 | 7 +- ...soft365DSC.AADAdministrativeUnit.Tests.ps1 | 90 +++++-------------- 3 files changed, 62 insertions(+), 86 deletions(-) diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 index 0402c1d400..cfc670e0ba 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 @@ -79,16 +79,11 @@ function Get-TargetResource $ManagedIdentity ) - # Note: Graph v1.0 names basic cmdlets xxx-MgDirectoryAdministrativeUnit(xxx) - # but the beta profile names latest cmdlets xxx-MgAdministrativeUnit(xxx) - # only the beta cmdlets support the preview enabling MembershipType and MembershipRuleProcessingState - # NB: Usage of these params require that the corresponding AAD preview feature is enabled - try { $ConnectionMode = New-M365DSCConnection -Workload 'MicrosoftGraph' ` -InboundParameters $PSBoundParameters ` - -ProfileName 'beta' + -ProfileName 'v1.0' } catch { @@ -365,7 +360,7 @@ function Set-TargetResource { $ConnectionMode = New-M365DSCConnection -Workload 'MicrosoftGraph' ` -InboundParameters $PSBoundParameters ` - -ProfileName 'beta' + -ProfileName 'v1.0' } catch { @@ -410,6 +405,7 @@ function Set-TargetResource { $CreateParameters = $currentParameters.Clone() + <# When using -BodyParameter it is not necessary to use AdditionalProperties for Dynamic AU params if ($CreateParameters.Containskey('MembershipType') -or $CreateParameters.Containskey('MembershipRule') -or $CreateParameters.Containskey('MembershipRuleProcessingState')) { $CreateParameters.Remove('MembershipType') | Out-Null @@ -429,6 +425,7 @@ function Set-TargetResource $CreateParameters.AdditionalProperties.Add('MembershipRuleProcessingState', $MembershipRuleProcessingState) } } + #> foreach ($key in ($CreateParameters.clone()).Keys) { @@ -449,7 +446,7 @@ function Set-TargetResource $memberIdentity = Get-MgUser -Filter "UserPrincipalName eq '$($Member.Identity)'" -ErrorAction Stop if ($memberIdentity) { - $memberSpecification += @{Id = $memberIdentity.Id } + $memberSpecification += @{Type="$($Member.Type)s";Id = $memberIdentity.Id } } else { @@ -461,7 +458,7 @@ function Set-TargetResource $memberIdentity = Get-MgGroup -Filter "DisplayName eq '$($Member.Identity)'" -ErrorAction Stop if ($memberIdentity) { - $memberSpecification += @{Id = $memberIdentity.Id } + $memberSpecification += @{Type="$($Member.Type)s";Id = $memberIdentity.Id } } else { @@ -473,7 +470,7 @@ function Set-TargetResource $memberIdentity = Get-MgDevice -Filter "DisplayName eq '$($Member.Identity)'" -ErrorAction Stop if ($memberIdentity) { - $memberSpecification += @{Id = $memberIdentity.Id } + $memberSpecification += @{Type="$($Member.Type)s";Id = $memberIdentity.Id } } else { @@ -485,7 +482,8 @@ function Set-TargetResource throw "AU {$($DisplayName)}: Member {$($Member.Identity)} has invalid type {$($Member.Type)}" } } - $CreateParameters.Members = $memberSpecification + # Members are added to the AU *after* it has been created + $CreateParameters.Remove('Members') | Out-Null } else { @@ -565,9 +563,17 @@ function Set-TargetResource } #region resource generator code - $policy = New-MgDirectoryAdministrativeUnit @CreateParameters + $policy = New-MgDirectoryAdministrativeUnit -BodyParameter $CreateParameters #endregion + foreach ($member in $memberSpecificationpecification) + { + $memberBodyParam = @{ + '@odata.id' = "https://graph.microsoft.com/v1.0/$($member.Type)/{$($member.Id)}" + } + + New-MgDirectoryAdministrativeUnitMemberByRef -AdministrativeUnitId $policy.Id -BodyParameter $memberBodyParam + } foreach ($scopedRoleMember in $scopedRoleMemberSpecification) { @@ -596,8 +602,9 @@ function Set-TargetResource #$UpdateParameters.Remove('Extensions') | Out-Null $UpdateParameters.Remove('Members') | Out-Null $UpdateParameters.Remove('ScopedRoleMembers') | Out-Null - $UpdateParameters.Remove('Visibility') | Out-Null + #$UpdateParameters.Remove('Visibility') | Out-Null + <# When using -BodyParameter it is not necessary to use AdditionalProperties for Dynamic AU params if ($UpdateParameters.Containskey('MembershipType') -or $UpdateParameters.Containskey('MembershipRule') -or $UpdateParameters.Containskey('MembershipRuleProcessingState')) { $UpdateParameters.Remove('MembershipType') | Out-Null @@ -617,12 +624,13 @@ function Set-TargetResource $UpdateParameters.AdditionalProperties.Add('MembershipRuleProcessingState', $MembershipRuleProcessingState) } } + #> # when updating the resource, update the AU first and its members (if any) afterwards. # The AU MembershipType may have changed from Dynamic to Static and that change has to be implemented before explicitly adding members #region resource generator code - Update-MgDirectoryAdministrativeUnit @UpdateParameters ` + Update-MgDirectoryAdministrativeUnit -BodyParameter $UpdateParameters ` -AdministrativeUnitId $currentInstance.Id #endregion @@ -660,6 +668,9 @@ function Set-TargetResource { $memberObject = Get-MgDevice -Filter "DisplayName eq '$($diff.Identity)'" $membertype = 'devices' + } else { + # throw if a *new* member has been specified with invalid type + throw "AU {$($DisplayName)}: Member {$($diff.Identity)} has invalid type {$($diff.Type)}" } if ($null -eq $memberObject) { @@ -667,15 +678,14 @@ function Set-TargetResource } if ($memberObject.Count -gt 1) { - throw "AU member {$($diff.Identity)} is not a unique $($diff.Type) (Count=$($memberObject.Count))" + throw "AU member {$($diff.Identity)} is not a unique $($diff.Type.ToLower()) (Count=$($memberObject.Count))" } if ($diff.SideIndicator -eq '=>') { Write-Verbose -Message "Adding new member {$($diff.Identity)}, type {$($diff.Type)} to Administrative Unit {$($currentInstance.DisplayName)}" $memberBodyParam = @{ - '@odata.id' = "https://graph.microsoft.com/v1.0b038744 Objectivism2o2! - b/$memberType/{$($memberObject.Id)}" + '@odata.id' = "https://graph.microsoft.com/v1.0/$memberType/{$($memberObject.Id)}" } New-MgDirectoryAdministrativeUnitMemberByRef -AdministrativeUnitId ($currentInstance.Id) -BodyParameter $memberBodyParam | Out-Null } @@ -948,6 +958,7 @@ function Test-TargetResource Write-Verbose -Message "Test-TargetResource returned $false" return $false } + $testResult = $true foreach ($key in $PSBoundParameters.Keys) @@ -1001,6 +1012,12 @@ function Test-TargetResource # Removing the visibility parameter from the check since this is always being returned as null currently by the Microsoft Graph. $ValuesToCheck.Remove('Visibility') | Out-Null + if ($MembershipType -ne 'Dynamic' -and $CurrentValues.MembershipType -ne 'Dynamic') + { + # Removing the MembershipType parameter from the check if it isn't Dynamic. If so, it can be aither Assigned or null + $ValuesToCheck.Remove('MembershipType') | Out-Null + } + Write-Verbose -Message "Current Values: $(Convert-M365DscHashtableToString -Hashtable $CurrentValues)" Write-Verbose -Message "Target Values: $(Convert-M365DscHashtableToString -Hashtable $ValuesToCheck)" diff --git a/Modules/Microsoft365DSC/Modules/M365DSCUtil.psm1 b/Modules/Microsoft365DSC/Modules/M365DSCUtil.psm1 index e50d72e8dc..4cea2a5e85 100644 --- a/Modules/Microsoft365DSC/Modules/M365DSCUtil.psm1 +++ b/Modules/Microsoft365DSC/Modules/M365DSCUtil.psm1 @@ -1286,7 +1286,10 @@ Public function Import-M365DSCDependencies { [CmdletBinding()] - param() + param( + [parameter()] + [switch]$Global + ) $currentPath = Join-Path -Path $PSScriptRoot -ChildPath '..\' -Resolve $manifest = Import-PowerShellDataFile "$currentPath/Dependencies/Manifest.psd1" @@ -1294,7 +1297,7 @@ function Import-M365DSCDependencies foreach ($dependency in $dependencies) { - Import-Module $dependency.ModuleName -RequiredVersion $dependency.RequiredVersion -Force + Import-Module $dependency.ModuleName -RequiredVersion $dependency.RequiredVersion -Force -Global:$Global } } diff --git a/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 index e745959c62..35a38733fc 100644 --- a/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 +++ b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 @@ -43,13 +43,13 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Mock -CommandName Invoke-MgGraphRequest -MockWith { } - Mock -CommandName Update-MgAdministrativeUnit -MockWith { + Mock -CommandName Update-MgDirectoryAdministrativeUnit -MockWith { } Mock -CommandName Remove-MgDirectoryAdministrativeUnit -MockWith { } - Mock -CommandName New-MgAdministrativeUnit -MockWith { + Mock -CommandName New-MgDirectoryAdministrativeUnit -MockWith { } Mock -CommandName New-MgDirectoryAdministrativeUnitMemberByRef -MockWith { @@ -67,7 +67,8 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Mock -CommandName Remove-MgDirectoryAdministrativeUnitScopedRoleMember -MockWith { } Mock -CommandName New-M365DSCConnection -MockWith { - return 'Credentials' + # Select-MgProfile beta # not anymore + return 'Credential' } # Mock Write-Host to hide output during the tests @@ -96,7 +97,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } } - Mock -CommandName Get-MgAdministrativeUnit -MockWith { + Mock -CommandName Get-MgDirectoryAdministrativeUnit -MockWith { return $null } } @@ -108,7 +109,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } It 'Should Create the AU from the Set method' { Set-TargetResource @testParams - Should -Invoke -CommandName New-MgAdministrativeUnit -Exactly 1 + Should -Invoke -CommandName New-MgDirectoryAdministrativeUnit -Exactly 1 } } @@ -126,7 +127,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Credential = $Credential } - Mock -CommandName Get-MgAdministrativeUnit -MockWith { + Mock -CommandName Get-MgDirectoryAdministrativeUnit -MockWith { return [pscustomobject]@{ Description = 'FakeStringValue2' DisplayName = 'FakeStringValue2' @@ -157,11 +158,11 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Context -Name 'The AU Exists and Values are already in the desired state' -Fixture { BeforeAll { $testParams = @{ - Description = 'DSCAU' - DisplayName = 'DSCAU' - Id = 'DSCAU' - Members = @( - (New-CimInstance -ClassName MSFT_MicrosoftGraphdirectoryobject -Property @{ + Description = 'DSCAU' + DisplayName = 'DSCAU' + Id = 'DSCAU' + Members = @( + (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ Identity = 'John.Doe@mytenant.com' Type = 'User' } -ClientOnly) @@ -185,15 +186,16 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } -ClientOnly) ) #> - Visibility = 'Public' - MembershipType = 'Assigned' - MembershipRule = 'Canada' + Visibility = 'Public' + MembershipType = 'Assigned' + # MembershipRule and -ProcessingState params are only used when MembershipType is Dynamic + MembershipRule = 'Canada' MembershipRuleProcessingState = 'On' Ensure = 'Present' Credential = $Credential } - Mock -CommandName Get-MgAdministrativeUnit -MockWith { + Mock -CommandName Get-MgDirectoryAdministrativeUnit -MockWith { return [pscustomobject]@{ Description = 'DSCAU' DisplayName = 'DSCAU' @@ -206,21 +208,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } ) #> - Members = @( - [pscustomobject]@{Id = '1234567890' } - ) - ScopedRoleMembers = @( - [pscustomobject]@{ - RoleId = '12345-67890' - RoleMemberInfo = @( - [pscustomobject]@{ - DisplayName = 'John Doe' - Id = '1234567890' - } - ) - } - ) - Visibility = 'Public' + Visibility = 'Public' AdditionalProperties = @{ membershipType = 'Assigned' membershipRule = 'Canada' @@ -289,7 +277,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { #> Id = 'DSCAU2' Members = @( - (New-CimInstance -ClassName MSFT_MicrosoftGraphdirectoryobject -Property @{ + (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ Identity = 'John.Doe@mytenant.com' Type = 'User' } -ClientOnly) @@ -309,24 +297,12 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Credential = $Credential } - Mock -CommandName Get-MgAdministrativeUnit -MockWith { + Mock -CommandName Get-MgDirectoryAdministrativeUnit -MockWith { return [pscustomobject]@{ Description = 'DSCAU2' DisplayName = 'DSCAU2' Id = 'DSCAU2' Visibility = 'Public' - Members = $null - ScopedRoleMembers = @( - [pscustomobject]@{ - RoleId = '12345-67890' - RoleMemberInfo = [pscustomobject]@( - @{ - DisplayName = 'John Doe' - Id = '1234567890' - } - ) - } - ) <# Extensions =@( [pscustomobject]@{ @@ -335,8 +311,6 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } ) #> - '@odata.type' = '#microsoft.graph.administrativeunit' - } } @@ -410,7 +384,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { ) #> Members = @( - (New-CimInstance -ClassName MSFT_MicrosoftGraphdirectoryobject -Property @{ + (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ Identity = 'John.Doe@mytenant.com' Type = 'User' } -ClientOnly) @@ -429,16 +403,12 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Credential = $Credential } - Mock -CommandName Get-MgAdministrativeUnit -MockWith { + Mock -CommandName Get-MgDirectoryAdministrativeUnit -MockWith { return [pscustomobject]@{ Description = 'DSCAU' DisplayName = 'DSCAU' Id = 'DSCAU' Visibility = 'Public' - Members = @( - [pscustomobject]@{Id = '1234567890' } - ) - ScopedRoleMembers = $null <# Extensions =@( [pscustomobject]@{ @@ -447,7 +417,6 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } ) #> - '@odata.type' = '#microsoft.graph.' } } @@ -515,7 +484,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Credential = $Credential } - Mock -CommandName Get-MgAdministrativeUnit -MockWith { + Mock -CommandName Get-MgDirectoryAdministrativeUnit -MockWith { return [pscustomobject]@{ Description = 'ExportDSCAU' DisplayName = 'ExportDSCAU' @@ -530,20 +499,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { ) #> Id = 'ExportDSCAU' - Members = @( - [pscustomobject]@{Id = '1234567890' } - ) - ScopedRoleMembers = @( - [pscustomobject]@{ - RoleId = '12345-67890' - RoleMemberInfo = [pscustomobject]@{ - DisplayName = 'John Doe' - Id = '1234567890' - } - } - ) Visibility = 'Public' - } } From 584ab65a71fb2e4cba66a78b565b48606733195e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Tue, 17 Jan 2023 12:41:02 +0100 Subject: [PATCH 17/29] Fixed issues with AADAdministrativeUnit --- .../MSFT_AADAdministrativeUnit.psm1 | 350 ++++++++++++------ .../MSFT_AADAdministrativeUnit.schema.mof | 12 +- ...soft365DSC.AADAdministrativeUnit.Tests.ps1 | 54 ++- 3 files changed, 279 insertions(+), 137 deletions(-) diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 index cfc670e0ba..a23359a6bc 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 @@ -159,37 +159,37 @@ function Get-TargetResource if ($auMembers) { $memberSpec = @() - } - foreach ($getMember in $auMembers) - { - # get object regardless of type - $memberObject = Invoke-MgGraphRequest -Method Get -Uri "https://graph.microsoft.com/v1.0/directoryObjects/$($getMember.Id)" - switch -regex ([regex]::Escape($memberObject.'@odata.type')) + foreach ($getMember in $auMembers) { - 'group' + # get object regardless of type + $memberObject = Invoke-MgGraphRequest -Method Get -Uri "https://graph.microsoft.com/v1.0/directoryObjects/$($getMember.Id)" + switch -regex ([regex]::Escape($memberObject.'@odata.type')) { - $memberSpec += @{ - Identity = $memberObject.DisplayName - Type = 'Group' + 'group' + { + $memberSpec += [pscustomobject]@{ + Identity = $memberObject.DisplayName; + Type = 'Group' + } } - } - 'user' - { - $memberSpec += @{ - Identity = $memberObject.UserPrincipalName - Type = 'User' + 'user' + { + $memberSpec += [pscustomobject]@{ + Identity = $memberObject.UserPrincipalName; + Type = 'User' + } } - } - 'device' - { - $memberSpec += @{ - Identity = $memberObject.DisplayName - Type = 'Device' + 'device' + { + $memberSpec += [pscustomobject]@{ + Identity = $memberObject.DisplayName; + Type = 'Device' + } } } } + $results.Add('Members', $memberSpec) } - $results.Add('Members', $memberSpec) } $scopedRoleMemberSpec = $null @@ -211,25 +211,25 @@ function Get-TargetResource { if ([regex]::Escape($roleMemberObject.'@odata.type') -match 'group') { - $memberType = 'Group' + $memberType = 'Group'; } else { - $memberType = 'ServicePrincipal' + $memberType = 'ServicePrincipal'; } $memberIdentity = $roleMemberObject.DisplayName } - $scopedRoleMemberInfo = @{ - RoleName = $roleObject.DisplayName - RoleMemberInfo = @{ - Identity = $memberIdentity - Type = $memberType - } + $scopedRoleMemberInfo = [pscustomobject]@{ + RoleName = $roleObject.DisplayName; + #RoleMemberInfo = @{ # avoid nested hashtable, can't handle it in Export + Identity = $memberIdentity + Type = $memberType + #} } $scopedRoleMemberSpec += $scopedRoleMemberInfo } + $results.Add('ScopedRoleMembers', $scopedRoleMemberSpec) } - $results.Add('ScopedRoleMembers', $scopedRoleMemberSpec) <# # Extensions are still too unwieldy @@ -356,16 +356,9 @@ function Set-TargetResource # ONLY the beta cmdlets support the preview enabling MembershipType and MembershipRuleProcessingState # NB: Usage of these params require that the corresponding AAD preview feature is enabled - try - { - $ConnectionMode = New-M365DSCConnection -Workload 'MicrosoftGraph' ` + $ConnectionMode = New-M365DSCConnection -Workload 'MicrosoftGraph' ` -InboundParameters $PSBoundParameters ` -ProfileName 'v1.0' - } - catch - { - Write-Verbose -Message $_ - } #Ensure the proper dependencies are installed in the current environment. Confirm-M365DSCDependencies @@ -427,6 +420,7 @@ function Set-TargetResource } #> + <# skipped - no use for hashtables foreach ($key in ($CreateParameters.clone()).Keys) { if ($CreateParameters[$key].getType().Fullname -like '*CimInstance*') @@ -434,19 +428,20 @@ function Set-TargetResource $CreateParameters[$key] = Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $CreateParameters[$key] } } + #> - # Resolve Members Type/Identity to user or group id - if ($currentParameters.Members) + $memberSpecification = @() + if ($MembershipType -ne 'Dynamic') { - $memberSpecification = @() - foreach ($Member in $Members) + # Resolve Members Type/Identity to user or group id + foreach ($Member in $currentParameters.Members) { if ($Member.Type -eq 'User') { $memberIdentity = Get-MgUser -Filter "UserPrincipalName eq '$($Member.Identity)'" -ErrorAction Stop if ($memberIdentity) { - $memberSpecification += @{Type="$($Member.Type)s";Id = $memberIdentity.Id } + $memberSpecification += [pscustomobject]@{Type="$($Member.Type)s";Id = $memberIdentity.Id } } else { @@ -458,7 +453,7 @@ function Set-TargetResource $memberIdentity = Get-MgGroup -Filter "DisplayName eq '$($Member.Identity)'" -ErrorAction Stop if ($memberIdentity) { - $memberSpecification += @{Type="$($Member.Type)s";Id = $memberIdentity.Id } + $memberSpecification += [pscustomobject]@{Type="$($Member.Type)s";Id = $memberIdentity.Id } } else { @@ -470,7 +465,7 @@ function Set-TargetResource $memberIdentity = Get-MgDevice -Filter "DisplayName eq '$($Member.Identity)'" -ErrorAction Stop if ($memberIdentity) { - $memberSpecification += @{Type="$($Member.Type)s";Id = $memberIdentity.Id } + $memberSpecification += [pscustomobject]@{Type="$($Member.Type)s";Id = $memberIdentity.Id } } else { @@ -483,51 +478,82 @@ function Set-TargetResource } } # Members are added to the AU *after* it has been created - $CreateParameters.Remove('Members') | Out-Null - } - else - { - $CreateParameters.Remove('Members') | Out-Null } + $CreateParameters.Remove('Members') | Out-Null # Resolve ScopedRoleMembers Type/Identity to user, group or service principal if ($currentParameters.ScopedRoleMembers) { + #write-verbose "AU {$DisplayName}: Set-TargetResource, Convert ScopedRoleMembers to Hashtable(s)" + $testScopedRoleMembers = Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $ScopedRoleMembers <#@() + foreach ($scopedRoleMember in $testScopedRoleMembers) + { + $testScopedRoleMember = @{} + foreach ($key in $scopedRoleMember.clone().Keys) + { + if ($scopedRoleMember[$key].GetType().FullName -like "*CimInstance*") + { + $testScopedRoleMember.Add($key, (Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $scopedRoleMember.$key)) + } + else + { + $testScopedRoleMember.Add($key, $scopedRoleMember.$key) + } + } + $testScopedRoleMembers += $testScopedRoleMember + } + #> $scopedRoleMemberSpecification = @() - foreach ($roleMember in $ScopedRoleMembers) + foreach ($roleMember in $testScopedRoleMembers) { + if ($roleMember.Contains('RoleMemberInfo')) + { + # flatten object if old schema-def with nested hashtable was used as input + if (string]::IsNullOrEmpty($roleMember.Identity) -and $roleMember.RoleMemberInfo.Identity) + { + $roleMember.Add('Identity', $roleMember.RoleMemberInfo.Identity) + } + if (string]::IsNullOrEmpty($roleMember.Type) -and $roleMember.RoleMemberInfo.Type) + { + $roleMember.Add('Type', $roleMember.RoleMemberInfo.Type) + } + } $roleObject = Get-MgDirectoryRole -Filter "DisplayName eq '$($roleMember.RoleName)'" -ErrorAction stop if ($null -eq $roleObject) { throw "AU {$($DisplayName)}: RoleName {$($roleMember.RoleName)} does not exist or is not enabled" } - if ($roleMember.RoleMemberInfo.Type -eq 'User') + if ($roleMember.Type -eq 'User') { - $roleMemberIdentity = Get-MgUser -Filter "UserPrincipalName eq '$($roleMember.RoleMemberInfo.Identity)'" -ErrorAction Stop + $roleMemberIdentity = Get-MgUser -Filter "UserPrincipalName eq '$($roleMember.Identity)'" -ErrorAction Stop if ($null -eq $roleMemberIdentity) { - throw "AU {$($DisplayName)}: Scoped Role User {$($roleMember.RoleMemberInfo.Identity)} does not exist" + throw "AU {$($DisplayName)}: Scoped Role User {$($roleMember.Identity)} does not exist" } } - elseif ($roleMember.RoleMemberInfo.Type -eq 'Group') + elseif ($roleMember.Type -eq 'Group') { - $roleMemberIdentity = Get-MgGroup -Filter "displayName eq '$($roleMember.RoleMemberInfo.Identity)'" -ErrorAction Stop + $roleMemberIdentity = Get-MgGroup -Filter "displayName eq '$($roleMember.Identity)'" -ErrorAction Stop if ($null -eq $roleMemberIdentity) { - throw "AU {$($DisplayName)}: Scoped Role Group {$($roleMember.RoleMemberInfo.Identity)} does not exist" + throw "AU {$($DisplayName)}: Scoped Role Group {$($roleMember.Identity)} does not exist" } } - elseif ($roleMember.RoleMemberInfo.Type -eq 'ServicePrincipal') + elseif ($roleMember.Type -eq 'ServicePrincipal') { - $roleMemberIdentity = Get-MgServicePrincipal -Filter "displayName eq '$($roleMember.RoleMemberInfo.Identity)'" -ErrorAction Stop + $roleMemberIdentity = Get-MgServicePrincipal -Filter "displayName eq '$($roleMember.Identity)'" -ErrorAction Stop if ($null -eq $roleMemberIdentity) { - throw "AU {$($DisplayName)}: Scoped Role ServicePrincipal {$($roleMember.RoleMemberInfo.Identity)} does not exist" + throw "AU {$($DisplayName)}: Scoped Role ServicePrincipal {$($roleMember.Identity)} does not exist" } } else { - throw "AU {$($DisplayName)}: Invalid ScopedRoleMember.RoleMemberInfo.Type {$($roleMember.RolememberInfo.Type)}" + throw "AU {$($DisplayName)}: Invalid ScopedRoleMember.Type {$($roleMember.Type)}" + } + if ($roleMemberIdentity.Count -gt 1) + { + throw "AU {$($DisplayName)}: ScopedRoleMember {$($roleMember.RoleName)}, Identity {$($roleMember.Identity)} of type {$($roleMember.Type)} is not unique" } $scopedRoleMemberSpecification += @{ RoleId = $roleObject.Id @@ -536,24 +562,21 @@ function Set-TargetResource } } } - #$CreateParameters.ScopedRoleMembers = $scopedRoleMemberSpecification # ScopedRoleMember-info is added after the AU is created } - else - { - $CreateParameters.Remove('ScopedRoleMembers') | Out-Null - } + $CreateParameters.Remove('ScopedRoleMembers') | Out-Null } if ($Ensure -eq 'Present' -and $currentInstance.Ensure -eq 'Absent') { Write-Verbose -Message "Creating AU {$DisplayName}" - $CreateParameters = Rename-M365DSCCimInstanceODataParameter -Properties $CreateParameters + #$CreateParameters = Rename-M365DSCCimInstanceODataParameter -Properties $CreateParameters $CreateParameters.Remove('Id') | Out-Null $CreateParameters.Remove('Verbose') | Out-Null + <# foreach ($key in ($CreateParameters.clone()).Keys) { if ($CreateParameters[$key].getType().Fullname -like '*CimInstance*') @@ -561,15 +584,16 @@ function Set-TargetResource $CreateParameters[$key] = Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $CreateParameters[$key] } } + #> #region resource generator code $policy = New-MgDirectoryAdministrativeUnit -BodyParameter $CreateParameters #endregion - foreach ($member in $memberSpecificationpecification) + foreach ($member in $memberSpecification) { $memberBodyParam = @{ - '@odata.id' = "https://graph.microsoft.com/v1.0/$($member.Type)/{$($member.Id)}" + '@odata.id' = "https://graph.microsoft.com/v1.0/$($member.Type)/$($member.Id)" } New-MgDirectoryAdministrativeUnitMemberByRef -AdministrativeUnitId $policy.Id -BodyParameter $memberBodyParam @@ -586,11 +610,12 @@ function Set-TargetResource Write-Verbose -Message "Updating AU {$DisplayName}" $UpdateParameters = $currentParameters.Clone() - $UpdateParameters = Rename-M365DSCCimInstanceODataParameter -Properties $UpdateParameters + #$UpdateParameters = Rename-M365DSCCimInstanceODataParameter -Properties $UpdateParameters $UpdateParameters.Remove('Id') | Out-Null $UpdateParameters.Remove('Verbose') | Out-Null + <# foreach ($key in ($UpdateParameters.clone()).Keys) { if ($UpdateParameters[$key].getType().Fullname -like '*CimInstance*') @@ -598,6 +623,7 @@ function Set-TargetResource $UpdateParameters[$key] = Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $UpdateParameters[$key] } } + #> #$UpdateParameters.Remove('Extensions') | Out-Null $UpdateParameters.Remove('Members') | Out-Null @@ -638,7 +664,7 @@ function Set-TargetResource if ($MembershipType -ne 'Dynamic' -and ($Members -or $backCurrentMembers)) { $currentMembersValue = @() - if ($currentInstance.Members.Length -ne 0) + if ($backCurrentMembers.Length -ne 0) { $currentMembersValue = $backCurrentMembers } @@ -651,7 +677,10 @@ function Set-TargetResource { $desiredMembersValue = @() } - $membersDiff = Compare-Object -ReferenceObject $currentMembersValue -DifferenceObject $desiredMembersValue -Property Identity, Type + write-verbose "AU {$DisplayName} Update Members: Current members: $($currentMembersValue.Identity -join ', ')" + write-verbose " Desired members: $($desiredMembersValue.Identity -join ', ')" + $membersDiff = Compare-Object -ReferenceObject $currentMembersValue -DifferenceObject $desiredMembersValue -Property Identity, Type -IncludeEqual + write-verbose " # compare results : $($membersDiff.Count -gt 0)" foreach ($diff in $membersDiff) { if ($diff.Type -eq 'User') @@ -668,9 +697,19 @@ function Set-TargetResource { $memberObject = Get-MgDevice -Filter "DisplayName eq '$($diff.Identity)'" $membertype = 'devices' - } else { - # throw if a *new* member has been specified with invalid type - throw "AU {$($DisplayName)}: Member {$($diff.Identity)} has invalid type {$($diff.Type)}" + } + else + { + if ($diff.Identity) + { + # throw if a *new* member has been specified with invalid type + throw "AU {$($DisplayName)}: Member {$($diff.Identity)} has invalid type {$($diff.Type)}" + } + else + { + write-verbose "Compare Members - skip processing blank Identity" + continue # don't process, continue to next diff if any + } } if ($null -eq $memberObject) { @@ -682,18 +721,22 @@ function Set-TargetResource } if ($diff.SideIndicator -eq '=>') { - Write-Verbose -Message "Adding new member {$($diff.Identity)}, type {$($diff.Type)} to Administrative Unit {$($currentInstance.DisplayName)}" + Write-Verbose -Message "Adding new member {$($diff.Identity)}, type {$($diff.Type)} to Administrative Unit {$DisplayName}" $memberBodyParam = @{ - '@odata.id' = "https://graph.microsoft.com/v1.0/$memberType/{$($memberObject.Id)}" + '@odata.id' = "https://graph.microsoft.com/v1.0/$memberType/$($memberObject.Id)" } New-MgDirectoryAdministrativeUnitMemberByRef -AdministrativeUnitId ($currentInstance.Id) -BodyParameter $memberBodyParam | Out-Null } elseif ($diff.SideIndicator -eq '<=') { - Write-Verbose -Message "Removing member {$($diff.Identity)}, type {$($diff.Type)} from Administrative UNit {$($currentInstance.DisplayName)}" + Write-Verbose -Message "Removing member {$($diff.Identity)}, type {$($diff.Type)} from Administrative Unit {$DisplayName}" Remove-MgDirectoryAdministrativeUnitMemberByRef -AdministrativeUnitId ($currentInstance.Id) -DirectoryObjectId ($memberObject.Id) | Out-Null } + else + { + Write-Verbose -Message "Unchanged member {$($diff.Identity)}, type {$($diff.Type)} of Administrative Unit {$DisplayName}" + } } } @@ -756,8 +799,7 @@ function Set-TargetResource if ($ScopedRoleMembers -or $backCurrentScopedRoleMembers) { - $currentScopedRoleMembersValue = @() - if ($currentInstance.ScopedRoleMembers.Length -ne 0) + if ($backCurrentScopedRoleMembers.Length -ne 0) { $currentScopedRoleMembersValue = $backCurrentScopedRoleMembers } @@ -770,7 +812,8 @@ function Set-TargetResource { $desiredScopedRoleMembersValue = @() } - # flatten objects to compare: + + <# # not necessary to flatten objects to compare when nested CimInstances are avoided in Schema/Get-TargetResource: $compareCurrentScopedRoleMembersValue = @() foreach ($roleMember in $currentScopedRoleMembersValue) { @@ -790,29 +833,52 @@ function Set-TargetResource } } $scopedRoleMembersDiff = Compare-Object -ReferenceObject $compareCurrentScopedRoleMembersValue -DifferenceObject $compareDesiredScopedRoleMembersValue -Property RoleName, Identity, Type + #> + write-verbose "AU {$DisplayName} Update ScopedRoleMembers: Current members: $($currentScopedRoleMembersValue.Identity -join ', ')" + write-verbose " Desired members: $($desiredScopedRoleMembersValue.Identity -join ', ')" + $scopedRoleMembersDiff = Compare-Object -ReferenceObject $CurrentScopedRoleMembersValue -DifferenceObject $DesiredScopedRoleMembersValue -Property RoleName, Identity, Type -IncludeEqual + write-verbose " # compare results : $($scopedRoleMembersDiff.Count -gt 0)" + foreach ($diff in $scopedRoleMembersDiff) { if ($diff.Type -eq 'User') { $memberObject = Get-MgUser -Filter "UserPrincipalName eq '$($diff.Identity)'" - $memberType = 'users' + #$memberType = 'users' } - else + elseif ($diff.Type -eq 'Group') { $memberObject = Get-MgGroup -Filter "DisplayName eq '$($diff.Identity)'" - $membertype = 'groups' + #$membertype = 'groups' } - if ($null -eq $memberobject) + elseif ($diff.Type -eq 'ServicePrincipal') { - throw "AU scoped role member {$($diff.Identity)} does not exist as a $(diff.Type)" + $memberObject = Get-MgServicePrincipal -Filter "DisplayName eq '$($diff.Identity)'" + #$memberType = "servicePrincipals" } - if ($memberobject.Count -gt 1) + else { - throw "AU scoped role member {$($diff.Identity)} is not a unique $($diff.Type)" + if ($diff.RoleName) + { + throw "AU {$DisplayName} scoped role {$($diff.RoleName)} member {$($diff.Identity)} has invalid type $($diff.Type)" + } + else + { + write-verbose "Compare ScopedRoleMembers - skip processing blank RoleName" + continue # don't process, + } + } + if ($null -eq $memberObject) + { + throw "AU scoped role member {$($diff.Identity)} does not exist as a $($diff.Type)" + } + if ($memberObject.Count -gt 1) + { + throw "AU scoped role member {$($diff.Identity)} is not a unique $($diff.Type) (Count=$($memberObject.Count))" } if ($diff.SideIndicator -ne '==') { - $roleObject = Get-MgDirectoryRole -Filter "DisplayName -eq '$($diff.RoleName)" + $roleObject = Get-MgDirectoryRole -Filter "DisplayName eq '$($diff.RoleName)'" if ($null -eq $roleObject) { throw "AU Scoped Role {$($diff.RoleName)} does not exist as an Azure AD role" @@ -820,7 +886,7 @@ function Set-TargetResource } if ($diff.SideIndicator -eq '=>') { - Write-Verbose -Message "Adding new scoped role {$($diff.RoleName)} member {$($diff.Identity)}, type {$($diff.Type)} to Administrative Unit {$($currentInstance.DisplayName)}" + Write-Verbose -Message "Adding new scoped role {$($diff.RoleName)} member {$($diff.Identity)}, type {$($diff.Type)} to Administrative Unit {$DisplayName}" $scopedRoleMemberParam = @{ RoleId = $roleObject.Id @@ -833,9 +899,16 @@ function Set-TargetResource } elseif ($diff.SideIndicator -eq '<=') { - Write-Verbose -Message "Removing scoped role {$($diff.RoleName)} member {$($diff.Identity)}, type {$($diff.Type)} from Administrative Unit {$($currentInstance.DisplayName)}" - $scopedRoleMemberObject = Get-MgDirectoryAdministrativeUnitScopedRoleMember -AdministrativeUnitId ($currentInstance.Id) -All | Where-Object -FilterScript { $_.RoleId -eq $roleObject.Id -and $_.RoleMemberInfo.Id -eq $memberObject.Id } - Remove-MgDirectoryAdministrativeUnitScopedRoleMember -AdministrativeUnitId ($currentInstance.Id) -ScopedRoleMembershipId $scopedRoleMemberObject.Id | Out-Null + if (-not [string]::IsNullOrEmpty($diff.Rolename)) + { + Write-Verbose -Message "Removing scoped role {$($diff.RoleName)} member {$($diff.Identity)}, type {$($diff.Type)} from Administrative Unit {$DisplayName}" + $scopedRoleMemberObject = Get-MgDirectoryAdministrativeUnitScopedRoleMember -AdministrativeUnitId ($currentInstance.Id) -All | Where-Object -FilterScript { $_.RoleId -eq $roleObject.Id -and $_.RoleMemberInfo.Id -eq $memberObject.Id } + Remove-MgDirectoryAdministrativeUnitScopedRoleMember -AdministrativeUnitId ($currentInstance.Id) -ScopedRoleMembershipId $scopedRoleMemberObject.Id | Out-Null + } + } + else + { + Write-Verbose -Message "Unchanged scoped role {$($diff.RoleName)} member {$($diff.Identity)}, type {$($diff.Type)} in Administrative Unit {$DisplayName}" } } } @@ -953,7 +1026,7 @@ function Test-TargetResource $CurrentValues = Get-TargetResource @PSBoundParameters $ValuesToCheck = ([Hashtable]$PSBoundParameters).clone() - if ($CurrentValues.Ensure -eq 'Absent') + if ($CurrentValues.Ensure -eq 'Absent' -and $Ensure -eq 'Present') { Write-Verbose -Message "Test-TargetResource returned $false" return $false @@ -961,13 +1034,61 @@ function Test-TargetResource $testResult = $true + if ($Members.Count -gt 0 -or $currentValues.Members.Count -gt 0) + { + if ($Members.Count -ne $currentValues.Members.Count) + { + $testresult = $false + } + $testMembers = $Members + if ($null -eq $testMembers) + { + $testMembers = @() + } + $testCurrentValuesMembers = $currentValues.Members + if ($null -eq $testCurrentValuesMembers) + { + $testCurrentValuesMembers = @() + } + if ((Compare-Object -ReferenceObject $testMembers -DifferenceObject $testCurrentValuesMembers -Property Identity, Type).Count -gt 0) + { + $testresult = $false + } + $ValuesToCheck.Remove('Members') | Out-Null + } + if ($true -eq $testResult -and ($ScopedRoleMembers.Count -gt 0 -or $currentValues.ScopedRoleMembers.Count -gt 0)) + { + if ($ScopedRoleMembers.Count -ne $currentValues.ScopedRoleMembers.Count) + { + $testresult = $false + } + $testScopedRoleMembers = $ScopedRoleMembers + if ($null -eq $testScopedRoleMembers) + { + $testScopedRoleMembers = @() + } + $testCurrentValuesScopedRoleMembers = $currentValues.ScopedRoleMembers + if ($null -eq $testCurrentValuesScopedRoleMembers) + { + $testCurrentValuesScopedRoleMembers = @() + } + if ((Compare-Object -ReferenceObject $testScopedRoleMembers -DifferenceObject $testCurrentValuesScopedRoleMembers -Property RoleName, Identity, Type).Count -gt 0) + { + $testresult = $false + } + $ValuesToCheck.Remove('ScopedRoleMembers') | Out-Null + } + <# foreach ($key in $PSBoundParameters.Keys) { if ($PSBoundParameters[$key].getType().Name -like '*CimInstance*') { $CIMArraySource = @() $CIMArrayTarget = @() - $CIMArraySource += $PSBoundParameters[$key] + if ($PSBoundParameters[$key]) + { + $CIMArraySource += $PSBoundParameters[$key] + } if ($CurrentValues.$key) { $CIMArrayTarget += $CurrentValues.$key @@ -989,18 +1110,21 @@ function Test-TargetResource if (-Not $testResult) { $testResult = $false - break + break; } } if (-Not $testResult) { $testResult = $false - break + break; } $ValuesToCheck.Remove($key) | Out-Null } } + #> + + Write-Verbose -Message "Test-TargetResource - testresult after comparing CimInstance(s): $testResult" $ValuesToCheck.Remove('Credential') | Out-Null $ValuesToCheck.Remove('ApplicationId') | Out-Null @@ -1014,7 +1138,7 @@ function Test-TargetResource if ($MembershipType -ne 'Dynamic' -and $CurrentValues.MembershipType -ne 'Dynamic') { - # Removing the MembershipType parameter from the check if it isn't Dynamic. If so, it can be aither Assigned or null + # Removing the MembershipType parameter from the check if it isn't Dynamic. If it isn't, it can be Assigned or null $ValuesToCheck.Remove('MembershipType') | Out-Null } @@ -1100,7 +1224,7 @@ function Export-TargetResource try { #region resource generator code - [array]$getValue = Get-MgAdministrativeUnit -All ` + [array]$getValue = Get-MgDirectoryAdministrativeUnit -All ` -ErrorAction Stop #endregion @@ -1135,7 +1259,7 @@ function Export-TargetResource if ($Results.Members) { - $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject $Results.Members -CIMInstanceName MicrosoftGraphIdentity + $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject $Results.Members -CIMInstanceName MicrosoftGraphMember if ($complexTypeStringResult) { $Results.Members = $complexTypeStringResult @@ -1147,7 +1271,7 @@ function Export-TargetResource } if ($Results.ScopedRoleMembers) { - $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject $Results.ScopedRoleMembers -CIMInstanceName MicrosoftGraphscopedrolemembership + $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject $Results.ScopedRoleMembers -CIMInstanceName MicrosoftGraphScopedRoleMembership if ($complexTypeStringResult) { $Results.ScopedRoleMembers = $complexTypeStringResult @@ -1306,9 +1430,11 @@ function Get-M365DSCDRGComplexTypeToString #If ComplexObject is an Array if ($ComplexObject.GetType().FullName -like '*[[\]]') { + write-verbose "object is an array" $currentProperty = @() foreach ($item in $ComplexObject) { + write-verbose "Item=$($item -join '|')" $currentProperty += Get-M365DSCDRGComplexTypeToString ` -ComplexObject $item ` -isArray:$true ` @@ -1338,6 +1464,7 @@ function Get-M365DSCDRGComplexTypeToString $keyNotNull = 0 foreach ($key in $ComplexObject.Keys) { + write-verbose "" if ($ComplexObject[$key]) { $keyNotNull++ @@ -1606,6 +1733,7 @@ function Compare-M365DSCComplexObject if ($Source[$skey].getType().FullName -like '*CimInstance*' -or $Target[$skey].getType().FullName -like '*CimInstance*') { #Recursive call for complex object + write-verbose "Compare values for $key" if ($Source[$skey].getType().FullName -like '*CimInstance*') { $complexSourceValue = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $Source[$skey] @@ -1622,6 +1750,22 @@ function Compare-M365DSCComplexObject { $complexTargetValue = $Target[$key] } + if ($complexSourceValue.GetType().FullName -ne 'System.Collections.Hashtable') + { + $tempSourceValue = @{} + $complexSourceValue.psobject.properties | Foreach-Object { + $tempSourceValue[$_.Name] = $_.Value + } + $complexSourceValue = $tempSourceValue + } + if ($complexTargetValue.GetType().FullName -ne 'System.Collections.Hashtable') + { + $tempTargetValue = @{} + $complexTargetValue.psobject.properties | Foreach-Object { + $tempTargetValue[$_.Name] = $_.Value + } + $complexTargetValue = $tempTargetValue + } $compareResult = Compare-M365DSCComplexObject ` -Source $complexSourceValue ` -Target $complexTargetValue diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.schema.mof b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.schema.mof index f3d6bd5c60..2759ea880e 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.schema.mof +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.schema.mof @@ -1,14 +1,16 @@ [ClassVersion("1.0.0")] -class MSFT_MicrosoftGraphIdentity +class MSFT_MicrosoftGraphMember { - [Write, Description("Identity of direcory-object. For users, specify a UserPrincipalName. For Groups and SPNs, specify DisplayName")] String Identity; - [Write, Description("Specify User, Group or ServicePrincipal to interpret the Identity")] String Type; + [Write, Description("Identity of member. For users, specify a UserPrincipalName. For groups and devices, specify DisplayName")] String Identity; + [Write, Description("Specify User, Group or Device to interpret the Identity")] String Type; }; [ClassVersion("1.0.0")] class MSFT_MicrosoftGraphScopedRoleMembership { [Write, Description("Name of the Azure AD Role that is assigned")] String RoleName; - [Write, Description("Member that is assigned the scoped role"), EmbeddedInstance("MSFT_MicrosoftGraphIdentity")] String RoleMemberInfo; + //[Write, Description("Member that is assigned the scoped role"), EmbeddedInstance("MSFT_MicrosoftGraphIdentity")] String RoleMemberInfo; + [Write, Description("Identity of member. For users, specify a UserPrincipalName. For groups and SPNs, specify DisplayName")] String Identity; + [Write, Description("Specify User, Group or ServicePrincipal to interpret the Identity")] String Type; }; /* Extensions not incorporated in initial version [ClassVersion("1.0.0")] @@ -29,7 +31,7 @@ class MSFT_AADAdministrativeUnit : OMI_BaseResource [Write, Description("Specify membership type. Possible values are Assigned and Dynamic if the AU-preview has been activated. Otherwise do not use")] String MembershipType; [Write, Description("Specify membership rule. Requires that MembershipType is set to Dynamic AND the AU-preview has been activated. Otherwise, do not use")] String MembershipRule; [Write, Description("Specify dynamic membership-rule processing-state. Valid values are 'On' and 'Paused'. Requires that MembershipType is set to Dynamic AND the AU-preview has been activated. Otherwise, do not use")] String MembershipRuleProcessingState; - [Write, Description("Specify members. Only specify if MembershipType is set to Static"), EmbeddedInstance("MSFT_MicrosoftGraphIdentity")] String Members[]; + [Write, Description("Specify members. Only specify if MembershipType is set to Static"), EmbeddedInstance("MSFT_MicrosoftGraphMember")] String Members[]; [Write, Description(""), EmbeddedInstance("MSFT_MicrosoftGraphScopedRoleMembership")] String ScopedRoleMembers[]; // [Write, Description("Extensions. See https://docs.microsoft.com/en-us/graph/extensibility-overview#open-extensions"), EmbeddedInstance("MSFT_MicrosoftGraphOpenExtension")] String Extensions[]; [Write, Description("Present ensures the Administrative Unit exists, absent ensures it is removed."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] string Ensure; diff --git a/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 index 35a38733fc..b3b703953b 100644 --- a/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 +++ b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 @@ -82,10 +82,12 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Description = 'FakeStringValue1' DisplayName = 'FakeStringValue1' Id = 'FakeStringValue1' - Members = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ - Type = 'User' + Members = @( + (New-CimInstance -ClassName MSFT_MicrosoftGraphMember -Property @{ + Type = 'User' Identity = 'john.smith@contoso.com' } -ClientOnly) + ) Visibility = 'Public' Ensure = 'Present' Credential = $Credential @@ -119,10 +121,12 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Description = 'FakeStringValue2' DisplayName = 'FakeStringValue2' Id = 'FakeStringValue2' - Members = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ - Type = 'User' + Members = @( + (New-CimInstance -ClassName MSFT_MicrosoftGraphMember -Property @{ + Type = 'User' Identity = 'john.smith@contoso.com' } -ClientOnly) + ) Ensure = 'Absent' Credential = $Credential } @@ -162,7 +166,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { DisplayName = 'DSCAU' Id = 'DSCAU' Members = @( - (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ + (New-CimInstance -ClassName MSFT_MicrosoftGraphMember -Property @{ Identity = 'John.Doe@mytenant.com' Type = 'User' } -ClientOnly) @@ -170,10 +174,10 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { ScopedRoleMembers = @( (New-CimInstance -ClassName MSFT_MicrosoftGraphScopedRoleMembership -Property @{ RoleName = 'User Administrator' - RoleMemberInfo = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ - Identity = 'John.Doe@mytenant.com' - Type = 'User' - } -ClientOnly) + #RoleMemberInfo = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ + Identity = 'John.Doe@mytenant.com' + Type = 'User' + # } -ClientOnly) } -ClientOnly) ) <# @@ -195,6 +199,8 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Credential = $Credential } + # Note: It is in fact possible to update the AU MembershipRule with any invalid value, but in the AAD-portal, updates are not possible unless the rule is valid. + Mock -CommandName Get-MgDirectoryAdministrativeUnit -MockWith { return [pscustomobject]@{ Description = 'DSCAU' @@ -277,7 +283,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { #> Id = 'DSCAU2' Members = @( - (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ + (New-CimInstance -ClassName MSFT_MicrosoftGraphMember -Property @{ Identity = 'John.Doe@mytenant.com' Type = 'User' } -ClientOnly) @@ -285,10 +291,10 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { ScopedRoleMembers = @( (New-CimInstance -ClassName MSFT_MicrosoftGraphScopedRoleMembership -Property @{ RoleName = 'User Administrator' - RoleMemberInfo = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ - Identity = 'John.Doe@mytenant.com' - Type = 'User' - } -ClientOnly) + #RoleMemberInfo = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ + Identity = 'John.Doe@mytenant.com' + Type = 'User' + # } -ClientOnly) } -ClientOnly) ) Visibility = 'Public' @@ -384,7 +390,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { ) #> Members = @( - (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ + (New-CimInstance -ClassName MSFT_MicrosoftGraphMember -Property @{ Identity = 'John.Doe@mytenant.com' Type = 'User' } -ClientOnly) @@ -392,10 +398,10 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { ScopedRoleMembers = @( (New-CimInstance -ClassName MSFT_MicrosoftGraphScopedRoleMembership -Property @{ RoleName = 'User Administrator' - RoleMemberInfo = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ - Identity = 'John.Doe@mytenant.com' - Type = 'User' - } -ClientOnly) + #RoleMemberInfo = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ + Identity = 'John.Doe@mytenant.com' + Type = 'User' + # } -ClientOnly) } -ClientOnly) ) Visibility = 'Public' @@ -420,15 +426,6 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } } - Mock -CommandName Invoke-MgGraphRequest -MockWith { - return [pscustomobject]@{ - '@odata.type' = '#microsoft.graph.user' - DisplayName = 'John Doe' - UserPrincipalName = 'John.Doe@mytenant.com' - Id = '1234567890' - } - } - Mock -CommandName Get-MgDirectoryAdministrativeUnitMember -MockWith { return [pscustomobject]@{ Id = '1234567890' @@ -446,7 +443,6 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Mock -CommandName Get-MgDirectoryAdministrativeUnitScopedRoleMember -MockWith { } - Mock -CommandName Invoke-MgGraphRequest -MockWith { return [pscustomobject]@{ '@odata.type' = '#microsoft.graph.user' From 3d6211f292733936279468fd9cfaa877bcd92370 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Tue, 17 Jan 2023 15:02:08 +0100 Subject: [PATCH 18/29] Improved Admin Units, but still not 'there' --- .../MSFT_AADAdministrativeUnit.psm1 | 51 ++++++++++++++----- 1 file changed, 39 insertions(+), 12 deletions(-) diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 index a23359a6bc..295349f166 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 @@ -1259,7 +1259,8 @@ function Export-TargetResource if ($Results.Members) { - $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject $Results.Members -CIMInstanceName MicrosoftGraphMember + $hashResults = Get-M365DSCDRGObjectArrayToHashtable -ArrayObject $Results.Members + $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject $hashResults -CIMInstanceName MicrosoftGraphMember if ($complexTypeStringResult) { $Results.Members = $complexTypeStringResult @@ -1271,7 +1272,8 @@ function Export-TargetResource } if ($Results.ScopedRoleMembers) { - $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject $Results.ScopedRoleMembers -CIMInstanceName MicrosoftGraphScopedRoleMembership + $hashResults = Get-M365DSCDRGObjectArrayToHashtable -ArrayObject $Results.ScopedRoleMembers + $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject $hashResults -CIMInstanceName MicrosoftGraphScopedRoleMembership if ($complexTypeStringResult) { $Results.ScopedRoleMembers = $complexTypeStringResult @@ -1300,6 +1302,7 @@ function Export-TargetResource -Results $Results ` -Credential $Credential + <# if ($Results.Members) { $isCIMArray = $false @@ -1319,6 +1322,8 @@ function Export-TargetResource } $currentDSCBlock = Convert-DSCStringParamToVariable -DSCBlock $currentDSCBlock -ParameterName 'ScopedRoleMembers' -IsCIMArray:$isCIMArray } + #> + <# if ($Results.Extensions) { $isCIMArray = $false @@ -1328,6 +1333,7 @@ function Export-TargetResource } $currentDSCBlock = Convert-DSCStringParamToVariable -DSCBlock $currentDSCBlock -ParameterName 'Extensions' -IsCIMArray:$isCIMArray } + #> $dscContent += $currentDSCBlock Save-M365DSCPartialExport -Content $currentDSCBlock ` @@ -1402,6 +1408,27 @@ function Get-M365DSCDRGComplexTypeToHashtable return $results } +function Get-M365DSCDRGObjectArrayToHashtable +{ + [cmdletbinding()] + [outputtype([system.object[]])] + param( + [Parameter()] + [system.object[]] + $ArrayObject + ) + $returnValue = @() + foreach ($element in $ArrayObject) + { + $hashTable = @{} + foreach ($property in $element.PSObject.Properties.Name) + { + $hashtable.Add($property, $element.$property) | Out-Null + } + $returnValue += $hashTable + } + return $returnValue +} function Get-M365DSCDRGComplexTypeToString { [CmdletBinding()] @@ -1465,14 +1492,14 @@ function Get-M365DSCDRGComplexTypeToString foreach ($key in $ComplexObject.Keys) { write-verbose "" - if ($ComplexObject[$key]) + if ($ComplexObject.$key) { $keyNotNull++ - if ($ComplexObject[$key].GetType().FullName -like 'Microsoft.Graph.PowerShell.Models.*') + if ($ComplexObject.$key.GetType().FullName -like 'Microsoft.Graph.PowerShell.Models.*') { - $hashPropertyType = $ComplexObject[$key].GetType().Name.tolower() - $hashProperty = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $ComplexObject[$key] + $hashPropertyType = $ComplexObject[$key].GetType().Name + $hashProperty = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $ComplexObject.$key if (Test-M365DSCComplexObjectHasValues -ComplexObject $hashProperty) { @@ -1493,7 +1520,7 @@ function Get-M365DSCDRGComplexTypeToString { $Whitespace = ' ' } - $currentProperty += Get-M365DSCDRGSimpleObjectTypeToString -Key $key -Value $ComplexObject[$key] -Space ($Whitespace + ' ') + $currentProperty += Get-M365DSCDRGSimpleObjectTypeToString -Key $key -Value $ComplexObject.$key -Space ($Whitespace + ' ') } } } @@ -1512,16 +1539,16 @@ function Test-M365DSCComplexObjectHasValues [OutputType([System.Boolean])] param( [Parameter(Mandatory = $true)] - [System.Collections.Hashtable] + [system.object] $ComplexObject ) - $keys = $ComplexObject.keys + $keys = $ComplexObject.psobject.properties.name $hasValue = $false foreach ($key in $keys) { - if ($ComplexObject[$key]) + if ($ComplexObject.$key) { - if ($ComplexObject[$key].GetType().FullName -like 'Microsoft.Graph.PowerShell.Models.*') + if ($ComplexObject.$key.GetType().FullName -like 'Microsoft.Graph.PowerShell.Models.*') { $hash = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $ComplexObject[$key] if (-Not $hash) @@ -1533,7 +1560,7 @@ function Test-M365DSCComplexObjectHasValues else { $hasValue = $true - return $hasValue + break } } } From 8abb3acce0b0cc008a840127f79a3293fc60d0ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Thu, 19 Jan 2023 14:26:45 +0100 Subject: [PATCH 19/29] Fixed AADAdministrativeUnit - NB update is a BR --- .../MSFT_AADAdministrativeUnit.psm1 | 1709 ++++++++--------- .../MSFT_AADAdministrativeUnit.schema.mof | 22 +- ...soft365DSC.AADAdministrativeUnit.Tests.ps1 | 143 +- 3 files changed, 837 insertions(+), 1037 deletions(-) diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 index 295349f166..6622563012 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 @@ -4,33 +4,37 @@ function Get-TargetResource [OutputType([System.Collections.Hashtable])] param ( - [Parameter(Mandatory = $true)] - [System.String] - $DisplayName, - + #region resource generator code [Parameter()] [System.String] - $Id, + $Description, - [Parameter()] + [Parameter(Mandatory=$true)] [System.String] - $Description, + $DisplayName, [Parameter()] [validateset('Public', 'HiddenMembership')] [System.String] $Visibility, + [Parameter()] + [System.String] + $Id, + [Parameter()] [validateset('Assigned', 'Dynamic')] - [System.String]$MembershipType, + [System.String] + $MembershipType, [Parameter()] - [System.String]$MembershipRule, + [System.String] + $MembershipRule, [Parameter()] [validateset('Paused', 'On')] - [System.String]$MembershipRuleProcessingState, + [System.String] + $MembershipRuleProcessingState, [Parameter()] [Microsoft.Management.Infrastructure.CimInstance[]] @@ -40,19 +44,12 @@ function Get-TargetResource [Microsoft.Management.Infrastructure.CimInstance[]] $ScopedRoleMembers, - <# - [Parameter()] - [Microsoft.Management.Infrastructure.CimInstance[]] - $Extensions, - #> - - #endregion [Parameter(Mandatory = $true)] [System.String] [ValidateSet('Absent', 'Present')] - $Ensure = 'Present', + $Ensure = $true, [Parameter()] [System.Management.Automation.PSCredential] @@ -84,180 +81,156 @@ function Get-TargetResource $ConnectionMode = New-M365DSCConnection -Workload 'MicrosoftGraph' ` -InboundParameters $PSBoundParameters ` -ProfileName 'v1.0' - } - catch - { - Write-Verbose -Message ($_) - } - #Ensure the proper dependencies are installed in the current environment. - Confirm-M365DSCDependencies + #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 + #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 - { - $getValue = $null + $nullResult = $PSBoundParameters + $nullResult.Ensure = 'Absent' + $getValue = $null #region resource generator code - if (-Not [string]::IsNullOrEmpty($Id)) + if (-not [string]::IsNullOrEmpty($Id)) { - $getValue = Get-MgDirectoryAdministrativeUnit -AdministrativeUnitId $Id -ErrorAction Stop + $getValue = Get-MgDirectoryAdministrativeUnit -AdministrativeUnitId $Id -ErrorAction SilentlyContinue } - if (-not $getValue -and -Not [string]::IsNullOrEmpty($DisplayName)) + if ($null -eq $getValue -and -not [string]::IsNullOrEmpty($DisplayName)) { - $getValue = Get-MgDirectoryAdministrativeUnit -Filter "DisplayName eq '$DisplayName'" -ErrorAction Stop + Write-Verbose -Message "Could not find an Azure AD Administrative Unit with Id {$Id}" + + if(-Not [string]::IsNullOrEmpty($DisplayName)) + { + $getValue = Get-MgDirectoryAdministrativeUnit -Filter "DisplayName eq '$DisplayName'" -ErrorAction Stop + } } #endregion - - if ($null -eq $getValue) { - Write-Verbose -Message "Nothing with id {$id} was found" + Write-Verbose -Message "Could not find an Azure AD Administrative Unit with DisplayName {$DisplayName}" return $nullResult } - - Write-Verbose -Message "Found AU with id {$($getValue.id)}, DisplayName {$($getValue.DisplayName)}" - + $Id = $getValue.Id + Write-Verbose -Message "An Azure AD Administrative Unit with Id {$Id} and DisplayName {$DisplayName} was found." $results = @{ - #region resource generator code - Id = $getValue.Id Description = $getValue.Description DisplayName = $getValue.DisplayName Visibility = $getValue.Visibility + Id = $getValue.Id Ensure = 'Present' Credential = $Credential ApplicationId = $ApplicationId TenantId = $TenantId ApplicationSecret = $ApplicationSecret CertificateThumbprint = $CertificateThumbprint - ManagedIdentity = $ManagedIdentity.IsPresent + Managedidentity = $ManagedIdentity.IsPresent + #endregion + } + + if (-not [string]::IsNullOrEmpty($getValue.AdditionalProperties.MembershipType)) + { + $results.Add('MembershipType', $getValue.AdditionalProperties.MembershipType) + } + if (-not [string]::IsNullOrEmpty($getValue.AdditionalProperties.MembershipRule)) + { + $results.Add('MembershipRule', $getValue.AdditionalProperties.MembershipRule) } - if (-not [string]::IsNullOrEmpty($getValue.AdditionalProperties.membershipType)) + if (-not [string]::IsNullOrEmpty($getValue.AdditionalProperties.MembershipRuleProcessingState)) { - # only include details about membership if values are present - $results.Add('MembershipType', $getValue.AdditionalProperties.membershipType) - $results.Add('MembershipRule', $getValue.AdditionalProperties.membershipRule) - $results.Add('MembershipRuleProcessingState', $getValue.AdditionalProperties.membershipRuleProcessingState) + $results.Add('MembershipRuleProcessingState', $getValue.AdditionalProperties.MembershipRuleProcessingState) } - $memberSpec = $null - if ($getValue.AdditionalProperties.MembershipType -ne 'Dynamic') + write-verbose "AU {$DisplayName} MembershipType {$($results.MembershipType)}" + if ($results.MembershipType -ne 'Dynamic') { - $auMembers = Get-MgDirectoryAdministrativeUnitMember -AdministrativeUnitId $getValue.Id -All - if ($auMembers) + write-verbose "AU {$DisplayName} get Members" + [array]$auMembers = Get-MgDirectoryAdministrativeUnitMember -AdministrativeUnitId $getValue.Id -All + if ($auMembers.Count -gt 0) { + write-verbose "AU {$DisplayName} process $($auMembers.Count) members" $memberSpec = @() - foreach ($getMember in $auMembers) + foreach ($auMember in $auMembers) { - # get object regardless of type - $memberObject = Invoke-MgGraphRequest -Method Get -Uri "https://graph.microsoft.com/v1.0/directoryObjects/$($getMember.Id)" - switch -regex ([regex]::Escape($memberObject.'@odata.type')) + $member = @{} + $memberObject = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/directoryobjects/$($auMember.Id)" + if ($memberObject.'@odata.type' -match 'user') { - 'group' - { - $memberSpec += [pscustomobject]@{ - Identity = $memberObject.DisplayName; - Type = 'Group' - } - } - 'user' - { - $memberSpec += [pscustomobject]@{ - Identity = $memberObject.UserPrincipalName; - Type = 'User' - } - } - 'device' - { - $memberSpec += [pscustomobject]@{ - Identity = $memberObject.DisplayName; - Type = 'Device' - } - } + $member.Add('Identity', $memberObject.UserPrincipalName) + $member.Add('Type', 'User') } - } - $results.Add('Members', $memberSpec) - } - } - - $scopedRoleMemberSpec = $null - $auScopedRoleMembers = Get-MgDirectoryAdministrativeUnitScopedRoleMember -AdministrativeUnitId $getValue.Id -All - if ($auScopedRoleMembers) - { - $scopedRoleMemberSpec = @() - foreach ($getMember in $auScopedRoleMembers) - { - $roleObject = Get-MgDirectoryRole -DirectoryRoleId $getMember.RoleId - # get object regardless of type - $roleMemberObject = Invoke-MgGraphRequest -Method Get -Uri "https://graph.microsoft.com/v1.0/directoryObjects/$($getMember.RoleMemberInfo.Id)" - if ([regex]::Escape($roleMemberObject.'@odata.type') -match 'user') - { - $memberType = 'User' - $memberIdentity = $roleMemberObject.UserPrincipalName - } - else - { - if ([regex]::Escape($roleMemberObject.'@odata.type') -match 'group') + elseif ($memberObject.'@odata.type' -match 'group') { - $memberType = 'Group'; + $member.Add('Identity', $memberObject.DisplayName) + $member.Add('Type', 'Group') } else { - $memberType = 'ServicePrincipal'; + $member.Add('Identity', $memberObject.DisplayName) + $member.Add('Type', 'Device') } - $memberIdentity = $roleMemberObject.DisplayName - } - $scopedRoleMemberInfo = [pscustomobject]@{ - RoleName = $roleObject.DisplayName; - #RoleMemberInfo = @{ # avoid nested hashtable, can't handle it in Export - Identity = $memberIdentity - Type = $memberType - #} + write-verbose "AU {$DisplayName} member found: Type '$($member.Type)' identity '$($member.Identity)'" + $memberSpec += $member } - $scopedRoleMemberSpec += $scopedRoleMemberInfo + write-verbose "AU {$DisplayName} add Members to results" + $results.Add('Members', $memberSpec) } - $results.Add('ScopedRoleMembers', $scopedRoleMemberSpec) } - <# - # Extensions are still too unwieldy - $auExtensions = Get-MgDirectoryAdministrativeUnitExtension -AdministrativeUnitId $getValue.Id -All - $extensionsSpec = $null - if ($auExtensions) + write-verbose "AU {$DisplayName} get Scoped Role Members" + $ErrorActionPreference = "Stop" + [array]$auScopedRoleMembers = Get-MgDirectoryAdministrativeUnitScopedRoleMember -AdministrativeUnitId $getValue.Id -All + if ($auScopedRoleMembers.Count -gt 0) { - $extensionsSpec = @() - $extensionDef = @{Id = $auExtensions.Id} - foreach ($auExtension in $auExtensions) + write-verbose "AU {$DisplayName} process $($auScopedRoleMembers.Count) scoped role members" + $scopedRoleMemberSpec = @() + foreach ($auScopedRoleMember in $auScopedRoleMembers) { - if ($auExtension.Properties -and $auExtension.Properties.Count % 2 -ne 0) - { - throw "AU {$($getValue.DisplayName)] has extension {$($auExtension.Id)} with properties without values" + write-verbose "AU {$DisplayName} verify RoleId {$($auScopedRoleMember.RoleId)}" + $roleObject = Get-MgDirectoryRole -DirectoryRoleId $auScopedRoleMember.RoleId -ErrorAction Stop + $scopedRoleMember = @{ + RoleName = $roleObject.DisplayName + Type = $null + Identity = $null + } + write-verbose "AU {$DisplayName} verify RoleMemberInfo.Id {$($auScopedRoleMember.RoleMemberInfo.Id)}" + $memberObject = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/directoryobjects/$($auScopedRoleMember.RoleMemberInfo.Id)" + write-verbose "AU {$DisplayName} @odata.Type={$($memberObject.'@odata.type')}" + if (($memberObject.'@odata.type') -match 'user') + { + write-verbose "AU {$DisplayName} UPN = {$($memberObject.UserPrincipalName)}" + $scopedRoleMember.Identity = $memberObject.UserPrincipalName + $scopedRoleMember.Type = 'User' + } + elseif (($memberObject.'@odata.type') -match 'group') + { + write-verbose "AU {$DisplayName} Group = {$($memberObject.DisplayName)}" + $scopedRoleMember.Identity = $memberObject.DisplayName + $scopedRoleMember.Type = 'Group' } - # address MSFT_KeyValuePair as an array containing key1,value1,key2,value2 etc - # see https://forums.powershell.org/t/hashtable-as-resource-parameter/2962/3 - # and https://learn.microsoft.com/en-us/answers/questions/440415/passing-a-key-value-pair-list-to-a-powershell-scri.html - for ($p = 0; $p -lt $auExtension.Properties.Count; $p = $p + 2) + else { - $extensionDef.Add($auExtension.Properties[$p], $auExtension.Properties[$p+1]) + write-verbose "AU {$DisplayName} SPN = {$($memberObject.DisplayName)}" + $scopedRoleMember.Identity = $memberObject.DisplayName + $scopedRoleMember.Type = 'ServicePrincipal' } + write-verbose "AU {$DisplayName} scoped role member: RoleName '$($scopedRoleMember.RoleName)' Type '$($scopedRoleMember.Type)' Identity '$($scopedRoleMember.Identity)'" + $scopedRoleMemberSpec += $scopedRoleMember } - $extensionsSpec += $extensionDef + write-verbose "AU {$DisplayName} add $($scopedRoleMemberSpec.Count) ScopedRoleMembers to results" + $results.Add('ScopedRoleMembers', $scopedRoleMemberSpec) } - $results.Add("Extensions", $extensionsSpec) - #> - return [System.Collections.Hashtable]$results + write-verbose "AU {$DisplayName} return results" + return [System.Collections.Hashtable] $results } catch { @@ -276,33 +249,37 @@ function Set-TargetResource [CmdletBinding()] param ( - [Parameter(Mandatory = $true)] - [System.String] - $DisplayName, - + #region resource generator code [Parameter()] [System.String] - $Id, + $Description, - [Parameter()] + [Parameter(Mandatory=$true)] [System.String] - $Description, + $DisplayName, [Parameter()] [validateset('Public', 'HiddenMembership')] [System.String] $Visibility, + [Parameter()] + [System.String] + $Id, + [Parameter()] [validateset('Assigned', 'Dynamic')] - [System.String]$MembershipType, + [System.String] + $MembershipType, [Parameter()] - [System.String]$MembershipRule, + [System.String] + $MembershipRule, [Parameter()] [validateset('Paused', 'On')] - [System.String]$MembershipRuleProcessingState, + [System.String] + $MembershipRuleProcessingState, [Parameter()] [Microsoft.Management.Infrastructure.CimInstance[]] @@ -312,19 +289,11 @@ function Set-TargetResource [Microsoft.Management.Infrastructure.CimInstance[]] $ScopedRoleMembers, - <# - [Parameter()] - [Microsoft.Management.Infrastructure.CimInstance[]] - $Extensions, - #> - - #endregion - [Parameter(Mandatory = $true)] [System.String] [ValidateSet('Absent', 'Present')] - $Ensure = 'Present', + $Ensure = $true, [Parameter()] [System.Management.Automation.PSCredential] @@ -351,15 +320,6 @@ function Set-TargetResource $ManagedIdentity ) - # Note: Graph names basic cmdlets xxx-MgDirectoryAdministrativeUnit(xxx) - # but the beta profile names latest cmdlets xxx-MgDirectoryAdministrativeUnit(xxx) - # ONLY the beta cmdlets support the preview enabling MembershipType and MembershipRuleProcessingState - # NB: Usage of these params require that the corresponding AAD preview feature is enabled - - $ConnectionMode = New-M365DSCConnection -Workload 'MicrosoftGraph' ` - -InboundParameters $PSBoundParameters ` - -ProfileName 'v1.0' - #Ensure the proper dependencies are installed in the current environment. Confirm-M365DSCDependencies @@ -374,98 +334,82 @@ function Set-TargetResource $currentInstance = Get-TargetResource @PSBoundParameters - $currentParameters = ([hashtable]$PSBoundParameters).Clone() - $currentParameters.Remove('Ensure') | Out-Null - $currentParameters.Remove('Credential') | Out-Null - $currentParameters.Remove('ApplicationId') | Out-Null - $currentParameters.Remove('ApplicationSecret') | Out-Null - $currentParameters.Remove('TenantId') | Out-Null - $currentParameters.Remove('CertificateThumbprint') | Out-Null - $currentParameters.Remove('ManagedIdentity') | Out-Null + $PSBoundParameters.Remove('Ensure') | Out-Null + $PSBoundParameters.Remove('Credential') | Out-Null + $PSBoundParameters.Remove('ApplicationId') | Out-Null + $PSBoundParameters.Remove('ApplicationSecret') | Out-Null + $PSBoundParameters.Remove('TenantId') | Out-Null + $PSBoundParameters.Remove('CertificateThumbprint') | Out-Null + $PSBoundParameters.Remove('ManagedIdentity') | Out-Null + $PSBoundParameters.Remove('Verbose') | Out-Null $backCurrentMembers = $currentInstance.Members $backCurrentScopedRoleMembers = $currentInstance.ScopedRoleMembers - #$backCurrentExtensions = $currentInstance.Extensions - $currentInstance.Remove('Members') | Out-Null - $currentInstance.Remove('ScopedRoleMembers') | Out-Null - - if ($MembershipType -eq 'Dynamic' -and $Members) - { - throw "AU {$($DisplayName)}: Members is not allowed when MembershipType is Dynamic" - } if ($Ensure -eq 'Present') { - $CreateParameters = $currentParameters.Clone() - - <# When using -BodyParameter it is not necessary to use AdditionalProperties for Dynamic AU params - if ($CreateParameters.Containskey('MembershipType') -or $CreateParameters.Containskey('MembershipRule') -or $CreateParameters.Containskey('MembershipRuleProcessingState')) + if ($MembershipType -eq 'Dynamic' -and $Members.Count -gt 0) { - $CreateParameters.Remove('MembershipType') | Out-Null - $CreateParameters.Remove('MembershipRule') | Out-Null - $CreateParameters.Remove('MembershipRuleProcessingState') | Out-Null - $CreateParameters.Add('AdditionalProperties', @{}) - if (-not [System.String]::IsNullOrEmpty($MembershipType)) - { - $CreateParameters.AdditionalProperties.Add('MembershipType', $MembershipType) - } - if (-not [System.String]::IsNullOrEmpty($MembershipType)) - { - $CreateParameters.AdditionalProperties.Add('MembershipRule', $MembershipRule) - } - if (-not [System.String]::IsNullOrEmpty($MembershipRuleProcessingState)) - { - $CreateParameters.AdditionalProperties.Add('MembershipRuleProcessingState', $MembershipRuleProcessingState) - } + throw "AU {$($DisplayName)}: Members is not allowed when MembershipType is Dynamic" } - #> + $CreateParameters = ([Hashtable]$PSBoundParameters).clone() + $CreateParameters = Rename-M365DSCCimInstanceParameter -Properties $CreateParameters + $CreateParameters.Remove('Id') | Out-Null - <# skipped - no use for hashtables - foreach ($key in ($CreateParameters.clone()).Keys) + $keys=(([Hashtable]$CreateParameters).clone()).Keys + foreach($key in $keys) { - if ($CreateParameters[$key].getType().Fullname -like '*CimInstance*') + if($null -ne $CreateParameters.$key -and $CreateParameters.$key.getType().Name -like "*cimInstance*") { - $CreateParameters[$key] = Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $CreateParameters[$key] + $CreateParameters.$key= Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $CreateParameters.$key } } - #> - - $memberSpecification = @() - if ($MembershipType -ne 'Dynamic') + $memberSpecification = $null + if ($CreateParameters.MembershipType -ne 'Dynamic' -and $CreateParameters.Members.Count -gt 0) { - # Resolve Members Type/Identity to user or group id - foreach ($Member in $currentParameters.Members) + $memberSpecification = @() + write-verbose "AU {$DisplayName} process $($CreateParameters.Members.Count) Members" + foreach ($member in $CreateParameters.Members) { - if ($Member.Type -eq 'User') + write-verbose "AU {$DisplayName} member Type '$($member.Type)' Identity '$($member.Identity)'" + if ($member.Type -eq 'User') { - $memberIdentity = Get-MgUser -Filter "UserPrincipalName eq '$($Member.Identity)'" -ErrorAction Stop + $memberIdentity = Get-MgUser -Filter "UserPrincipalName eq '$($member.Identity)'" -ErrorAction Stop if ($memberIdentity) { - $memberSpecification += [pscustomobject]@{Type="$($Member.Type)s";Id = $memberIdentity.Id } + $memberSpecification += [pscustomobject]@{Type="$($member.Type)s";Id = $memberIdentity.Id } } else { - throw "AU {$($DisplayName)}: User {$($Member.Identity)} does not exist" + throw "AU {$($DisplayName)}: User {$($member.Identity)} does not exist" } } - elseif ($Member.Type -eq 'Group') + elseif ($member.Type -eq 'Group') { - $memberIdentity = Get-MgGroup -Filter "DisplayName eq '$($Member.Identity)'" -ErrorAction Stop + $memberIdentity = Get-MgGroup -Filter "DisplayName eq '$($member.Identity)'" -ErrorAction Stop if ($memberIdentity) { - $memberSpecification += [pscustomobject]@{Type="$($Member.Type)s";Id = $memberIdentity.Id } + if ($memberIdentity.Count -gt 1) + { + throw "AU {$($DisplayName)}: Group displayname {$($member.Identity)} is not unique" + } + $memberSpecification += [pscustomobject]@{Type="$($member.Type)s";Id = $memberIdentity.Id } } else { - throw "AU {$($DisplayName)}: Group {$($Member.Identity)} does not exist" + throw "AU {$($DisplayName)}: Group {$($member.Identity)} does not exist" } } - elseif ($Member.Type -eq 'Device') + elseif ($member.Type -eq 'Device') { - $memberIdentity = Get-MgDevice -Filter "DisplayName eq '$($Member.Identity)'" -ErrorAction Stop + $memberIdentity = Get-MgDevice -Filter "DisplayName eq '$($member.Identity)'" -ErrorAction Stop if ($memberIdentity) { - $memberSpecification += [pscustomobject]@{Type="$($Member.Type)s";Id = $memberIdentity.Id } + if ($memberIdentity.Count -gt 1) + { + throw "AU {$($DisplayName)}: Device displayname {$($member.Identity)} is not unique" + } + $memberSpecification += [pscustomobject]@{Type="$($member.Type)s";Id = $memberIdentity.Id } } else { @@ -482,53 +426,36 @@ function Set-TargetResource $CreateParameters.Remove('Members') | Out-Null # Resolve ScopedRoleMembers Type/Identity to user, group or service principal - if ($currentParameters.ScopedRoleMembers) + if ($CreateParameters.ScopedRoleMembers) { - #write-verbose "AU {$DisplayName}: Set-TargetResource, Convert ScopedRoleMembers to Hashtable(s)" - $testScopedRoleMembers = Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $ScopedRoleMembers <#@() - foreach ($scopedRoleMember in $testScopedRoleMembers) - { - $testScopedRoleMember = @{} - foreach ($key in $scopedRoleMember.clone().Keys) - { - if ($scopedRoleMember[$key].GetType().FullName -like "*CimInstance*") - { - $testScopedRoleMember.Add($key, (Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $scopedRoleMember.$key)) - } - else - { - $testScopedRoleMember.Add($key, $scopedRoleMember.$key) - } - } - $testScopedRoleMembers += $testScopedRoleMember - } - #> + write-verbose "AU {$DisplayName} process $($CreateParameters.ScopedRoleMembers.Count) ScopedRoleMembers" $scopedRoleMemberSpecification = @() - foreach ($roleMember in $testScopedRoleMembers) + foreach ($roleMember in $CreateParameters.ScopedRoleMembers) { - if ($roleMember.Contains('RoleMemberInfo')) - { - # flatten object if old schema-def with nested hashtable was used as input - if (string]::IsNullOrEmpty($roleMember.Identity) -and $roleMember.RoleMemberInfo.Identity) - { - $roleMember.Add('Identity', $roleMember.RoleMemberInfo.Identity) - } - if (string]::IsNullOrEmpty($roleMember.Type) -and $roleMember.RoleMemberInfo.Type) + write-verbose "AU {$DisplayName} member: role '$($roleMember.RoleName)' type '$($roleMember.Type)' identity $($roleMember.Identity)" + try { + $roleObject = Get-MgDirectoryRole -Filter "DisplayName eq '$($roleMember.RoleName)'" -ErrorAction stop + write-verbose "AU {$DisplayName} role is enabled" + } + catch { + write-verbose -Message "Azure AD role {$($rolemember.RoleName)} is not enabled" + $roleTemplate = Get-MgdirectoryRoleTemplate -Filter "DisplayName eq '$($roleMember.RoleName)'" -ErrorAction Stop + if ($null -ne $roleTemplate) { - $roleMember.Add('Type', $roleMember.RoleMemberInfo.Type) + write-verbose -Message "Enable Azure AD role {$($rolemember.RoleName)} with id {$($roleTemplate.Id)}" + $roleObject = New-MgDirectoryRole -RoleTemplateId $roleTemplate.Id -ErrorAction Stop } } - $roleObject = Get-MgDirectoryRole -Filter "DisplayName eq '$($roleMember.RoleName)'" -ErrorAction stop if ($null -eq $roleObject) { - throw "AU {$($DisplayName)}: RoleName {$($roleMember.RoleName)} does not exist or is not enabled" + throw "AU {$($DisplayName)}: RoleName {$($roleMember.RoleName)} does not exist" } if ($roleMember.Type -eq 'User') { $roleMemberIdentity = Get-MgUser -Filter "UserPrincipalName eq '$($roleMember.Identity)'" -ErrorAction Stop if ($null -eq $roleMemberIdentity) { - throw "AU {$($DisplayName)}: Scoped Role User {$($roleMember.Identity)} does not exist" + throw "AU {$($DisplayName)}: Scoped Role User {$($roleMember.Identity)} for role {$($roleMember.RoleName)} does not exist" } } elseif ($roleMember.Type -eq 'Group') @@ -536,7 +463,7 @@ function Set-TargetResource $roleMemberIdentity = Get-MgGroup -Filter "displayName eq '$($roleMember.Identity)'" -ErrorAction Stop if ($null -eq $roleMemberIdentity) { - throw "AU {$($DisplayName)}: Scoped Role Group {$($roleMember.Identity)} does not exist" + throw "AU {$($DisplayName)}: Scoped Role Group {$($roleMember.Identity)} for role {$($roleMember.RoleName)} does not exist" } } elseif ($roleMember.Type -eq 'ServicePrincipal') @@ -544,7 +471,7 @@ function Set-TargetResource $roleMemberIdentity = Get-MgServicePrincipal -Filter "displayName eq '$($roleMember.Identity)'" -ErrorAction Stop if ($null -eq $roleMemberIdentity) { - throw "AU {$($DisplayName)}: Scoped Role ServicePrincipal {$($roleMember.Identity)} does not exist" + throw "AU {$($DisplayName)}: Scoped Role ServicePrincipal {$($roleMember.Identity)} for role {$($roleMember.RoleName)} does not exist" } } else @@ -553,7 +480,7 @@ function Set-TargetResource } if ($roleMemberIdentity.Count -gt 1) { - throw "AU {$($DisplayName)}: ScopedRoleMember {$($roleMember.RoleName)}, Identity {$($roleMember.Identity)} of type {$($roleMember.Type)} is not unique" + throw "AU {$($DisplayName)}: ScopedRoleMember for role {$($roleMember.RoleName)}: $($roleMember.Type) {$($roleMember.Identity)} is not unique" } $scopedRoleMemberSpecification += @{ RoleId = $roleObject.Id @@ -565,38 +492,26 @@ function Set-TargetResource # ScopedRoleMember-info is added after the AU is created } $CreateParameters.Remove('ScopedRoleMembers') | Out-Null + } if ($Ensure -eq 'Present' -and $currentInstance.Ensure -eq 'Absent') { - Write-Verbose -Message "Creating AU {$DisplayName}" - - #$CreateParameters = Rename-M365DSCCimInstanceODataParameter -Properties $CreateParameters + Write-Verbose -Message "Creating an Azure AD Administrative Unit with DisplayName {$DisplayName}" - $CreateParameters.Remove('Id') | Out-Null - $CreateParameters.Remove('Verbose') | Out-Null + #region resource generator code + $policy=New-MgDirectoryAdministrativeUnit -BodyParameter $CreateParameters - <# - foreach ($key in ($CreateParameters.clone()).Keys) + if ($MembershipType -ne 'Dynamic') { - if ($CreateParameters[$key].getType().Fullname -like '*CimInstance*') + foreach ($member in $memberSpecification) { - $CreateParameters[$key] = Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $CreateParameters[$key] - } - } - #> - - #region resource generator code - $policy = New-MgDirectoryAdministrativeUnit -BodyParameter $CreateParameters + $memberBodyParam = @{ + '@odata.id' = "https://graph.microsoft.com/v1.0/$($member.Type)/$($member.Id)" + } - #endregion - foreach ($member in $memberSpecification) - { - $memberBodyParam = @{ - '@odata.id' = "https://graph.microsoft.com/v1.0/$($member.Type)/$($member.Id)" + New-MgDirectoryAdministrativeUnitMemberByRef -AdministrativeUnitId $policy.Id -BodyParameter $memberBodyParam } - - New-MgDirectoryAdministrativeUnitMemberByRef -AdministrativeUnitId $policy.Id -BodyParameter $memberBodyParam } foreach ($scopedRoleMember in $scopedRoleMemberSpecification) @@ -604,200 +519,100 @@ function Set-TargetResource New-MgDirectoryAdministrativeUnitScopedRoleMember -AdministrativeUnitId $policy.Id -BodyParameter $scopedRoleMember } + + #endregion } elseif ($Ensure -eq 'Present' -and $currentInstance.Ensure -eq 'Present') { - Write-Verbose -Message "Updating AU {$DisplayName}" + Write-Verbose -Message "Updating the Azure AD Administrative Unit with Id {$($currentInstance.Id)}" - $UpdateParameters = $currentParameters.Clone() - #$UpdateParameters = Rename-M365DSCCimInstanceODataParameter -Properties $UpdateParameters + $UpdateParameters = ([Hashtable]$PSBoundParameters).clone() + $UpdateParameters = Rename-M365DSCCimInstanceParameter -Properties $UpdateParameters $UpdateParameters.Remove('Id') | Out-Null - $UpdateParameters.Remove('Verbose') | Out-Null - <# - foreach ($key in ($UpdateParameters.clone()).Keys) + $keys=(([Hashtable]$UpdateParameters).clone()).Keys + foreach($key in $keys) { - if ($UpdateParameters[$key].getType().Fullname -like '*CimInstance*') + if($null -ne $UpdateParameters.$key -and $UpdateParameters.$key.getType().Name -like "*cimInstance*") { - $UpdateParameters[$key] = Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $UpdateParameters[$key] + $UpdateParameters.$key = Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $UpdateParameters.$key } } - #> - #$UpdateParameters.Remove('Extensions') | Out-Null + $requestedMembers = $UpdateParameters.Members $UpdateParameters.Remove('Members') | Out-Null + $requestedScopedRoleMembers = $UpdateParameters.ScopedRoleMembers $UpdateParameters.Remove('ScopedRoleMembers') | Out-Null - #$UpdateParameters.Remove('Visibility') | Out-Null - - <# When using -BodyParameter it is not necessary to use AdditionalProperties for Dynamic AU params - if ($UpdateParameters.Containskey('MembershipType') -or $UpdateParameters.Containskey('MembershipRule') -or $UpdateParameters.Containskey('MembershipRuleProcessingState')) - { - $UpdateParameters.Remove('MembershipType') | Out-Null - $UpdateParameters.Remove('MembershipRule') | Out-Null - $UpdateParameters.Remove('MembershipRuleProcessingState') | Out-Null - $UpdateParameters.Add('AdditionalProperties', @{}) - if (-not [System.String]::IsNullOrEmpty($MembershipType)) - { - $UpdateParameters.AdditionalProperties.Add('MembershipType', $MembershipType) - } - if (-not [System.String]::IsNullOrEmpty($MembershipType)) - { - $UpdateParameters.AdditionalProperties.Add('MembershipRule', $MembershipRule) - } - if (-not [System.String]::IsNullOrEmpty($MembershipRuleProcessingState)) - { - $UpdateParameters.AdditionalProperties.Add('MembershipRuleProcessingState', $MembershipRuleProcessingState) - } - } - #> - - # when updating the resource, update the AU first and its members (if any) afterwards. - # The AU MembershipType may have changed from Dynamic to Static and that change has to be implemented before explicitly adding members #region resource generator code - Update-MgDirectoryAdministrativeUnit -BodyParameter $UpdateParameters ` - -AdministrativeUnitId $currentInstance.Id - + Update-MgDirectoryAdministrativeUnit -AdministrativeUnitId $currentInstance.Id -BodyParameter $UpdateParameters #endregion - if ($MembershipType -ne 'Dynamic' -and ($Members -or $backCurrentMembers)) + if ($MembershipType -ne 'Dynamic') { - $currentMembersValue = @() - if ($backCurrentMembers.Length -ne 0) - { - $currentMembersValue = $backCurrentMembers - } - if ($null -eq $currentMembersValue) - { - $currentMembersValue = @() - } - $desiredMembersValue = $Members - if ($null -eq $desiredMembersValue) - { - $desiredMembersValue = @() - } - write-verbose "AU {$DisplayName} Update Members: Current members: $($currentMembersValue.Identity -join ', ')" - write-verbose " Desired members: $($desiredMembersValue.Identity -join ', ')" - $membersDiff = Compare-Object -ReferenceObject $currentMembersValue -DifferenceObject $desiredMembersValue -Property Identity, Type -IncludeEqual - write-verbose " # compare results : $($membersDiff.Count -gt 0)" - foreach ($diff in $membersDiff) + if ($backCurrentMembers.Count -gt 0 -or $requestedMembers.Count -gt 0) { - if ($diff.Type -eq 'User') - { - $memberObject = Get-MgUser -Filter "UserPrincipalName eq '$($diff.Identity)'" - $memberType = 'users' - } - elseif ($diff.Type -eq 'Group') + $currentMembers = @() + foreach ($member in $backCurrentMembers) { - $memberObject = Get-MgGroup -Filter "DisplayName eq '$($diff.Identity)'" - $membertype = 'groups' + $currentMembers += [pscustomobject]@{Type=$member.Type; Identity = $member.Identity} } - elseif ($diff.Type -eq 'Device') + $desiredMembers = @() + foreach ($member in $requestedMembers) { - $memberObject = Get-MgDevice -Filter "DisplayName eq '$($diff.Identity)'" - $membertype = 'devices' + $desiredMembers += [pscustomobject]@{Type=$member.Type; Identity = $member.Identity} } - else + $membersDiff = Compare-Object -ReferenceObject $currentMembers -DifferenceObject $desiredMembers -Property Identity, Type + foreach ($diff in $membersDiff) { - if ($diff.Identity) + if ($diff.Type -eq 'User') { - # throw if a *new* member has been specified with invalid type - throw "AU {$($DisplayName)}: Member {$($diff.Identity)} has invalid type {$($diff.Type)}" + $memberObject = Get-MgUser -Filter "UserPrincipalName eq '$($diff.Identity)'" + $memberType = 'users' + } + elseif ($diff.Type -eq 'Group') + { + $memberObject = Get-MgGroup -Filter "DisplayName eq '$($diff.Identity)'" + $membertype = 'groups' + } + elseif ($diff.Type -eq 'Device') + { + $memberObject = Get-MgDevice -Filter "DisplayName eq '$($diff.Identity)'" + $membertype = 'devices' } else { - write-verbose "Compare Members - skip processing blank Identity" - continue # don't process, continue to next diff if any + # a *new* member has been specified with invalid type + throw "AU {$($DisplayName)}: Member {$($diff.Identity)} has invalid type {$($diff.Type)}" } - } - if ($null -eq $memberObject) - { - throw "AU member {$($diff.Identity)} does not exist as a $($diff.Type)" - } - if ($memberObject.Count -gt 1) - { - throw "AU member {$($diff.Identity)} is not a unique $($diff.Type.ToLower()) (Count=$($memberObject.Count))" - } - if ($diff.SideIndicator -eq '=>') - { - Write-Verbose -Message "Adding new member {$($diff.Identity)}, type {$($diff.Type)} to Administrative Unit {$DisplayName}" - - $memberBodyParam = @{ - '@odata.id' = "https://graph.microsoft.com/v1.0/$memberType/$($memberObject.Id)" + if ($null -eq $memberObject) + { + throw "AU member {$($diff.Identity)} does not exist as a $($diff.Type)" } - New-MgDirectoryAdministrativeUnitMemberByRef -AdministrativeUnitId ($currentInstance.Id) -BodyParameter $memberBodyParam | Out-Null - } - elseif ($diff.SideIndicator -eq '<=') - { - Write-Verbose -Message "Removing member {$($diff.Identity)}, type {$($diff.Type)} from Administrative Unit {$DisplayName}" - Remove-MgDirectoryAdministrativeUnitMemberByRef -AdministrativeUnitId ($currentInstance.Id) -DirectoryObjectId ($memberObject.Id) | Out-Null - } - else - { - Write-Verbose -Message "Unchanged member {$($diff.Identity)}, type {$($diff.Type)} of Administrative Unit {$DisplayName}" - } - } - } - - <# - if ($Extensions -or $backCurrentExtensions) - { - $currentExtensionsValue = @() - if ($currentInstance.Extensions.Length -ne 0) - { - $currentExtensionsValue = $backCurrentExtensions - } - if ($null -eq $currentExtensionsValue) - { - $currentExtensionsValue = @() - } - $desiredExtensionsValue = $Extensions - if ($null -eq $desiredExtensionsValue) - { - $desiredExtensionsValue = @() - } - $membersDiff = Compare-Object -ReferenceObject $currentExtensionsValue -DifferenceObject $desiredExtensionsValue -Property Id - foreach ($diff in $membersDiff) - { - if ($diff.SideIndicator -eq '=>') - { - Write-Verbose -Message "Adding new extension {$($diff.InputObject.Id)} to Administrative Unit {$($currentInstance.DisplayName)}" - $additionalPropertiesArg = @{} - if ($diff.InputObject.Properties.Count -gt 0) + if ($memberObject.Count -gt 1) { - $additionalPropertiesArg.AdditionalProperties = @{} - for ($p = 0; $p -lt $diff.InputObject.Properties.Count; $p = $p + 1) - { - $additionalPropertiesArg.AdditionalProperties.Add($diff.InputObject.Properties[$p], $diff.InputObject.Properties[$p+1]) - } + throw "AU member {$($diff.Identity)} is not a unique $($diff.Type.ToLower()) (Count=$($memberObject.Count))" } - New-MgDirectoryAdministrativeUnitExtension -AdministrativeUnitId ($currentInstance.Id) @additionalPropertiesArg -BodyParameter @{ExtensionId = $diff.InputObject.Id} | Out-Null - } - elseif ($diff.SideIndicator -eq '<=') - { - Write-Verbose -Message "Removing extension {$($diff.InputObject.Id)} from Administrative Unit {$($currentInstance.DisplayName)}" - Remove-MgDirectoryAdministrativeUnitExtension -AdministrativeUnitId ($currentInstance.Id) -ExtensionId ($diff.InputObject.Id) | Out-Null - } - else - { - #update AU Extension to use specified Properties only - $additionalPropertiesArg = @{} - if ($diff.InputObject.Properties.Count -gt 0) + if ($diff.SideIndicator -eq '=>') { - $additionalPropertiesArg.AdditionalProperties = @{} - for ($p = 0; $p -lt $diff.InputObject.Properties.Count; $p = $p + 1) - { - $additionalPropertiesArg.AdditionalProperties.Add($diff.InputObject.Properties[$p], $diff.InputObject.Properties[$p+1]) + Write-Verbose "AdministrativeUnit {$DisplayName} Adding member {$($diff.Identity)}, type {$($diff.Type)}" + + $memberBodyParam = @{ + '@odata.id' = "https://graph.microsoft.com/v1.0/$memberType/$($memberObject.Id)" } + New-MgDirectoryAdministrativeUnitMemberByRef -AdministrativeUnitId ($currentInstance.Id) -BodyParameter $memberBodyParam | Out-Null + } + else + { + Write-Verbose "Administrative Unit {$DisplayName} Removing member {$($diff.Identity)}, type {$($diff.Type)}" + Remove-MgDirectoryAdministrativeUnitMemberByRef -AdministrativeUnitId ($currentInstance.Id) -DirectoryObjectId ($memberObject.Id) | Out-Null } - Update-MgDirectoryAdministrativeUnitExtension -AdministrativeUnitId ($currentInstance.Id) -ExtensionId $diff.InputObject.Id @additionalPropertiesArg } } } - #> - if ($ScopedRoleMembers -or $backCurrentScopedRoleMembers) + if ($backCurrentScopedRoleMembers.Count -gt 0 -or $requestedScopedRoleMembers.Count -gt 0) { if ($backCurrentScopedRoleMembers.Length -ne 0) { @@ -807,20 +622,19 @@ function Set-TargetResource { $currentScopedRoleMembersValue = @() } - $desiredScopedRoleMembersValue = $ScopedRoleMembers + $desiredScopedRoleMembersValue = $requestedScopedRoleMembers if ($null -eq $desiredScopedRoleMembersValue) { $desiredScopedRoleMembersValue = @() } - <# # not necessary to flatten objects to compare when nested CimInstances are avoided in Schema/Get-TargetResource: $compareCurrentScopedRoleMembersValue = @() foreach ($roleMember in $currentScopedRoleMembersValue) { $compareCurrentScopedRoleMembersValue += [pscustomobject]@{ RoleName = $roleMember.RoleName - Identity = $roleMember.RoleMemberInfo.Identity - Type = $roleMember.RoleMemberInfo.Type + Identity = $roleMember.Identity + Type = $roleMember.Type } } $compareDesiredScopedRoleMembersValue = @() @@ -828,15 +642,14 @@ function Set-TargetResource { $compareDesiredScopedRoleMembersValue += [pscustomobject]@{ RoleName = $roleMember.RoleName - Identity = $roleMember.RoleMemberInfo.Identity - Type = $roleMember.RoleMemberInfo.Type + Identity = $roleMember.Identity + Type = $roleMember.Type } } + write-verbose "AU {$DisplayName} Update ScopedRoleMembers: Current members: $($compareCurrentScopedRoleMembersValue.Identity -join ', ')" + write-verbose " Desired members: $($compareDesiredScopedRoleMembersValue.Identity -join ', ')" $scopedRoleMembersDiff = Compare-Object -ReferenceObject $compareCurrentScopedRoleMembersValue -DifferenceObject $compareDesiredScopedRoleMembersValue -Property RoleName, Identity, Type - #> - write-verbose "AU {$DisplayName} Update ScopedRoleMembers: Current members: $($currentScopedRoleMembersValue.Identity -join ', ')" - write-verbose " Desired members: $($desiredScopedRoleMembersValue.Identity -join ', ')" - $scopedRoleMembersDiff = Compare-Object -ReferenceObject $CurrentScopedRoleMembersValue -DifferenceObject $DesiredScopedRoleMembersValue -Property RoleName, Identity, Type -IncludeEqual + # $scopedRoleMembersDiff = Compare-Object -ReferenceObject $CurrentScopedRoleMembersValue -DifferenceObject $DesiredScopedRoleMembersValue -Property RoleName, Identity, Type write-verbose " # compare results : $($scopedRoleMembersDiff.Count -gt 0)" foreach ($diff in $scopedRoleMembersDiff) @@ -881,7 +694,7 @@ function Set-TargetResource $roleObject = Get-MgDirectoryRole -Filter "DisplayName eq '$($diff.RoleName)'" if ($null -eq $roleObject) { - throw "AU Scoped Role {$($diff.RoleName)} does not exist as an Azure AD role" + throw "AU {$DisplayName} Scoped Role {$($diff.RoleName)} does not exist as an Azure AD role" } } if ($diff.SideIndicator -eq '=>') @@ -897,37 +710,24 @@ function Set-TargetResource # addition of scoped rolemember may throw if role is not supported as a scoped role New-MgDirectoryAdministrativeUnitScopedRoleMember -AdministrativeUnitId ($currentInstance.Id) -BodyParameter $scopedRoleMemberParam -ErrorAction Stop | Out-Null } - elseif ($diff.SideIndicator -eq '<=') + else { if (-not [string]::IsNullOrEmpty($diff.Rolename)) { Write-Verbose -Message "Removing scoped role {$($diff.RoleName)} member {$($diff.Identity)}, type {$($diff.Type)} from Administrative Unit {$DisplayName}" $scopedRoleMemberObject = Get-MgDirectoryAdministrativeUnitScopedRoleMember -AdministrativeUnitId ($currentInstance.Id) -All | Where-Object -FilterScript { $_.RoleId -eq $roleObject.Id -and $_.RoleMemberInfo.Id -eq $memberObject.Id } - Remove-MgDirectoryAdministrativeUnitScopedRoleMember -AdministrativeUnitId ($currentInstance.Id) -ScopedRoleMembershipId $scopedRoleMemberObject.Id | Out-Null + Remove-MgDirectoryAdministrativeUnitScopedRoleMember -AdministrativeUnitId ($currentInstance.Id) -ScopedRoleMembershipId $scopedRoleMemberObject.Id -ErrorAction Stop | Out-Null } } - else - { - Write-Verbose -Message "Unchanged scoped role {$($diff.RoleName)} member {$($diff.Identity)}, type {$($diff.Type)} in Administrative Unit {$DisplayName}" - } } } - } elseif ($Ensure -eq 'Absent' -and $currentInstance.Ensure -eq 'Present') { - Write-Verbose -Message "Removing AU {$DisplayName}" - - - #region resource generator code - #endregion - - - + Write-Verbose -Message "Removing the Azure AD Administrative Unit with Id {$($currentInstance.Id)}" #region resource generator code Remove-MgDirectoryAdministrativeUnit -AdministrativeUnitId $currentInstance.Id #endregion - } } @@ -937,33 +737,36 @@ function Test-TargetResource [OutputType([System.Boolean])] param ( - [Parameter(Mandatory = $true)] - [System.String] - $DisplayName, - - [Parameter()] - [System.String] - $Id, - + #region resource generator code [Parameter()] [System.String] $Description, + [Parameter(Mandatory=$true)] + [System.String] + $DisplayName, [Parameter()] [validateset('Public', 'HiddenMembership')] [System.String] $Visibility, + [Parameter()] + [System.String] + $Id, + [Parameter()] [validateset('Assigned', 'Dynamic')] - [System.String]$MembershipType, + [System.String] + $MembershipType, [Parameter()] - [System.String]$MembershipRule, + [System.String] + $MembershipRule, [Parameter()] [validateset('Paused', 'On')] - [System.String]$MembershipRuleProcessingState, + [System.String] + $MembershipRuleProcessingState, [Parameter()] [Microsoft.Management.Infrastructure.CimInstance[]] @@ -973,16 +776,12 @@ function Test-TargetResource [Microsoft.Management.Infrastructure.CimInstance[]] $ScopedRoleMembers, - <# - [Parameter()] - [Microsoft.Management.Infrastructure.CimInstance[]] - $Extensions, - #> + #endregion [Parameter(Mandatory = $true)] [System.String] [ValidateSet('Absent', 'Present')] - $Ensure = 'Present', + $Ensure = $true, [Parameter()] [System.Management.Automation.PSCredential] @@ -1021,140 +820,56 @@ function Test-TargetResource Add-M365DSCTelemetryEvent -Data $data #endregion - Write-Verbose -Message "Testing configuration of {$id}" + Write-Verbose -Message "Testing configuration of the Azure AD Administrative Unit with Id {$Id} and DisplayName {$DisplayName}" $CurrentValues = Get-TargetResource @PSBoundParameters $ValuesToCheck = ([Hashtable]$PSBoundParameters).clone() - if ($CurrentValues.Ensure -eq 'Absent' -and $Ensure -eq 'Present') + if ($CurrentValues.Ensure -ne $PSBoundParameters.Ensure) { - Write-Verbose -Message "Test-TargetResource returned $false" + Write-Verbose -Message "Test-TargetResource returned $false - Ensure not the same" return $false } - $testResult = $true - if ($Members.Count -gt 0 -or $currentValues.Members.Count -gt 0) - { - if ($Members.Count -ne $currentValues.Members.Count) - { - $testresult = $false - } - $testMembers = $Members - if ($null -eq $testMembers) - { - $testMembers = @() - } - $testCurrentValuesMembers = $currentValues.Members - if ($null -eq $testCurrentValuesMembers) - { - $testCurrentValuesMembers = @() - } - if ((Compare-Object -ReferenceObject $testMembers -DifferenceObject $testCurrentValuesMembers -Property Identity, Type).Count -gt 0) - { - $testresult = $false - } - $ValuesToCheck.Remove('Members') | Out-Null - } - if ($true -eq $testResult -and ($ScopedRoleMembers.Count -gt 0 -or $currentValues.ScopedRoleMembers.Count -gt 0)) - { - if ($ScopedRoleMembers.Count -ne $currentValues.ScopedRoleMembers.Count) - { - $testresult = $false - } - $testScopedRoleMembers = $ScopedRoleMembers - if ($null -eq $testScopedRoleMembers) - { - $testScopedRoleMembers = @() - } - $testCurrentValuesScopedRoleMembers = $currentValues.ScopedRoleMembers - if ($null -eq $testCurrentValuesScopedRoleMembers) - { - $testCurrentValuesScopedRoleMembers = @() - } - if ((Compare-Object -ReferenceObject $testScopedRoleMembers -DifferenceObject $testCurrentValuesScopedRoleMembers -Property RoleName, Identity, Type).Count -gt 0) - { - $testresult = $false - } - $ValuesToCheck.Remove('ScopedRoleMembers') | Out-Null - } <# + #Compare Cim instances foreach ($key in $PSBoundParameters.Keys) { - if ($PSBoundParameters[$key].getType().Name -like '*CimInstance*') + $source = $PSBoundParameters.$key + $target = $CurrentValues.$key + if ($source.getType().Name -like '*CimInstance*') { - $CIMArraySource = @() - $CIMArrayTarget = @() - if ($PSBoundParameters[$key]) - { - $CIMArraySource += $PSBoundParameters[$key] - } - if ($CurrentValues.$key) - { - $CIMArrayTarget += $CurrentValues.$key - } - if ($CIMArraySource.count -ne $CIMArrayTarget.count) - { - Write-Verbose -Message "Configuration drift:Number of items does not match: Source=$($CIMArraySource.count) Target=$($CIMArrayTarget.count)" - $testResult = $false - break - } - $i = 0 - foreach ($item in $CIMArraySource ) - { - $testResult = Compare-M365DSCComplexObject ` - -Source (Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $CIMArraySource[$i]) ` - -Target ($CIMArrayTarget[$i]) + $source = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $source + + $testResult = Compare-M365DSCComplexObject ` + -Source ($source) ` + -Target ($target) - $i++ - if (-Not $testResult) - { - $testResult = $false - break; - } - } if (-Not $testResult) { + Write-Verbose -Message "Difference found for $key" $testResult = $false break; } $ValuesToCheck.Remove($key) | Out-Null + } } #> - Write-Verbose -Message "Test-TargetResource - testresult after comparing CimInstance(s): $testResult" - $ValuesToCheck.Remove('Credential') | Out-Null $ValuesToCheck.Remove('ApplicationId') | Out-Null $ValuesToCheck.Remove('TenantId') | Out-Null $ValuesToCheck.Remove('ApplicationSecret') | Out-Null - $ValuesToCheck.Remove('CertificateThumbprint') | Out-Null - $ValuesToCheck.Remove('ManagedIdentity') | Out-Null - # Removing the visibility parameter from the check since this is always being returned as null currently by the Microsoft Graph. + # Visibility is currently not returned by Get-TargetResource $ValuesToCheck.Remove('Visibility') | Out-Null - if ($MembershipType -ne 'Dynamic' -and $CurrentValues.MembershipType -ne 'Dynamic') - { - # Removing the MembershipType parameter from the check if it isn't Dynamic. If it isn't, it can be Assigned or null - $ValuesToCheck.Remove('MembershipType') | Out-Null - } - Write-Verbose -Message "Current Values: $(Convert-M365DscHashtableToString -Hashtable $CurrentValues)" Write-Verbose -Message "Target Values: $(Convert-M365DscHashtableToString -Hashtable $ValuesToCheck)" - #Convert any DateTime to String - foreach ($key in $ValuesToCheck.Keys) - { - if (($null -ne $CurrentValues[$key]) ` - -and ($CurrentValues[$key].getType().Name -eq 'DateTime')) - { - $CurrentValues[$key] = $CurrentValues[$key].toString() - } - } - if ($testResult) { $testResult = Test-M365DSCParameterState -CurrentValues $CurrentValues ` @@ -1201,13 +916,7 @@ function Export-TargetResource $ConnectionMode = New-M365DSCConnection -Workload 'MicrosoftGraph' ` -InboundParameters $PSBoundParameters ` - -ProfileName 'beta' - $context = Get-MgContext - if ($null -eq $context) - { - $ConnectionMode = New-M365DSCConnection -Workload 'MicrosoftGraph' ` - -InboundParameters $PSBoundParameters -ProfileName 'beta' - } + -ProfileName 'v1.0' #Ensure the proper dependencies are installed in the current environment. Confirm-M365DSCDependencies @@ -1226,7 +935,6 @@ function Export-TargetResource #region resource generator code [array]$getValue = Get-MgDirectoryAdministrativeUnit -All ` -ErrorAction Stop - #endregion $i = 1 @@ -1241,100 +949,71 @@ function Export-TargetResource } foreach ($config in $getValue) { - Write-Host " |---[$i/$($getValue.Count)] $($config.DisplayName)" -NoNewline + $displayedKey = $config.Id + if (-not [String]::IsNullOrEmpty($config.displayName)) + { + $displayedKey = $config.displayName + } + Write-Host " |---[$i/$($getValue.Count)] $displayedKey" -NoNewline $params = @{ DisplayName = $config.DisplayName + Id = $config.Id Ensure = 'Present' Credential = $Credential ApplicationId = $ApplicationId TenantId = $TenantId ApplicationSecret = $ApplicationSecret CertificateThumbprint = $CertificateThumbprint - ManagedIdentity = $ManagedIdentity + ManagedIdentity = $ManagedIdentity.IsPresent } $Results = Get-TargetResource @Params - $Results = Update-M365DSCExportAuthenticationResults -ConnectionMode $ConnectionMode ` - -Results $Results - if ($Results.Members) + if ($null -ne $Results.ScopedRoleMembers) { - $hashResults = Get-M365DSCDRGObjectArrayToHashtable -ArrayObject $Results.Members - $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject $hashResults -CIMInstanceName MicrosoftGraphMember - if ($complexTypeStringResult) - { - $Results.Members = $complexTypeStringResult - } - else - { - $Results.Remove('Members') | Out-Null - } - } - if ($Results.ScopedRoleMembers) - { - $hashResults = Get-M365DSCDRGObjectArrayToHashtable -ArrayObject $Results.ScopedRoleMembers - $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject $hashResults -CIMInstanceName MicrosoftGraphScopedRoleMembership - if ($complexTypeStringResult) - { - $Results.ScopedRoleMembers = $complexTypeStringResult - } - else + $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject ([Array]$Results.ScopedRoleMembers) ` + -CIMInstanceName MicrosoftGraphScopedRoleMembership + + write-verbose "ScopedRoleMembers on next line:`r`n$complexTypeStringResult" + + $Results.ScopedRoleMembers = $complexTypeStringResult + + if ([String]::IsNullOrEmpty($complexTypeStringResult)) { $Results.Remove('ScopedRoleMembers') | Out-Null } } - <# - if ($Results.Extensions) + if ($null -ne $Results.Members) { - $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject $Results.Extensions -CIMInstanceName MicrosoftGraphextension - if ($complexTypeStringResult) - { - $Results.Extensions = $complexTypeStringResult } - else + $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject ([Array]$Results.Members) ` + -CIMInstanceName MicrosoftGraphMember + write-verbose "Members on next line:`r`n$complexTypeStringResult" + $Results.Members = $complexTypeStringResult + + if ([String]::IsNullOrEmpty($complexTypeStringResult)) { - $Results.Remove('Extensions') | Out-Null + $Results.Remove('Members') | Out-Null } } - #> + + $Results = Update-M365DSCExportAuthenticationResults -ConnectionMode $ConnectionMode ` + -Results $Results + $currentDSCBlock = Get-M365DSCExportContentForResource -ResourceName $ResourceName ` -ConnectionMode $ConnectionMode ` -ModulePath $PSScriptRoot ` -Results $Results ` -Credential $Credential - <# - if ($Results.Members) - { - $isCIMArray = $false - if ($Results.Members.getType().Fullname -like '*[[\]]') - { - $isCIMArray = $true - } - $currentDSCBlock = Convert-DSCStringParamToVariable -DSCBlock $currentDSCBlock -ParameterName 'Members' -IsCIMArray:$isCIMArray - $currentDSCBlock = $currentDSCBlock.Replace('}");', '});') - } - if ($Results.ScopedRoleMembers) + if ($null -ne $Results.ScopedRoleMembers) { - $isCIMArray = $false - if ($Results.ScopedRoleMembers.getType().Fullname -like '*[[\]]') - { - $isCIMArray = $true - } - $currentDSCBlock = Convert-DSCStringParamToVariable -DSCBlock $currentDSCBlock -ParameterName 'ScopedRoleMembers' -IsCIMArray:$isCIMArray + $currentDSCBlock = Convert-DSCStringParamToVariable -DSCBlock $currentDSCBlock -ParameterName "ScopedRoleMembers" -isCIMArray $true } - #> - <# - if ($Results.Extensions) + if ($null -ne $Results.Members) { - $isCIMArray = $false - if ($Results.Extensions.getType().Fullname -like '*[[\]]') - { - $isCIMArray = $true - } - $currentDSCBlock = Convert-DSCStringParamToVariable -DSCBlock $currentDSCBlock -ParameterName 'Extensions' -IsCIMArray:$isCIMArray + $currentDSCBlock = Convert-DSCStringParamToVariable -DSCBlock $currentDSCBlock -ParameterName "Members" -isCIMArray $true } - #> - + write-verbose "currentDSCBlock on next line:`r`n$($currentDSCBlock -join "`r`n")" $dscContent += $currentDSCBlock Save-M365DSCPartialExport -Content $currentDSCBlock ` -FileName $Global:PartialExportFileName @@ -1356,83 +1035,195 @@ function Export-TargetResource return '' } } +function Rename-M365DSCCimInstanceParameter +{ + [CmdletBinding()] + [OutputType([System.Collections.Hashtable],[System.Collections.Hashtable[]])] + param( + [Parameter(Mandatory = 'true')] + $Properties + ) + + $keyToRename=@{ + "odataType"="@odata.type" + } + + $result=$Properties + + $type=$Properties.getType().FullName + + #region Array + if ($type -like '*[[\]]') + { + $values = @() + foreach ($item in $Properties) + { + $values += Rename-M365DSCCimInstanceParameter $item + } + $result=$values + + return ,$result + } + #endregion + + #region Single + if($type -like "*Hashtable") + { + $result=([Hashtable]$Properties).clone() + } + if($type -like '*CimInstance*' -or $type -like '*Hashtable*'-or $type -like '*Object*') + { + $hashProperties = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $result + $keys=($hashProperties.clone()).keys + foreach($key in $keys) + { + $keyName=$key.substring(0,1).tolower()+$key.substring(1,$key.length-1) + if ($key -in $keyToRename.Keys) + { + $keyName=$keyToRename.$key + } + $property=$hashProperties.$key + if($null -ne $property) + { + $hashProperties.Remove($key) + $hashProperties.add($keyName,(Rename-M365DSCCimInstanceParameter $property)) + } + } + $result = $hashProperties + } + return $result + #endregion +} function Get-M365DSCDRGComplexTypeToHashtable { [CmdletBinding()] - [OutputType([System.Collections.Hashtable])] + [OutputType([hashtable],[hashtable[]])] param( [Parameter()] $ComplexObject ) - if ($null -eq $ComplexObject) + if($null -eq $ComplexObject) { return $null } - if ($ComplexObject.GetType().FullName -like '*[[\]]') + if($ComplexObject.gettype().fullname -like "*[[\]]") { - $results = @() + $results=@() - foreach ($item in $ComplexObject) + foreach($item in $ComplexObject) { - if ($item) + if($item) { $hash = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $item - $results += $hash + $results+=$hash } } - if ($results.Count -eq 0) + + # PowerShell returns all non-captured stream output, not just the argument of the return statement. + #An empty array is mangled into $null in the process. + #However, an array can be preserved on return by prepending it with the array construction operator (,) + return ,[hashtable[]]$results + } + + if($ComplexObject.getType().fullname -like '*Dictionary*') + { + $results = @{} + + $ComplexObject=[hashtable]::new($ComplexObject) + $keys=$ComplexObject.Keys + foreach ($key in $keys) { - return $null + if($null -ne $ComplexObject.$key) + { + $keyName = $key + + $keyType=$ComplexObject.$key.gettype().fullname + + if($keyType -like "*CimInstance*" -or $keyType -like "*Dictionary*" -or $keyType -like "Microsoft.Graph.PowerShell.Models.*" -or $keyType -like "*[[\]]") + { + $hash = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $ComplexObject.$key + + $results.Add($keyName, $hash) + } + else + { + $results.Add($keyName, $ComplexObject.$key) + } + } } - return $results + return [hashtable]$results } $results = @{} - $keys = $ComplexObject | Get-Member | Where-Object -FilterScript { $_.MemberType -eq 'Property' -and $_.Name -ne 'AdditionalProperties' } - foreach ($key in $keys) + if($ComplexObject.getType().Fullname -like "*hashtable") { - if ($ComplexObject.$($key.Name)) - { - $results.Add($key.Name, $ComplexObject.$($key.Name)) - } + $keys = $ComplexObject.keys } - if ($results.count -eq 0) + else { - return $null + $keys = $ComplexObject | Get-Member | Where-Object -FilterScript {$_.MemberType -eq 'Property'} } - return $results -} -function Get-M365DSCDRGObjectArrayToHashtable -{ - [cmdletbinding()] - [outputtype([system.object[]])] - param( - [Parameter()] - [system.object[]] - $ArrayObject - ) - $returnValue = @() - foreach ($element in $ArrayObject) + foreach ($key in $keys) { - $hashTable = @{} - foreach ($property in $element.PSObject.Properties.Name) + $keyName=$key + if($ComplexObject.getType().Fullname -notlike "*hashtable") { - $hashtable.Add($property, $element.$property) | Out-Null + $keyName=$key.Name + } + + if($null -ne $ComplexObject.$keyName) + { + $keyType=$ComplexObject.$keyName.gettype().fullname + if($keyType -like "*CimInstance*" -or $keyType -like "*Dictionary*" -or $keyType -like "Microsoft.Graph.PowerShell.Models.*" ) + { + $hash = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $ComplexObject.$keyName + + $results.Add($keyName, $hash) + } + else + { + $results.Add($keyName, $ComplexObject.$keyName) + } } - $returnValue += $hashTable } - return $returnValue + + return [hashtable]$results } + +<# + Use ComplexTypeMapping to overwrite the type of nested CIM + Example + $complexMapping=@( + @{ + Name="ApprovalStages" + CimInstanceName="MSFT_MicrosoftGraphapprovalstage1" + IsRequired=$false + } + @{ + Name="PrimaryApprovers" + CimInstanceName="MicrosoftGraphuserset" + IsRequired=$false + } + @{ + Name="EscalationApprovers" + CimInstanceName="MicrosoftGraphuserset" + IsRequired=$false + } + ) + With + Name: the name of the parameter to be overwritten + CimInstanceName: The type of the CIM instance (can include or not the prefix MSFT_) + IsRequired: If isRequired equals true, an empty hashtable or array will be returned. Some of the Graph parameters are required even though they are empty +#> function Get-M365DSCDRGComplexTypeToString { [CmdletBinding()] - #[OutputType([System.String])] param( [Parameter()] $ComplexObject, @@ -1441,131 +1232,210 @@ function Get-M365DSCDRGComplexTypeToString [System.String] $CIMInstanceName, + [Parameter()] + [Array] + $ComplexTypeMapping, + [Parameter()] [System.String] - $Whitespace = '', + $Whitespace='', + + [Parameter()] + [System.uint32] + $IndentLevel=3, [Parameter()] [switch] - $isArray = $false + $isArray=$false ) + if ($null -eq $ComplexObject) { return $null } + write-verbose "Get-M365DSCDRGComplexTypeToString $CIMInstanceName isArray=$isArray" + + $indent='' + for ($i = 0; $i -lt $IndentLevel ; $i++) + { + $indent+=' ' + } #If ComplexObject is an Array - if ($ComplexObject.GetType().FullName -like '*[[\]]') + if ($ComplexObject.GetType().FullName -like "*[[\]]") { - write-verbose "object is an array" - $currentProperty = @() + $currentProperty=@() + $IndentLevel++ foreach ($item in $ComplexObject) { - write-verbose "Item=$($item -join '|')" - $currentProperty += Get-M365DSCDRGComplexTypeToString ` - -ComplexObject $item ` - -isArray:$true ` - -CIMInstanceName $CIMInstanceName ` - -Whitespace ' ' + $splat=@{ + 'ComplexObject'=$item + 'CIMInstanceName'=$CIMInstanceName + 'IndentLevel'=$IndentLevel + } + if ($ComplexTypeMapping) + { + $splat.add('ComplexTypeMapping',$ComplexTypeMapping) + } + $currentProperty += Get-M365DSCDRGComplexTypeToString -isArray:$true @splat } - if ([string]::IsNullOrEmpty($currentProperty)) - { - return $null - } - return $currentProperty + write-verbose "return array currentProperty on next line:`r`n $($currentProperty -join "`r`n")" + + # PowerShell returns all non-captured stream output, not just the argument of the return statement. + #An empty array is mangled into $null in the process. + #However, an array can be preserved on return by prepending it with the array construction operator (,) + return ,$currentProperty } - #If ComplexObject is a single CIM Instance - if (-Not (Test-M365DSCComplexObjectHasValues -ComplexObject $ComplexObject)) + $currentProperty='' + if($isArray) { - return $null + $currentProperty += "`r`n" + $currentProperty += $indent } - $currentProperty = '' - if ($isArray) + + $CIMInstanceName=$CIMInstanceName.replace("MSFT_","") + $currentProperty += "MSFT_$CIMInstanceName{`r`n" + $IndentLevel++ + $indent='' + for ($i = 0; $i -lt $IndentLevel ; $i++) { - $currentProperty += "`r`n" + $indent+=' ' } - $currentProperty += "$whitespace`MSFT_$CIMInstanceName{`r`n" $keyNotNull = 0 + + if ($ComplexObject.Keys.count -eq 0) + { + return $null + } + foreach ($key in $ComplexObject.Keys) { - write-verbose "" - if ($ComplexObject.$key) + write-verbose "ComplexObject key=$key" + if ($null -ne $ComplexObject.$key) { + write-verbose "`tnot null" $keyNotNull++ - - if ($ComplexObject.$key.GetType().FullName -like 'Microsoft.Graph.PowerShell.Models.*') + if ($ComplexObject.$key.GetType().FullName -like "Microsoft.Graph.PowerShell.Models.*" -or $key -in $ComplexTypeMapping.Name) { - $hashPropertyType = $ComplexObject[$key].GetType().Name - $hashProperty = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $ComplexObject.$key + $hashPropertyType=$ComplexObject[$key].GetType().Name.tolower() + + $isArray=$false + if($ComplexObject[$key].GetType().FullName -like "*[[\]]") + { + $isArray=$true + } + #overwrite type if object defined in mapping complextypemapping + if($key -in $ComplexTypeMapping.Name) + { + $hashPropertyType=($ComplexTypeMapping|Where-Object -FilterScript {$_.Name -eq $key}).CimInstanceName + $hashProperty=$ComplexObject[$key] + } + else + { + $hashProperty=Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $ComplexObject[$key] + } + + if(-not $isArray) + { + $currentProperty += $indent + $key + ' = ' + } - if (Test-M365DSCComplexObjectHasValues -ComplexObject $hashProperty) + if($isArray -and $key -in $ComplexTypeMapping.Name ) { - $Whitespace += ' ' - if (-not $isArray) + if($ComplexObject.$key.count -gt 0) { - $currentProperty += ' ' + $key + ' = ' + $currentProperty += $indent + $key + ' = ' + $currentProperty += "@(" } + } + + if ($isArray) + { + $IndentLevel++ + foreach ($item in $ComplexObject[$key]) + { + if ($ComplexObject.$key.GetType().FullName -like "Microsoft.Graph.PowerShell.Models.*") + { + $item=Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $item + } + $currentProperty += Get-M365DSCDRGComplexTypeToString ` + -ComplexObject $item ` + -CIMInstanceName $hashPropertyType ` + -IndentLevel $IndentLevel ` + -ComplexTypeMapping $ComplexTypeMapping ` + -IsArray:$true + } + $IndentLevel-- + } + else + { $currentProperty += Get-M365DSCDRGComplexTypeToString ` - -ComplexObject $hashProperty ` - -CIMInstanceName $hashPropertyType ` - -Whitespace $Whitespace + -ComplexObject $hashProperty ` + -CIMInstanceName $hashPropertyType ` + -IndentLevel $IndentLevel ` + -ComplexTypeMapping $ComplexTypeMapping } + if($isArray) + { + if($ComplexObject.$key.count -gt 0) + { + $currentProperty += $indent + $currentProperty += ')' + $currentProperty += "`r`n" + } + } + $isArray=$PSBoundParameters.IsArray } else { - if (-not $isArray) + $currentProperty += Get-M365DSCDRGSimpleObjectTypeToString -Key $key -Value $ComplexObject[$key] -Space ($indent) + } + } + else + { + $mappedKey=$ComplexTypeMapping|where-object -filterscript {$_.name -eq $key} + + if($mappedKey -and $mappedKey.isRequired) + { + if($mappedKey.isArray) { - $Whitespace = ' ' + $currentProperty += "$indent$key = @()`r`n" + } + else + { + $currentProperty += "$indent$key = `$null`r`n" } - $currentProperty += Get-M365DSCDRGSimpleObjectTypeToString -Key $key -Value $ComplexObject.$key -Space ($Whitespace + ' ') } } } - $currentProperty += ' }' - - if ($keyNotNull -eq 0) + $indent='' + for ($i = 0; $i -lt $IndentLevel-1 ; $i++) { - $currentProperty = $null + $indent+=' ' + } + $currentProperty += "$indent}" + if($isArray -or $IndentLevel -gt 4) + { + $currentProperty += "`r`n" } - return $currentProperty -} -function Test-M365DSCComplexObjectHasValues -{ - [CmdletBinding()] - [OutputType([System.Boolean])] - param( - [Parameter(Mandatory = $true)] - [system.object] - $ComplexObject - ) - $keys = $ComplexObject.psobject.properties.name - $hasValue = $false - foreach ($key in $keys) + #Indenting last parenthese when the cim instance is an array + if($IndentLevel -eq 5) { - if ($ComplexObject.$key) + $indent='' + for ($i = 0; $i -lt $IndentLevel-2 ; $i++) { - if ($ComplexObject.$key.GetType().FullName -like 'Microsoft.Graph.PowerShell.Models.*') - { - $hash = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $ComplexObject[$key] - if (-Not $hash) - { - return $false - } - $hasValue = Test-M365DSCComplexObjectHasValues -ComplexObject ($hash) - } - else - { - $hasValue = $true - break - } + $indent+=' ' } + $currentProperty += $indent } - return $hasValue + write-verbose "return item currentProperty on next line:`r`n$currentProperty" + return $currentProperty } + Function Get-M365DSCDRGSimpleObjectTypeToString { [CmdletBinding()] @@ -1580,49 +1450,50 @@ Function Get-M365DSCDRGSimpleObjectTypeToString [Parameter()] [System.String] - $Space = ' ' + $Space=" " ) - $returnValue = '' + write-verbose "Get-M365DSCDRGSimpleObjectTypeToString key='$Key', value='$Value'. Type=$($value.gettype().fullname)" + $returnValue="" switch -Wildcard ($Value.GetType().Fullname ) { - '*.Boolean' + "*.Boolean" { - $returnValue = $Space + $Key + " = `$" + $Value.ToString() + "`r`n" + $returnValue= $Space + $Key + " = `$" + $Value.ToString() + "`r`n" } - '*.String' + "*.String" { - if ($key -eq '@odata.type') + if($key -eq '@odata.type') { - $key = 'odataType' + $key='odataType' } - $returnValue = $Space + $Key + " = '" + $Value + "'`r`n" + $returnValue= $Space + $Key + " = '" + $Value + "'`r`n" } - '*.DateTime' + "*.DateTime" { - $returnValue = $Space + $Key + " = '" + $Value + "'`r`n" + $returnValue= $Space + $Key + " = '" + $Value + "'`r`n" } - '*[[\]]' + "*[[\]]" { - $returnValue = $Space + $key + ' = @(' - $whitespace = '' - $newline = '' - if ($Value.Count -gt 1) + $returnValue= $Space + $key + " = @(" + $whitespace="" + $newline="" + if($Value.count -gt 1) { $returnValue += "`r`n" - $whitespace = $Space + ' ' - $newline = "`r`n" + $whitespace=$Space+" " + $newline="`r`n" } foreach ($item in $Value) { switch -Wildcard ($item.GetType().Fullname ) { - '*.String' + "*.String" { $returnValue += "$whitespace'$item'$newline" } - '*.DateTime' + "*.DateTime" { $returnValue += "$whitespace'$item'$newline" } @@ -1632,7 +1503,7 @@ Function Get-M365DSCDRGSimpleObjectTypeToString } } } - if ($Value.Count -gt 1) + if($Value.count -gt 1) { $returnValue += "$Space)`r`n" } @@ -1644,232 +1515,212 @@ Function Get-M365DSCDRGSimpleObjectTypeToString } Default { - $returnValue = $Space + $Key + ' = ' + $Value + "`r`n" + $returnValue= $Space + $Key + " = " + $Value + "`r`n" } } + write-verbose "return '$returnValue'" return $returnValue } -function Rename-M365DSCCimInstanceODataParameter -{ - [CmdletBinding()] - [OutputType([System.Collections.Hashtable])] - param( - [Parameter(Mandatory = 'true')] - [System.Collections.Hashtable] - $Properties - ) - $CIMparameters = $Properties.GetEnumerator() | Where-Object -FilterScript { $_.Value.GetType().Fullname -like '*CimInstance*' } - foreach ($CIMParam in $CIMparameters) - { - if ($CIMParam.Value.GetType().Fullname -like '*[[\]]') - { - $CIMvalues = @() - foreach ($item in $CIMParam.Value) - { - $CIMHash = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $item - $keys = ($CIMHash.Clone()).Keys - if ($keys -contains 'odataType') - { - $CIMHash.Add('@odata.type', $CIMHash.odataType) - $CIMHash.Remove('odataType') - } - $CIMvalues += $CIMHash - } - $Properties.($CIMParam.Key) = $CIMvalues - } - else - { - $CIMHash = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $CIMParam.value - $keys = ($CIMHash.Clone()).Keys - if ($keys -contains 'odataType') - { - $CIMHash.Add('@odata.type', $CIMHash.odataType) - $CIMHash.Remove('odataType') - $Properties.($CIMParam.Key) = $CIMHash - } - } - } - return $Properties -} function Compare-M365DSCComplexObject { [CmdletBinding()] [OutputType([System.Boolean])] param( [Parameter()] - [System.Collections.Hashtable] $Source, [Parameter()] - [System.Collections.Hashtable] $Target ) - $keys = $Source.Keys | Where-Object -FilterScript { $_ -ne 'PSComputerName' } - foreach ($key in $keys) + #Comparing full objects + if($null -eq $Source -and $null -eq $Target) + { + return $true + } + + $sourceValue="" + $targetValue="" + if (($null -eq $Source) -xor ($null -eq $Target)) + { + if($null -eq $Source) + { + $sourceValue="Source is null" + } + + if($null -eq $Target) + { + $targetValue="Target is null" + } + Write-Verbose -Message "Configuration drift - Complex object: {$sourceValue$targetValue}" + return $false + } + + if($Source.getType().FullName -like "*CimInstance[[\]]" -or $Source.getType().FullName -like "*Hashtable[[\]]") { - Write-Verbose -Message "Comparing Source-key: {$key}" - $skey = $key - if ($key -eq 'odataType') + if($source.count -ne $target.count) { - $skey = '@odata.type' + Write-Verbose -Message "Configuration drift - The complex array have different number of items: Source {$($source.count)} Target {$($target.count)}" + return $false + } + if($source.count -eq 0) + { + return $true } - #Marking Target[key] to null if empty complex object or array - if ($null -ne $Target[$key]) + foreach($item in $Source) { - switch -Wildcard ($Target[$key].getType().Fullname ) + + $hashSource=Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $item + foreach($targetItem in $Target) { - 'Microsoft.Graph.PowerShell.Models.*' - { - $hashProperty = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $Target[$key] - if (-not (Test-M365DSCComplexObjectHasValues -ComplexObject $hashProperty)) - { - $Target[$key] = $null - } - } - '*[[\]]' + $compareResult= Compare-M365DSCComplexObject ` + -Source $hashSource ` + -Target $targetItem + + if ($compareResult) { - if ($Target[$key].count -eq 0) - { - $Target[$key] = $null - } + write-verbose "Compare-M365DSCComplexObject: Diff found" + break } } + + if(-not $compareResult) + { + Write-Verbose -Message "Configuration drift - The complex array items are not identical" + return $false + } } - $sourceValue = $Source[$key] - $targetValue = $Target[$key] - #One of the item is null - if (($null -eq $Source[$skey]) -xor ($null -eq $Target[$key])) + return $true + } + + $keys= $Source.Keys|Where-Object -FilterScript {$_ -ne "PSComputerName"} + foreach ($key in $keys) + { + #Matching possible key names between Source and Target + $skey=$key + $tkey=$key + + $sourceValue=$Source.$key + $targetValue=$Target.$tkey + #One of the item is null and not the other + if (($null -eq $Source.$key) -xor ($null -eq $Target.$tkey)) { - if ($null -eq $Source[$skey]) + + if($null -eq $Source.$key) { - $sourceValue = 'null' + $sourceValue="null" } - if ($null -eq $Target[$key]) + if($null -eq $Target.$tkey) { - $targetValue = 'null' + $targetValue="null" } - Write-Verbose -Message "Configuration drift - key: $key Source{$sourceValue} Target{$targetValue}" + + #Write-Verbose -Message "Configuration drift - key: $key Source {$sourceValue} Target {$targetValue}" return $false } - #Both source and target aren't null or empty - if (($null -ne $Source[$skey]) -and ($null -ne $Target[$key])) + + #Both keys aren't null or empty + if(($null -ne $Source.$key) -and ($null -ne $Target.$tkey)) { - if ($Source[$skey].getType().FullName -like '*CimInstance*' -or $Target[$skey].getType().FullName -like '*CimInstance*') + if($Source.$key.getType().FullName -like "*CimInstance*" -or $Source.$key.getType().FullName -like "*hashtable*" ) { #Recursive call for complex object - write-verbose "Compare values for $key" - if ($Source[$skey].getType().FullName -like '*CimInstance*') - { - $complexSourceValue = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $Source[$skey] - } - else - { - $complexSourceValue = $Source[$key] - } - if ($Target[$skey].getType().FullName -like '*CimInstance*') - { - $complexTargetValue = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $Target[$key] - } - else - { - $complexTargetValue = $Target[$key] - } - if ($complexSourceValue.GetType().FullName -ne 'System.Collections.Hashtable') - { - $tempSourceValue = @{} - $complexSourceValue.psobject.properties | Foreach-Object { - $tempSourceValue[$_.Name] = $_.Value - } - $complexSourceValue = $tempSourceValue - } - if ($complexTargetValue.GetType().FullName -ne 'System.Collections.Hashtable') - { - $tempTargetValue = @{} - $complexTargetValue.psobject.properties | Foreach-Object { - $tempTargetValue[$_.Name] = $_.Value - } - $complexTargetValue = $tempTargetValue - } - $compareResult = Compare-M365DSCComplexObject ` - -Source $complexSourceValue ` - -Target $complexTargetValue + $compareResult= Compare-M365DSCComplexObject ` + -Source (Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $Source.$key) ` + -Target $Target.$tkey - if (-not $compareResult) + if(-not $compareResult) { - Write-Verbose -Message "Configuration drift - key: $key Source{$sourceValue} Target{$targetValue}" + + #Write-Verbose -Message "Configuration drift - complex object key: $key Source {$sourceValue} Target {$targetValue}" return $false } } else { #Simple object comparison - $referenceObject = $Target[$key] - $differenceObject = $Source[$skey] + $referenceObject=$Target.$tkey + $differenceObject=$Source.$key - $compareResult = Compare-Object ` - -ReferenceObject ($referenceObject) ` - -DifferenceObject ($differenceObject) + #Identifying date from the current values + $targetType=($Target.$tkey.getType()).Name + if($targetType -like "*Date*") + { + $compareResult=$true + $sourceDate= [DateTime]$Source.$key + if($sourceDate -ne $targetType) + { + $compareResult=$null + } + } + else + { + $compareResult = Compare-Object ` + -ReferenceObject ($referenceObject) ` + -DifferenceObject ($differenceObject) + } if ($null -ne $compareResult) { - Write-Verbose -Message "Configuration drift - key: $key Source{$sourceValue} Target{$targetValue}" + #Write-Verbose -Message "Configuration drift - simple object key: $key Source {$sourceValue} Target {$targetValue}" return $false } - } } } return $true } - function Convert-M365DSCDRGComplexTypeToHashtable { [CmdletBinding()] + [OutputType([hashtable],[hashtable[]])] param( [Parameter(Mandatory = 'true')] $ComplexObject ) - if ($ComplexObject.getType().Fullname -like '*[[\]]') + + if($ComplexObject.getType().Fullname -like "*[[\]]") { - $results = @() - foreach ($item in $ComplexObject) - { - $hash = Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $item - if (Test-M365DSCComplexObjectHasValues -ComplexObject $hash) - { - $results += $hash - } - } - if ($results.count -eq 0) + $results=@() + foreach($item in $ComplexObject) { - return $null + $hash=Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $item + $results+=$hash } - return $Results + + #Write-Verbose -Message ("Convert-M365DSCDRGComplexTypeToHashtable >>> results: "+(convertTo-JSON $results -Depth 20)) + # PowerShell returns all non-captured stream output, not just the argument of the return statement. + #An empty array is mangled into $null in the process. + #However, an array can be preserved on return by prepending it with the array construction operator (,) + return ,[hashtable[]]$results } $hashComplexObject = Get-M365DSCDRGComplexTypeToHashtable -ComplexObject $ComplexObject - if ($hashComplexObject) + + if($null -ne $hashComplexObject) { - $results = $hashComplexObject.clone() - $keys = $hashComplexObject.Keys | Where-Object -FilterScript { $_ -ne 'PSComputerName' } + + $results=$hashComplexObject.clone() + $keys=$hashComplexObject.Keys|Where-Object -FilterScript {$_ -ne 'PSComputerName'} foreach ($key in $keys) { - if (($null -ne $hashComplexObject[$key]) -and ($hashComplexObject[$key].getType().Fullname -like '*CimInstance*')) + if($hashComplexObject[$key] -and $hashComplexObject[$key].getType().Fullname -like "*CimInstance*") { - $results[$key] = Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $hashComplexObject[$key] + $results[$key]=Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $hashComplexObject[$key] } - if ($null -eq $results[$key]) + else { - $results.remove($key) | Out-Null + $propertyName = $key[0].ToString().ToLower() + $key.Substring(1, $key.Length - 1) + $propertyValue=$results[$key] + $results.remove($key)|out-null + $results.add($propertyName,$propertyValue) } - } } - return $results + return [hashtable]$results } Export-ModuleMember -Function *-TargetResource diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.schema.mof b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.schema.mof index 2759ea880e..908e8f85ac 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.schema.mof +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.schema.mof @@ -8,18 +8,9 @@ class MSFT_MicrosoftGraphMember class MSFT_MicrosoftGraphScopedRoleMembership { [Write, Description("Name of the Azure AD Role that is assigned")] String RoleName; - //[Write, Description("Member that is assigned the scoped role"), EmbeddedInstance("MSFT_MicrosoftGraphIdentity")] String RoleMemberInfo; - [Write, Description("Identity of member. For users, specify a UserPrincipalName. For groups and SPNs, specify DisplayName")] String Identity; + [Write, Description("Identity of member. For users, specify a UserPrincipalName. For groups and SPNs, specify the DisplayName")] String Identity; [Write, Description("Specify User, Group or ServicePrincipal to interpret the Identity")] String Type; }; -/* Extensions not incorporated in initial version -[ClassVersion("1.0.0")] -class MSFT_MicrosoftGraphOpenExtension -{ - [Write, Description("See https://docs.microsoft.com/en-us/graph/extensibility-overview#open-extensions")] String Id; - [Write, Description("Optional list of properties and their values"), EmbeddedInstance("MSFT_KeyValuePair")] String Properties[]; -}; -*/ [ClassVersion("1.0.0.0"), FriendlyName("AADAdministrativeUnit")] class MSFT_AADAdministrativeUnit : OMI_BaseResource @@ -28,12 +19,11 @@ class MSFT_AADAdministrativeUnit : OMI_BaseResource [Write, Description("Object-Id of the Administrative Unit")] String Id; [Write, Description("Description of the Administrative Unit")] String Description; [Write, Description("Visibility of the Administrative Unit. Specify HiddenMembership if members of the AU are hidden")] String Visibility; - [Write, Description("Specify membership type. Possible values are Assigned and Dynamic if the AU-preview has been activated. Otherwise do not use")] String MembershipType; - [Write, Description("Specify membership rule. Requires that MembershipType is set to Dynamic AND the AU-preview has been activated. Otherwise, do not use")] String MembershipRule; - [Write, Description("Specify dynamic membership-rule processing-state. Valid values are 'On' and 'Paused'. Requires that MembershipType is set to Dynamic AND the AU-preview has been activated. Otherwise, do not use")] String MembershipRuleProcessingState; - [Write, Description("Specify members. Only specify if MembershipType is set to Static"), EmbeddedInstance("MSFT_MicrosoftGraphMember")] String Members[]; - [Write, Description(""), EmbeddedInstance("MSFT_MicrosoftGraphScopedRoleMembership")] String ScopedRoleMembers[]; -// [Write, Description("Extensions. See https://docs.microsoft.com/en-us/graph/extensibility-overview#open-extensions"), EmbeddedInstance("MSFT_MicrosoftGraphOpenExtension")] String Extensions[]; + [Write, Description("Specify membership type. Possible values are Assigned and Dynamic. Note that the functionality is currently in preview.")] String MembershipType; + [Write, Description("Specify membership rule. Requires that MembershipType is set to Dynamic. Note that the functionality is currently in preview.")] String MembershipRule; + [Write, Description("Specify dynamic membership-rule processing-state. Valid values are 'On' and 'Paused'. Requires that MembershipType is set to Dynamic. Note that the functionality is currently in preview.")] String MembershipRuleProcessingState; + [Write, Description("Specify members. Only specify if MembershipType is NOT set to Dynamic"), EmbeddedInstance("MSFT_MicrosoftGraphMember")] String Members[]; + [Write, Description("Specify Scoped Role Membership."), EmbeddedInstance("MSFT_MicrosoftGraphScopedRoleMembership")] String ScopedRoleMembers[]; [Write, Description("Present ensures the Administrative Unit exists, absent ensures it is removed."), ValueMap{"Present","Absent"}, Values{"Present","Absent"}] string Ensure; [Write, Description("Credentials of the Intune Admin"), EmbeddedInstance("MSFT_Credential")] string Credential; [Write, Description("Id of the Azure Active Directory application to authenticate with.")] String ApplicationId; diff --git a/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 index b3b703953b..4482c6a7ad 100644 --- a/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 +++ b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 @@ -102,6 +102,12 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Mock -CommandName Get-MgDirectoryAdministrativeUnit -MockWith { return $null } + Mock -CommandName Get-MgDirectoryAdministrativeUnitMember -MockWith { + return $null + } + Mock -CommandName Get-MgDirectoryAdministrativeUnitScopedRoleMember -MockWith { + return $null + } } It 'Should return Values from the Get method' { (Get-TargetResource @testParams).Ensure | Should -Be 'Absent' @@ -132,7 +138,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } Mock -CommandName Get-MgDirectoryAdministrativeUnit -MockWith { - return [pscustomobject]@{ + return @{ Description = 'FakeStringValue2' DisplayName = 'FakeStringValue2' Id = 'FakeStringValue2' @@ -173,23 +179,11 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { ) ScopedRoleMembers = @( (New-CimInstance -ClassName MSFT_MicrosoftGraphScopedRoleMembership -Property @{ - RoleName = 'User Administrator' - #RoleMemberInfo = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ + RoleName = 'User Administrator' Identity = 'John.Doe@mytenant.com' Type = 'User' - # } -ClientOnly) } -ClientOnly) ) - <# - Extensions =@( - (New-CimInstance -ClassName MSFT_MicrosoftGraphExtension -Property @{ - Id = '0123456789' - Properties = (New-CimInstance -ClassName MSFT_KeyValuePair -Property @{ - SomeAttribute = "somevalue" - } -ClientOnly) - } -ClientOnly) - ) - #> Visibility = 'Public' MembershipType = 'Assigned' # MembershipRule and -ProcessingState params are only used when MembershipType is Dynamic @@ -202,18 +196,10 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { # Note: It is in fact possible to update the AU MembershipRule with any invalid value, but in the AAD-portal, updates are not possible unless the rule is valid. Mock -CommandName Get-MgDirectoryAdministrativeUnit -MockWith { - return [pscustomobject]@{ - Description = 'DSCAU' - DisplayName = 'DSCAU' - Id = 'DSCAU' - <# - Extensions =@( - [pscustomobject]@{ - Id = '0123456789' - SomeAttribute = "somevalue" - } - ) - #> + return @{ + Description = 'DSCAU' + DisplayName = 'DSCAU' + Id = 'DSCAU' Visibility = 'Public' AdditionalProperties = @{ membershipType = 'Assigned' @@ -224,7 +210,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } Mock -CommandName Invoke-MgGraphRequest -MockWith { - return [pscustomobject]@{ + return @{ '@odata.type' = '#microsoft.graph.user' DisplayName = 'John Doe' UserPrincipalName = 'John.Doe@mytenant.com' @@ -233,23 +219,23 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } Mock -CommandName Get-MgDirectoryAdministrativeUnitMember -MockWith { - return [pscustomobject] { + return @(@{ Id = '1234567890' - } + }) } Mock -CommandName Get-MgDirectoryAdministrativeUnitScopedRoleMember -MockWith { - return [pscustomobject]@{ + return @(@{ RoleId = '12345-67890' - RoleMemberINfo = [pscustomobject]@{ + RoleMemberInfo = @{ DisplayName = 'John Doe' Id = '1234567890' } - } + }) } Mock -CommandName Get-MgDirectoryRole -MockWith { - return [pscustomobject]@{ + return @{ Id = '12345-67890' DisplayName = 'User Administrator' } @@ -271,16 +257,6 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { $testParams = @{ Description = 'DSCAU2' DisplayName = 'DSCAU2' - <# - Extensions =@( - (New-CimInstance -ClassName MSFT_MicrosoftGraphextension -Property @{ - Id = '0123456789' - Properties = (New-CimInstance -ClassName MSFT_KeyValuePair -Property @{ - SomeAttribute = "somevalue" - } -ClientOnly) - } -ClientOnly) - ) - #> Id = 'DSCAU2' Members = @( (New-CimInstance -ClassName MSFT_MicrosoftGraphMember -Property @{ @@ -291,10 +267,8 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { ScopedRoleMembers = @( (New-CimInstance -ClassName MSFT_MicrosoftGraphScopedRoleMembership -Property @{ RoleName = 'User Administrator' - #RoleMemberInfo = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ Identity = 'John.Doe@mytenant.com' Type = 'User' - # } -ClientOnly) } -ClientOnly) ) Visibility = 'Public' @@ -309,14 +283,6 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { DisplayName = 'DSCAU2' Id = 'DSCAU2' Visibility = 'Public' - <# - Extensions =@( - [pscustomobject]@{ - Id = "FakeExtensionIdentity" - SomeAttribute = "SomeValue" - } - ) - #> } } @@ -335,7 +301,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Mock -CommandName Get-MgDirectoryAdministrativeUnitScopedRoleMember -MockWith { return [pscustomobject]@{ RoleId = '12345-67890' - RoleMemberInfo = [pscustomobject]@{ + RoleMemberInfo = @{ DisplayName = 'John Doe' Id = '1234567890' } @@ -379,16 +345,6 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Description = 'DSCAU' DisplayName = 'DSCAU' Id = 'DSCAU' - <# - Extensions =@( - (New-CimInstance -ClassName MSFT_MicrosoftGraphextension -Property @{ - Id = '0123456789' - Properties = (New-CimInstance -ClassName MSFT_KeyValuePair -Property @{ - SomeAttribute = "somevalue" - } -ClientOnly) - } -ClientOnly) - ) - #> Members = @( (New-CimInstance -ClassName MSFT_MicrosoftGraphMember -Property @{ Identity = 'John.Doe@mytenant.com' @@ -398,10 +354,8 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { ScopedRoleMembers = @( (New-CimInstance -ClassName MSFT_MicrosoftGraphScopedRoleMembership -Property @{ RoleName = 'User Administrator' - #RoleMemberInfo = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ Identity = 'John.Doe@mytenant.com' Type = 'User' - # } -ClientOnly) } -ClientOnly) ) Visibility = 'Public' @@ -415,14 +369,6 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { DisplayName = 'DSCAU' Id = 'DSCAU' Visibility = 'Public' - <# - Extensions =@( - [pscustomobject]@{ - Id = "FakeExtensionId" - SomeAttribute = "SomeValue" - } - ) - #> } } @@ -441,6 +387,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } Mock -CommandName Get-MgDirectoryAdministrativeUnitScopedRoleMember -MockWith { + return $null } Mock -CommandName Invoke-MgGraphRequest -MockWith { @@ -481,19 +428,9 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } Mock -CommandName Get-MgDirectoryAdministrativeUnit -MockWith { - return [pscustomobject]@{ + return @{ Description = 'ExportDSCAU' DisplayName = 'ExportDSCAU' - <# - Extensions =@( - (New-CimInstance -ClassName MSFT_MicrosoftGraphextension -Property @{ - CIMType = "MSFT_MicrosoftGraphextension" - Name = "Extensions" - isArray = $True - - } -ClientOnly) - ) - #> Id = 'ExportDSCAU' Visibility = 'Public' } @@ -514,16 +451,23 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } Mock -CommandName Get-MgDirectoryAdministrativeUnitScopedRoleMember -MockWith { - return [pscustomobject]@{ + return @([pscustomobject]@{ RoleId = '12345-67890' - RoleMemberInfo = [pscustomobject]@{ + RoleMemberInfo = @{ DisplayName = 'John Doe' Id = '1234567890' } - } + }, + [pscustomobject]@{ + RoleId = '09876-54321' + RoleMemberInfo = @{ + DisplayName = 'Group' + Id = '0987654321' + } + }) } - Mock -CommandName Invoke-MgGraphRequest -MockWith { + Mock -CommandName Invoke-MgGraphRequest -ParameterFilter {$Uri -match '1234567890$'} -MockWith { return [pscustomobject]@{ '@odata.type' = '#microsoft.graph.user' DisplayName = 'John Doe' @@ -532,15 +476,30 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } } - Mock -CommandName Get-MgDirectoryRole -MockWith { + Mock -CommandName Invoke-MgGraphRequest -ParameterFilter {$Uri -match '0987654321$'} -MockWith { + return [pscustomobject]@{ + '@odata.type' = '#microsoft.graph.group' + DisplayName = 'FakeRoleGroup' + Id = '0987654321' + } + } + + Mock -CommandName Get-MgDirectoryRole -ParameterFilter {$Id -eq '12345-67890'} -MockWith { return [pscustomobject]@{ Id = '12345-67890' - DisplayName = 'User Administrator' + DisplayName = 'DSC User Administrator' + } + } + + Mock -CommandName Get-MgDirectoryRole -ParameterFilter {$Id -eq '09876-54321'} -MockWith { + return [pscustomobject]@{ + Id = '09876-54321' + DisplayName = 'DSC Groups Administrator' } } } It 'Should Reverse Engineer resource from the Export method' { - $result = Export-TargetResource @testParams + $result = Export-TargetResource @testParams -verbose $result | Should -Not -BeNullOrEmpty } } From 8896b0b7744270e05e7f42b13bf194faa0ce9594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Thu, 19 Jan 2023 14:46:42 +0100 Subject: [PATCH 20/29] No test of MembershipType if Dynamic isn't present --- .../MSFT_AADAdministrativeUnit.psm1 | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 index 6622563012..62fbeb0f6f 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 @@ -867,6 +867,12 @@ function Test-TargetResource # Visibility is currently not returned by Get-TargetResource $ValuesToCheck.Remove('Visibility') | Out-Null + if ($MembershipType -ne 'Dynamic' -and $CurrentValues.MembershipType -ne 'Dynamic') + { + # MembershipType may be returned as null or Assigned with same effect. Only compare if Dynamic is specified or returned + $ValuesToCheck.Remove('MembershipType') | Out-Null + } + Write-Verbose -Message "Current Values: $(Convert-M365DscHashtableToString -Hashtable $CurrentValues)" Write-Verbose -Message "Target Values: $(Convert-M365DscHashtableToString -Hashtable $ValuesToCheck)" From 9882f5ffeca02280adb9275120498f89666b28be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Fri, 20 Jan 2023 08:35:09 +0100 Subject: [PATCH 21/29] added example using Members and SCopedRoleMembers --- .../azure-ad/AADAdministrativeUnit.md | 50 +++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/docs/docs/resources/azure-ad/AADAdministrativeUnit.md b/docs/docs/resources/azure-ad/AADAdministrativeUnit.md index 7887d6cc80..3520d16b03 100644 --- a/docs/docs/resources/azure-ad/AADAdministrativeUnit.md +++ b/docs/docs/resources/azure-ad/AADAdministrativeUnit.md @@ -21,13 +21,13 @@ | **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. | | -### MSFT_MicrosoftGraphIdentity +### MSFT_MicrosoftGraphMember #### Parameters | Parameter | Attribute | DataType | Description | Allowed Values | | --- | --- | --- | --- | --- | -| **Identity** | Write | String | Identity of direcory-object. For users, specify a UserPrincipalName. For Groups and SPNs, specify DisplayName | | +| **Identity** | Write | String | Identity of directory-object. For users, specify a UserPrincipalName. For Groups and SPNs, specify DisplayName | | | **Type** | Write | String | Specify User, Group or ServicePrincipal to interpret the Identity | | ### MSFT_MicrosoftGraphScopedRoleMembership @@ -37,7 +37,8 @@ | Parameter | Attribute | DataType | Description | Allowed Values | | --- | --- | --- | --- | --- | | **RoleName** | Write | String | Name of the Azure AD Role that is assigned | | -| **RoleMemberInfo** | Write | MSFT_MicrosoftGraphIdentity | Member that is assigned the scoped role | | +| **Identity** | Write | String | Identity of directory-object to be assigned the role. For users, specify a UserPrincipalName. For Groups and SPNs, specify DisplayName | | +| **Type** | Write | String | Specify User, Group or ServicePrincipal to interpret the Identity | | ## Description @@ -103,4 +104,47 @@ Configuration Example } } ``` +### Example 2 + +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(Mandatory = $true)] + [PSCredential] + $credsCredential + ) + Import-DscResource -ModuleName Microsoft365DSC + + node localhost + { + AADAdministrativeUnit 'TestUnit' + { + Credential = $credsCredential; + DisplayName = "Test-Unit"; + Ensure = "Present"; + Members = @( + MSFT_MicrosoftGraphMember{ + Identity = "jane.doe@mytenant.com" + Type = "User" + } + MSFT_MicrosoftGraphMember{ + Identity = "Sales" + Type = "Group" + } + ) + ScopedRoleMembers = @( + MSFT_MicrosoftGraphScopedRoleMembership{ + RoleName = "User Administrator" + Type = "User" + Identity = "john.doe@mytenant.com" + } + ) + } + } +} +``` From 90eb3a52a85f1803e09bd5bacf8cfdf8775decba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Wed, 25 Jan 2023 13:49:32 +0100 Subject: [PATCH 22/29] Updated unit-test --- .../Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 index 4482c6a7ad..1035c71aff 100644 --- a/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 +++ b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 @@ -499,7 +499,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } } It 'Should Reverse Engineer resource from the Export method' { - $result = Export-TargetResource @testParams -verbose + $result = Export-TargetResource @testParams $result | Should -Not -BeNullOrEmpty } } From b3282354f9812d09527c6477f1246cfaa41408f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Thu, 26 Jan 2023 09:59:30 +0100 Subject: [PATCH 23/29] Refactored so it's not a BR --- .../MSFT_AADAdministrativeUnit.psm1 | 60 +++++++++++-------- .../MSFT_AADAdministrativeUnit.schema.mof | 9 +-- ...soft365DSC.AADAdministrativeUnit.Tests.ps1 | 40 ++++++++----- 3 files changed, 65 insertions(+), 44 deletions(-) diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 index 62fbeb0f6f..02dd5e06a5 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 @@ -197,10 +197,13 @@ function Get-TargetResource { write-verbose "AU {$DisplayName} verify RoleId {$($auScopedRoleMember.RoleId)}" $roleObject = Get-MgDirectoryRole -DirectoryRoleId $auScopedRoleMember.RoleId -ErrorAction Stop + write-verbose "Found DirectoryRole '$($roleObject.DisplayName)' with id $($roleObject.Id)" $scopedRoleMember = @{ RoleName = $roleObject.DisplayName - Type = $null - Identity = $null + RoleMemberInfo = @{ + Type = $null + Identity = $null + } } write-verbose "AU {$DisplayName} verify RoleMemberInfo.Id {$($auScopedRoleMember.RoleMemberInfo.Id)}" $memberObject = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/directoryobjects/$($auScopedRoleMember.RoleMemberInfo.Id)" @@ -208,22 +211,22 @@ function Get-TargetResource if (($memberObject.'@odata.type') -match 'user') { write-verbose "AU {$DisplayName} UPN = {$($memberObject.UserPrincipalName)}" - $scopedRoleMember.Identity = $memberObject.UserPrincipalName - $scopedRoleMember.Type = 'User' + $scopedRoleMember.RoleMemberInfo.Identity = $memberObject.UserPrincipalName + $scopedRoleMember.RoleMemberInfo.Type = 'User' } elseif (($memberObject.'@odata.type') -match 'group') { write-verbose "AU {$DisplayName} Group = {$($memberObject.DisplayName)}" - $scopedRoleMember.Identity = $memberObject.DisplayName - $scopedRoleMember.Type = 'Group' + $scopedRoleMember.RoleMemberInfo.Identity = $memberObject.DisplayName + $scopedRoleMember.RoleMemberInfo.Type = 'Group' } else { write-verbose "AU {$DisplayName} SPN = {$($memberObject.DisplayName)}" - $scopedRoleMember.Identity = $memberObject.DisplayName - $scopedRoleMember.Type = 'ServicePrincipal' + $scopedRoleMember.RoleMemberInfo.Identity = $memberObject.DisplayName + $scopedRoleMember.RoleMemberInfo.Type = 'ServicePrincipal' } - write-verbose "AU {$DisplayName} scoped role member: RoleName '$($scopedRoleMember.RoleName)' Type '$($scopedRoleMember.Type)' Identity '$($scopedRoleMember.Identity)'" + write-verbose "AU {$DisplayName} scoped role member: RoleName '$($scopedRoleMember.RoleName)' Type '$($scopedRoleMember.RoleMemberInfo.Type)' Identity '$($scopedRoleMember.RoleMemberInfo.Identity)'" $scopedRoleMemberSpec += $scopedRoleMember } write-verbose "AU {$DisplayName} add $($scopedRoleMemberSpec.Count) ScopedRoleMembers to results" @@ -450,37 +453,37 @@ function Set-TargetResource { throw "AU {$($DisplayName)}: RoleName {$($roleMember.RoleName)} does not exist" } - if ($roleMember.Type -eq 'User') + if ($roleMember.RoleMemberInfo.Type -eq 'User') { - $roleMemberIdentity = Get-MgUser -Filter "UserPrincipalName eq '$($roleMember.Identity)'" -ErrorAction Stop + $roleMemberIdentity = Get-MgUser -Filter "UserPrincipalName eq '$($roleMember.RoleMemberInfo.Identity)'" -ErrorAction Stop if ($null -eq $roleMemberIdentity) { - throw "AU {$($DisplayName)}: Scoped Role User {$($roleMember.Identity)} for role {$($roleMember.RoleName)} does not exist" + throw "AU {$($DisplayName)}: Scoped Role User {$($roleMember.RoleMemberInfo.Identity)} for role {$($roleMember.RoleName)} does not exist" } } elseif ($roleMember.Type -eq 'Group') { - $roleMemberIdentity = Get-MgGroup -Filter "displayName eq '$($roleMember.Identity)'" -ErrorAction Stop + $roleMemberIdentity = Get-MgGroup -Filter "displayName eq '$($roleMember.RoleMemberInfo.Identity)'" -ErrorAction Stop if ($null -eq $roleMemberIdentity) { - throw "AU {$($DisplayName)}: Scoped Role Group {$($roleMember.Identity)} for role {$($roleMember.RoleName)} does not exist" + throw "AU {$($DisplayName)}: Scoped Role Group {$($roleMember.RoleMemberInfo.Identity)} for role {$($roleMember.RoleName)} does not exist" } } elseif ($roleMember.Type -eq 'ServicePrincipal') { - $roleMemberIdentity = Get-MgServicePrincipal -Filter "displayName eq '$($roleMember.Identity)'" -ErrorAction Stop + $roleMemberIdentity = Get-MgServicePrincipal -Filter "displayName eq '$($roleMember.RoleMemberInfo.Identity)'" -ErrorAction Stop if ($null -eq $roleMemberIdentity) { - throw "AU {$($DisplayName)}: Scoped Role ServicePrincipal {$($roleMember.Identity)} for role {$($roleMember.RoleName)} does not exist" + throw "AU {$($DisplayName)}: Scoped Role ServicePrincipal {$($roleMember.RoleMemberInfo.Identity)} for role {$($roleMember.RoleName)} does not exist" } } else { - throw "AU {$($DisplayName)}: Invalid ScopedRoleMember.Type {$($roleMember.Type)}" + throw "AU {$($DisplayName)}: Invalid ScopedRoleMember.RoleMemberInfo.Type {$($roleMember.RoleMemberInfo.Type)}" } if ($roleMemberIdentity.Count -gt 1) { - throw "AU {$($DisplayName)}: ScopedRoleMember for role {$($roleMember.RoleName)}: $($roleMember.Type) {$($roleMember.Identity)} is not unique" + throw "AU {$($DisplayName)}: ScopedRoleMember for role {$($roleMember.RoleName)}: $($roleMember.RoleMemberInfo.Type) {$($roleMember.RoleMemberInfo.Identity)} is not unique" } $scopedRoleMemberSpecification += @{ RoleId = $roleObject.Id @@ -628,13 +631,14 @@ function Set-TargetResource $desiredScopedRoleMembersValue = @() } + # flatten hashtabls for compare $compareCurrentScopedRoleMembersValue = @() foreach ($roleMember in $currentScopedRoleMembersValue) { $compareCurrentScopedRoleMembersValue += [pscustomobject]@{ RoleName = $roleMember.RoleName - Identity = $roleMember.Identity - Type = $roleMember.Type + Identity = $roleMember.RoleMemberInfo.Identity + Type = $roleMember.RoleMemberInfo.Type } } $compareDesiredScopedRoleMembersValue = @() @@ -642,8 +646,8 @@ function Set-TargetResource { $compareDesiredScopedRoleMembersValue += [pscustomobject]@{ RoleName = $roleMember.RoleName - Identity = $roleMember.Identity - Type = $roleMember.Type + Identity = $roleMember.RoleMemberInfo.Identity + Type = $roleMember.RoleMemberInfo.Type } } write-verbose "AU {$DisplayName} Update ScopedRoleMembers: Current members: $($compareCurrentScopedRoleMembersValue.Identity -join ', ')" @@ -832,7 +836,6 @@ function Test-TargetResource } $testResult = $true - <# #Compare Cim instances foreach ($key in $PSBoundParameters.Keys) { @@ -857,7 +860,6 @@ function Test-TargetResource } } - #> $ValuesToCheck.Remove('Credential') | Out-Null $ValuesToCheck.Remove('ApplicationId') | Out-Null @@ -977,8 +979,14 @@ function Export-TargetResource if ($null -ne $Results.ScopedRoleMembers) { + $complexMapping = @( + @{ + Name = 'RoleMemberInfo' + CimInstanceName = 'MicrosoftGraphIdentity' + } + ) $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject ([Array]$Results.ScopedRoleMembers) ` - -CIMInstanceName MicrosoftGraphScopedRoleMembership + -CIMInstanceName MicrosoftGraphScopedRoleMembership -ComplexTypeMapping $complexMapping write-verbose "ScopedRoleMembers on next line:`r`n$complexTypeStringResult" @@ -992,7 +1000,7 @@ function Export-TargetResource if ($null -ne $Results.Members) { $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject ([Array]$Results.Members) ` - -CIMInstanceName MicrosoftGraphMember + -CIMInstanceName MicrosoftGraphIdentity write-verbose "Members on next line:`r`n$complexTypeStringResult" $Results.Members = $complexTypeStringResult diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.schema.mof b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.schema.mof index 908e8f85ac..d6581911c1 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.schema.mof +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.schema.mof @@ -1,15 +1,16 @@ [ClassVersion("1.0.0")] -class MSFT_MicrosoftGraphMember +class MSFT_MicrosoftGraphIdentity { [Write, Description("Identity of member. For users, specify a UserPrincipalName. For groups and devices, specify DisplayName")] String Identity; - [Write, Description("Specify User, Group or Device to interpret the Identity")] String Type; + [Write, Description("Specify User, Group or Device to interpret the identity. Can be Principal in ScopedRoleMembers")] String Type; }; [ClassVersion("1.0.0")] class MSFT_MicrosoftGraphScopedRoleMembership { [Write, Description("Name of the Azure AD Role that is assigned")] String RoleName; - [Write, Description("Identity of member. For users, specify a UserPrincipalName. For groups and SPNs, specify the DisplayName")] String Identity; - [Write, Description("Specify User, Group or ServicePrincipal to interpret the Identity")] String Type; + [Write, Description("Member that is assigned the scoped role"), EmbeddedInstance("MSFT_MicrosoftGraphIdentity")] String RoleMemberInfo; + // [Write, Description("Identity of member. For users, specify a UserPrincipalName. For groups and SPNs, specify the DisplayName")] String Identity; + // [Write, Description("Specify User, Group or ServicePrincipal to interpret the Identity")] String Type; }; [ClassVersion("1.0.0.0"), FriendlyName("AADAdministrativeUnit")] diff --git a/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 index 1035c71aff..267aad43da 100644 --- a/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 +++ b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 @@ -83,7 +83,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { DisplayName = 'FakeStringValue1' Id = 'FakeStringValue1' Members = @( - (New-CimInstance -ClassName MSFT_MicrosoftGraphMember -Property @{ + (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ Type = 'User' Identity = 'john.smith@contoso.com' } -ClientOnly) @@ -128,7 +128,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { DisplayName = 'FakeStringValue2' Id = 'FakeStringValue2' Members = @( - (New-CimInstance -ClassName MSFT_MicrosoftGraphMember -Property @{ + (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ Type = 'User' Identity = 'john.smith@contoso.com' } -ClientOnly) @@ -172,7 +172,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { DisplayName = 'DSCAU' Id = 'DSCAU' Members = @( - (New-CimInstance -ClassName MSFT_MicrosoftGraphMember -Property @{ + (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ Identity = 'John.Doe@mytenant.com' Type = 'User' } -ClientOnly) @@ -180,8 +180,12 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { ScopedRoleMembers = @( (New-CimInstance -ClassName MSFT_MicrosoftGraphScopedRoleMembership -Property @{ RoleName = 'User Administrator' - Identity = 'John.Doe@mytenant.com' - Type = 'User' + RoleMemberInfo = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ + Identity = 'John.Doe@mytenant.com' + Type = 'User' + } -ClientOnly) + #Identity = 'John.Doe@mytenant.com' + #Type = 'User' } -ClientOnly) ) Visibility = 'Public' @@ -259,7 +263,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { DisplayName = 'DSCAU2' Id = 'DSCAU2' Members = @( - (New-CimInstance -ClassName MSFT_MicrosoftGraphMember -Property @{ + (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ Identity = 'John.Doe@mytenant.com' Type = 'User' } -ClientOnly) @@ -267,8 +271,12 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { ScopedRoleMembers = @( (New-CimInstance -ClassName MSFT_MicrosoftGraphScopedRoleMembership -Property @{ RoleName = 'User Administrator' - Identity = 'John.Doe@mytenant.com' - Type = 'User' + RoleMemberInfo = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ + Identity = 'John.Doe@mytenant.com' + Type = 'User' + } -ClientOnly) + #Identity = 'John.Doe@mytenant.com' + #Type = 'User' } -ClientOnly) ) Visibility = 'Public' @@ -346,7 +354,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { DisplayName = 'DSCAU' Id = 'DSCAU' Members = @( - (New-CimInstance -ClassName MSFT_MicrosoftGraphMember -Property @{ + (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ Identity = 'John.Doe@mytenant.com' Type = 'User' } -ClientOnly) @@ -354,8 +362,12 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { ScopedRoleMembers = @( (New-CimInstance -ClassName MSFT_MicrosoftGraphScopedRoleMembership -Property @{ RoleName = 'User Administrator' - Identity = 'John.Doe@mytenant.com' - Type = 'User' + RoleMemberInfo = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ + Identity = 'John.Doe@mytenant.com' + Type = 'User' + } -ClientOnly) + #Identity = 'John.Doe@mytenant.com' + #Type = 'User' } -ClientOnly) ) Visibility = 'Public' @@ -461,7 +473,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { [pscustomobject]@{ RoleId = '09876-54321' RoleMemberInfo = @{ - DisplayName = 'Group' + DisplayName = 'FakeRoleGroup' Id = '0987654321' } }) @@ -484,14 +496,14 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } } - Mock -CommandName Get-MgDirectoryRole -ParameterFilter {$Id -eq '12345-67890'} -MockWith { + Mock -CommandName Get-MgDirectoryRole -ParameterFilter {$DirectoryRoleId -eq '12345-67890'} -MockWith { return [pscustomobject]@{ Id = '12345-67890' DisplayName = 'DSC User Administrator' } } - Mock -CommandName Get-MgDirectoryRole -ParameterFilter {$Id -eq '09876-54321'} -MockWith { + Mock -CommandName Get-MgDirectoryRole -ParameterFilter {$DirectoryRoleId -eq '09876-54321'} -MockWith { return [pscustomobject]@{ Id = '09876-54321' DisplayName = 'DSC Groups Administrator' From 249ade5b1caba9bf3250d57a50a78c3441982c96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Thu, 26 Jan 2023 15:00:09 +0100 Subject: [PATCH 24/29] fix order of props for export of ScopedRoleMembers --- .../MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 index 02dd5e06a5..6757cee4ea 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 @@ -198,7 +198,7 @@ function Get-TargetResource write-verbose "AU {$DisplayName} verify RoleId {$($auScopedRoleMember.RoleId)}" $roleObject = Get-MgDirectoryRole -DirectoryRoleId $auScopedRoleMember.RoleId -ErrorAction Stop write-verbose "Found DirectoryRole '$($roleObject.DisplayName)' with id $($roleObject.Id)" - $scopedRoleMember = @{ + $scopedRoleMember = [ordered]@{ RoleName = $roleObject.DisplayName RoleMemberInfo = @{ Type = $null From 5b725143686fdbd190c368ef18f15e1b4b7c1c3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Thu, 16 Feb 2023 11:25:27 +0100 Subject: [PATCH 25/29] AADAdministrativeUNit: added PR to changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7379c7b233..87fff4d49e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change log for Microsoft365DSC +# Unreleased + +* AADAdministrativeUnit + * Fixed general issues caused by improper handling of nested CIMInstances + Fixes #2775, #2776, #2786 + # 1.23.301.1 * IntuneDeviceEnrollmentConfigurationWindows10 From ef7a51b302b8d77661a8a3a7434fcc047b75b244 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Thu, 23 Feb 2023 08:30:00 +0100 Subject: [PATCH 26/29] Remove Ensure default value --- .../MSFT_AADAdministrativeUnit.psm1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 index 6757cee4ea..6f9b08d55e 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 @@ -49,7 +49,7 @@ function Get-TargetResource [Parameter(Mandatory = $true)] [System.String] [ValidateSet('Absent', 'Present')] - $Ensure = $true, + $Ensure, [Parameter()] [System.Management.Automation.PSCredential] @@ -296,7 +296,7 @@ function Set-TargetResource [Parameter(Mandatory = $true)] [System.String] [ValidateSet('Absent', 'Present')] - $Ensure = $true, + $Ensure, [Parameter()] [System.Management.Automation.PSCredential] From 381aa6e75fe8c5533268750cdb7689a80dfd0ecc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Wed, 1 Mar 2023 11:00:28 +0100 Subject: [PATCH 27/29] AADAdministrativeUnit: Fixed Ensure default value --- .../MSFT_AADAdministrativeUnit.psm1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 index 6f9b08d55e..02b33d58c4 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 @@ -49,7 +49,7 @@ function Get-TargetResource [Parameter(Mandatory = $true)] [System.String] [ValidateSet('Absent', 'Present')] - $Ensure, + $Ensure = 'Present', [Parameter()] [System.Management.Automation.PSCredential] @@ -296,7 +296,7 @@ function Set-TargetResource [Parameter(Mandatory = $true)] [System.String] [ValidateSet('Absent', 'Present')] - $Ensure, + $Ensure = 'Present', [Parameter()] [System.Management.Automation.PSCredential] @@ -785,7 +785,7 @@ function Test-TargetResource [Parameter(Mandatory = $true)] [System.String] [ValidateSet('Absent', 'Present')] - $Ensure = $true, + $Ensure = 'Present', [Parameter()] [System.Management.Automation.PSCredential] From f7309e31b0e7cf632963d6f24a9d5e91862afa68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Thu, 2 Mar 2023 08:15:30 +0100 Subject: [PATCH 28/29] fixed failing test after rebase --- .../Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 index 78cce1c94a..267aad43da 100644 --- a/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 +++ b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 @@ -43,7 +43,6 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Mock -CommandName Invoke-MgGraphRequest -MockWith { } - Mock -CommandName Update-MgDirectoryAdministrativeUnit -MockWith { Mock -CommandName Update-MgDirectoryAdministrativeUnit -MockWith { } From 53382b421070cc41b7c028cddc505dadcf667f0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Albeck?= Date: Fri, 3 Mar 2023 09:04:29 +0100 Subject: [PATCH 29/29] fix mock-error in unit tests --- .../MSFT_AADAdministrativeUnit.psm1 | 6 ++---- .../Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 index 02b33d58c4..8d9b13c4fd 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 @@ -988,8 +988,6 @@ function Export-TargetResource $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject ([Array]$Results.ScopedRoleMembers) ` -CIMInstanceName MicrosoftGraphScopedRoleMembership -ComplexTypeMapping $complexMapping - write-verbose "ScopedRoleMembers on next line:`r`n$complexTypeStringResult" - $Results.ScopedRoleMembers = $complexTypeStringResult if ([String]::IsNullOrEmpty($complexTypeStringResult)) @@ -1001,7 +999,6 @@ function Export-TargetResource { $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject ([Array]$Results.Members) ` -CIMInstanceName MicrosoftGraphIdentity - write-verbose "Members on next line:`r`n$complexTypeStringResult" $Results.Members = $complexTypeStringResult if ([String]::IsNullOrEmpty($complexTypeStringResult)) @@ -1027,7 +1024,6 @@ function Export-TargetResource { $currentDSCBlock = Convert-DSCStringParamToVariable -DSCBlock $currentDSCBlock -ParameterName "Members" -isCIMArray $true } - write-verbose "currentDSCBlock on next line:`r`n$($currentDSCBlock -join "`r`n")" $dscContent += $currentDSCBlock Save-M365DSCPartialExport -Content $currentDSCBlock ` -FileName $Global:PartialExportFileName @@ -1038,6 +1034,8 @@ function Export-TargetResource } catch { + write-verbose "Exception: $($_.Exception.Message)" + Write-Host $Global:M365DSCEmojiRedX New-M365DSCLogEntry -Message 'Error during Export:' ` diff --git a/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 index 267aad43da..3700d9ed30 100644 --- a/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 +++ b/Tests/Unit/Microsoft365DSC/Microsoft365DSC.AADAdministrativeUnit.Tests.ps1 @@ -68,7 +68,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } Mock -CommandName New-M365DSCConnection -MockWith { # Select-MgProfile beta # not anymore - return 'Credential' + return 'Credentials' } # Mock Write-Host to hide output during the tests @@ -511,7 +511,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } } It 'Should Reverse Engineer resource from the Export method' { - $result = Export-TargetResource @testParams + $result = Export-TargetResource @testParams -Verbose $result | Should -Not -BeNullOrEmpty } }