GpadTools.psm1

<#
Disclaimer: The scripts are not supported under any Microsoft standard support program or service.
The scripts are provided AS IS without warranty of any kind. Microsoft further disclaims all implied
warranties including, without limitation, any implied warranties of merchantability or of fitness for a
particular purpose. The entire risk arising out of the use or performance of the scripts and
documentation remains with you. In no event shall Microsoft, its authors, or anyone else involved in the
creation, production, or delivery of the scripts be liable for any damages whatsoever (including, without
limitation, damages for loss of business profits, business interruption, loss of business information, or
other pecuniary loss) arising out of the use of or inability to use the scripts or documentation,
even if Microsoft has been advised of the possibility of such damages.
#>


#----------------------------------------------------------------------------------------------------------
#
# Copyright © 2024 Microsoft Corporation. All rights reserved.
#
#----------------------------------------------------------------------------------------------------------
#
# NAME:
# Group Provisioning to AD (GPAD) Module for Entra CloudSync
#
#----------------------------------------------------------------------------------------------------------
#
# RELEASE NOTES
#
# Version 0.0.1 - 2024-10-14
# - Beta Release:
# Get-GpadGroups
# Get-GpadSyncedGroupMembers
# Get-GpadSynchronizationIdentifiers
# Get-GpadWritebackEnabledExtensionName
# Get-GpadWritebackEnabledGroups
# Set-GpadWritebackEnabledExtension
# Start-GpadOnDemandProvisionGroup
# Confirm-GpadReconciliationNeeded
# Update-GpadWritebackEnabledExtension
#
#----------------------------------------------------------------------------------------------------------

<#
.SYNOPSIS
    Group Provisioning to Active Directory (Gpad) Tools Module.
 
.DESCRIPTION
    This module provides functions to manage group provisioning from Microsoft Entra ID to Active Directory using Microsoft Graph PowerShell SDK.
 
.PREREQUISITES
    - Microsoft Graph PowerShell SDK: Install-Module Microsoft.Graph
    - Microsoft Active Directory PowerShell Module: Install-WindowsFeature RSAT-AD-Tools
 
.USAGE
    Connect to Microsoft Graph before using the functions:
    Connect-MgGraph -Scopes "Application.ReadWrite.All, Directory.ReadWrite.All, Synchronization.ReadWrite.All"
#>


$ErrorActionPreference = [System.Management.Automation.ActionPreference]::Stop


<#
.SYNOPSIS
    Get the name of the custom extension property for writeback enabled.
 
.DESCRIPTION
    Retrieves the name of the custom extension property that indicates if writeback is enabled.
 
.EXAMPLE
    Get-GpadWritebackEnabledExtensionName
 
.OUTPUTS
    System.String. The name of the custom extension property.
 
#>

function Get-GpadWritebackEnabledExtensionName 
{
    [CmdletBinding()]
    param ()

    # Import Graph modules
    try
    {
        Import-Module Microsoft.Graph.Authentication
        Import-Module Microsoft.Graph.Applications
    }
    catch
    {
        Throw "'CloudSyncCustomExtensionsApp' application not found. Error details: $($_.Exception.Message)"
    }

    # Get CloudSyncCustomExtensionsApp
    try
    {
        $tenantId = (Get-MgOrganization).Id
        $cloudSyncCustomExtApp = Get-MgApplication -Filter "identifierUris/any(uri:uri eq 'api://$tenantId/CloudSyncCustomExtensionsApp')"
    }
    catch
    {
        Throw "'CloudSyncCustomExtensionsApp' application not found. Error details: $($_.Exception.Message)"
    }

    # Get Custom Extension Property Name
    try 
    {
        $gwbEnabledExtAttrib = Get-MgBetaApplicationExtensionProperty -ApplicationId $cloudSyncCustomExtApp.Id | 
            Where-Object { $_.Name -Like '*WritebackEnabled' } | Select-Object -First 1
        if (-not $gwbEnabledExtAttrib) 
        {
            Throw "Failed to retrieve 'WritebackEnabled' custom extension property."
        }
        $gwbEnabledExtName = $gwbEnabledExtAttrib.Name
    }
    catch 
    {
        Throw "Failed to get 'WritebackEnabled' custom extension property. Error details: $($_.Exception.Message)"
    }
    
    return  $gwbEnabledExtName
}


<#
.SYNOPSIS
    Get synchronization identifiers for Microsoft Entra ID to Active Directory Provisioning.
 
