Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
213 changes: 161 additions & 52 deletions scripts/aad-replace-owner-with-a-different-one/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

Find all the Microsoft 365 Groups that a user is an Owner of and replace them with someone else useful for when an employee leaves and ownership needs to be updated.

Example usage of CLI for Microsoft 365 sample:

![Example CLI for Microsoft 365](assets/exampleCLI.png)

# [PnP PowerShell](#tab/pnpps)

```powershell
Expand Down Expand Up @@ -69,63 +73,167 @@ Disconnect-PnPOnline
# [CLI for Microsoft 365](#tab/cli-m365-ps)

```powershell
$oldOwnerUPN = Read-Host "Enter the old owner UPN to be replaced with" #testUser1@contose.onmicrosoft.com
$newOwnerUPN = Read-Host "Enter the new owner UPN to replace with" #testuser2@contoso.onmicrosoft.com

#Get Credentials to connect
$m365Status = m365 status
if ($m365Status -match "Logged Out") {
m365 login
[CmdletBinding(SupportsShouldProcess = $true)]
param(
[Parameter(Mandatory, HelpMessage = "UPN of the current owner to replace.")]
[string]$OldOwnerUpn,

[Parameter(Mandatory, HelpMessage = "UPN of the new owner to add.")]
[string]$NewOwnerUpn,

[Parameter(HelpMessage = "Filter applied to group display names when querying Microsoft 365 groups.")]
[string]$DisplayNameFilter = "Permission",

[Parameter(HelpMessage = "Directory where the CSV report will be created.")]
[string]$OutputDirectory,

[Parameter(HelpMessage = "Prefix for the generated CSV report file name.")]
[string]$ReportNamePrefix = "m365GroupOwnersReport",

[switch]$Force
)

begin {
m365 login --ensure

if (-not $OutputDirectory) {
$OutputDirectory = if ($MyInvocation.MyCommand.Path) {
Join-Path -Path (Split-Path -Path $MyInvocation.MyCommand.Path) -ChildPath 'Logs'
} else {
Join-Path -Path (Get-Location).Path -ChildPath 'Logs'
}
}

if (-not (Test-Path -Path $OutputDirectory -PathType Container)) {
New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null
}

$timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$script:ReportPath = Join-Path -Path $OutputDirectory -ChildPath ("{0}-{1}.csv" -f $ReportNamePrefix, $timestamp)
$script:ReportItems = [System.Collections.Generic.List[psobject]]::new()
$script:Summary = [ordered]@{
GroupsEvaluated = 0
OwnersReplaced = 0
OwnersSimulated = 0
OwnersNotFound = 0
ReplacementFails = 0
}

Write-Host "Starting ownership replacement audit..."
Write-Host "Report will be saved to $ReportPath"
}

$dateTime = (Get-Date).toString("dd-MM-yyyy")
$invocation = (Get-Variable MyInvocation).Value
$directorypath = Split-Path $invocation.MyCommand.Path
$fileName = "m365GroupOwnersReport-" + $dateTime + ".csv"
$OutPutView = $directorypath + "\Logs\"+ $fileName

#Array to Hold Result - PSObjects
$m365GroupCollection = @()

#Retrieve any M365 group starting with "Permission" (you can use filter as per your requirements)
$m365Groups = m365 entra m365group list --displayName Permission | ConvertFrom-Json

$m365Groups | ForEach-Object {
$ExportVw = New-Object PSObject
$ExportVw | Add-Member -MemberType NoteProperty -name "Group Name" -value $_.displayName
$m365GroupOwnersName = "";

try
{
#Check if old user is an owner of the group
$oldOwner = m365 entra m365group user list --groupId $_.id --role Owner --filter "userPrincipalName eq '$($oldOwnerUPN)'"

if($oldOwner)
{
#Add new user as an owner of the group
m365 entra m365group user add --groupId $_.id --userName $newOwnerUPN --role Owner

#Remove old user from the group
m365 entra m365group user remove --groupId $_.id --userName $oldOwnerUPN --force
}
}
catch
{
write-host $("Error occured while updating the group " + $_.displayName + $Error)
}

#For auditing purposes - get owners of the group
$m365GroupOwnersName = (m365 entra m365group user list --groupId $_.id --role Owner | ConvertFrom-Json | select -ExpandProperty displayName) -join ";";

$ExportVw | Add-Member -MemberType NoteProperty -name " Group Owners" -value $m365GroupOwnersName
$m365GroupCollection += $ExportVw
process {
$groupArgs = @('entra', 'm365group', 'list', '--output', 'json')
if ($DisplayNameFilter) {
$groupArgs += @('--displayName', $DisplayNameFilter)
}

$groupsOutput = & m365 @groupArgs 2>&1
if ($LASTEXITCODE -ne 0) {
throw "Failed to retrieve Microsoft 365 groups. CLI output: $groupsOutput"
}

$groups = if ([string]::IsNullOrWhiteSpace($groupsOutput)) { @() } else { $groupsOutput | ConvertFrom-Json }
if (-not $groups) {
Write-Host "No groups matched filter '$DisplayNameFilter'."
return
}

foreach ($group in $groups) {
$Summary.GroupsEvaluated++
Write-Host "Processing group '$($group.displayName)' ($($group.id))"

$action = 'Skipped'
$ownersForReport = @()

do {
$ownersOutput = & m365 entra m365group user list --groupId $group.id --role Owner --output json 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Warning " Unable to retrieve owners for $($group.displayName). CLI: $ownersOutput"
$Summary.ReplacementFails++
$ownersForReport = 'Owners unavailable'
$action = 'Failed - Owners lookup'
break
}

$owners = if ([string]::IsNullOrWhiteSpace($ownersOutput)) { @() } else { $ownersOutput | ConvertFrom-Json }
$ownersForReport = $owners
$oldOwner = $owners | Where-Object { $_.userPrincipalName -eq $OldOwnerUpn }

if (-not $oldOwner) {
Write-Host " Old owner '$OldOwnerUpn' not found; skipping"
$Summary.OwnersNotFound++
$action = 'Original owner missing'
break
}

if ($PSCmdlet.ShouldProcess($group.displayName, "Replace owner '$OldOwnerUpn' with '$NewOwnerUpn'")) {
Write-Host " Adding '$NewOwnerUpn' as owner"
$addOutput = & m365 entra m365group user add --groupId $group.id --userNames $NewOwnerUpn --role Owner --output json 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Warning " Failed to add '$NewOwnerUpn'. CLI: $addOutput"
$Summary.ReplacementFails++
$action = 'Failed - Add owner'
break
}

Write-Host " Removing '$OldOwnerUpn' as owner"
$removeArgs = @('entra', 'm365group', 'user', 'remove', '--groupId', $group.id, '--userNames', $OldOwnerUpn, '--output', 'json')
if ($Force) { $removeArgs += '--force' }

$removeOutput = & m365 @removeArgs 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Warning " Failed to remove '$OldOwnerUpn'. CLI: $removeOutput"
$Summary.ReplacementFails++
$action = 'Failed - Remove owner'
break
}

$Summary.OwnersReplaced++
$action = 'Replaced'

$ownersAfterOutput = & m365 entra m365group user list --groupId $group.id --role Owner --output json 2>&1
if ($LASTEXITCODE -eq 0 -and -not [string]::IsNullOrWhiteSpace($ownersAfterOutput)) {
$ownersForReport = $ownersAfterOutput | ConvertFrom-Json
}
} else {
Write-Host " WhatIf: would add '$NewOwnerUpn' and remove '$OldOwnerUpn'"
$Summary.OwnersSimulated++
$action = 'Simulated'
}
} while ($false)

$ownerNames = if ($ownersForReport -is [string]) {
$ownersForReport
} elseif ($ownersForReport) {
($ownersForReport | Select-Object -ExpandProperty displayName -ErrorAction SilentlyContinue) -join ';'
} else {
''
}

$ReportItems.Add([pscustomobject]@{
'Group Name' = $group.displayName
'Group Id' = $group.id
'Owners' = $ownerNames
'Old Owner UPN' = $OldOwnerUpn
'New Owner UPN' = $NewOwnerUpn
'Action' = $action
})
}
}

