Skip to content

Set-GlobalSiteAdmin

Adds a site collection administrator to all SharePoint sites.

Syntax

.\Set-GlobalSiteAdmin.ps1 [-AdminUrl <String>] [-AppId <String>] [-Tenant <String>] [-AddAdmin <String>] [-Cloud <String>] [-Site <String>] [-Out <String>]

Description

Adds a specified user as a site collection administrator to all SharePoint sites in the tenant (excluding OneDrive, admin, and system sites). Results are exported to an Excel file showing success/failure for each site.

Examples

Example 1

.\Set-GlobalSiteAdmin.ps1 -AdminUrl 'https://contoso-admin.sharepoint.com' -AppId 'xxx' -Tenant 'contoso.onmicrosoft.com' -AddAdmin 'admin@contoso.com'
# Add user as admin to all sites

Example 2

.\Set-GlobalSiteAdmin.ps1 -AdminUrl 'https://contoso-admin.sharepoint.com' -AppId 'xxx' -Tenant 'contoso.onmicrosoft.com' -AddAdmin 'admin@contoso.com' -Site 'https://contoso.sharepoint.com/sites/test'
# Test on a single site first

Parameters

-AdminUrl

The SharePoint Admin Center URL (e.g., https://contoso-admin.sharepoint.com).

  • Type: String
  • Required: No

-AppId

The Entra ID application (client) ID for authentication.

  • Type: String
  • Required: No

-Tenant

Tenant ID or domain for the connection.

  • Type: String
  • Required: No

-AddAdmin

The UPN of the user to add as site collection administrator.

  • Type: String
  • Required: No

-Cloud

Cloud environment to connect to. Defaults to 'Global'. Valid values: Global, USGov, USGovDoD, Germany, China

  • Type: String
  • Required: No
  • Valid values: Global, GCC, GCCH, DoD, Germany, China

-Site

Optional single site URL to test on before running against all sites.

  • Type: String
  • Required: No

-Out

Output file path for the Excel export. If not specified, generates a filename based on tenant name and current timestamp.

  • Type: String
  • Required: No

Notes

Requires PnP.PowerShell and ImportExcel modules.

Full Script

Copy this complete standalone script (includes all helper functions):

#Requires -Version 7.0
#Requires -Modules ImportExcel, PnP.PowerShell

<#
.SYNOPSIS
    Adds a site collection administrator to all SharePoint sites.
.DESCRIPTION
    Adds a specified user as a site collection administrator to all SharePoint
    sites in the tenant (excluding OneDrive, admin, and system sites).
    Results are exported to an Excel file showing success/failure for each site.
.PARAMETER AdminUrl
    The SharePoint Admin Center URL (e.g., https://contoso-admin.sharepoint.com).
.PARAMETER AppId
    The Entra ID application (client) ID for authentication.
.PARAMETER Tenant
    Tenant ID or domain for the connection.
.PARAMETER AddAdmin
    The UPN of the user to add as site collection administrator.
.PARAMETER Cloud
    Cloud environment to connect to. Defaults to 'Global'.
    Valid values: Global, USGov, USGovDoD, Germany, China
.PARAMETER Site
    Optional single site URL to test on before running against all sites.
.PARAMETER Out
    Output file path for the Excel export. If not specified, generates
    a filename based on tenant name and current timestamp.
.EXAMPLE
    .\Set-GlobalSiteAdmin.ps1 -AdminUrl 'https://contoso-admin.sharepoint.com' -AppId 'xxx' -Tenant 'contoso.onmicrosoft.com' -AddAdmin 'admin@contoso.com'
    # Add user as admin to all sites
.EXAMPLE
    .\Set-GlobalSiteAdmin.ps1 -AdminUrl 'https://contoso-admin.sharepoint.com' -AppId 'xxx' -Tenant 'contoso.onmicrosoft.com' -AddAdmin 'admin@contoso.com' -Site 'https://contoso.sharepoint.com/sites/test'
    # Test on a single site first
.NOTES
    Requires PnP.PowerShell and ImportExcel modules.
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory)

#region Helper Functions

function Write-TimeLog {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0)]
        [string]$Message,

        [Parameter()]
        [ConsoleColor]$Color = 'White'
    )

    Write-Host "[$(Get-Date -Format 'HH:mm')] $Message" -ForegroundColor $Color
}