.DESCRIPTION
    Retrieves the service principal ID, synchronization job ID, and synchronization rules ID for a given domain name.
 
.PARAMETER DomainName
    The domain name to get the synchronization identifiers for.
 
.EXAMPLE
    Get-GpadSynchronizationIdentifiers -DomainName Contoso.com
 
.OUTPUTS
    PSCustomObject. Contains ServicePrincipalId, SynchronizationJobId, and SynchronizationRulesId.
#>

function Get-GpadSynchronizationIdentifiers 
{
    [CmdletBinding()]
    param (
        # Group Identifier
        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=0)]
        [string] $DomainName
    )

    # Get "Microsoft Entra ID to Active Directory Provisioning" Application from App template Id
    # Note: Getting service principal (SP) by display name is not feasible because all Cloud Sync SPs use the domain name so it's not possible to determine which SP is
    # for Entra ID to Active Directory Provisioning"
    # Reference: https://learn.microsoft.com/en-us/graph/api/resources/synchronization-overview?view=graph-rest-beta&preserve-view=true
    $entraIDToADapp = Get-MgApplication -Filter "ApplicationTemplateId eq 'fb81332f-3eca-4ecf-a939-4278e501d330'" -Property id, AppId, DisplayName  |
        Where-Object { $_.DisplayName -eq  $DomainName }

    if ($entraIDToADapp)
    {
        # Get the Service Principal
        $entraIDToADsp = Get-MgServicePrincipal -Filter "AppId eq '$($entraIDToADapp.AppId)'"

        # Get the Synchronization Job
        $entraIDToADjob = Get-MgServicePrincipalSynchronizationJob -ServicePrincipalId $entraIDToADsp.Id -Property Id

        # Get the Synchronization Rule Id
        $entraIDToADschema = Get-MgServicePrincipalSynchronizationJobSchema -ServicePrincipalId $entraIDToADsp.Id -SynchronizationJobId $entraIDToADjob.Id -Property * 

        $result = [PSCustomObject]@{
            ServicePrincipalId = $entraIDToADsp.Id
            SynchronizationJobId    =  $entraIDToADjob.Id
            SynchronizationRulesId = $entraIDToADschema.SynchronizationRules.id
        }
    }
    else 
    {
        Throw "Microsoft Entra ID to Active Directory Provisioning for '$DomainName' domain not found."
    }
    Return $result
}


<#
.SYNOPSIS
    Retrieves security groups with WritebackEnabled extension set to true.
 
.DESCRIPTION
    This function retrieves all security groups where the WritebackEnabled extension is set to true.
    It excludes cloud M365/Unified groups that have WritebackEnabled extension set to False/null.
 
.EXAMPLE
    Get-GpadWritebackEnabledGroups
#>

function Get-GpadWritebackEnabledGroups
{
    [CmdletBinding()]
    param ()

    # Get all Security groups with WritebackEnabled extension == True
    $gwbEnabledExtName = Get-GpadWritebackEnabledExtensionName
    $p = @('DisplayName', 'Id', 'GroupTypes', 'OnPremisesSyncEnabled', 'WritebackConfiguration', $gwbEnabledExtName)
    $groups = Get-MgBetaGroup -Property $p -ConsistencyLevel eventual -All  | Where-Object {$_.OnPremisesSyncEnabled -ne 'True'}
    foreach ($group in $groups) 
    {
        if ((-not ($group.GroupTypes -Match 'Unified')) -and $group.AdditionalProperties.$gwbEnabledExtName) 
        {
            $group | Select-Object DisplayName, Id, `
                @{Name = 'WritebackConfiguration_IsEnabled'; Expression = { $_.WritebackConfiguration.IsEnabled } }, `
                @{Name = $gwbEnabledExtName; Expression = { $_.AdditionalProperties.$gwbEnabledExtName } }
        }
    }
}


<#
.SYNOPSIS
    Get members of a group that are synced from on-premises AD.
 
.DESCRIPTION
    Retrieves the members of a specified group that are synchronized from on-premises Active Directory.
 
.PARAMETER GroupId
    The ID of the group to get members for.
 
.EXAMPLE
    Get-GpadSyncedGroupMembers -GroupId '3b100a44-2fdc-48d6-a72e-aefa9835c3e0'
 
.OUTPUTS
    System.Collections.ArrayList. A list of members with object ID and type.
#>

