diff --git a/CHANGELOG.md b/CHANGELOG.md index f7a9466c11..7fad95c333 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 * EXOManagementRoleAssignment * Added delays before disconnecting from EXO to ensure new permissions are applied. FIXES [#2523](https://github.com/microsoft/Microsoft365DSC/issues/2523) @@ -48,6 +51,11 @@ # 1.23.222.1 +* TeamsOnlineVoiceUser + * Fix issue where the cmdlet Get-CsOnlineVoiceUser is now deprecated. + +# 1.23.222.1 + * AADEntitlementManagementAccessPackageAssignmentPolicy * Initial release * IntuneDeviceEnrollmentConfigurationWindows10 diff --git a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.psm1 index e424a8b8b1..8d9b13c4fd 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,13 +44,6 @@ function Get-TargetResource [Microsoft.Management.Infrastructure.CimInstance[]] $ScopedRoleMembers, - <# - [Parameter()] - [Microsoft.Management.Infrastructure.CimInstance[]] - $Extensions, - #> - - #endregion [Parameter(Mandatory = $true)] @@ -79,190 +76,164 @@ 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' - } - catch - { - Write-Verbose -Message ($_) - } - - #Ensure the proper dependencies are installed in the current environment. - Confirm-M365DSCDependencies + -ProfileName 'v1.0' + + #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' - $nullResult = $PSBoundParameters - $nullResult.Ensure = 'Absent' - try - { $getValue = $null - #region resource generator code - if (-Not [string]::IsNullOrEmpty($Id)) + if (-not [string]::IsNullOrEmpty($Id)) { - $getValue = Get-MgAdministrativeUnit -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-MgAdministrativeUnit -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) - { - # 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 ($auMember in $auMembers) { - 'group' + $member = @{} + $memberObject = Invoke-MgGraphRequest -Uri "https://graph.microsoft.com/v1.0/directoryobjects/$($auMember.Id)" + if ($memberObject.'@odata.type' -match 'user') { - $memberSpec += @{ - Identity = $memberObject.DisplayName - Type = 'Group' - } + $member.Add('Identity', $memberObject.UserPrincipalName) + $member.Add('Type', 'User') } - 'user' + elseif ($memberObject.'@odata.type' -match 'group') { - $memberSpec += @{ - Identity = $memberObject.UserPrincipalName - Type = 'User' - } + $member.Add('Identity', $memberObject.DisplayName) + $member.Add('Type', 'Group') } - 'device' + else { - $memberSpec += @{ - Identity = $memberObject.DisplayName - Type = 'Device' - } + $member.Add('Identity', $memberObject.DisplayName) + $member.Add('Type', 'Device') } + write-verbose "AU {$DisplayName} member found: Type '$($member.Type)' identity '$($member.Identity)'" + $memberSpec += $member } + write-verbose "AU {$DisplayName} add Members to results" + $results.Add('Members', $memberSpec) } - $results.Add('Members', $memberSpec) } - $scopedRoleMemberSpec = $null - $auScopedRoleMembers = Get-MgDirectoryAdministrativeUnitScopedRoleMember -AdministrativeUnitId $getValue.Id -All - if ($auScopedRoleMembers) + write-verbose "AU {$DisplayName} get Scoped Role Members" + $ErrorActionPreference = "Stop" + [array]$auScopedRoleMembers = Get-MgDirectoryAdministrativeUnitScopedRoleMember -AdministrativeUnitId $getValue.Id -All + if ($auScopedRoleMembers.Count -gt 0) { + write-verbose "AU {$DisplayName} process $($auScopedRoleMembers.Count) scoped role members" $scopedRoleMemberSpec = @() - foreach ($getMember in $auScopedRoleMembers) + foreach ($auScopedRoleMember 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 + 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 = [ordered]@{ + RoleName = $roleObject.DisplayName + 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)" + write-verbose "AU {$DisplayName} @odata.Type={$($memberObject.'@odata.type')}" + if (($memberObject.'@odata.type') -match 'user') { - if ([regex]::Escape($roleMemberObject.'@odata.type') -match 'group') - { - $memberType = 'Group' - } - else - { - $memberType = 'ServicePrincipal' - } - $memberIdentity = $roleMemberObject.DisplayName - } - $scopedRoleMemberInfo = @{ - RoleName = $roleObject.DisplayName - RoleMemberInfo = @{ - Identity = $memberIdentity - Type = $memberType - } + write-verbose "AU {$DisplayName} UPN = {$($memberObject.UserPrincipalName)}" + $scopedRoleMember.RoleMemberInfo.Identity = $memberObject.UserPrincipalName + $scopedRoleMember.RoleMemberInfo.Type = 'User' } - $scopedRoleMemberSpec += $scopedRoleMemberInfo - } - } - $results.Add('ScopedRoleMembers', $scopedRoleMemberSpec) - - <# - # Extensions are still too unwieldy - $auExtensions = Get-MgDirectoryAdministrativeUnitExtension -AdministrativeUnitId $getValue.Id -All - $extensionsSpec = $null - if ($auExtensions) - { - $extensionsSpec = @() - $extensionDef = @{Id = $auExtensions.Id} - foreach ($auExtension in $auExtensions) - { - if ($auExtension.Properties -and $auExtension.Properties.Count % 2 -ne 0) + elseif (($memberObject.'@odata.type') -match 'group') { - throw "AU {$($getValue.DisplayName)] has extension {$($auExtension.Id)} with properties without values" + write-verbose "AU {$DisplayName} Group = {$($memberObject.DisplayName)}" + $scopedRoleMember.RoleMemberInfo.Identity = $memberObject.DisplayName + $scopedRoleMember.RoleMemberInfo.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.RoleMemberInfo.Identity = $memberObject.DisplayName + $scopedRoleMember.RoleMemberInfo.Type = 'ServicePrincipal' } + write-verbose "AU {$DisplayName} scoped role member: RoleName '$($scopedRoleMember.RoleName)' Type '$($scopedRoleMember.RoleMemberInfo.Type)' Identity '$($scopedRoleMember.RoleMemberInfo.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 { @@ -281,33 +252,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[]] @@ -317,15 +292,7 @@ function Set-TargetResource [Microsoft.Management.Infrastructure.CimInstance[]] $ScopedRoleMembers, - <# - [Parameter()] - [Microsoft.Management.Infrastructure.CimInstance[]] - $Extensions, - #> - - #endregion - [Parameter(Mandatory = $true)] [System.String] [ValidateSet('Absent', 'Present')] @@ -356,22 +323,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 - - try - { - $ConnectionMode = New-M365DSCConnection -Workload 'MicrosoftGraph' ` - -InboundParameters $PSBoundParameters ` - -ProfileName 'beta' - } - catch - { - Write-Verbose -Message $_ - } - #Ensure the proper dependencies are installed in the current environment. Confirm-M365DSCDependencies @@ -386,94 +337,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() - - 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 - 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 } } - - # Resolve Members Type/Identity to user or group id - if ($currentParameters.Members) + $memberSpecification = $null + if ($CreateParameters.MembershipType -ne 'Dynamic' -and $CreateParameters.Members.Count -gt 0) { $memberSpecification = @() - foreach ($Member in $Members) + 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 += @{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 += @{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 += @{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 { @@ -485,51 +424,66 @@ function Set-TargetResource throw "AU {$($DisplayName)}: Member {$($Member.Identity)} has invalid type {$($Member.Type)}" } } - $CreateParameters.Members = $memberSpecification - } - else - { - $CreateParameters.Remove('Members') | Out-Null + # Members are added to the AU *after* it has been created } + $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} process $($CreateParameters.ScopedRoleMembers.Count) ScopedRoleMembers" $scopedRoleMemberSpecification = @() - foreach ($roleMember in $ScopedRoleMembers) + foreach ($roleMember in $CreateParameters.ScopedRoleMembers) { - $roleObject = Get-MgDirectoryRole -Filter "DisplayName eq '$($roleMember.RoleName)'" -ErrorAction stop + 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) + { + write-verbose -Message "Enable Azure AD role {$($rolemember.RoleName)} with id {$($roleTemplate.Id)}" + $roleObject = New-MgDirectoryRole -RoleTemplateId $roleTemplate.Id -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.RoleMemberInfo.Type -eq 'User') { $roleMemberIdentity = Get-MgUser -Filter "UserPrincipalName eq '$($roleMember.RoleMemberInfo.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.RoleMemberInfo.Identity)} for role {$($roleMember.RoleName)} 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 if ($null -eq $roleMemberIdentity) { - throw "AU {$($DisplayName)}: Scoped Role Group {$($roleMember.RoleMemberInfo.Identity)} does not exist" + throw "AU {$($DisplayName)}: Scoped Role Group {$($roleMember.RoleMemberInfo.Identity)} for role {$($roleMember.RoleName)} 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 if ($null -eq $roleMemberIdentity) { - throw "AU {$($DisplayName)}: Scoped Role ServicePrincipal {$($roleMember.RoleMemberInfo.Identity)} 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.RoleMemberInfo.Type {$($roleMember.RolememberInfo.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.RoleMemberInfo.Type) {$($roleMember.RoleMemberInfo.Identity)} is not unique" } $scopedRoleMemberSpecification += @{ RoleId = $roleObject.Id @@ -538,216 +492,132 @@ 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}" + Write-Verbose -Message "Creating an Azure AD Administrative Unit with DisplayName {$DisplayName}" - $CreateParameters = Rename-M365DSCCimInstanceODataParameter -Properties $CreateParameters - - $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] + $memberBodyParam = @{ + '@odata.id' = "https://graph.microsoft.com/v1.0/$($member.Type)/$($member.Id)" + } + + New-MgDirectoryAdministrativeUnitMemberByRef -AdministrativeUnitId $policy.Id -BodyParameter $memberBodyParam } } - #region resource generator code - $policy = New-MgAdministrativeUnit @CreateParameters - - #endregion - foreach ($scopedRoleMember in $scopedRoleMemberSpecification) { 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 - - 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-MgAdministrativeUnit @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 ($currentInstance.Members.Length -ne 0) - { - $currentMembersValue = $backCurrentMembers - } - if ($null -eq $currentMembersValue) - { - $currentMembersValue = @() - } - $desiredMembersValue = $Members - if ($null -eq $desiredMembersValue) - { - $desiredMembersValue = @() - } - $membersDiff = Compare-Object -ReferenceObject $currentMembersValue -DifferenceObject $desiredMembersValue -Property Identity, Type - 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') - { - $memberObject = Get-MgGroup -Filter "DisplayName eq '$($diff.Identity)'" - $membertype = 'groups' - } - elseif ($diff.Type -eq 'Device') + $currentMembers = @() + foreach ($member in $backCurrentMembers) { - $memberObject = Get-MgDevice -Filter "DisplayName eq '$($diff.Identity)'" - $membertype = 'devices' + $currentMembers += [pscustomobject]@{Type=$member.Type; Identity = $member.Identity} } - if ($null -eq $memberObject) - { - throw "AU member {$($diff.Identity)} does not exist as a $($diff.Type)" - } - if ($memberObject.Count -gt 1) + $desiredMembers = @() + foreach ($member in $requestedMembers) { - throw "AU member {$($diff.Identity)} is not a unique $($diff.Type) (Count=$($memberObject.Count))" + $desiredMembers += [pscustomobject]@{Type=$member.Type; Identity = $member.Identity} } - if ($diff.SideIndicator -eq '=>') + $membersDiff = Compare-Object -ReferenceObject $currentMembers -DifferenceObject $desiredMembers -Property Identity, Type + foreach ($diff in $membersDiff) { - 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)}" + if ($diff.Type -eq 'User') + { + $memberObject = Get-MgUser -Filter "UserPrincipalName eq '$($diff.Identity)'" + $memberType = 'users' } - 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)}" - Remove-MgDirectoryAdministrativeUnitMemberByRef -AdministrativeUnitId ($currentInstance.Id) -DirectoryObjectId ($memberObject.Id) | Out-Null - } - } - } - - <# - 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) + elseif ($diff.Type -eq 'Group') { - $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]) - } + $memberObject = Get-MgGroup -Filter "DisplayName eq '$($diff.Identity)'" + $membertype = 'groups' } - 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) + elseif ($diff.Type -eq 'Device') { - $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]) + $memberObject = Get-MgDevice -Filter "DisplayName eq '$($diff.Identity)'" + $membertype = 'devices' + } + else + { + # 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 "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) { - $currentScopedRoleMembersValue = @() - if ($currentInstance.ScopedRoleMembers.Length -ne 0) + if ($backCurrentScopedRoleMembers.Length -ne 0) { $currentScopedRoleMembersValue = $backCurrentScopedRoleMembers } @@ -755,12 +625,13 @@ function Set-TargetResource { $currentScopedRoleMembersValue = @() } - $desiredScopedRoleMembersValue = $ScopedRoleMembers + $desiredScopedRoleMembersValue = $requestedScopedRoleMembers if ($null -eq $desiredScopedRoleMembersValue) { $desiredScopedRoleMembersValue = @() } - # flatten objects to compare: + + # flatten hashtabls for compare $compareCurrentScopedRoleMembersValue = @() foreach ($roleMember in $currentScopedRoleMembersValue) { @@ -779,38 +650,60 @@ function Set-TargetResource Type = $roleMember.RoleMemberInfo.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 + # $scopedRoleMembersDiff = Compare-Object -ReferenceObject $CurrentScopedRoleMembersValue -DifferenceObject $DesiredScopedRoleMembersValue -Property RoleName, Identity, Type + 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 + { + 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)" + 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" + throw "AU {$DisplayName} Scoped Role {$($diff.RoleName)} does not exist as an Azure AD role" } } 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 @@ -821,30 +714,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 { - 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 -ErrorAction Stop | Out-Null + } } } } - } 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 - } } @@ -854,33 +741,36 @@ function Test-TargetResource [OutputType([System.Boolean])] 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[]] @@ -890,11 +780,7 @@ function Test-TargetResource [Microsoft.Management.Infrastructure.CimInstance[]] $ScopedRoleMembers, - <# - [Parameter()] - [Microsoft.Management.Infrastructure.CimInstance[]] - $Extensions, - #> + #endregion [Parameter(Mandatory = $true)] [System.String] @@ -938,56 +824,40 @@ 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') + 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 + #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 = @() - $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 + break; } $ValuesToCheck.Remove($key) | Out-Null + } } @@ -995,25 +865,19 @@ function Test-TargetResource $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 - 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 ($MembershipType -ne 'Dynamic' -and $CurrentValues.MembershipType -ne 'Dynamic') { - if (($null -ne $CurrentValues[$key]) ` - -and ($CurrentValues[$key].getType().Name -eq 'DateTime')) - { - $CurrentValues[$key] = $CurrentValues[$key].toString() - } + # 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)" + if ($testResult) { $testResult = Test-M365DSCParameterState -CurrentValues $CurrentValues ` @@ -1060,13 +924,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 @@ -1083,9 +941,8 @@ function Export-TargetResource try { #region resource generator code - [array]$getValue = Get-MgAdministrativeUnit -All ` + [array]$getValue = Get-MgDirectoryAdministrativeUnit -All ` -ErrorAction Stop - #endregion $i = 1 @@ -1100,94 +957,73 @@ 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) - { - $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject $Results.Members -CIMInstanceName MicrosoftGraphIdentity - if ($complexTypeStringResult) - { - $Results.Members = $complexTypeStringResult - } - else - { - $Results.Remove('Members') | Out-Null - } - } - if ($Results.ScopedRoleMembers) + if ($null -ne $Results.ScopedRoleMembers) { - $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject $Results.ScopedRoleMembers -CIMInstanceName MicrosoftGraphscopedrolemembership - if ($complexTypeStringResult) - { - $Results.ScopedRoleMembers = $complexTypeStringResult - } - else + $complexMapping = @( + @{ + Name = 'RoleMemberInfo' + CimInstanceName = 'MicrosoftGraphIdentity' + } + ) + $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString -ComplexObject ([Array]$Results.ScopedRoleMembers) ` + -CIMInstanceName MicrosoftGraphScopedRoleMembership -ComplexTypeMapping $complexMapping + + $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 MicrosoftGraphIdentity + $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) + if ($null -ne $Results.ScopedRoleMembers) { - $isCIMArray = $false - if ($Results.Members.getType().Fullname -like '*[[\]]') - { - $isCIMArray = $true - } - $currentDSCBlock = Convert-DSCStringParamToVariable -DSCBlock $currentDSCBlock -ParameterName 'Members' -IsCIMArray:$isCIMArray - $currentDSCBlock = $currentDSCBlock.Replace('}");', '});') + $currentDSCBlock = Convert-DSCStringParamToVariable -DSCBlock $currentDSCBlock -ParameterName "ScopedRoleMembers" -isCIMArray $true } - if ($Results.ScopedRoleMembers) + if ($null -ne $Results.Members) { - $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 "Members" -isCIMArray $true } - if ($Results.Extensions) - { - $isCIMArray = $false - if ($Results.Extensions.getType().Fullname -like '*[[\]]') - { - $isCIMArray = $true - } - $currentDSCBlock = Convert-DSCStringParamToVariable -DSCBlock $currentDSCBlock -ParameterName 'Extensions' -IsCIMArray:$isCIMArray - } - $dscContent += $currentDSCBlock Save-M365DSCPartialExport -Content $currentDSCBlock ` -FileName $Global:PartialExportFileName @@ -1198,6 +1034,8 @@ function Export-TargetResource } catch { + write-verbose "Exception: $($_.Exception.Message)" + Write-Host $Global:M365DSCEmojiRedX New-M365DSCLogEntry -Message 'Error during Export:' ` @@ -1209,62 +1047,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' } + + if($ComplexObject.getType().Fullname -like "*hashtable") + { + $keys = $ComplexObject.keys + } + else + { + $keys = $ComplexObject | Get-Member | Where-Object -FilterScript {$_.MemberType -eq 'Property'} + } foreach ($key in $keys) { - if ($ComplexObject.$($key.Name)) + $keyName=$key + if($ComplexObject.getType().Fullname -notlike "*hashtable") { - $results.Add($key.Name, $ComplexObject.$($key.Name)) + $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) + } } } - if ($results.count -eq 0) - { - return $null - } - return $results + + 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, @@ -1273,128 +1244,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 "*[[\]]") { - $currentProperty = @() + $currentProperty=@() + $IndentLevel++ foreach ($item in $ComplexObject) { - $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) { - 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.tolower() - $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 (Test-M365DSCComplexObjectHasValues -ComplexObject $hashProperty) + if(-not $isArray) { - $Whitespace += ' ' - if (-not $isArray) + $currentProperty += $indent + $key + ' = ' + } + + if($isArray -and $key -in $ComplexTypeMapping.Name ) + { + if($ComplexObject.$key.count -gt 0) + { + $currentProperty += $indent + $key + ' = ' + $currentProperty += "@(" + } + } + + if ($isArray) + { + $IndentLevel++ + foreach ($item in $ComplexObject[$key]) { - $currentProperty += ' ' + $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) + { + $currentProperty += "$indent$key = @()`r`n" + } + else { - $Whitespace = ' ' + $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.Collections.Hashtable] - $ComplexObject - ) - $keys = $ComplexObject.keys - $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 - return $hasValue - } + $indent+=' ' } + $currentProperty += $indent } - return $hasValue + write-verbose "return item currentProperty on next line:`r`n$currentProperty" + return $currentProperty } + Function Get-M365DSCDRGSimpleObjectTypeToString { [CmdletBinding()] @@ -1409,49 +1462,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" } @@ -1461,7 +1515,7 @@ Function Get-M365DSCDRGSimpleObjectTypeToString } } } - if ($Value.Count -gt 1) + if($Value.count -gt 1) { $returnValue += "$Space)`r`n" } @@ -1473,215 +1527,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)) { - Write-Verbose -Message "Comparing Source-key: {$key}" - $skey = $key - if ($key -eq 'odataType') + if($null -eq $Source) { - $skey = '@odata.type' + $sourceValue="Source is null" } - #Marking Target[key] to null if empty complex object or array - if ($null -ne $Target[$key]) + if($null -eq $Target) { - switch -Wildcard ($Target[$key].getType().Fullname ) + $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[[\]]") + { + if($source.count -ne $target.count) + { + 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 + } + + foreach($item in $Source) + { + + $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 - 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] - } - $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) + $results=@() + foreach($item in $ComplexObject) { - $hash = Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $item - if (Test-M365DSCComplexObjectHasValues -ComplexObject $hash) - { - $results += $hash - } + $hash=Convert-M365DSCDRGComplexTypeToHashtable -ComplexObject $item + $results+=$hash } - if ($results.count -eq 0) - { - return $null - } - 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 f3d6bd5c60..d6581911c1 100644 --- a/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.schema.mof +++ b/Modules/Microsoft365DSC/DSCResources/MSFT_AADAdministrativeUnit/MSFT_AADAdministrativeUnit.schema.mof @@ -1,23 +1,17 @@ [ClassVersion("1.0.0")] class MSFT_MicrosoftGraphIdentity { - [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. 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("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; }; -/* 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 @@ -26,12 +20,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_MicrosoftGraphIdentity")] 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/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..3700d9ed30 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,6 +67,7 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Mock -CommandName Remove-MgDirectoryAdministrativeUnitScopedRoleMember -MockWith { } Mock -CommandName New-M365DSCConnection -MockWith { + # Select-MgProfile beta # not anymore return 'Credentials' } @@ -81,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_MicrosoftGraphIdentity -Property @{ + Type = 'User' Identity = 'john.smith@contoso.com' } -ClientOnly) + ) Visibility = 'Public' Ensure = 'Present' Credential = $Credential @@ -96,7 +99,13 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } } - Mock -CommandName Get-MgAdministrativeUnit -MockWith { + Mock -CommandName Get-MgDirectoryAdministrativeUnit -MockWith { + return $null + } + Mock -CommandName Get-MgDirectoryAdministrativeUnitMember -MockWith { + return $null + } + Mock -CommandName Get-MgDirectoryAdministrativeUnitScopedRoleMember -MockWith { return $null } } @@ -108,7 +117,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 } } @@ -118,16 +127,18 @@ 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_MicrosoftGraphIdentity -Property @{ + Type = 'User' Identity = 'john.smith@contoso.com' } -ClientOnly) + ) Ensure = 'Absent' Credential = $Credential } - Mock -CommandName Get-MgAdministrativeUnit -MockWith { - return [pscustomobject]@{ + Mock -CommandName Get-MgDirectoryAdministrativeUnit -MockWith { + return @{ Description = 'FakeStringValue2' DisplayName = 'FakeStringValue2' Id = 'FakeStringValue2' @@ -157,70 +168,43 @@ 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) ) ScopedRoleMembers = @( (New-CimInstance -ClassName MSFT_MicrosoftGraphScopedRoleMembership -Property @{ - RoleName = 'User Administrator' + RoleName = 'User Administrator' RoleMemberInfo = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ Identity = 'John.Doe@mytenant.com' Type = 'User' } -ClientOnly) + #Identity = 'John.Doe@mytenant.com' + #Type = 'User' } -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 = '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 { - return [pscustomobject]@{ - Description = 'DSCAU' - DisplayName = 'DSCAU' - Id = 'DSCAU' - <# - Extensions =@( - [pscustomobject]@{ - Id = '0123456789' - SomeAttribute = "somevalue" - } - ) - #> - Members = @( - [pscustomobject]@{Id = '1234567890' } - ) - ScopedRoleMembers = @( - [pscustomobject]@{ - RoleId = '12345-67890' - RoleMemberInfo = @( - [pscustomobject]@{ - DisplayName = 'John Doe' - Id = '1234567890' - } - ) - } - ) - Visibility = 'Public' + # 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 @{ + Description = 'DSCAU' + DisplayName = 'DSCAU' + Id = 'DSCAU' + Visibility = 'Public' AdditionalProperties = @{ membershipType = 'Assigned' membershipRule = 'Canada' @@ -230,7 +214,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' @@ -239,23 +223,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' } @@ -277,19 +261,9 @@ 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_MicrosoftGraphdirectoryobject -Property @{ + (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ Identity = 'John.Doe@mytenant.com' Type = 'User' } -ClientOnly) @@ -298,9 +272,11 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { (New-CimInstance -ClassName MSFT_MicrosoftGraphScopedRoleMembership -Property @{ RoleName = 'User Administrator' RoleMemberInfo = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ - Identity = 'John.Doe@mytenant.com' - Type = 'User' - } -ClientOnly) + Identity = 'John.Doe@mytenant.com' + Type = 'User' + } -ClientOnly) + #Identity = 'John.Doe@mytenant.com' + #Type = 'User' } -ClientOnly) ) Visibility = 'Public' @@ -309,34 +285,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]@{ - Id = "FakeExtensionIdentity" - SomeAttribute = "SomeValue" - } - ) - #> - '@odata.type' = '#microsoft.graph.administrativeunit' - } } @@ -355,7 +309,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' } @@ -399,18 +353,8 @@ 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_MicrosoftGraphdirectoryobject -Property @{ + (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ Identity = 'John.Doe@mytenant.com' Type = 'User' } -ClientOnly) @@ -419,9 +363,11 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { (New-CimInstance -ClassName MSFT_MicrosoftGraphScopedRoleMembership -Property @{ RoleName = 'User Administrator' RoleMemberInfo = (New-CimInstance -ClassName MSFT_MicrosoftGraphIdentity -Property @{ - Identity = 'John.Doe@mytenant.com' - Type = 'User' - } -ClientOnly) + Identity = 'John.Doe@mytenant.com' + Type = 'User' + } -ClientOnly) + #Identity = 'John.Doe@mytenant.com' + #Type = 'User' } -ClientOnly) ) Visibility = 'Public' @@ -429,34 +375,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]@{ - Id = "FakeExtensionId" - SomeAttribute = "SomeValue" - } - ) - #> - '@odata.type' = '#microsoft.graph.' - } - } - - Mock -CommandName Invoke-MgGraphRequest -MockWith { - return [pscustomobject]@{ - '@odata.type' = '#microsoft.graph.user' - DisplayName = 'John Doe' - UserPrincipalName = 'John.Doe@mytenant.com' - Id = '1234567890' } } @@ -475,9 +399,9 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { } Mock -CommandName Get-MgDirectoryAdministrativeUnitScopedRoleMember -MockWith { + return $null } - Mock -CommandName Invoke-MgGraphRequest -MockWith { return [pscustomobject]@{ '@odata.type' = '#microsoft.graph.user' @@ -515,35 +439,12 @@ Describe -Name $Global:DscHelper.DescribeHeader -Fixture { Credential = $Credential } - Mock -CommandName Get-MgAdministrativeUnit -MockWith { - return [pscustomobject]@{ + Mock -CommandName Get-MgDirectoryAdministrativeUnit -MockWith { + return @{ Description = 'ExportDSCAU' DisplayName = 'ExportDSCAU' - <# - Extensions =@( - (New-CimInstance -ClassName MSFT_MicrosoftGraphextension -Property @{ - CIMType = "MSFT_MicrosoftGraphextension" - Name = "Extensions" - isArray = $True - - } -ClientOnly) - ) - #> Id = 'ExportDSCAU' - Members = @( - [pscustomobject]@{Id = '1234567890' } - ) - ScopedRoleMembers = @( - [pscustomobject]@{ - RoleId = '12345-67890' - RoleMemberInfo = [pscustomobject]@{ - DisplayName = 'John Doe' - Id = '1234567890' - } - } - ) Visibility = 'Public' - } } @@ -562,16 +463,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 = 'FakeRoleGroup' + 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' @@ -580,15 +488,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 {$DirectoryRoleId -eq '12345-67890'} -MockWith { return [pscustomobject]@{ Id = '12345-67890' - DisplayName = 'User Administrator' + DisplayName = 'DSC User Administrator' + } + } + + Mock -CommandName Get-MgDirectoryRole -ParameterFilter {$DirectoryRoleId -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 } } diff --git a/docs/docs/resources/azure-ad/AADAdministrativeUnit.md b/docs/docs/resources/azure-ad/AADAdministrativeUnit.md index 7887d6cc80..e94376b4b0 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,90 @@ 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" + } + ) + } + } +} +``` +### 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" + } + ) + } + } +} +```