Extract SharePoint Permission with PowerShell

Worth a direct correction before anything else: a script that connects with Connect-SPOService and then reads $site.RootWeb and $web.RoleAssignments was never going to work, independent of the auth method — Connect-SPOService connects to the SharePoint Online Management Shell’s tenant-admin cmdlets, and Get-SPOSite returns a simple property object with no RootWeb on it, not a live CSOM site object. Reading permissions off a site requires PnP PowerShell’s actual CSOM-backed cmdlets, connected directly to the site in question. This post covers the pattern that actually works, including the parts that fail silently rather than throwing an error if you get them slightly wrong.

In this post: Connecting directly to the site · Getting the site’s role assignments · Expanding SharePoint group membership · Exporting to CSV, correctly · Finding which lists and items have broken inheritance first · A real bug worth knowing about in recursive functions · Related reading


Connecting directly to the site

Reading permissions is a per-site operation, so connect PnP PowerShell to the specific site rather than the tenant admin URL. Interactive sign-in, not a username/password object — legacy username/password authentication was fully retired tenant-wide as of May 1, 2026 and no longer works at all, independent of this post’s other corrections:

Connect-PnPOnline -Url "https://yourtenant.sharepoint.com/sites/yoursite" -Interactive

Getting the site’s role assignments

Get-PnPWeb -Includes RoleAssignments is the real starting point — it loads the web object with its role assignments explicitly included, which matters because CSOM objects load lazily by default and an un-included property comes back empty rather than throwing an error, a common silent-failure trap:

$web = Get-PnPWeb -Includes RoleAssignments

foreach ($roleAssignment in $web.RoleAssignments) {
    Get-PnPProperty -ClientObject $roleAssignment -Property RoleDefinitionBindings, Member

    [PSCustomObject]@{
        Member      = $roleAssignment.Member.Title
        LoginName   = $roleAssignment.Member.LoginName
        Permissions = ($roleAssignment.RoleDefinitionBindings | Select-Object -ExpandProperty Name) -join ", "
    }
}

The `Get-PnPProperty` call inside the loop is doing real, necessary work, not defensive boilerplate — `RoleDefinitionBindings` and `Member` are both properties that need to be explicitly requested per role assignment, separately from the initial `-Includes` on the web object itself. Skipping it doesn’t error either; it just returns empty values for both, which is a genuinely confusing failure mode to debug without knowing this pattern ahead of time.


Select-Object Name on a RoleDefinitionBindings collection, then joined with -join, produces garbled object-notation text — -ExpandProperty Name is what actually extracts the plain role names.

Expanding SharePoint group membership

A role assignment’s `Member` is very often a SharePoint group, not an individual person — a top-level permission report showing “Marketing Team Members – Contribute” doesn’t answer the actual question of who that ends up meaning in practice. Where the member is a group, its user list needs a separate, explicit load, the same lazy-loading pattern as above:

if ($roleAssignment.Member.PrincipalType -eq "SharePointGroup") {
    Get-PnPProperty -ClientObject $roleAssignment.Member -Property Users
    $roleAssignment.Member.Users | ForEach-Object {
        [PSCustomObject]@{
            Group     = $roleAssignment.Member.Title
            Member    = $_.Title
            LoginName = $_.LoginName
        }
    }
}

Worth noting as a real, separate limitation rather than something this script can just fix: a SharePoint group can itself contain an Entra security group as a member, not just individual users — expanding that level requires a Graph call against the security group’s membership, a genuinely different API from anything covered above, not a deeper CSOM property to include.


Exporting to CSV, correctly

Putting the site-level and group-expansion pieces together into one export:

$web = Get-PnPWeb -Includes RoleAssignments
$results = foreach ($roleAssignment in $web.RoleAssignments) {
    Get-PnPProperty -ClientObject $roleAssignment -Property RoleDefinitionBindings, Member
    $permissions = ($roleAssignment.RoleDefinitionBindings | Select-Object -ExpandProperty Name) -join ", "

    if ($roleAssignment.Member.PrincipalType -eq "SharePointGroup") {
        Get-PnPProperty -ClientObject $roleAssignment.Member -Property Users
        foreach ($user in $roleAssignment.Member.Users) {
            [PSCustomObject]@{ Group = $roleAssignment.Member.Title; Member = $user.Title; LoginName = $user.LoginName; Permissions = $permissions }
        }
    } else {
        [PSCustomObject]@{ Group = ""; Member = $roleAssignment.Member.Title; LoginName = $roleAssignment.Member.LoginName; Permissions = $permissions }
    }
}

$results | Export-Csv -Path "C:\Path\To\Export\Permissions.csv" -NoTypeInformation

Finding which lists and items have broken inheritance first

Everything above extracts a site’s own top-level permissions — worth knowing before assuming that’s the whole picture: any list, folder, or item can have permission inheritance broken individually, at which point its own permissions diverge entirely from the site’s. Checking for this before running a full extraction is worth doing, since a broken-inheritance item won’t show up at all in a site-level-only report:

$list = Get-PnPList -Identity "Documents" -Includes HasUniqueRoleAssignments
if ($list.HasUniqueRoleAssignments) {
    Write-Host "The list itself has broken inheritance -- check its own role assignments separately."
}

Get-PnPListItem -List "Documents" -PageSize 500 | ForEach-Object {
    Get-PnPProperty -ClientObject $_ -Property HasUniqueRoleAssignments
    if ($_.HasUniqueRoleAssignments) {
        Write-Host "Item $($_.Id) has broken inheritance"
    }
}

On a large library, checking every item’s `HasUniqueRoleAssignments` individually is a real, meaningful amount of extra work — each check is its own round trip against the lazily-loaded property. Worth running as a deliberate, occasional audit rather than baking into every routine extraction, and worth pairing with `-PageSize` the same way any other large-list operation needs it to avoid the 5,000-item list view threshold.


A real bug worth knowing about in recursive functions

Worth flagging as a genuinely common PowerShell mistake, not specific to SharePoint: a function that builds up results by taking an array parameter and appending to it with `$outputArray += $item` doesn’t actually return anything to the caller. PowerShell arrays are fixed-size and passed by value — `+=` inside the function creates a new local array and discards it when the function returns, silently. A function meant to accumulate results across recursive calls needs to either emit objects to the pipeline (letting the caller collect them with `$results = MyFunction …`) or use a genuinely mutable reference type like `[System.Collections.Generic.List[object]]` passed in explicitly. The script above sidesteps the whole issue by using `foreach` with pipeline output instead of a manually-accumulated array, which is the simpler fix in most cases.



The pattern above — explicit property loading, expanding group membership deliberately, and pipeline output instead of array mutation — generalizes to most CSOM-backed permission scripts, not just this one; worth internalizing the shape of it rather than treating each script as a one-off.

App Catalog Authentication Automation Backup Compliance Content Type CSS Flows Google Javascript Limitations List Metadata MFA Microsoft Node NodeJs O365 OneDrive Permissions PnP PnPJS Policy PowerApps Power Automate PowerAutomate PowerPlatform PowerShell React ReactJs Rest API Rest Endpoint Security Send an HTTP Request to SharePoint SharePoint SharePoint List SharePoint Modern SharePoint Online SPFX SPO Sync Tags Teams Termstore Versioning

1 thought on “Extract SharePoint Permission with PowerShell”

  1. Kleurplaat Voetbal

    Terrific post however , I was wanting to know if you
    could write a litte more on this topic? I’d be very thankful if you could elaborate a little bit more.
    Thanks!

Leave a Comment

Your email address will not be published. Required fields are marked *