function Get-GpadSyncedGroupMembers 
{
    [CmdletBinding()]
    param 
    (
        # Group Identifier
        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=0)]
        [string] $GroupId
    )
    
    Write-Host "Getting membership for group '$GroupId'..." -ForegroundColor Cyan
    $members = Get-MgGroupMember -GroupId $GroupId -All -Property Id, onPremisesSyncEnabled

    $membersHt = [System.Collections.ArrayList] @()
    ForEach ($m in $members)
    {
        if ($m.AdditionalProperties.onPremisesSyncEnabled)
        {
            switch ($m.AdditionalProperties.'@odata.type') 
            {
                '#microsoft.graph.user' { $type = 'User'}
                '#microsoft.graph.group' { $type = 'Group' }
                Default { continue } # unknown object type, skipped!
            }
    
            $entry = @{
                objectId       = $m.Id
                objectTypeName = $type
            }
            $membersHt += $entry
        }
    }
    return $membersHt
}


<#
.SYNOPSIS
    Updates the WritebackEnabled extension for security groups.
 
.DESCRIPTION
    This function updates the WritebackEnabled extension property for all security groups based on
    their WritebackConfiguration property. It only processes groups where WritebackConfiguration
    property differs from WritebackEnabled extension.
 
.EXAMPLE
    Update-GpadWritebackEnabledExtension
#>

function Update-GpadWritebackEnabledExtension
{
    [CmdletBinding()]
    param ()
    
    Write-Host "Updating security groups' WritebackEnabled extension property. Please wait..." -ForegroundColor Cyan

    # Properties to fetch from groups
    $gwbEnabledExtName = Get-GpadWritebackEnabledExtensionName
    $p = @('DisplayName', 'Id', 'MailNickname', 'Description', 'GroupTypes', 'OnPremisesSyncEnabled', 'WritebackConfiguration', $gwbEnabledExtName)

    # Get cloud groups
    try 
    {
        $groups = Get-MgBetaGroup -Property $p -ConsistencyLevel eventual -All  | Where-Object {$_.OnPremisesSyncEnabled -ne 'True'}
        Write-Verbose "$($groups.count) cloud groups returned..."
    } 
    catch 
    {
        Throw "Failed to retrieve groups. Error details: $($_.Exception.Message)"
    }

    # Output groups with Writeback property and WritebackEnabled extension
    foreach ($group in $groups)
    {
        # Filter security groups only
        if (-not ($group.GroupTypes -Match 'Unified'))
        {
            $shouldUpdate = ($group.WritebackConfiguration.IsEnabled -and (-not $group.AdditionalProperties.$gwbEnabledExtName)) -or
                            ((-not $group.WritebackConfiguration.IsEnabled) -and $group.AdditionalProperties.$gwbEnabledExtName)
            Write-Verbose "Group '$($group.DisplayName)' needs update: $shouldUpdate"
            if ($shouldUpdate)
            {
                $updatedValue = [bool]$group.WritebackConfiguration.IsEnabled
                try
                {
                    Update-MgGroup -GroupId $group.Id -AdditionalProperties @{$gwbEnabledExtName = $updatedValue }
                    # show update
                    $group.AdditionalProperties.$gwbEnabledExtName = $updatedValue
                    $group | Select-Object DisplayName, Id, `
                        @{Name = 'WritebackConfiguration_IsEnabled'; Expression = { $_.WritebackConfiguration.IsEnabled } }, `
                        @{Name = $gwbEnabledExtName; Expression = { $_.AdditionalProperties.$gwbEnabledExtName } }
                }
                catch
                {
                    Write-Error "There was an error updating group '$($group.Id)'. Error Details: $($_.Exception.Message)"
                }
            }
        }
    }
}

<#
.SYNOPSIS
    Lists security groups and outputs WritebackConfiguration property and WritebackEnabled extension.
 
.DESCRIPTION
    This function retrieves and lists all security groups showing their WritebackConfiguration property
    and WritebackEnabled extension.
 
.EXAMPLE
    Get-GpadGroups
#>

function Get-GpadGroups
{
    [CmdletBinding()]
    param ()

    # Get WritebackEnabled Extension name
    $gwbEnabledExtName = Get-GpadWritebackEnabledExtensionName
    $p = @('DisplayName', 'Id', 'GroupTypes', 'OnPremisesSyncEnabled', 'WritebackConfiguration', $gwbEnabledExtName)

    try
    {
        # NOTE: Cannot use Get-MgGroup (non-beta) as it doesn't retrieve WritebackConfiguration.IsEnabled property
        $groups = Get-MgBetaGroup -Property $p -ConsistencyLevel eventual -All  | Where-Object {$_.OnPremisesSyncEnabled -ne 'True'}
    }
    catch
    {
        Throw "Failed to retrieve groups. Error details: $($_.Exception.Message)"
    }

    foreach ($group in $groups)
    {
        # Ensure the group is not a Unified group
        if (-not ($group.GroupTypes -Match 'Unified'))
        {
            $group | Select-Object DisplayName, Id, `
                @{Name = 'WritebackConfiguration_IsEnabled'; Expression = { $_.WritebackConfiguration.IsEnabled } }, `
                @{Name = $gwbEnabledExtName; Expression = { $_.AdditionalProperties.$gwbEnabledExtName } }
        }
    }
}

