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

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 92 additions & 32 deletions scripts/aad-ensure-ownersaremembers-m365groups/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

It may happen that owners are not members of the m365 group because of the various methods of managing M365 group permissions, such as through the Teams admin center, Microsoft Teams, SharePoint admin center, SharePoint connected sites, Planner, or scripting using PowerShell. The script will help identify these discrepancies and ensures m365 group owners are also m365 group members.

CLI for Microsoft 365 script sample usage example:

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

# [PnP PowerShell](#tab/pnpps)

```powershell
Expand Down Expand Up @@ -49,44 +53,99 @@ $m365GroupCollection | sort-object "Site Name" | Export-CSV $OutPutView -Force -
# [CLI for Microsoft 365](#tab/cli-m365-ps)

```powershell
$m365Status = m365 status
if ($m365Status -match "Logged Out") {
m365 login
[CmdletBinding()]
param(
[Parameter(HelpMessage = "Directory path where the CSV report will be stored.")]
[string]$OutputDirectory,

[Parameter(HelpMessage = "Optional custom file name (with or without .csv) for the owners-not-members report.")]
[string]$ReportFileName
)

begin {
m365 login --ensure

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

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

if (-not $ReportFileName) {
$ReportFileName = 'm365OwnersNotMembers-{0}.csv' -f (Get-Date -Format 'yyyyMMdd-HHmmss')
} elseif (-not $ReportFileName.EndsWith('.csv')) {
$ReportFileName = "$ReportFileName.csv"
}

$script:ReportPath = Join-Path -Path $OutputDirectory -ChildPath $ReportFileName
$script:ReportItems = [System.Collections.Generic.List[psobject]]::new()
$script:Summary = [ordered]@{
GroupsEvaluated = 0
OwnersAdded = 0
OwnersFailed = 0
}

Write-Host "Starting owner membership 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 = "m365OwnersNotMembers-" + $dateTime + ".csv"
$OutPutView = $directorypath + "\" + $fileName
# Array to Hold Result - PSObjects
$m365GroupCollection = @()
#Write-host $"$ownerName not part of member in $siteUrl";
$m365Sites = m365 spo site list --query "[?Template == 'GROUP#0' && Template != 'RedirectSite#0'].{GroupId:GroupId, Url:Url, Title:Title}" --output json | ConvertFrom-Json
$m365Sites | ForEach-Object {
$groupId = $_.GroupId -replace "/Guid\((.*)\)/",'$1';
$siteUrl = $_.Url;
$siteName = $_.Title
#if owner is not part of m365 group member
(m365 entra m365group user list --role Owner --groupId $groupId --output json | ConvertFrom-Json) | foreach-object {
$owner = $_;
$ownerDisplayName = $owner.displayName
if (!(m365 entra m365group user list --role Member --groupId $groupId --query "[?displayName == '$ownerDisplayName']" --output json | ConvertFrom-Json)) {
$ExportVw = New-Object PSObject
$ExportVw | Add-Member -MemberType NoteProperty -name "Site Name" -value $siteName
$ExportVw | Add-Member -MemberType NoteProperty -name "Site URL" -value $siteUrl
$ExportVw | Add-Member -MemberType NoteProperty -name "Owner Name" -value $ownerDisplayName
$m365GroupCollection += $ExportVw
m365 entra m365group user add --role Owner --groupId $groupId --userName $owner.userPrincipalName
Write-host "$ownerDisplayName has been added as member in $siteUrl";
process {
$sites = m365 spo site list --query "[?Template == 'GROUP#0' && Template != 'RedirectSite#0'].{GroupId:GroupId, Url:Url, Title:Title}" --output json | ConvertFrom-Json

foreach ($site in $sites) {
$Summary.GroupsEvaluated++
Write-Host "Processing group '$($site.Title)' ($($site.Url))"

$groupId = $site.GroupId -replace "/Guid\((.*)\)/", '$1'
$owners = m365 entra m365group user list --role Owner --groupId $groupId --output json | ConvertFrom-Json

foreach ($owner in $owners) {
$ownerUserPrincipalName = $owner.userPrincipalName
$isMember = m365 entra m365group user list --role Member --groupId $groupId --query "[?userPrincipalName == '$ownerUserPrincipalName']" --output json | ConvertFrom-Json

if (-not $isMember) {
Write-Host " Owner '$ownerUserPrincipalName' missing from members, attempting to add..."

$ReportItems.Add([pscustomobject]@{
'Site Name' = $site.Title
'Site URL' = $site.Url
'Owner Name' = $ownerUserPrincipalName
})

$addResult = m365 entra m365group user add --role Member --groupId $groupId --userNames $ownerUserPrincipalName --output json 2>&1

if ($LASTEXITCODE -ne 0) {
Write-Warning "Failed to add $ownerUserPrincipalName as member in $($site.Url). CLI returned: $addResult"
$Summary.OwnersFailed++
continue
}

Write-Host " Added $ownerUserPrincipalName as member in $($site.Url)"
$Summary.OwnersAdded++
} else {
Write-Host " Owner '$ownerUserPrincipalName' already a member; skipping"
}
}
}
}
# Export the result array to CSV file
$m365GroupCollection | sort-object "Site Name" | Export-CSV $OutPutView -Force -NoTypeInformation

#Disconnect SharePoint online connection
m365 logout
end {
if ($ReportItems.Count -gt 0) {
$ReportItems | Sort-Object 'Site Name' | Export-Csv -Path $ReportPath -NoTypeInformation -Force
Write-Host "Report exported to $ReportPath"
} else {
Write-Host "No discrepancies detected across the evaluated groups."
}

Write-Host ("Summary: {0} groups checked, {1} owners added as members, {2} owners failed to add." -f $Summary.GroupsEvaluated, $Summary.OwnersAdded, $Summary.OwnersFailed)
}

```

[!INCLUDE [More about CLI for Microsoft 365](../../docfx/includes/MORE-CLIM365.md)]
Expand All @@ -103,6 +162,7 @@ Sample first appeared on [Ensuring Owners Are Members](https://reshmeeauckloo.co
| ----------------------------------------- |
| [Reshmee Auckloo (Main author)](https://github.com/reshmee011) |
| [Michał Kornet (CLI for M365 version)](https://github.com/mkm17) |
| 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": "Ensuring m365 group owners are m365 group members",
"url": "https://pnp.github.io/script-samples/aad-ensure-ownersaremembers-m365groups/README.html",
"creationDateTime": "2023-10-29",
"updateDateTime": "2024-06-14",
"updateDateTime": "2025-11-11",
"shortDescription": "Ensuring m365 group owners are m365 group members",
"longDescription": ["M365 group owners are not always m365 group members because of the various methods of managing M365 group permissions, such as through the Teams admin center, Microsoft Teams, SharePoint admin center, SharePoint connected sites, Planner, or scripting using PowerShell. The script will help identify these discrepancies and ensures m365 group owners are also m365 group members."],
"products": [
Expand Down Expand Up @@ -37,7 +37,7 @@
},
{
"key": "CLI-FOR-MICROSOFT365",
"value": "7.7.0"
"value": "11.0.0"
}
],
"thumbnails": [
Expand All @@ -61,6 +61,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 All @@ -73,7 +79,7 @@
"name": "Want to learn more about CLI for Microsoft 365 and the commands",
"description": "Check out the CLI for Microsoft 365 site to get started and for the reference to the commands.",
"url": "https://aka.ms/cli-m365"
},
}
]
}
]