Public/Sync-EntraVacAccessPackage.ps1

function Sync-EntraVacAccessPackage {
    <#
    .SYNOPSIS
        Syncs membership for an Entra ID access package based on its auto-assignment policy filter.

    .DESCRIPTION
        Reads the auto-assignment policy filter for the given access package, evaluates it against
        all users in the tenant, and performs adminAdd/adminRemove to reconcile actual assignments.

    .PARAMETER AccessPackageId
        The object ID of the access package to sync.

    .PARAMETER WhatIf
        Preview changes without applying them.

    .EXAMPLE
        Sync-EntraVacAccessPackage -AccessPackageId "ad524555-24ef-412a-8d20-e070c088a42d"
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory)]
        [string] $AccessPackageId
    )

    # Get the auto-assignment policy for this package
    $policies = Invoke-MgGraphRequest -Method GET `
        -Uri "https://graph.microsoft.com/v1.0/identityGovernance/entitlementManagement/assignmentPolicies?`$filter=accessPackage/id eq '$AccessPackageId'" |
        Select-Object -ExpandProperty value

    $autoPolicy = $policies | Where-Object { $_.requestorSettings.scopeType -eq 'AllExistingDirectoryMemberUsers' -or $null -ne $_.automaticRequestSettings } |
        Select-Object -First 1

    if (-not $autoPolicy) {
        Write-Warning "No auto-assignment policy found for access package $AccessPackageId"
        return
    }

    # Get current assignments
    $assignments = Invoke-MgGraphRequest -Method GET `
        -Uri "https://graph.microsoft.com/v1.0/identityGovernance/entitlementManagement/assignments?`$filter=accessPackage/id eq '$AccessPackageId' and state eq 'Delivered'&`$expand=target" |
        Select-Object -ExpandProperty value

    $assignedUserIds = $assignments | ForEach-Object { $_.target.objectId }

    # Translate policy filter to Graph OData filter and fetch target users
    $graphFilter = Convert-PolicyFilterToGraphFilter -PolicyFilter $autoPolicy.automaticRequestSettings.requestorFilter
    $targetUsers  = Invoke-MgGraphRequest -Method GET `
        -Uri "https://graph.microsoft.com/v1.0/users?`$filter=$graphFilter&`$select=id" |
        Select-Object -ExpandProperty value

    $targetUserIds = $targetUsers | ForEach-Object { $_.id }

    # Diff
    $toAdd    = $targetUserIds | Where-Object { $_ -notin $assignedUserIds }
    $toRemove = $assignedUserIds | Where-Object { $_ -notin $targetUserIds }

    Write-Verbose "Access package $AccessPackageId - adding $($toAdd.Count), removing $($toRemove.Count)"

    foreach ($userId in $toAdd) {
        if ($PSCmdlet.ShouldProcess($userId, "adminAdd to access package $AccessPackageId")) {
            Invoke-MgGraphRequest -Method POST `
                -Uri "https://graph.microsoft.com/v1.0/identityGovernance/entitlementManagement/assignmentRequests" `
                -Body (@{
                    requestType   = 'adminAdd'
                    accessPackageAssignment = @{
                        targetId      = $userId
                        assignmentPolicyId = $autoPolicy.id
                        accessPackageId    = $AccessPackageId
                    }
                } | ConvertTo-Json -Depth 5) | Out-Null
        }
    }

    foreach ($assignment in ($assignments | Where-Object { $_.target.objectId -in $toRemove })) {
        if ($PSCmdlet.ShouldProcess($assignment.target.objectId, "adminRemove from access package $AccessPackageId")) {
            Invoke-MgGraphRequest -Method POST `
                -Uri "https://graph.microsoft.com/v1.0/identityGovernance/entitlementManagement/assignmentRequests" `
                -Body (@{
                    requestType              = 'adminRemove'
                    accessPackageAssignmentId = $assignment.id
                } | ConvertTo-Json -Depth 3) | Out-Null
        }
    }
}