#Export the result Array to CSV file
$m365GroupCollection | sort "Group Name" |Export-CSV $OutPutView -Force -NoTypeInformation
end {
if ($ReportItems.Count -gt 0) {
$ReportItems | Sort-Object 'Group Name' | Export-Csv -Path $ReportPath -NoTypeInformation -Force
Write-Host "Report exported to $ReportPath"
} else {
Write-Host "No groups matched the criteria; nothing exported."
}

#Disconnect online connection
m365 logout
Write-Host ("Summary: {0} groups evaluated, {1} owners replaced, {2} simulated, {3} groups missing original owner, {4} failures." -f `
$Summary.GroupsEvaluated, $Summary.OwnersReplaced, $Summary.OwnersSimulated, $Summary.OwnersNotFound, $Summary.ReplacementFails)
}
```

[!INCLUDE [More about CLI for Microsoft 365](../../docfx/includes/MORE-CLIM365.md)]
Expand All @@ -138,6 +246,7 @@ m365 logout
|-----------|
| Reshmee Auckloo |
| [Ganesh Sanap](https://ganeshsanapblogs.wordpress.com/) |
| Adam Wójcik |


[!INCLUDE [DISCLAIMER](../../docfx/includes/DISCLAIMER.md)]
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"title": "Replace an owner in a Microsoft 365 Group or Microsoft Team",
"url": "https://pnp.github.io/script-samples/aad-replace-owner-with-a-different-one/README.html",
"creationDateTime": "2021-05-04",
"updateDateTime": "2024-03-10",
"updateDateTime": "2025-11-11",
"shortDescription": "Find all the Microsoft 365 Groups that a user is an Owner of and replace them with someone",
"longDescription": null,
"products": [
Expand All @@ -30,12 +30,10 @@
"Remove-PnPMicrosoft365Group",
"Remove-PnPMicrosoft365GroupOwner",
"m365 login",
"m365 status",
"m365 entra m365group list",
"m365 entra m365group user list",
"m365 entra m365group user add",
"m365 entra m365group user remove",
"m365 logout"
"m365 entra m365group user remove"
],
"metadata": [
{
Expand All @@ -44,7 +42,7 @@
},
{
"key": "CLI-FOR-MICROSOFT365",
"value": "7.5.0"
"value": "11.0.0"
}
],
"thumbnails": [
Expand All @@ -68,6 +66,12 @@
"company": "",
"pictureUrl": "https://avatars.githubusercontent.com/u/7693852?v=4",
"name": "Reshmee Auckloo"
},
{
"gitHubAccount": "Adam-it",
"company": "",
"pictureUrl": "https://avatars.githubusercontent.com/u/58668583?v=4",
"name": "Adam Wójcik"
}
],
"references": [
Expand Down