function Connect-PnPIfNeeded {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$SiteUrl,

        [Parameter(Mandatory)]
        [string]$AppId,

        [Parameter(Mandatory)]
        [string]$Tenant,

        [Parameter()]
        [ValidateSet('Global', 'GCC', 'GCCH', 'DoD', 'Germany', 'China')]
        [string]$Cloud = 'Global'
    )

    try {
        $currentConnection = Get-PnPConnection -ErrorAction Stop

        if ($currentConnection.Url -eq $SiteUrl) {
            Write-TimeLog "Using existing PnP connection to $SiteUrl" -Color Cyan
            return
        }

        Write-TimeLog "Switching PnP connection from $($currentConnection.Url) to $SiteUrl" -Color Yellow
    }
    catch {
        # Not connected - will connect below
    }

    $envMap = @{
        'Global'  = 'Production'
        'GCC'     = 'USGovernment'
        'GCCH'    = 'USGovernmentHigh'
        'DoD'     = 'USGovernmentDoD'
        'Germany' = 'Germany'
        'China'   = 'China'
    }

    $connectParams = @{
        Url              = $SiteUrl
        ClientId         = $AppId
        Tenant           = $Tenant
        Interactive      = $true
        ErrorAction      = 'Stop'
    }

    if ($Cloud -ne 'Global') {
        $connectParams['AzureEnvironment'] = $envMap[$Cloud]
    }

    try {
        Connect-PnPOnline @connectParams
        Write-TimeLog "Connected to PnP: $SiteUrl" -Color Green
    }
    catch {
        Write-TimeLog "Failed to connect to PnP: $($_.Exception.Message)" -Color Red
        throw
    }
}

function Get-DefaultExcelParams {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Path
    )

    @{
        Path         = $Path
        TableStyle   = 'Medium2'
        AutoSize     = $true
        FreezeTopRow = $true
    }
}

function New-ReportPath {
    [CmdletBinding()]
    param(
        [Parameter()]
        [string]$Out,

        [Parameter(Mandatory)]
        [string]$FriendlyName,

        [Parameter(Mandatory)]
        [string]$OrgDisplayName,

        [Parameter()]
        [string]$Extension = 'xlsx'
    )

    if (!$Out) {
        "$OrgDisplayName-$FriendlyName-$(Get-Date -Format 'yyyy-MM-dd_HH-mm').$Extension"
    }
    elseif ($Out -notlike "*.$Extension") {
        "$Out.$Extension"
    }
    else {
        $Out
    }
}

#endregion Helper Functions
]
    [ValidatePattern('^https?://')]
    [string]$AdminUrl,

    [Parameter(Mandatory)]
    [string]$AppId,

    [Parameter(Mandatory)]
    [string]$Tenant,

    [Parameter(Mandatory)]
    [string]$AddAdmin,

    [ValidateSet('Global', 'GCC', 'GCCH', 'DoD', 'Germany', 'China')]
    [string]$Cloud = 'Global',

    [string]$Site,

    [string]$Out
)

Connect-PnPIfNeeded -SiteUrl $AdminUrl -AppId $AppId -Tenant $Tenant -Cloud $Cloud

# Generate output path
$tenantName = ($AdminUrl -replace 'https?://', '' -replace '-admin\.sharepoint\..+$', '')
$Out = New-ReportPath -Out $Out -FriendlyName 'GlobalSiteAdmin' -OrgDisplayName $tenantName

$results = [System.Collections.Generic.List[object]]::new()

if ($Site) {
    Write-TimeLog "Single site mode: $Site"
    $sites = @([PSCustomObject]@{ Url = $Site; Title = $Site })
}
else {
    Write-TimeLog "Fetching all SharePoint sites..."
    $sites = Get-PnPTenantSite | Where-Object {
        $_.Url -notlike '*-admin.sharepoint.*' -and
        $_.Url -notlike '*-my.sharepoint.*' -and
        $_.Url -notlike '*/portals/hub' -and
        $_.Url -notlike '*/search'
    }
    Write-TimeLog "Found $($sites.Count) sites to process"
}

$i = 0
foreach ($s in $sites) {
    $i++
    $siteUrl = $s.Url
    $siteTitle = $s.Title

    Write-TimeLog "[$i/$($sites.Count)] Processing: $siteTitle ($siteUrl)"

    try {
        Set-PnPTenantSite -Identity $siteUrl -Owners $AddAdmin -ErrorAction Stop

        $results.Add([PSCustomObject]@{
            SiteUrl   = $siteUrl
            SiteTitle = $siteTitle
            Status    = 'Success'
            Error     = $null
        })
        Write-TimeLog "  Added $AddAdmin as admin" -Color Green
    }
    catch {
        $results.Add([PSCustomObject]@{
            SiteUrl   = $siteUrl
            SiteTitle = $siteTitle
            Status    = 'Failed'
            Error     = $_.Exception.Message
        })
        Write-TimeLog "  Failed: $($_.Exception.Message)" -Color Red
    }
}

# Export results
$excelParams = Get-DefaultExcelParams -Path $Out
$results | Export-Excel @excelParams -WorksheetName 'Results'

Write-TimeLog "Exported results to: $Out" -Color Green
Write-TimeLog "Summary: $($results.Where({$_.Status -eq 'Success'}).Count) succeeded, $($results.Where({$_.Status -eq 'Failed'}).Count) failed"