Skip to content

Commit 223171f

Browse files
committed
refactors aad replace owner with a different one script
1 parent 28621f2 commit 223171f

File tree

2 files changed

+165
-57
lines changed

2 files changed

+165
-57
lines changed

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

Lines changed: 156 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -69,63 +69,167 @@ Disconnect-PnPOnline
6969
# [CLI for Microsoft 365](#tab/cli-m365-ps)
7070

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

131235
[!INCLUDE [More about CLI for Microsoft 365](../../docfx/includes/MORE-CLIM365.md)]

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/45694692?v=4",
74+
"name": "Adam Wójcik"
7175
}
7276
],
7377
"references": [

0 commit comments

Comments
 (0)