<#
.SYNOPSIS
Checks if reconciliation is needed between an Entra ID group and its corresponding AD group.
 
.DESCRIPTION
This function retrieves the members of an Entra ID group and its corresponding AD group, compares its members,
and determines if there are any discrepancies. If discrepancies are found, it indicates that reconciliation
is needed.
 
.PARAMETER GroupId
The identifier(s) of the Entra ID group(s) to be checked.
 
.PARAMETER GroupWritebackOU
(Optional) The organizational unit for the group writeback. This is used as the search base when looking for
written back groups in AD.
 
.EXAMPLE
PS C:\> Confirm-GpadReconciliationNeeded -GroupId "07b12a06-8f02-4526-9739-b3f9fae3c9fb"
 
.EXAMPLE
PS C:\> Confirm-GpadReconciliationNeeded -GroupId "07b12a06-8f02-4526-9739-b3f9fae3c9fb" -GroupWritebackOU "OU=Groups,DC=example,DC=com"
 
.EXAMPLE
PS C:\> $groups = '3b100a44-2fdc-48d6-a72e-aefa9835c3e0', '07b12a06-8f02-4526-9739-b3f9fae3c9fb' |
            Confirm-GpadReconciliationNeeded -Verbose
.NOTES
This function uses the Microsoft Graph module to retrieve Entra ID group members and the Active Directory
module to retrieve AD group members.
#>

function Confirm-GpadReconciliationNeeded
{    
    [CmdletBinding()]
    param 
    (
        # Group Identifier
        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   ValueFromPipeline=$true,
                   Position=0)]
        [string[]] $GroupId,

        # Cloud Sync Group Writeback OU in Active Directory
        [Parameter(Mandatory=$false,
                   ValueFromPipelineByPropertyName=$false,
                   Position=1)]
        [string] $GroupWritebackOU
    )
    
    begin {}
    process
    {
        Write-Verbose "Getting membership for Entra ID group '$GroupId'..."
        $allCloudGroupMembers = Get-MgGroupMember -GroupId $GroupId[0] -All -Property Id, onPremisesSyncEnabled, onPremisesSecurityIdentifier

        $allCloudGroupSyncedMembersSid = [System.Collections.ArrayList] @()
        ForEach ($m in $allCloudGroupMembers)
        {
            if ($m.AdditionalProperties.onPremisesSyncEnabled)
            {
                $allCloudGroupSyncedMembersSid += $m.AdditionalProperties.onPremisesSecurityIdentifier
            }
        }
        Write-Verbose "Entra ID group '$GroupId' has $($allCloudGroupSyncedMembersSid.Count) synced members."
        
        # Get the respetive written back group from AD
        Write-Verbose "Getting membership for AD group with 'Group_$GroupId' anchor..."
        try
        {
            if ([string]::IsNullOrEmpty($GroupWritebackOU))
            {
                $adGroup = Get-ADObject -Filter "adminDescription -eq 'Group_$($GroupId[0])'" -Properties member
            }
            else 
            {
                $adGroup = Get-ADObject -Filter "adminDescription -eq 'Group_$($GroupId[0])'" -SearchBase $GroupWritebackOU -Properties member
            }
        }
        catch
        {
            Throw "Failed to retrieve members from AD group with 'Group_$GroupId' anchor. Error details: $($_.Exception.Message)"
        }

        if ($adGroup -eq $null)
        {
            Write-Verbose "Failed to find AD group with 'Group_$GroupId' anchor. Group needs reprovisoning."
            [bool] $groupNotProvisioned = $true
        }
        else 
        {
            try
            {
                $allAdGroupMembersSid = ($adGroup.member | Get-ADObject -Properties objectSid | select -ExpandProperty objectSid).Value
            }
            catch
            {
                Throw "Failed to retrieve objectSid from AD group members with 'Group_$GroupId' anchor. Error details: $($_.Exception.Message)"
            }
    
            Write-Verbose "AD group with 'Group_$GroupId' anchor has $($allAdGroupMembersSid.Count) synced members."
    
            # Get the members that are not present in both groups
            $membersNotInCommon = [System.Collections.ArrayList] @()
            $membersNotInCommon = ($allAdGroupMembersSid | Where {$allCloudGroupSyncedMembersSid -NotContains $_}) 
            $membersNotInCommon += ($allCloudGroupSyncedMembersSid | Where {$allAdGroupMembersSid -NotContains $_})    
        }

        $result = "" | select Id, ReconciliationNeeded
        $result.Id = $GroupId[0]

        if ($membersNotInCommon.Count)
        {
            Write-Warning "Entra ID group '$GroupId' has $($membersNotInCommon.Count) synced members diverging."
            $result.ReconciliationNeeded = $true
        }
        elseif ($groupNotProvisioned)
        {
            Write-Warning "Entra ID group '$GroupId' is not provisioned in Active Directory."
            $result.ReconciliationNeeded = $true
        }
        else
        {
            Write-Verbose "Entra ID group '$GroupId' is consistent."
            $result.ReconciliationNeeded = $false
        }
        return $result
    }
    
    end {}
}


