Saturday, March 28, 2026

Managing All Group Types with CRUD Operations

Managing All Group Types with CRUD Operations

Introduction

Managing groups in Microsoft 365 is a routine task for administrators and developers — but the challenge is that M365 exposes four distinct group types, each managed through different Exchange Online cmdlets. A script that works on a Unified Group will silently fail on a Distribution List.

 

In this post, we will build a single PowerShell script that auto-detects the group type and exposes a clean CRUD interface — Read, Add, Remove, and Modify — for both owners and members across all four group types:

 

       Microsoft 365 Groups (Unified Groups)

       Distribution Lists

       Dynamic Distribution Lists

       Mail-Enabled Security Groups

 

Prerequisites

No Admin Rights Required The ExchangeOnlineManagement module installs per-user with -Scope CurrentUser.

 

Before running the script, ensure you have:

 

       PowerShell 5.1 or PowerShell 7+

       An Exchange Online account with sufficient permissions to manage groups

       Internet connectivity (the script will install the module automatically)

 

Understanding the Four Group Types

 

Group Type

Use Case

Owners Stored In

Member Control

Microsoft 365 Group

Teams, SharePoint, shared mailbox

UnifiedGroupLinks

Manual

Distribution List

Email broadcast lists

ManagedBy property

Manual

Mail-Enabled Security

Email + permissions

ManagedBy property

Manual

Dynamic Distribution List

Auto-membership via OPATH filter

ManagedBy property

Automatic (filter)

 

Note on Dynamic DLs: Members are computed from an OPATH filter and cannot be added or removed manually. The script handles this gracefully with a clear warning.

 

CRUD Operations Overview

 

Operation

$Operation Value

What It Does

Read

Read

Lists all owners and members with count

Add Owner

AddOwner

Adds $TargetUPN as a group owner

Remove Owner

RemoveOwner

Removes $TargetUPN; skips if only 1 owner

Add Member

AddMember

Adds $TargetUPN as a group member

Remove Member

RemoveMember

Removes $TargetUPN from members

Modify

Modify

Updates DisplayName and/or Notes

 

Script Configuration

At the top of the script, set these four variables before running:

 

# ── Required ──────────────────────────────────────────────────────

$GroupEmail  = "group@yourdomain.com"      # Target group email address

$TargetUPN   = "user@yourdomain.com"       # User UPN for add/remove ops

$Operation   = "Read"                      # Read | AddOwner | RemoveOwner

                                            # AddMember | RemoveMember | Modify

 

# ── For Modify only ───────────────────────────────────────────────

$NewDisplayName = "New Group Name"         # Leave blank ("") to skip

$NewNotes       = "Updated notes"          # Leave blank ("") to skip

 

Full PowerShell Script

 

# ═══════════════════════════════════════════════════════════════════════════

#  M365 All Group Types - CRUD Operations Script

#  Author  : Sreekantha Reddy Udayagiri

#  Blog    : udayagirisreekanthreddy.com

#  Supports: Microsoft 365 | Distribution List |

#            Dynamic Distribution List | Mail-Enabled Security

#  Operations: Read | Add Owner/Member | Remove Owner/Member | Modify Group

# ═══════════════════════════════════════════════════════════════════════════

 

# ── Inputs ────────────────────────────────────────────────────────────────

$GroupEmail  = "group@yourdomain.com"      # Group email address

$TargetUPN   = "user@yourdomain.com"       # UPN for add/remove operations

$Operation   = "Read"                      # Read | AddOwner | RemoveOwner |

                                            # AddMember | RemoveMember | Modify

 

# ── For Modify operation only ─────────────────────────────────────────────

$NewDisplayName    = ""    # Leave blank to skip

$NewNotes          = ""    # Leave blank to skip

 

# ── Step 1: Install ExchangeOnlineManagement if not present ───────────────

$moduleName = "ExchangeOnlineManagement"

