Skip to content

Commit 5698ad3

Browse files
authored
Merge pull request #888 from Adam-it/refactors-aad-replace-owner-with-a-different-one
Refactors aad replace owner with a different one
2 parents d4c46c6 + b7cdda5 commit 5698ad3

File tree

3 files changed

+170
-57
lines changed

3 files changed

+170
-57
lines changed

scripts/aad-replace-owner-with-a-different-one/README.md

Lines changed: 161 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66

77
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.
88

9+
Example usage of CLI for Microsoft 365 sample:
10+
11+
![Example CLI for Microsoft 365](assets/exampleCLI.png)
12+
913
# [PnP PowerShell](#tab/pnpps)
1014

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

7175
```powershell
72-
$oldOwnerUPN = Read-Host "Enter the old owner UPN to be replaced with" #testUser1@contose.onmicrosoft.com
73-
$newOwnerUPN = Read-Host "Enter the new owner UPN to replace with" #testuser2@contoso.onmicrosoft.com
74-
75-
#Get Credentials to connect
76-
$m365Status = m365 status
77-
if ($m365Status -match "Logged Out") {
78-
m365 login
76+
[CmdletBinding(SupportsShouldProcess = $true)]
77+
param(
78+
[Parameter(Mandatory, HelpMessage = "UPN of the current owner to replace.")]
79+
[string]$OldOwnerUpn,
80+
81+
[Parameter(Mandatory, HelpMessage = "UPN of the new owner to add.")]
82+
[string]$NewOwnerUpn,
83+
84+
[Parameter(HelpMessage = "Filter applied to group display names when querying Microsoft 365 groups.")]
85+
[string]$DisplayNameFilter = "Permission",
86+
87+
[Parameter(HelpMessage = "Directory where the CSV report will be created.")]
88+
[string]$OutputDirectory,
89+
90+
[Parameter(HelpMessage = "Prefix for the generated CSV report file name.")]
91+
[string]$ReportNamePrefix = "m365GroupOwnersReport",
92+
93+
[switch]$Force
94+
)
95+
96+
begin {
97+
m365 login --ensure
98+
99+
if (-not $OutputDirectory) {
100+
$OutputDirectory = if ($MyInvocation.MyCommand.Path) {
101+
Join-Path -Path (Split-Path -Path $MyInvocation.MyCommand.Path) -ChildPath 'Logs'
102+
} else {
103+
Join-Path -Path (Get-Location).Path -ChildPath 'Logs'
104+
}
105+
}
106+
107+
if (-not (Test-Path -Path $OutputDirectory -PathType Container)) {
108+
New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null
109+
}
110+
111+
$timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
112+
$script:ReportPath = Join-Path -Path $OutputDirectory -ChildPath ("{0}-{1}.csv" -f $ReportNamePrefix, $timestamp)
113+
$script:ReportItems = [System.Collections.Generic.List[psobject]]::new()
114+
$script:Summary = [ordered]@{
115+
GroupsEvaluated = 0
116+
OwnersReplaced = 0
117+
OwnersSimulated = 0
118+
OwnersNotFound = 0
119+
ReplacementFails = 0
120+
}
121+
122+
Write-Host "Starting ownership replacement audit..."
123+
Write-Host "Report will be saved to $ReportPath"
79124
}
80125
81-
$dateTime = (Get-Date).toString("dd-MM-yyyy")
82-
$invocation = (Get-Variable MyInvocation).Value
83-
$directorypath = Split-Path $invocation.MyCommand.Path
84-
$fileName = "m365GroupOwnersReport-" + $dateTime + ".csv"
85-
$OutPutView = $directorypath + "\Logs\"+ $fileName
86-
87-
#Array to Hold Result - PSObjects
88-
$m365GroupCollection = @()
89-
90-
#Retrieve any M365 group starting with "Permission" (you can use filter as per your requirements)
91-
$m365Groups = m365 entra m365group list --displayName Permission | ConvertFrom-Json
92-
93-
$m365Groups | ForEach-Object {
94-
$ExportVw = New-Object PSObject
95-
$ExportVw | Add-Member -MemberType NoteProperty -name "Group Name" -value $_.displayName
96-
$m365GroupOwnersName = "";
97-
98-
try
99-
{
100-
#Check if old user is an owner of the group
101-
$oldOwner = m365 entra m365group user list --groupId $_.id --role Owner --filter "userPrincipalName eq '$($oldOwnerUPN)'"
102-
103-
if($oldOwner)
104-
{
105-
#Add new user as an owner of the group
106-
m365 entra m365group user add --groupId $_.id --userName $newOwnerUPN --role Owner
107-
108-
#Remove old user from the group
109-
m365 entra m365group user remove --groupId $_.id --userName $oldOwnerUPN --force
110-
}
111-
}
112-
catch
113-
{
114-
write-host $("Error occured while updating the group " + $_.displayName + $Error)
115-
}
116-
117-
#For auditing purposes - get owners of the group
118-
$m365GroupOwnersName = (m365 entra m365group user list --groupId $_.id --role Owner | ConvertFrom-Json | select -ExpandProperty displayName) -join ";";
119-
120-
$ExportVw | Add-Member -MemberType NoteProperty -name " Group Owners" -value $m365GroupOwnersName
121-
$m365GroupCollection += $ExportVw
126+
process {
127+
$groupArgs = @('entra', 'm365group', 'list', '--output', 'json')
128+
if ($DisplayNameFilter) {
129+
$groupArgs += @('--displayName', $DisplayNameFilter)
130+
}
131+
132+
$groupsOutput = & m365 @groupArgs 2>&1
133+
if ($LASTEXITCODE -ne 0) {
134+
throw "Failed to retrieve Microsoft 365 groups. CLI output: $groupsOutput"
135+
}
136+
137+
$groups = if ([string]::IsNullOrWhiteSpace($groupsOutput)) { @() } else { $groupsOutput | ConvertFrom-Json }
138+
if (-not $groups) {
139+
Write-Host "No groups matched filter '$DisplayNameFilter'."
140+
return
141+
}
142+
143+
foreach ($group in $groups) {
144+
$Summary.GroupsEvaluated++
145+
Write-Host "Processing group '$($group.displayName)' ($($group.id))"
146+
147+
$action = 'Skipped'
148+
$ownersForReport = @()
149+
150+
do {
151+
$ownersOutput = & m365 entra m365group user list --groupId $group.id --role Owner --output json 2>&1
152+
if ($LASTEXITCODE -ne 0) {
153+
Write-Warning " Unable to retrieve owners for $($group.displayName). CLI: $ownersOutput"
154+
$Summary.ReplacementFails++
155+
$ownersForReport = 'Owners unavailable'
156+
$action = 'Failed - Owners lookup'
157+
break
158+
}
159+
160+
$owners = if ([string]::IsNullOrWhiteSpace($ownersOutput)) { @() } else { $ownersOutput | ConvertFrom-Json }
161+
$ownersForReport = $owners
162+
$oldOwner = $owners | Where-Object { $_.userPrincipalName -eq $OldOwnerUpn }
163+
164+
if (-not $oldOwner) {
165+
Write-Host " Old owner '$OldOwnerUpn' not found; skipping"
166+
$Summary.OwnersNotFound++
167+
$action = 'Original owner missing'
168+
break
169+
}
170+
171+
if ($PSCmdlet.ShouldProcess($group.displayName, "Replace owner '$OldOwnerUpn' with '$NewOwnerUpn'")) {
172+
Write-Host " Adding '$NewOwnerUpn' as owner"
173+
$addOutput = & m365 entra m365group user add --groupId $group.id --userNames $NewOwnerUpn --role Owner --output json 2>&1
174+
if ($LASTEXITCODE -ne 0) {
175+
Write-Warning " Failed to add '$NewOwnerUpn'. CLI: $addOutput"
176+
$Summary.ReplacementFails++
177+
$action = 'Failed - Add owner'
178+
break
179+
}
180+
181+
Write-Host " Removing '$OldOwnerUpn' as owner"
182+
$removeArgs = @('entra', 'm365group', 'user', 'remove', '--groupId', $group.id, '--userNames', $OldOwnerUpn, '--output', 'json')
183+
if ($Force) { $removeArgs += '--force' }
184+
185+
$removeOutput = & m365 @removeArgs 2>&1
186+
if ($LASTEXITCODE -ne 0) {
187+
Write-Warning " Failed to remove '$OldOwnerUpn'. CLI: $removeOutput"
188+
$Summary.ReplacementFails++
189+
$action = 'Failed - Remove owner'
190+
break
191+
}
192+
193+
$Summary.OwnersReplaced++
194+
$action = 'Replaced'
195+
196+
$ownersAfterOutput = & m365 entra m365group user list --groupId $group.id --role Owner --output json 2>&1
197+
if ($LASTEXITCODE -eq 0 -and -not [string]::IsNullOrWhiteSpace($ownersAfterOutput)) {
198+
$ownersForReport = $ownersAfterOutput | ConvertFrom-Json
199+
}
200+
} else {
201+
Write-Host " WhatIf: would add '$NewOwnerUpn' and remove '$OldOwnerUpn'"
202+
$Summary.OwnersSimulated++
203+
$action = 'Simulated'
204+
}
205+
} while ($false)
206+
207+
$ownerNames = if ($ownersForReport -is [string]) {
208+
$ownersForReport
209+
} elseif ($ownersForReport) {
210+
($ownersForReport | Select-Object -ExpandProperty displayName -ErrorAction SilentlyContinue) -join ';'
211+
} else {
212+
''
213+
}
214+
215+
$ReportItems.Add([pscustomobject]@{
216+
'Group Name' = $group.displayName
217+
'Group Id' = $group.id
218+
'Owners' = $ownerNames
219+
'Old Owner UPN' = $OldOwnerUpn
220+
'New Owner UPN' = $NewOwnerUpn
221+
'Action' = $action
222+
})
223+
}
122224
}
123225
124-
#Export the result Array to CSV file
125-
$m365GroupCollection | sort "Group Name" |Export-CSV $OutPutView -Force -NoTypeInformation
226+
end {
227+
if ($ReportItems.Count -gt 0) {
228+
$ReportItems | Sort-Object 'Group Name' | Export-Csv -Path $ReportPath -NoTypeInformation -Force
229+
Write-Host "Report exported to $ReportPath"
230+
} else {
231+
Write-Host "No groups matched the criteria; nothing exported."
232+
}
126233
127-
#Disconnect online connection
128-
m365 logout
234+
Write-Host ("Summary: {0} groups evaluated, {1} owners replaced, {2} simulated, {3} groups missing original owner, {4} failures." -f `
235+
$Summary.GroupsEvaluated, $Summary.OwnersReplaced, $Summary.OwnersSimulated, $Summary.OwnersNotFound, $Summary.ReplacementFails)
236+
}
129237
```
130238

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

142251

143252
[!INCLUDE [DISCLAIMER](../../docfx/includes/DISCLAIMER.md)]
194 KB
Loading

scripts/aad-replace-owner-with-a-different-one/assets/sample.json

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
"title": "Replace an owner in a Microsoft 365 Group or Microsoft Team",
88
"url": "https://pnp.github.io/script-samples/aad-replace-owner-with-a-different-one/README.html",
99
"creationDateTime": "2021-05-04",
10-
"updateDateTime": "2024-03-10",
10+
"updateDateTime": "2025-11-11",
1111
"shortDescription": "Find all the Microsoft 365 Groups that a user is an Owner of and replace them with someone",
1212
"longDescription": null,
1313
"products": [
@@ -30,12 +30,10 @@
3030
"Remove-PnPMicrosoft365Group",
3131
"Remove-PnPMicrosoft365GroupOwner",
3232
"m365 login",
33-
"m365 status",
3433
"m365 entra m365group list",
3534
"m365 entra m365group user list",
3635
"m365 entra m365group user add",
37-
"m365 entra m365group user remove",
38-
"m365 logout"
36+
"m365 entra m365group user remove"
3937
],
4038
"metadata": [
4139
{
@@ -44,7 +42,7 @@
4442
},
4543
{
4644
"key": "CLI-FOR-MICROSOFT365",
47-
"value": "7.5.0"
45+
"value": "11.0.0"
4846
}
4947
],
5048
"thumbnails": [
@@ -68,6 +66,12 @@
6866
"company": "",
6967
"pictureUrl": "https://avatars.githubusercontent.com/u/7693852?v=4",
7068
"name": "Reshmee Auckloo"
69+
},
70+
{
71+
"gitHubAccount": "Adam-it",
72+
"company": "",
73+
"pictureUrl": "https://avatars.githubusercontent.com/u/58668583?v=4",
74+
"name": "Adam Wójcik"
7175
}
7276
],
7377
"references": [

0 commit comments

Comments
 (0)