<#
.SYNOPSIS
    Call synchronization job on-demand for Entra ID groups.
 
.DESCRIPTION
    Triggers synchronization job on-demand for groups.
 
.PARAMETER GroupId
    The ID of the group to trigger synchronization for.
    Accepts multiple group IDs from the pipeline.
 
.PARAMETER ServicePrincipalId
    The service principal ID for the synchronization job.
 
.PARAMETER SynchronizationJobId
    The synchronization job ID.
 
.PARAMETER SynchronizationRulesId
    The synchronization rules ID.
 
.PARAMETER GroupWritebackOU
    (Optional) The organizational unit for the group writeback. This is used as the
    search base when looking for written back groups in AD.
 
.EXAMPLE
    $onDemandProv = Get-GpadSynchronizationIdentifiers -DomainName 'Contoso.com'
    Start-GpadOnDemandProvisionGroup -GroupId '3b100a44-2fdc-48d6-a72e-aefa9835c3e0'
                    -ServicePrincipalId $onDemandProv.ServicePrincipalId `
                    -SynchronizationJobId $onDemandProv.SynchronizationJobId `
                    -SynchronizationRulesId $onDemandProv.SynchronizationRulesId `
                    -GroupWritebackOU 'OU=GWB,OU=SYNC,DC=Contoso,DC=com'
 
.EXAMPLE
    $onDemandProv = Get-GpadSynchronizationIdentifiers -DomainName 'Contoso.com'
    '3b100a44-2fdc-48d6-a72e-aefa9835c3e0', 'b92b5d22-e5d4-497e-b4b2-2b288c6fba8d' |
        Start-GpadOnDemandProvisionGroup `
            -ServicePrincipalId $onDemandProv.ServicePrincipalId `
            -SynchronizationJobId $onDemandProv.SynchronizationJobId `
            -SynchronizationRulesId $onDemandProv.SynchronizationRulesId
#>