if (-not (Get-Module -ListAvailable -Name $moduleName)) {

    Write-Host "Installing $moduleName ..." -ForegroundColor Yellow

    Install-Module -Name $moduleName -Scope CurrentUser -Force -AllowClobber

    Write-Host "$moduleName installed." -ForegroundColor Green

} else {

    Write-Host "$moduleName already present." -ForegroundColor Cyan

}

 

# ── Step 2: Connect ───────────────────────────────────────────────────────

Connect-ExchangeOnline -ShowBanner:`$false

 

# ── Step 3: Auto-detect group type ───────────────────────────────────────

$groupType   = $null

$groupObject = $null

 

$groupObject = Get-UnifiedGroup -Identity $GroupEmail -ErrorAction SilentlyContinue

if ($groupObject) { $groupType = "M365" }

 

if (-not $groupType) {

    $groupObject = Get-DistributionGroup -Identity $GroupEmail -ErrorAction SilentlyContinue

    if ($groupObject) {

        $groupType = ($groupObject.GroupType -match "SecurityEnabled") ? "MailEnabledSecurity" : "DistributionList"

    }

}

 

if (-not $groupType) {

    $groupObject = Get-DynamicDistributionGroup -Identity $GroupEmail -ErrorAction SilentlyContinue

    if ($groupObject) { $groupType = "DynamicDistributionList" }

}

 

if (-not $groupType) {

    Write-Warning "Group not found: $GroupEmail"; Disconnect-ExchangeOnline -Confirm:$false; exit 1

}

 

Write-Host "`nGroup   : $($groupObject.DisplayName)" -ForegroundColor Green

Write-Host "Type    : $groupType"                    -ForegroundColor Green

 

# ── Helper: Get Owners ────────────────────────────────────────────────────

function Get-GroupOwners($email, $type, $obj) {

    if ($type -eq "M365") {

        return Get-UnifiedGroupLinks -Identity $email -LinkType Owners

    }

    return $obj.ManagedBy | ForEach-Object {

        Get-Recipient -Identity $_ -ErrorAction SilentlyContinue

    }

}

 

# ── Helper: Get Members ───────────────────────────────────────────────────

function Get-GroupMembers($email, $type) {

    switch ($type) {

        "M365"                  { return Get-UnifiedGroupLinks -Identity $email -LinkType Members }

        "DistributionList"      { return Get-DistributionGroupMember -Identity $email }

        "MailEnabledSecurity"   { return Get-DistributionGroupMember -Identity $email }

        "DynamicDistributionList" {

            Write-Warning 'Dynamic DLs use OPATH filters — members are computed dynamically.'

            return Get-DynamicDistributionGroupMember -Identity $email -ErrorAction SilentlyContinue

        }

    }

}

 

# ══════════════════════════════════════════════════════════

# ── CRUD SWITCH ───────────────────────────────────────────

# ══════════════════════════════════════════════════════════

