Mass reconciling / reprocessing access packages into groups

I just had a customer where there were around 3000 access package assignments that has not successfully added users to groups, for some reason. The way to fix this is to find the assignment and click “Reprocess”:

Clicking this button thousands of times is not really how I wanted to spend my evening, so we therefore wanted to figure out which users were affected, and triggering a reprocess for all of these.

In order to achieve this, we use PowerShell 7, with the Microsoft Graph module. First we connect and get all access packages and assignments from our tenant:

Install-Module Microsoft.Graph -Scope CurrentUser

Connect-MgGraph -Scopes "EntitlementManagement.ReadWrite.All", "Group.Read.All"
$groupcache = @{}

Write-Verbose "Getting all access packages"
$accessPackages = Get-MgEntitlementManagementAccessPackage -All -ExpandProperty "ResourceRoleScopes(`$expand=role,scope)" -Debug -Verbose

Write-Verbose "Getting all access package assignments"
$assignments = Get-MgEntitlementManagementAssignment -All -PageSize 999 -ExpandProperty "AccessPackage", "Target"

Next, we create a for each loop that loops through all assignments for all access packages, and for each group these are assigning permissions for – to create a PSCustomObject. This means that the $result variable will be a list of all assignments, with the IsMember property set to true or false, and we are looking for the false ones:

Write-Verbose "Create report over access package assignments and group memberships"
$result = $accessPackages | ForEach-Object {
    $accessPackage = $_ # For dev purposes: $accessPackage = $accessPackages | get-random -count 1
    
    $_assignments = $assignments | Where-Object { $_.AccessPackage.Id -eq $accessPackage.Id }

    if (!$_assignments) {
        Write-Verbose "No assignments found for access package: $($accessPackage.DisplayName)"
        return
    }

    $count = $_assignments | measure-object | select-object -expandproperty count
    Write-Verbose "Found $count assignments for access package: $($accessPackage.DisplayName)"

    $accessPackageMemberGroups = $accessPackage.ResourceRoleScopes | Where-Object { $_.scope.originSystem -eq "AadGroup" -and $_.role.originId -like "Member_*" } | foreach-object { $_.scope.originId }
    
    if ($accessPackageMemberGroups) {
        $accessPackageMemberGroups | foreach-object {
            Write-Verbose "Processing group ID: $_"
            $groupcache[$_] ??= Get-MgGroup -GroupId $_ -ErrorAction SilentlyContinue
            if ($groupcache[$_]) {
                $groupcache["$($_)_members"] ??= Get-MgGroupMember -GroupId $groupcache[$_].Id -All -PageSize 999
                $memberCount = $groupcache["$($_)_members"] | measure-object | select-object -expandproperty count
                Write-Verbose "Group $($groupcache[$_].DisplayName) has $memberCount members."
                $membersMap = ($groupcache["$($_)_members"] | Group-Object -Property Id -AsHashTable) ?? @{}

                $_assignments | ForEach-Object {
                    [PSCustomObject]@{
                        AccessPackageName     = $accessPackage.DisplayName
                        AccessPackageId       = $accessPackage.Id
                        AssignmentId          = $_.Id
                        AssignmentState       = $_.State
                        AssignmentStatus      = $_.Status
                        
                        AssignedTo            = $_.Target.Id
                        AssignedToUPN         = $_.Target.PrincipalName
                        AssignedToSubjectType = $_.Target.SubjectType
                        AssignedToDisplayName = $_.Target.DisplayName

                        GroupName             = $groupcache[$_].DisplayName
                        GroupId               = $groupcache[$_].Id
                        IsMember              = $membersMap.ContainsKey($_.Target.Id)
                    }
                }
            }
            else {
                Write-Warning "Group with ID $_ not found."
            }
        }
    }
    else {
        Write-Verbose "Access package: $($accessPackage.DisplayName) does not grant membership to any groups."
    }
}

We can now use the $result variable, looping through it and calling the reprocess endpoint:

# Output all results
$result | Out-gridview

# Find missing memberships
$missing = $result | Where-Object AssignmentState -ne "Expired" | Where-Object IsMember -eq $false

# Invoke reprocess for any assignment that has missing memberships
$_group = $missing | Group-Object AssignmentId
$inc = 0
$_group | ForEach-Object {
    $inc += 1
    Write-Progress -Activity "Invoking reprocess for missing memberships" -Status "Processing assignment $inc of $($_group.Count)" -PercentComplete (($inc / $_group.Count) * 100)
    $entry = $_.Group[0]
    Write-Output "Invoking reprocessing for`n - Assignment id: $($entry.AssignmentId)`n - User: $($entry.AssignedToUPN)`n - Access package: $($entry.AccessPackageName)"

    Invoke-MgGraphRequest -Method POST -Uri "https://graph.microsoft.com/v1.0/identityGovernance/entitlementManagement/assignments/$($entry.AssignmentId)/reprocess" | Out-Null
}
Write-Progress -Activity "Invoking reprocess for missing memberships" -Completed -Status "Done"

Leave a comment