function Start-GpadOnDemandProvisionGroup
{
    [CmdletBinding()]
    param 
    (
        # Group Identifier
        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   ValueFromPipeline=$true,
                   Position=0)]
        [string[]] $GroupId,

        # Cloud Sync ServicePrincipalId
        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$false,
                   Position=1)]
        [string] $ServicePrincipalId,

        # Cloud Sync SynchronizationJobId
        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$false,
                   Position=2)]
        [string] $SynchronizationJobId,

        # Cloud Sync SynchronizationJobId
        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$false,
                   Position=3)]
        [string] $SynchronizationRulesId,

        # Cloud Sync Group Writeback OU in Active Directory
        [Parameter(Mandatory=$false,
                   ValueFromPipelineByPropertyName=$false,
                   Position=4)]
        [string] $GroupWritebackOU
    )
   
    begin {}
    process
    {
        # Get cloud group members from Entra ID
        $allGroupMembersHt = Get-GpadSyncedGroupMembers -GroupId $GroupId[0]
        Write-Host "Processing group '$GroupId' with '$($allGroupMembersHt.count)' members. Please wait..." -ForegroundColor Cyan
        
        # Get the respetive written back group from AD
        try
        {
            if ([string]::IsNullOrEmpty($GroupWritebackOU))
            {
                $adGroup = Get-ADObject -Filter "adminDescription -eq 'Group_$($GroupId[0])'" -Properties member
            }
            else 
            {
                $adGroup = Get-ADObject -Filter "adminDescription -eq 'Group_$($GroupId[0])'" -SearchBase $GroupWritebackOU  -Properties member
            }
        }
        catch
        {
            Throw "Failed to retrieve members from AD group with 'Group_$GroupId' anchor. Error: $($_.Exception.Message)"
        }

        # Obsolete members in group written back to AD
        try
        {
            if ($adGroup.member.count)
            {
                Remove-ADGroupMember -Identity $adGroup -Members $adGroup.member -Confirm:$false
            }
        }
        catch
        {
            Throw "Failed to remove members from AD group with 'Group_$GroupId' anchor. Error: $($_.Exception.Message)"
        }
    
        # Reconcile members, split by membersLimit
        $membersLimit = 5
        $i = 0
        while ($i -lt $allGroupMembersHt.Count)
        {        
            $groupMembersHt = [System.Collections.ArrayList] @()
            for ($j = 0; $j -lt $membersLimit; $j++) 
            {
                if ($i -lt $allGroupMembersHt.Count)
                {
                    $groupMembersHt += $allGroupMembersHt[$i]
                    $i++   
                }
            }
            
            # Build body parameter with members
            Write-Host "Processing $($groupMembersHt.Count) members... " -ForegroundColor Cyan
            $params = @{
                parameters = @(
                    @{
                        ruleId   = $SynchronizationRulesId
                        subjects = @(
                            @{
                                objectId       = $GroupId[0]
                                objectTypeName = "Group"
                                links          = @{
                                    members = @($groupMembersHt)
                                }                    
                            }
                        )
                    }
                )
            }

            # Call Synchronization Job On-Demand
            $result = New-MgServicePrincipalSynchronizationJobOnDemand `
                                    -ServicePrincipalId $ServicePrincipalId `
                                    -SynchronizationJobId $SynchronizationJobId `
                                    -BodyParameter $params        

            $result.Value | ConvertFrom-Json
        }
    }
    end {}
}

<#
.SYNOPSIS
    Sets the WritebackEnabled property and extension for a cloud group.
 
.DESCRIPTION
    This function updates (True/False) the WritebackEnabled property from group schema and the
    WritebackEnabled extension in directory schema for a given cloud group.
    Accepts multiple Group Ids from the pipeline.
 
.EXAMPLE
    Set-GpadWritebackEnabledExtension -GrouppId 'b92b5d22-e5d4-497e-b4b2-2b288c6fba8d' -Value $true
#>

function Set-GpadWritebackEnabledExtension 
{
    [CmdletBinding()]
    param 
    (
        # Group Identifier
        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   ValueFromPipeline=$true,
                   Position=0)]
        [string[]] $GroupId,

        # Group Identifier
        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   ValueFromPipeline=$false,
                   Position=1)]
        [string] $Value
    )
    begin 
    {
        $gwbEnabledExtName = Get-GpadWritebackEnabledExtensionName
    }
    process 
    {
        # Set Group Writeback flag
        
        Update-MgGroup -GroupId $GroupId[0] -AdditionalProperties @{$gwbEnabledExtName = $Value}  

        # Set Group Writeback flag
        Update-MgBetaGroup -GroupId $GroupId[0] -WritebackConfiguration @{isEnabled = $Value}  
    }
    end{}
}

#----------------------------------------------------------------------------------------------------------
# TODO:
# - Start-GpadOnDemandProvisionGroup is not accepting PropertyName from Get-GpadSynchronizationIdentifiers
# using pipelining (issue). E.g., Get-GpadSynchronizationIdentifiers Contoso.com |
# Start-GpadOnDemandProvisionGroup - GroupId 3b100a44-2fdc-48d6-a72e-aefa9835c3e0
#
#----------------------------------------------------------------------------------------------------------









#----------------------------------------------------------------------------------------------------------
# --- MAIN ----
$introMsg = @"
Welcome to Group Provisioning for Active Directory module (BETA). This module has the following pre-requisites:
- Microsoft Graph PowerShell SDK: Install-Module Microsoft.Graph
- Microsoft Active Directory PowerShell Module: Install-WindowsFeature RSAT-AD-Tools
 
Before starting, connect to Microsoft Graph:
Connect-MgGraph -Scopes "Application.ReadWrite.All, Directory.ReadWrite.All, Synchronization.ReadWrite.All"
 
"@

Write-Host $introMsg -ForegroundColor Cyan