switch ($Operation) {

 

    # ── READ ──────────────────────────────────────────────

    "Read" {

        Write-Host "`n── OWNERS ──────────────────────────" -ForegroundColor Cyan

        $owners = Get-GroupOwners $GroupEmail $groupType $groupObject

        $owners | ForEach-Object {

            Write-Host "  Owner : $($_.PrimarySmtpAddress)" -ForegroundColor White

        }

        Write-Host "  Total owners: $(($owners | Measure-Object).Count)" -ForegroundColor Yellow

 

        Write-Host "`n── MEMBERS ─────────────────────────" -ForegroundColor Cyan

        $members = Get-GroupMembers $GroupEmail $groupType

        $members | ForEach-Object {

            Write-Host "  Member: $($_.PrimarySmtpAddress)" -ForegroundColor White

        }

        Write-Host "  Total members: $(($members | Measure-Object).Count)" -ForegroundColor Yellow

    }

 

    # ── ADD OWNER ─────────────────────────────────────────

    "AddOwner" {

        switch ($groupType) {

            "M365" {

                Add-UnifiedGroupLinks -Identity $GroupEmail -LinkType Owners -Links $TargetUPN

            }

            { $_ -in "DistributionList","MailEnabledSecurity" } {

                $existing = Get-GroupOwners $GroupEmail $groupType $groupObject |

                    Select-Object -ExpandProperty DistinguishedName

                $newOwner = (Get-Recipient -Identity $TargetUPN).DistinguishedName

                Set-DistributionGroup -Identity $GroupEmail -ManagedBy ($existing + $newOwner) -BypassSecurityGroupManagerCheck

            }

            "DynamicDistributionList" {

                $existing = Get-GroupOwners $GroupEmail $groupType $groupObject |

                    Select-Object -ExpandProperty DistinguishedName

                $newOwner = (Get-Recipient -Identity $TargetUPN).DistinguishedName

                Set-DynamicDistributionGroup -Identity $GroupEmail -ManagedBy ($existing + $newOwner)

            }

        }

        Write-Host "$TargetUPN added as owner." -ForegroundColor Green

    }

 

    # ── REMOVE OWNER ──────────────────────────────────────

    "RemoveOwner" {

        $owners = Get-GroupOwners $GroupEmail $groupType $groupObject

        if (($owners | Measure-Object).Count -le 1) {

            Write-Warning "Only 1 owner exists. Removal skipped to avoid orphaned group."

            break

        }

        $target = $owners | Where-Object { $_.PrimarySmtpAddress -eq $TargetUPN }

        if (-not $target) { Write-Warning "$TargetUPN is not an owner."; break }

 

        switch ($groupType) {

            "M365" {

                Remove-UnifiedGroupLinks -Identity $GroupEmail -LinkType Owners -Links $TargetUPN -Confirm:`$false

            }

            { $_ -in "DistributionList","MailEnabledSecurity" } {

                $updated = $owners | Where-Object { $_.PrimarySmtpAddress -ne $TargetUPN } |

                    Select-Object -ExpandProperty DistinguishedName

                Set-DistributionGroup -Identity $GroupEmail -ManagedBy $updated -BypassSecurityGroupManagerCheck

            }

            "DynamicDistributionList" {

                $updated = $owners | Where-Object { $_.PrimarySmtpAddress -ne $TargetUPN } |

                    Select-Object -ExpandProperty DistinguishedName

                Set-DynamicDistributionGroup -Identity $GroupEmail -ManagedBy $updated

            }

        }

        Write-Host "$TargetUPN removed from owners." -ForegroundColor Green

    }

 

    # ── ADD MEMBER ────────────────────────────────────────

    "AddMember" {

        switch ($groupType) {

            "M365" {

                Add-UnifiedGroupLinks -Identity $GroupEmail -LinkType Members -Links $TargetUPN

            }

            { $_ -in "DistributionList","MailEnabledSecurity" } {

                Add-DistributionGroupMember -Identity $GroupEmail -Member $TargetUPN

            }

            "DynamicDistributionList" {

                Write-Warning 'Dynamic DLs use OPATH filters — members cannot be added manually.'

            }

        }

        Write-Host "$TargetUPN added as member." -ForegroundColor Green

    }

 

    # ── REMOVE MEMBER ─────────────────────────────────────

    "RemoveMember" {

        switch ($groupType) {

            "M365" {

                Remove-UnifiedGroupLinks -Identity $GroupEmail -LinkType Members -Links $TargetUPN -Confirm:`$false

            }

            { $_ -in "DistributionList","MailEnabledSecurity" } {

                Remove-DistributionGroupMember -Identity $GroupEmail -Member $TargetUPN -Confirm:`$false

            }

            "DynamicDistributionList" {

                Write-Warning 'Dynamic DLs use OPATH filters — members cannot be removed manually.'

            }

        }

        Write-Host "$TargetUPN removed from members." -ForegroundColor Green

    }

 

    # ── MODIFY GROUP ──────────────────────────────────────

    "Modify" {

        switch ($groupType) {

            "M365" {

                $params = @{ Identity = $GroupEmail }

                if ($NewDisplayName) { $params["DisplayName"] = $NewDisplayName }

                if ($NewNotes)       { $params["Notes"] = $NewNotes }

                Set-UnifiedGroup @params

            }

            { $_ -in "DistributionList","MailEnabledSecurity" } {

                $params = @{ Identity = $GroupEmail }

                if ($NewDisplayName) { $params["DisplayName"] = $NewDisplayName }

                if ($NewNotes)       { $params["Notes"] = $NewNotes }

                Set-DistributionGroup @params

            }

            "DynamicDistributionList" {

                $params = @{ Identity = $GroupEmail }

                if ($NewDisplayName) { $params["DisplayName"] = $NewDisplayName }

                if ($NewNotes)       { $params["Notes"] = $NewNotes }

                Set-DynamicDistributionGroup @params

            }

        }

        Write-Host "Group updated successfully." -ForegroundColor Green

    }

 

    default {

        Write-Warning "Unknown operation: $Operation"

        Write-Host "Valid values: Read | AddOwner | RemoveOwner | AddMember | RemoveMember | Modify"

    }

}

 

# ── Disconnect ────────────────────────────────────────────

Disconnect-ExchangeOnline -Confirm:`$false

Write-Host "`nDone." -ForegroundColor Green

 

How It Works — Step by Step

Step 1: Auto-Install the Module

The script checks for ExchangeOnlineManagement using Get-Module -ListAvailable and installs it with -Scope CurrentUser if absent — no admin rights needed.

Step 2: Auto-Detect Group Type

Instead of asking you to specify the group type, the script probes in order: Unified Group → Distribution Group (then checks for SecurityEnabled flag) → Dynamic Distribution Group. The first successful match sets the $groupType variable used throughout.

Step 3: CRUD Switch Block

A switch statement routes execution based on $Operation. Inside each operation, a nested switch handles the group-type-specific cmdlets so the logic remains clean and readable.

Step 4: Owner Safety Guard

For RemoveOwner, the script first counts owners. If only one owner exists, removal is skipped with a warning to prevent an orphaned group — regardless of group type.

Step 5: ManagedBy Rebuild Pattern

For Distribution Lists, Mail-Enabled Security Groups, and Dynamic DLs, owners are stored in the ManagedBy property. Adding or removing requires rebuilding the full array and writing it back with Set-DistributionGroup or Set-DynamicDistributionGroup.

 

Example Console Output

Read Operation

Group   : Engineering Team

Type    : M365

 

── OWNERS ──────────────────────────

  Owner : alice@contoso.com

  Owner : bob@contoso.com

  Total owners: 2

 

── MEMBERS ─────────────────────────

  Member: alice@contoso.com

  Member: bob@contoso.com

  Member: carol@contoso.com

  Total members: 3

 

RemoveOwner with 1 Owner Guard

Group   : Finance DL

Type    : DistributionList

 

WARNING: Only 1 owner exists. Removal skipped to avoid orphaned group.

 

Dynamic DL Member Add Warning

Group   : All Employees

Type    : DynamicDistributionList

 

WARNING: Dynamic DLs use OPATH filters — members cannot be added manually.

 

CRUD Compatibility by Group Type

 

Operation

M365 Group

Distribution List

Mail-Enabled Security

Dynamic DL

Read Owners

Read Members

⚠️ Computed

Add Owner

Remove Owner

Add Member

❌ Filter-based

Remove Member

❌ Filter-based

Modify

 

Conclusion

This script gives you a single, reusable tool to manage all four Exchange Online group types without switching cmdlets or remembering which group uses which property. Key benefits:

 

       Zero-touch module installation with -Scope CurrentUser

       Auto group type detection — no manual configuration

       Owner safety guard prevents orphaned groups

       Graceful warnings for Dynamic DL filter-based membership

       Clean CRUD switch pattern — easy to extend

 

Featured Post

Managing All Group Types with CRUD Operations

Managing All Group Types with CRUD Operations Introduction Managing groups in Microsoft 365 is a routine task for administrators and d...

Popular posts