DSCResources/MSFT_AADGroup/MSFT_AADGroup.psm1
|
Confirm-M365DSCModuleDependency -ModuleName 'MSFT_AADGroup' function Get-TargetResource { [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param ( [Parameter(Mandatory = $true)] [System.String] $DisplayName, [Parameter(Mandatory = $true)] [System.String] $MailNickname, [Parameter()] [System.String] $Id, [Parameter()] [System.String[]] $Owners, [Parameter()] [System.String[]] $Members, [Parameter()] [System.String[]] $GroupAsMembers, [Parameter()] [System.String[]] $MemberOf, [Parameter()] [System.String] $Description, [Parameter()] [System.Boolean] $GroupLifecyclePolicySelectedEnabled, [Parameter()] [System.String[]] $GroupTypes, [Parameter()] [System.String] $MembershipRule, [Parameter()] [ValidateSet('On', 'Paused')] [System.String] $MembershipRuleProcessingState, [Parameter(Mandatory = $true)] [System.Boolean] $SecurityEnabled, [Parameter(Mandatory = $true)] [System.Boolean] $MailEnabled, [Parameter()] [System.Boolean] $IsAssignableToRole, [Parameter()] [System.String[]] $AssignedToRole, [Parameter()] [ValidateSet('Public', 'Private', 'HiddenMembership')] [System.String] $Visibility, [Parameter()] [Microsoft.Management.Infrastructure.CimInstance[]] $AssignedLicenses, [Parameter()] [ValidateSet('Present', 'Absent')] [System.String] $Ensure = 'Present', [Parameter()] [System.Management.Automation.PSCredential] $Credential, [Parameter()] [System.String] $ApplicationId, [Parameter()] [System.String] $TenantId, [Parameter()] [System.Management.Automation.PSCredential] $ApplicationSecret, [Parameter()] [System.String] $CertificateThumbprint, [Parameter()] [System.String] $CertificatePath, [Parameter()] [System.Management.Automation.PSCredential] $CertificatePassword, [Parameter()] [Switch] $ManagedIdentity, [Parameter()] [System.String[]] $AccessTokens ) Write-Verbose -Message "Getting configuration of AzureAD Group with DisplayName {$DisplayName}" try { if (-not $Script:exportedInstance -or $Script:exportedInstance.DisplayName -ne $DisplayName) { $null = New-M365DSCConnection -Workload 'MicrosoftGraph' ` -InboundParameters $PSBoundParameters #Ensure the proper dependencies are installed in the current environment. Confirm-M365DSCDependencies #region Telemetry $ResourceName = $MyInvocation.MyCommand.ModuleName -replace 'MSFT_', '' $CommandName = $MyInvocation.MyCommand $data = Format-M365DSCTelemetryParameters -ResourceName $ResourceName ` -CommandName $CommandName ` -Parameters $PSBoundParameters Add-M365DSCTelemetryEvent -Data $data #endregion $nullReturn = $PSBoundParameters $nullReturn.Ensure = 'Absent' $nullReturn.Owners = @() $nullReturn.Members = @() $nullReturn.GroupAsMembers = @() $nullReturn.MemberOf = @() $nullReturn.AssignedToRole = @() $nullReturn.AssignedLicenses = @() if ($PSBoundParameters.ContainsKey('Id') -and -not [System.String]::IsNullOrEmpty($Id)) { Write-Verbose -Message 'GroupID was specified' try { $Group = Get-MgBetaGroup -GroupId $Id -ExpandProperty 'members' -ErrorAction Stop } catch { Write-Verbose -Message "Couldn't get group by ID, trying by name" [array]$Group = Get-MgBetaGroup -Filter "DisplayName eq '$($DisplayName -replace "'", "''")'" -ExpandProperty 'members' -ErrorAction Stop if ($Group.Count -gt 1) { throw "Duplicate AzureAD Groups named $DisplayName exist in tenant" } } } else { Write-Verbose -Message 'Id was NOT specified' ## Can retreive multiple AAD Groups since displayname is not unique [array]$Group = Get-MgBetaGroup -Filter "DisplayName eq '$($DisplayName -replace "'", "''")'" -ExpandProperty 'members' -ErrorAction Stop if ($Group.Count -gt 1) { throw "Duplicate AzureAD Groups named $DisplayName exist in tenant" } } if ($null -eq $Group) { Write-Verbose -Message 'Group was null, returning null' return $nullReturn } } else { $Group = $Script:exportedInstance } Write-Verbose -Message 'Found existing AzureAD Group' $batchRequests = @( @{ id = 'Owners' method = 'GET' url = "/groups/$($Group.Id)/owners" } @{ id = 'MemberOf' method = 'GET' url = "/groups/$($Group.Id)/memberOf" } @{ id = 'Licenses' method = 'GET' url = "/groups/$($Group.Id)/assignedLicenses" } @{ id = 'GroupLifecyclePolicies' method = 'GET' url = "/groups/$($Group.Id)/groupLifecyclePolicies" } ) $batchResponse = Invoke-M365DSCGraphBatchRequest -Requests $batchRequests # Owners [Array]$owners = ($batchResponse | Where-Object -FilterScript { $_.id -eq 'Owners' }).body.value $OwnersValues = @() foreach ($owner in $owners) { if ($null -ne $owner.userPrincipalName) { $OwnersValues += $owner.userPrincipalName } elseif ($owner.'@odata.type' -eq '#microsoft.graph.servicePrincipal') { $OwnersValues += $owner.displayName } } $MembersValues = $null $result = @{} $MembersValues = [System.Collections.Generic.List[System.String]]::new() $GroupAsMembersValues = [System.Collections.Generic.List[System.String]]::new() # If the Members and GroupAsMembers parameters are not specified, do not attempt to retrieve them as part of the Get-TargetResource. if ($Group.MembershipRuleProcessingState -ne 'On' -and ($PSBoundParameters.ContainsKey('Members') -or $PSBoundParameters.ContainsKey('GroupAsMembers'))) { # Members $groupMembers = $Group.Members if ($Group.Members.Count -eq 20 -or $Script:requireGroupMemberFetching -eq $true) { # Fetch all group members $groupMembers = Get-MgGroupMember -GroupId $Group.Id -All -Top 999 } foreach ($member in $groupMembers) { switch ($member.'@odata.type') { '#microsoft.graph.user' { $MembersValues.Add($member.userPrincipalName) } '#microsoft.graph.servicePrincipal' { $MembersValues.Add($member.displayName) } '#microsoft.graph.device' { $MembersValues.Add($member.displayName) } '#microsoft.graph.group' { $GroupAsMembersValues.Add($member.displayName) } } } } # MemberOf [Array]$memberOf = ($batchResponse | Where-Object -FilterScript { $_.id -eq 'MemberOf' }).body.value $MemberOfValues = @() # Note: only process security-groups that this group is a member of and not directory roles (if any) foreach ($member in ($memberOf | Where-Object -FilterScript { $_.'@odata.type' -eq '#microsoft.graph.group' })) { if ($null -ne $member.displayName) { $MemberOfValues += $member.displayName } } if ($null -eq $Script:DirectoryRoleDefinitions) { $Script:DirectoryRoleDefinitions = [System.Collections.Generic.Dictionary[string, string]]::new() $allRoleDefinitions = Get-MgBetaRoleManagementDirectoryRoleDefinition -All foreach ($roleDefinition in $allRoleDefinitions) { $Script:DirectoryRoleDefinitions.Add($roleDefinition.Id, $roleDefinition.DisplayName) } } # AssignedToRole $AssignedToRoleValues = @() if ($Group.IsAssignableToRole -eq $true) { $roleAssignments = Get-MgBetaRoleManagementDirectoryRoleAssignment -Filter "PrincipalId eq '$($Group.Id)'" foreach ($assignment in $roleAssignments) { $roleDefinition = Get-MgBetaRoleManagementDirectoryRoleDefinition -UnifiedRoleDefinitionId $assignment.RoleDefinitionId $AssignedToRoleValues += $roleDefinition.DisplayName } } # Licenses $assignedLicensesValues = @() $assignedLicensesRequest = ($batchResponse | Where-Object -FilterScript { $_.id -eq 'Licenses' }).body if ($assignedLicensesRequest.value.Length -gt 0) { [Array]$assignedLicensesValues = Get-M365DSCAzureADGroupLicenses -AssignedLicenses $assignedLicensesRequest.value } # GroupLifecyclePolicies $groupLifecyclePoliciesRequest = ($batchResponse | Where-Object -FilterScript { $_.id -eq 'GroupLifecyclePolicies' }).body.value $isGroupLifecyclePoliciesEnabled = $null -ne $groupLifecyclePoliciesRequest -and ` $groupLifecyclePoliciesRequest.managedGroupTypes -eq 'selected' $policySettings = @{ DisplayName = $Group.DisplayName Id = $Group.Id Owners = $OwnersValues MemberOf = $MemberOfValues Description = $Group.Description GroupTypes = [System.String[]]$Group.GroupTypes MembershipRule = $Group.MembershipRule MembershipRuleProcessingState = $Group.MembershipRuleProcessingState GroupAsMembers = $GroupAsMembersValues Members = $MembersValues SecurityEnabled = $Group.SecurityEnabled MailEnabled = $Group.MailEnabled IsAssignableToRole = $false -or $Group.IsAssignableToRole AssignedToRole = $AssignedToRoleValues MailNickname = $Group.MailNickname Visibility = $Group.Visibility AssignedLicenses = $assignedLicensesValues Ensure = 'Present' ApplicationId = $ApplicationId TenantId = $TenantId CertificateThumbprint = $CertificateThumbprint ApplicationSecret = $ApplicationSecret Credential = $Credential ManagedIdentity = $ManagedIdentity.IsPresent AccessTokens = $AccessTokens } $result += $policySettings if ($result.MailEnabled) { $result.Add('GroupLifecyclePolicySelectedEnabled', $isGroupLifecyclePoliciesEnabled) } return $result } catch { New-M365DSCLogEntry -Message 'Error retrieving data:' ` -Exception $_ ` -Source $($MyInvocation.MyCommand.Source) ` -TenantId $TenantId ` -Credential $Credential throw } } function Set-TargetResource { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [System.String] $DisplayName, [Parameter(Mandatory = $true)] [System.String] $MailNickname, [Parameter()] [System.String] $Id, [Parameter()] [System.String[]] $Owners, [Parameter()] [System.String[]] $Members, [Parameter()] [System.String[]] $GroupAsMembers, [Parameter()] [System.String[]] $MemberOf, [Parameter()] [System.String] $Description, [Parameter()] [System.Boolean] $GroupLifecyclePolicySelectedEnabled, [Parameter()] [System.String[]] $GroupTypes, [Parameter()] [System.String] $MembershipRule, [Parameter()] [ValidateSet('On', 'Paused')] [System.String] $MembershipRuleProcessingState, [Parameter(Mandatory = $true)] [System.Boolean] $SecurityEnabled, [Parameter(Mandatory = $true)] [System.Boolean] $MailEnabled, [Parameter()] [System.Boolean] $IsAssignableToRole, [Parameter()] [System.string[]] $AssignedToRole, [Parameter()] [ValidateSet('Public', 'Private', 'HiddenMembership')] [System.String] $Visibility, [Parameter()] [Microsoft.Management.Infrastructure.CimInstance[]] $AssignedLicenses, [Parameter()] [ValidateSet('Present', 'Absent')] [System.String] $Ensure = 'Present', [Parameter()] [System.Management.Automation.PSCredential] $Credential, [Parameter()] [System.String] $ApplicationId, [Parameter()] [System.String] $TenantId, [Parameter()] [System.Management.Automation.PSCredential] $ApplicationSecret, [Parameter()] [System.String] $CertificateThumbprint, [Parameter()] [System.String] $CertificatePath, [Parameter()] [System.Management.Automation.PSCredential] $CertificatePassword, [Parameter()] [Switch] $ManagedIdentity, [Parameter()] [System.String[]] $AccessTokens ) Write-Verbose -Message 'Setting configuration of Azure AD Groups' #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 $currentParameters = Remove-M365DSCAuthenticationParameter -BoundParameters $PSBoundParameters $currentGroup = Get-TargetResource @PSBoundParameters $backCurrentOwners = $currentGroup.Owners $backCurrentMembers = $currentGroup.Members $backCurrentGroupAsMembers = $currentGroup.GroupAsMembers $backCurrentMemberOf = $currentGroup.MemberOf $backCurrentAssignedToRole = $currentGroup.AssignedToRole $currentParameters.Remove('Owners') | Out-Null $currentParameters.Remove('Members') | Out-Null $currentParameters.Remove('GroupAsMembers') | Out-Null $currentParameters.Remove('MemberOf') | Out-Null $currentParameters.Remove('AssignedToRole') | Out-Null if ($Ensure -eq 'Present' -and ` ($null -ne $GroupTypes -and $GroupTypes.Contains('Unified')) -and ` ($null -ne $MailEnabled -and $MailEnabled -eq $false)) { Write-Verbose -Message 'Cannot set mailenabled to false if GroupTypes is set to Unified when creating group.' throw 'Cannot set mailenabled to false if GroupTypes is set to Unified when creating a group.' } $currentValuesToCheck = @() if ($currentGroup.AssignedLicenses.Length -gt 0) { $currentValuesToCheck = $currentGroup.AssignedLicenses.SkuId } $desiredValuesToCheck = @() if ($AssignedLicenses.Length -gt 0) { $desiredValuesToCheck = $AssignedLicenses.SkuId } [Array]$licensesDiff = Compare-Object -ReferenceObject $currentValuesToCheck -DifferenceObject $desiredValuesToCheck -IncludeEqual $toAdd = @() $toRemove = @() foreach ($diff in $licensesDiff) { if ($diff.SideIndicator -eq '=>') { $toAdd += $diff.InputObject } elseif ($diff.SideIndicator -eq '<=') { $toRemove += $diff.InputObject } elseif ($diff.SideIndicator -eq '==') { # This will take care of the scenario where the license is already assigned but has different disabled plans $toAdd += $diff.InputObject } } # Convert AssignedLicenses from SkuPartNumber back to GUID $licensesToAdd = @() $licensesToRemove = @() [Array]$AllLicenses = Get-M365DSCCombinedLicenses -DesiredLicenses $AssignedLicenses -CurrentLicenses $currentGroup.AssignedLicenses $allSkus = Get-MgBetaSubscribedSku # Create complete list of all Service Plans $allServicePlans = @() Write-Verbose -Message 'Getting all Service Plans' foreach ($sku in $allSkus) { foreach ($serviceplan in $sku.ServicePlans) { if ($allServicePlans.Length -eq 0 -or -not $allServicePlans.ServicePlanName.Contains($servicePlan.ServicePlanName)) { $allServicePlans += @{ ServicePlanId = $serviceplan.ServicePlanId ServicePlanName = $serviceplan.ServicePlanName } } } } foreach ($assignedLicense in $AllLicenses) { $skuInfo = $allSkus | Where-Object -FilterScript { ($_.SkuPartNumber -replace [char]0xFEFF, '') -eq $assignedLicense.SkuId } if ($skuInfo) { if ($toAdd.Contains($assignedLicense.SkuId)) { $disabledPlansValues = @() foreach ($plan in $assignedLicense.DisabledPlans) { $foundItem = $allServicePlans | Where-Object -FilterScript { $_.ServicePlanName -eq $plan } $disabledPlansValues += $foundItem.ServicePlanId } $skuInfo = $allSkus | Where-Object -FilterScript { ($_.SkuPartNumber -replace [char]0xFEFF, '') -eq $assignedLicense.SkuId } $licensesToAdd += @{ disabledPlans = $disabledPlansValues skuId = $skuInfo.SkuId } } elseif ($toRemove.Contains($assignedLicense.SkuId)) { $licensesToRemove += $skuInfo.SkuId } } else { Write-Warning -Message "Specified Sku {$($assignedLicense.SkuId)} could not be found on the tenant." } } $currentParameters.Remove('AssignedLicenses') | Out-Null if ($Ensure -eq 'Present' -and $currentGroup.Ensure -eq 'Absent') { Write-Verbose -Message "Checking to see if an existing deleted group exists with DisplayName {$DisplayName}" $restoringExisting = $false # Not using Get-MgBetaDirectoryDeletedItemAsGroup because the URI from Find-MgGraphCommand is not correct [Array]$groups = (Invoke-MgGraphRequest -Uri "/beta/directory/deletedItems/microsoft.graph.group?`$filter=DisplayName eq '$($DisplayName -replace "'", "''")'").value if ($groups.Length -gt 1) { throw "Multiple deleted groups with the name {$DisplayName} were found. Cannot restore the existing group. Please ensure that you either have no instance of the group in the deleted list or that you have a single one." } if ($groups.Length -eq 1) { Write-Verbose -Message "Found an instance of a deleted group {$DisplayName}. Restoring it." Restore-MgBetaDirectoryDeletedItem -DirectoryObjectId $groups[0].Id $restoringExisting = $true do { $currentGroup = Get-MgBetaGroup -Filter "DisplayName eq '$($DisplayName -replace "'", "''")'" -ErrorAction Stop } while ($null -eq $currentGroup) $null = Invoke-M365DSCCommand -ScriptBlock { Get-MgBetaGroup -GroupId $currentGroup.Id -ErrorAction Stop } -RetryOnNotFoundError $null = Invoke-M365DSCCommand -ScriptBlock { Get-MgBetaGroupMember -GroupId $currentGroup.Id -ErrorAction Stop } -RetryOnNotFoundError $commandParameters = ([Hashtable]$PSBoundParameters).Clone() $currentGroup = Invoke-M365DSCCommand -ScriptBlock { Get-TargetResource @commandParameters } -RetryOnNotFoundError $backCurrentOwners = $currentGroup.Owners $backCurrentMembers = $currentGroup.Members } if (-not $restoringExisting) { Write-Verbose -Message "Creating new group {$DisplayName}" $currentParameters.Remove('Id') | Out-Null try { Write-Verbose -Message "Creating Group with Values: $(Convert-M365DscHashtableToString -Hashtable $currentParameters)" $currentGroup = New-MgGroup -BodyParameter $currentParameters Write-Verbose -Message "Created Group $($currentGroup.id), wait for sync to complete" Invoke-M365DSCCommand -ScriptBlock { Get-MgBetaGroup -GroupId $currentGroup.Id -Property Id -ErrorAction Stop } -RetryOnNotFoundError | Out-Null } catch { Write-Verbose -Message $_ New-M365DSCLogEntry -Message "Couldn't create group $DisplayName" ` -Exception $_ ` -Source $MyInvocation.MyCommand.ModuleName } } } if ($Ensure -eq 'Present') { Write-Verbose -Message "Group {$DisplayName} exists and it should." try { if ($currentGroup.Ensure -eq 'Present') { Write-Verbose -Message "Updating settings by ID for group {$DisplayName}" if ($currentParameters.ContainsKey('IsAssignableToRole')) { Write-Verbose -Message 'Cannot set IsAssignableToRole once group is created.' $currentParameters.Remove('IsAssignableToRole') | Out-Null } if ($currentParameters.ContainsKey('Id')) { $currentParameters.Remove('Id') | Out-Null } Write-Verbose -Message "Updating existing Group with Values: $(Convert-M365DscHashtableToString -Hashtable $currentParameters)" Update-MgGroup -GroupId $currentGroup.Id -BodyParameter $currentParameters -ErrorAction Stop } if (($licensesToAdd.Length -gt 0 -or $licensesToRemove.Length -gt 0) -and $PSBoundParameters.ContainsKey('AssignedLicenses')) { try { Write-Verbose -Message "Setting Group Licenses with:`r`nLicensesToAdd: $(ConvertTo-Json $licensesToAdd)`r`nLicensesToRemove: $(ConvertTo-Json $licensesToRemove)" Set-MgGroupLicense -GroupId $currentGroup.Id ` -AddLicenses $licensesToAdd ` -RemoveLicenses $licensesToRemove ` -ErrorAction Stop | Out-Null } catch { Write-Verbose -Message $_ } } } catch { New-M365DSCLogEntry -Message "Couldn't set group $DisplayName" ` -Exception $_ ` -Source $MyInvocation.MyCommand.ModuleName } } elseif ($Ensure -eq 'Absent' -and $currentGroup.Ensure -eq 'Present') { try { Remove-MgGroup -GroupId $currentGroup.Id | Out-Null } catch { New-M365DSCLogEntry -Message "Couldn't delete group $DisplayName" ` -Exception $_ ` -Source $MyInvocation.MyCommand.ModuleName } } if ($Ensure -ne 'Absent') { #Owners Write-Verbose -Message 'Updating Owners' if ($PSBoundParameters.ContainsKey('Owners')) { $desiredOwnersValue = @() if ($Owners.Length -gt 0) { $desiredOwnersValue = $Owners } if ($null -eq $backCurrentOwners) { $backCurrentOwners = @() } $ownersDiff = Compare-Object -ReferenceObject $backCurrentOwners -DifferenceObject $desiredOwnersValue foreach ($diff in $ownersDiff) { $directoryObject = Get-MgUser -UserId $diff.InputObject -ErrorAction SilentlyContinue if ($null -eq $directoryObject) { Write-Verbose -Message "Trying to retrieve Service Principal {$($diff.InputObject)}" [array]$app = Get-MgApplication -Filter "DisplayName eq '$($diff.InputObject -replace "'", "''")'" if ($app.Count -gt 0) { $directoryObject = Get-MgServicePrincipal -Filter "AppId eq '$($app.AppId)'" } else { [array]$spInstances = Get-MgServicePrincipal -Filter "DisplayName eq '$($diff.InputObject -replace "'", "''")'" if ($spInstances.Count -gt 1) { throw "Duplicate Service Principals named '$($diff.InputObject)' exist in tenant" } elseif ($spInstances.Count -eq 1) { $directoryObject = $spInstances } } } if ($diff.SideIndicator -eq '=>') { Write-Verbose -Message "Adding new owner {$($diff.InputObject)} to AAD Group {$($currentGroup.DisplayName)}" $ownerObject = @{ '@odata.id' = (Get-MSCloudLoginConnectionProfile -Workload MicrosoftGraph).ResourceUrl + "v1.0/directoryObjects/{$($directoryObject.Id)}" } try { New-MgGroupOwnerByRef -GroupId ($currentGroup.Id) -BodyParameter $ownerObject -ErrorAction Stop | Out-Null } catch { if ($_.Exception.Message -notlike '*One or more added object references already exist for the following modified properties*') { throw $_ } } } elseif ($diff.SideIndicator -eq '<=') { Write-Verbose -Message "Removing new owner {$($diff.InputObject)} to AAD Group {$($currentGroup.DisplayName)}" Remove-MgGroupOwnerDirectoryObjectByRef -GroupId ($currentGroup.Id) -DirectoryObjectId ($directoryObject.Id) | Out-Null } } } #Members Write-Verbose -Message 'Updating Members' if ($MembershipRuleProcessingState -ne 'On' -and $PSBoundParameters.ContainsKey('Members')) { $desiredMembersValue = @() if ($Members.Length -ne 0) { $desiredMembersValue = $Members } if ($null -eq $backCurrentMembers) { $backCurrentMembers = @() } Write-Verbose -Message 'Comparing current members and desired list' $membersDiff = Compare-Object -ReferenceObject $backCurrentMembers -DifferenceObject $desiredMembersValue foreach ($diff in $membersDiff) { Write-Verbose -Message "Found difference for member {$($diff.InputObject)}" $directoryObject = Get-MgUser -UserId $diff.InputObject -ErrorAction SilentlyContinue if ($null -eq $directoryObject) { Write-Verbose -Message "Trying to retrieve Service Principal {$($diff.InputObject)}" [array]$app = Get-MgApplication -Filter "DisplayName eq '$($diff.InputObject -replace "'", "''")'" if ($app.Count -gt 0) { $directoryObject = Get-MgServicePrincipal -Filter "AppId eq '$($app.AppId)'" } else { [array]$spInstances = Get-MgServicePrincipal -Filter "DisplayName eq '$($diff.InputObject -replace "'", "''")'" if ($spInstances.Count -gt 1) { throw "Duplicate Service Principals named '$($diff.InputObject)' exist in tenant" } elseif ($spInstances.Count -eq 1) { $directoryObject = $spInstances } } } if ($null -eq $directoryObject) { Write-Verbose -Message "Trying to retrieve Device {$($diff.InputObject)}" $directoryObject = Get-MgDevice -Filter "DisplayName eq '$($diff.InputObject -replace "'", "''")'" } if ($diff.SideIndicator -eq '=>') { Write-Verbose -Message "Adding new member {$($diff.InputObject)} to AAD Group {$($currentGroup.DisplayName)}" New-MgGroupMemberByRef -GroupId ($currentGroup.Id) -BodyParameter @{ '@odata.id' = (Get-MSCloudLoginConnectionProfile -Workload MicrosoftGraph).ResourceUrl + "v1.0/directoryObjects/{$($directoryObject.Id)}" } } elseif ($diff.SideIndicator -eq '<=') { Write-Verbose -Message "Removing new member {$($diff.InputObject)} from AAD Group {$($currentGroup.DisplayName)}" Remove-MgGroupMemberDirectoryObjectByRef -GroupId ($currentGroup.Id) -DirectoryObjectId ($directoryObject.Id) | Out-Null } } } elseif ($MembershipRuleProcessingState -eq 'On') { Write-Verbose -Message 'Ignoring membership since this is a dynamic group.' } #GroupAsMembers Write-Verbose -Message 'Updating GroupAsMembers' if ($MembershipRuleProcessingState -ne 'On' -and $PSBoundParameters.ContainsKey('GroupAsMembers')) { $desiredGroupAsMembersValue = @() if ($GroupAsMembers.Length -ne 0) { $desiredGroupAsMembersValue = $GroupAsMembers } if ($null -eq $backCurrentGroupAsMembers) { $backCurrentGroupAsMembers = @() } $groupAsMembersDiff = Compare-Object -ReferenceObject $backCurrentGroupAsMembers -DifferenceObject $desiredGroupAsMembersValue foreach ($diff in $groupAsMembersDiff) { try { $groupAsMember = Get-MgBetaGroup -Filter "DisplayName eq '$($diff.InputObject -replace "'", "''")'" -ErrorAction SilentlyContinue } catch { $groupAsMember = $null } if ($null -eq $groupAsMember) { throw "Group '$($diff.InputObject)' does not exist" } else { if ($diff.SideIndicator -eq '=>') { Write-Verbose -Message "Adding AAD group {$($groupAsMember.DisplayName)} as member of AAD group {$($currentGroup.DisplayName)}" $groupAsMemberObject = @{ '@odata.id' = (Get-MSCloudLoginConnectionProfile -Workload MicrosoftGraph).ResourceUrl + "v1.0/directoryObjects/$($groupAsMember.Id)" } New-MgBetaGroupMemberByRef -GroupId ($currentGroup.Id) -Body $groupAsMemberObject | Out-Null } if ($diff.SideIndicator -eq '<=') { Write-Verbose -Message "Removing AAD Group {$($groupAsMember.DisplayName)} from AAD group {$($currentGroup.DisplayName)}" Remove-MgBetaGroupMemberDirectoryObjectByRef -GroupId ($currentGroup.Id) -DirectoryObjectId ($groupAsMember.Id) | Out-Null } } } } #MemberOf Write-Verbose -Message 'Updating MemberOf' if ($PSBoundParameters.ContainsKey('MemberOf')) { $desiredMemberOfValue = @() if ($MemberOf.Length -ne 0) { $desiredMemberOfValue = $MemberOf } if ($null -eq $backCurrentMemberOf) { $backCurrentMemberOf = @() } $memberOfDiff = Compare-Object -ReferenceObject $backCurrentMemberOf -DifferenceObject $desiredMemberOfValue foreach ($diff in $memberOfDiff) { try { $memberOfGroup = Get-MgBetaGroup -Filter "DisplayName eq '$($diff.InputObject -replace "'", "''")'" -ErrorAction Stop } catch { $memberOfGroup = $null } if ($null -eq $memberOfGroup) { throw "Security-group or directory role '$($diff.InputObject)' does not exist" } else { if ($diff.SideIndicator -eq '=>') { # see if memberOfGroup contains property SecurityEnabled (it can be true or false) if ($memberOfGroup.SecurityEnabled) { Write-Verbose -Message "Adding AAD group {$($currentGroup.DisplayName)} as member of AAD group {$($memberOfGroup.DisplayName)}" New-MgGroupMemberByRef -GroupId ($memberOfGroup.Id) -BodyParameter @{ '@odata.id' = (Get-MSCloudLoginConnectionProfile -Workload MicrosoftGraph).ResourceUrl + "v1.0/directoryObjects/$($currentGroup.Id)" } | Out-Null } else { throw "Cannot add AAD group {$($currentGroup.DisplayName)} to {$($memberOfGroup.DisplayName)} as it is not a security-group" } } elseif ($diff.SideIndicator -eq '<=') { if ($memberOfGroup.SecurityEnabled) { Write-Verbose -Message "Removing AAD Group {$($currentGroup.DisplayName)} from AAD group {$($memberOfGroup.DisplayName)}" Remove-MgGroupMemberDirectoryObjectByRef -GroupId ($memberOfGroup.Id) -DirectoryObjectId ($currentGroup.Id) | Out-Null } else { throw "Cannot remove AAD group {$($currentGroup.DisplayName)} from {$($memberOfGroup.DisplayName)} as it is not a security-group" } } } } } if ($currentGroup.IsAssignableToRole -eq $true -and $PSBoundParameters.ContainsKey('AssignedToRole')) { $desiredAssignedToRoleValue = @() if ($AssignedToRole.Length -ne 0) { $desiredAssignedToRoleValue = $AssignedToRole } if ($null -eq $backCurrentAssignedToRole) { $backCurrentAssignedToRole = @() } $assignedToRoleDiff = Compare-Object -ReferenceObject $backCurrentAssignedToRole -DifferenceObject $desiredAssignedToRoleValue foreach ($diff in $assignedToRoleDiff) { try { $role = Get-MgBetaRoleManagementDirectoryRoleDefinition -Filter "DisplayName eq '$($diff.InputObject -replace "'", "''")'" } catch { $role = $null } if ($null -eq $role) { throw "Directory Role '$($diff.InputObject)' does not exist" } else { if ($diff.SideIndicator -eq '=>') { Write-Verbose -Message "Assigning AAD group {$($currentGroup.DisplayName)} to Directory Role {$($diff.InputObject)}" New-MgBetaRoleManagementDirectoryRoleAssignment -RoleDefinitionId $role.Id -PrincipalId $currentGroup.Id -DirectoryScopeId '/' } elseif ($diff.SideIndicator -eq '<=') { Write-Verbose -Message "Removing AAD group {$($currentGroup.DisplayName)} from Directory Role {$($role.DisplayName)}" $roleAssignment = Get-MgBetaRoleManagementDirectoryRoleAssignment -Filter "PrincipalId eq '$($currentGroup.Id)' and RoleDefinitionId eq '$($role.Id)'" Remove-MgBetaRoleManagementDirectoryRoleAssignment -UnifiedRoleAssignmentId $roleAssignment.Id } } } } # GroupLifecyclePolicies if ($PSBoundParameters.ContainsKey('GroupLifecyclePolicySelectedEnabled')) { if ($null -eq $Script:GroupLifecyclePolicy) { $Script:GroupLifecyclePolicy = Get-MgBetaGroupLifecyclePolicy } if ($Script:GroupLifecyclePolicy.ManagedGroupTypes -ne 'selected') { Write-Warning -Message "Cannot assign or remove group from lifecycle policy because the current mode is not 'Selected'." return } if (-not $currentGroup.MailEnabled) { Write-Warning -Message 'Cannot assign or remove group from lifecycle policy because it is not a Microsoft 365 Group.' return } if ($GroupLifecyclePolicySelectedEnabled -and -not $currentGroup.GroupLifecyclePolicySelectedEnabled) { Write-Verbose -Message "Enabling Group Lifecycle Policy for AAD group {$($currentGroup.DisplayName)}" Add-MgBetaGroupToLifecyclePolicy -GroupLifecyclePolicyId $Script:GroupLifecyclePolicy.Id -GroupId $currentGroup.Id } elseif (-not $GroupLifecyclePolicySelectedEnabled -and $currentGroup.GroupLifecyclePolicySelectedEnabled) { Write-Verbose -Message "Removing AAD group {$($currentGroup.DisplayName)} from Group Lifecycle Policy" Remove-MgBetaGroupFromLifecyclePolicy -GroupLifecyclePolicyId $Script:GroupLifecyclePolicy.Id -GroupId $currentGroup.Id } } } } function Test-TargetResource { [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory = $true)] [System.String] $DisplayName, [Parameter(Mandatory = $true)] [System.String] $MailNickname, [Parameter()] [System.String] $Id, [Parameter()] [System.String[]] $Owners, [Parameter()] [System.String[]] $Members, [Parameter()] [System.String[]] $GroupAsMembers, [Parameter()] [System.String[]] $MemberOf, [Parameter()] [System.String] $Description, [Parameter()] [System.Boolean] $GroupLifecyclePolicySelectedEnabled, [Parameter()] [System.String[]] $GroupTypes, [Parameter()] [System.String] $MembershipRule, [Parameter()] [ValidateSet('On', 'Paused')] [System.String] $MembershipRuleProcessingState, [Parameter(Mandatory = $true)] [System.Boolean] $SecurityEnabled, [Parameter(Mandatory = $true)] [System.Boolean] $MailEnabled, [Parameter()] [System.Boolean] $IsAssignableToRole, [Parameter()] [System.String[]] $AssignedToRole, [Parameter()] [ValidateSet('Public', 'Private', 'HiddenMembership')] [System.String] $Visibility, [Parameter()] [Microsoft.Management.Infrastructure.CimInstance[]] $AssignedLicenses, [Parameter()] [ValidateSet('Present', 'Absent')] [System.String] $Ensure = 'Present', [Parameter()] [System.Management.Automation.PSCredential] $Credential, [Parameter()] [System.String] $ApplicationId, [Parameter()] [System.String] $TenantId, [Parameter()] [System.Management.Automation.PSCredential] $ApplicationSecret, [Parameter()] [System.String] $CertificateThumbprint, [Parameter()] [System.String] $CertificatePath, [Parameter()] [System.Management.Automation.PSCredential] $CertificatePassword, [Parameter()] [Switch] $ManagedIdentity, [Parameter()] [System.String[]] $AccessTokens ) #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 $compareParameters = Get-CompareParameters $result = Test-M365DSCTargetResource -DesiredValues $PSBoundParameters ` -ResourceName $($MyInvocation.MyCommand.Source).Replace('MSFT_', '') ` @compareParameters return $result } function Export-TargetResource { [CmdletBinding()] [OutputType([System.String])] param ( [Parameter()] [System.String] $Filter, [Parameter()] [System.Management.Automation.PSCredential] $Credential, [Parameter()] [System.String] $ApplicationId, [Parameter()] [System.String] $TenantId, [Parameter()] [System.Management.Automation.PSCredential] $ApplicationSecret, [Parameter()] [System.String] $CertificateThumbprint, [Parameter()] [System.String] $CertificatePath, [Parameter()] [System.Management.Automation.PSCredential] $CertificatePassword, [Parameter()] [Switch] $ManagedIdentity, [Parameter()] [System.String[]] $AccessTokens ) $ConnectionMode = New-M365DSCConnection -Workload 'MicrosoftGraph' ` -InboundParameters $PSBoundParameters #Ensure the proper dependencies are installed in the current environment. Confirm-M365DSCDependencies #region Telemetry $ResourceName = $MyInvocation.MyCommand.ModuleName -replace 'MSFT_', '' $CommandName = $MyInvocation.MyCommand $data = Format-M365DSCTelemetryParameters -ResourceName $ResourceName ` -CommandName $CommandName ` -Parameters $PSBoundParameters Add-M365DSCTelemetryEvent -Data $data #endregion try { $ExportParameters = @{ Filter = $Filter All = [switch]$true ExpandProperty = 'members' ErrorAction = 'Stop' Sort = 'DisplayName' } # Define the list of attributes $attributesToCheck = @( 'description', 'displayName', 'hasMembersWithLicenseErrors', 'mail', 'mailNickname', 'onPremisesSecurityIdentifier', 'onPremisesSyncEnabled', 'preferredLanguage' ) # Initialize a flag to indicate whether any attribute matches the condition $matchConditionFound = $false # Check each attribute in the list foreach ($attribute in $attributesToCheck) { if ($Filter -like "*$attribute eq *") { $matchConditionFound = $true break } } # If any attribute matches, add parameters to $ExportParameters if ($matchConditionFound -or ($Filter -like '*endsWith*') -or ($Filter -like '*not*')) { $ExportParameters.Add('CountVariable', 'count') $ExportParameters.Add('ConsistencyLevel', 'eventual') $ExportParameters.Remove('ExpandProperty') | Out-Null $Script:requireGroupMemberFetching = $true } # Exclude Distribution Groups and mail enabled security groups from Exchange [array] $Script:exportedGroups = Get-MgBetaGroup @ExportParameters $Script:exportedGroups = $Script:exportedGroups | Where-Object -FilterScript { -not ($_.MailEnabled -and ($null -eq $_.GroupTypes -or $_.GroupTypes.Count -eq 0)) -and ` -not ($_.MailEnabled -and $_.SecurityEnabled -and ($null -eq $_.GroupTypes -or $_.GroupTypes.Count -eq 0)) } | Sort-Object -Property DisplayName $i = 1 $dscContent = [System.Text.StringBuilder]::new() Write-M365DSCHost -Message "`r`n" -DeferWrite foreach ($group in $Script:exportedGroups) { if ($null -ne $Global:M365DSCExportResourceInstancesCount) { $Global:M365DSCExportResourceInstancesCount++ } Write-M365DSCHost -Message " |---[$i/$($Script:exportedGroups.Count)] $($group.DisplayName)" -DeferWrite $Params = @{ ApplicationSecret = $ApplicationSecret DisplayName = $group.DisplayName MailNickName = $group.MailNickName Members = @('toextract') SecurityEnabled = $true MailEnabled = $true Id = $group.Id Credential = $Credential ApplicationId = $ApplicationId TenantId = $TenantId CertificateThumbprint = $CertificateThumbprint CertificatePath = $CertificatePath CertificatePassword = $CertificatePassword ManagedIdentity = $ManagedIdentity.IsPresent AccessTokens = $AccessTokens } $Script:exportedInstance = $group $Results = Get-TargetResource @Params if ($null -ne $Results.AssignedLicenses) { $complexMapping = @( @{ Name = 'AssignedLicenses' CimInstanceName = 'AADGroupLicense' IsRequired = $False } ) $complexTypeStringResult = Get-M365DSCDRGComplexTypeToString ` -ComplexObject $Results.AssignedLicenses ` -CIMInstanceName 'AADGroupLicense' ` -ComplexTypeMapping $complexMapping if (-not [String]::IsNullOrWhiteSpace($complexTypeStringResult)) { $Results.AssignedLicenses = $complexTypeStringResult } else { $Results.Remove('AssignedLicenses') | Out-Null } } $currentDSCBlock = Get-M365DSCExportContentForResource -ResourceName $ResourceName ` -ConnectionMode $ConnectionMode ` -ModulePath $PSScriptRoot ` -Results $Results ` -Credential $Credential ` -NoEscape @('AssignedLicenses') [void]$dscContent.Append($currentDSCBlock) Save-M365DSCPartialExport -Content $currentDSCBlock ` -FileName $Global:PartialExportFileName Write-M365DSCHost -Message $Global:M365DSCEmojiGreenCheckMark -CommitWrite $i++ } return $dscContent.ToString() } catch { New-M365DSCLogEntry -Message 'Error during Export:' ` -Exception $_ ` -Source $($MyInvocation.MyCommand.Source) ` -TenantId $TenantId ` -Credential $Credential throw } } function Get-M365DSCAzureADGroupLicenses { [CmdletBinding()] [OutputType([System.Collections.Hashtable[]])] param( [Parameter(Mandatory = $true)] $AssignedLicenses ) $returnValue = @() if ($null -eq $Script:SubscribedSkus) { $Script:SubscribedSkus = Get-MgBetaSubscribedSku } # Create complete list of all Service Plans $allServicePlans = @() Write-Verbose -Message 'Getting all Service Plans' foreach ($sku in $Script:SubscribedSkus) { foreach ($serviceplan in $sku.ServicePlans) { if ($allServicePlans.Length -eq 0 -or -not $allServicePlans.ServicePlanName.Contains($servicePlan.ServicePlanName)) { $allServicePlans += @{ ServicePlanId = $serviceplan.ServicePlanId ServicePlanName = $serviceplan.ServicePlanName } } } } foreach ($assignedLicense in $AssignedLicenses) { $skuPartNumber = $Script:SubscribedSkus | Where-Object -FilterScript { $_.SkuId -eq $assignedLicense.SkuId } $disabledPlansValues = @() foreach ($plan in $assignedLicense.DisabledPlans) { $foundItem = $allServicePlans | Where-Object -FilterScript { $_.ServicePlanId -eq $plan } $disabledPlansValues += $foundItem.ServicePlanName } $currentLicense = @{ disabledPlans = $disabledPlansValues skuId = $skuPartNumber.SkuPartNumber -replace [char]0xFEFF } $returnValue += $currentLicense } return $returnValue } function Get-M365DSCCombinedLicenses { [CmdletBinding()] [OutputType([System.Object[]])] param( [Parameter()] [System.Object[]] $CurrentLicenses, [Parameter()] [System.Object[]] $DesiredLicenses ) $result = @() if ($currentLicenses.Length -gt 0) { foreach ($license in $CurrentLicenses) { Write-Verbose -Message "Including Current $license" $result += @{ skuId = $license.SkuId disabledPlans = $license.DisabledPlans } } } if ($DesiredLicenses.Length -gt 0) { foreach ($license in $DesiredLicenses) { $licenseSkuId = $license.SkuId if ($result.Length -eq 0) { $result += @{ skuId = $licenseSkuId disabledPlans = $license.DisabledPlans } } else { if (-not $result.skuId.Contains($licenseSkuId)) { $result += @{ skuId = $licenseSkuId disabledPlans = $license.DisabledPlans } } else { # Set the Desired Disabled Plans if the sku is already added to the list foreach ($item in $result) { if ($item.skuId -eq $licenseSkuId) { $item.disabledPlans = $license.disabledPlans } } } } } } return $result } function Get-CompareParameters { [CmdletBinding()] [OutputType([System.Collections.Hashtable])] param() return @{ PostProcessing = { param($DesiredValues, $CurrentValues, $ValuesToCheck, $ignore) if ($DesiredValues.ContainsKey('GroupLifecyclePolicySelectedEnabled') -and -not $CurrentValues.MailEnabled) { Write-Verbose -Message "Removing 'GroupLifecyclePolicySelectedEnabled' from comparison because group is not a Microsoft 365 Group." $ValuesToCheck.Remove('GroupLifecyclePolicySelectedEnabled') | Out-Null } return [System.Tuple[Hashtable, Hashtable, Hashtable]]::new($DesiredValues, $CurrentValues, $ValuesToCheck) } } } Export-ModuleMember -Function @('*-TargetResource', 'Get-CompareParameters') |