Public/New-RBACforAppUnifiedGroup.ps1

<#
.SYNOPSIS
Ensures the scoped Microsoft 365 Unified Group used by New-RBACforAppEntry exists and is configured.

.DESCRIPTION
New-RBACforAppUnifiedGroup creates a private, hidden Unified Group (when it does not already exist)
to act as the recipient scope for Exchange Online application RBAC. The group is first created with
the smallest set of essential attributes (DisplayName/Name/Alias, AccessType Private, the
creation-only HiddenGroupMembershipEnabled, and the owner/bootstrap member); the remaining settings
(member edit, auto-subscribe, calendar subscribe, language, subscription, address-list visibility,
and connectors) are then applied via Set-UnifiedGroup. If the group already exists it is left in
place and a warning is emitted. A summary object describing the resolved group is returned.

The function supports -WhatIf and -Confirm through SupportsShouldProcess.

.PARAMETER Name
Name and Alias of the Unified Group. Expected to already be a safe value (<= 63 chars,
alphanumeric/dash); callers such as New-RBACforAppEntry sanitize it with Get-SafeName first.

.PARAMETER DisplayName
Display name for the group. Defaults to "{Name} - RBAC for APP".

.PARAMETER ManagedBy
Recipient assigned as the group owner. Defaults to the GraphAPI-Dummy-owner placeholder.

.PARAMETER BootstrapMember
Optional initial member passed during group creation. Defaults to the GraphAPI-Dummy placeholder.

.EXAMPLE
New-RBACforAppUnifiedGroup -Name 'Um365RAo1-ContosoMailApp' -WhatIf -Verbose

Shows the planned Unified Group creation without making changes.

.OUTPUTS
PSCustomObject

A summary object describing the group: Name, DisplayName, OwnerRequested (the -ManagedBy input),
OwnerAdded (the owner actually applied/in place), AlreadyExisted, and Group (the underlying Exchange
Online Unified Group object, existing or newly created).

.NOTES
Requires a connected Exchange Online session (Get-UnifiedGroup, New-UnifiedGroup, Set-UnifiedGroup,
Get-Recipient) and a connected Microsoft Graph session for the debug calling-context snapshot.
Companion to New-RBACforAppEntry.
#>

function New-RBACforAppUnifiedGroup {
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    [OutputType([object])]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string] $Name,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string] $DisplayName,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string] $ManagedBy = 'GraphAPI-Dummy-owner',

        [Parameter()]
        [string] $BootstrapMember = 'GraphAPI-Dummy'
    )

    process {
        if (-not $DisplayName) { $DisplayName = '{0} - RBAC for APP' -f $Name }

        Write-Verbose -Message ("Checking Unified Group '{0}'." -f $Name)
        $existingGroup = Get-UnifiedGroup -Identity $Name -ErrorAction SilentlyContinue
        if ($existingGroup) {
            $existingOwner = ($existingGroup.ManagedBy | Where-Object { $_ }) -join ', '
            Write-Warning -Message ("UnifiedGroup '{0}' already exists; will only add missing members / assignments." -f $Name)
            Write-Verbose -Message ("Unified Group '{0}' already exists; skipping creation." -f $Name)
            Write-Debug -Message ("Existing Unified Group details: DisplayName='{0}'; Identity='{1}'; ManagedBy='{2}'" -f $existingGroup.DisplayName, $existingGroup.Identity, $existingOwner)
            return [pscustomobject]@{
                Name           = $Name
                DisplayName    = $existingGroup.DisplayName
                OwnerRequested = $ManagedBy
                OwnerAdded     = $existingOwner
                AlreadyExisted = $true
                Group          = $existingGroup
            }
        }

        Write-Warning -Message ('{0} do not yet exists' -f $Name)

        # Resolve the requested owner (like members are resolved via Get-Recipient). Unlike a member,
        # an owner cannot be skipped: the group must be created with a ManagedBy, so if the recipient
        # cannot be resolved we warn and fall back to the raw -ManagedBy value.
        $resolvedOwner = $ManagedBy
        $ownerRecipient = Get-Recipient -Identity $ManagedBy -ErrorAction SilentlyContinue
        if ($ownerRecipient) {
            $resolvedOwner = [string]$ownerRecipient.PrimarySmtpAddress
            Write-Verbose -Message ("Owner '{0}' resolved to '{1}'." -f $ManagedBy, $resolvedOwner)
        }
        else {
            Write-Warning -Message ("Owner recipient '{0}' could not be resolved; using the requested value as-is." -f $ManagedBy)
        }

        $initialMembers = @()
        if ($BootstrapMember) { $initialMembers += $BootstrapMember }
        Write-Verbose -Message ("Unified Group '{0}' not found. Creating new group." -f $Name)

        # ---- Pre-call debug snapshot (minimal/essential creation parameters only) ----
        Write-Debug -Message ("[New-UnifiedGroup] PRE-CALL parameter snapshot (essential attributes):")
        Write-Debug -Message (" -DisplayName : '{0}'" -f $DisplayName)
        Write-Debug -Message (" -Name : '{0}'" -f $Name)
        Write-Debug -Message (" -Alias : '{0}'" -f $Name)
        Write-Debug -Message (" -AccessType : Private")
        Write-Debug -Message (" -HiddenGroupMembershipEnabled : True (creation-only)")
        Write-Debug -Message (" -ManagedBy : '{0}' (requested: '{1}')" -f $resolvedOwner, $ManagedBy)
        Write-Debug -Message (" -Members (count) : {0} Values: [{1}]" -f $initialMembers.Count, (($initialMembers | Where-Object { $_ }) -join ', '))
        Write-Debug -Message (" Calling context : TenantId='{0}'; CallerAccount='{1}'" -f (Get-MgContext | Select-Object -ExpandProperty TenantId), (Get-MgContext | Select-Object -ExpandProperty Account))

        if (-not $PSCmdlet.ShouldProcess($Name, 'Create')) { return }

        Write-Debug -Message ("[New-UnifiedGroup] ShouldProcess approved - invoking New-UnifiedGroup for '{0}'." -f $Name)
        $nugInvokeStart = [datetime]::UtcNow
        try {
            $nug = New-UnifiedGroup `
                -DisplayName $DisplayName `
                -Name $Name `
                -Alias $Name `
                -AccessType Private `
                -HiddenGroupMembershipEnabled:$true `
                -ManagedBy $resolvedOwner `
                -Members $initialMembers `
                -ErrorAction Stop
            $nugElapsed = ([datetime]::UtcNow - $nugInvokeStart).TotalSeconds
            Write-Debug -Message ("[New-UnifiedGroup] Cmdlet returned after {0:N2} seconds. Raw return type: '{1}'." -f $nugElapsed, $(if ($null -ne $nug) { $nug.GetType().FullName } else { '<null>' }))
        }
        catch {
            $nugElapsed = ([datetime]::UtcNow - $nugInvokeStart).TotalSeconds
            Write-Debug -Message ("[New-UnifiedGroup] EXCEPTION after {0:N2} seconds." -f $nugElapsed)
            Write-Debug -Message ("[New-UnifiedGroup] Failed for '{0}'." -f $Name)
            Write-Debug -Message (" ManagedBy : '{0}' (requested: '{1}')" -f $resolvedOwner, $ManagedBy)
            Write-Debug -Message (" BootstrapMembers: '{0}'" -f (($initialMembers | Where-Object { $_ }) -join ', '))
            Write-Debug -Message (" Exception type : '{0}'" -f $_.Exception.GetType().FullName)
            Write-Debug -Message (" Exception msg : '{0}'" -f $_.Exception.Message)
            Write-Debug -Message (" ScriptStackTrace: {0}" -f $_.ScriptStackTrace)
            throw
        }

        if ($null -ne $nug) {
            Write-Verbose -Message ("Unified Group '{0}' created successfully." -f $Name)
            Write-Debug -Message ("[New-UnifiedGroup] Created object properties:")
            Write-Debug -Message (" DisplayName : '{0}'" -f $nug.DisplayName)
            Write-Debug -Message (" ExternalDirectoryObjectId : '{0}'" -f $nug.ExternalDirectoryObjectId)
            Write-Debug -Message (" PrimarySmtpAddress : '{0}'" -f $nug.PrimarySmtpAddress)
            Write-Debug -Message (" Alias : '{0}'" -f $nug.Alias)
            Write-Debug -Message (" AccessType : '{0}'" -f $nug.AccessType)
            Write-Debug -Message (" HiddenGroupMembershipEnabled : '{0}'" -f $nug.HiddenGroupMembershipEnabled)
            Write-Debug -Message (" WhenCreatedUTC : '{0}'" -f $nug.WhenCreatedUTC)

            Write-Verbose -Message ("Applying post-creation settings to Unified Group '{0}'." -f $Name)
            Write-Debug -Message ("[Set-UnifiedGroup] Applying settings to '{0}':" -f $Name)
            Write-Debug -Message (" -IsMemberAllowedToEditContent : False")
            Write-Debug -Message (" -AutoSubscribeNewMembers : False")
            Write-Debug -Message (" -AlwaysSubscribeMembersToCalendarEvents : False")
            Write-Debug -Message (" -Language : en-us")
            Write-Debug -Message (" -SubscriptionEnabled : False")
            Write-Debug -Message (" -HiddenFromAddressListsEnabled : True")
            Write-Debug -Message (" -ConnectorsEnabled : False")
            Set-UnifiedGroup -Identity $Name `
                -IsMemberAllowedToEditContent $false `
                -AutoSubscribeNewMembers:$false `
                -AlwaysSubscribeMembersToCalendarEvents:$false `
                -Language en-us `
                -SubscriptionEnabled:$false `
                -HiddenFromAddressListsEnabled:$true `
                -ConnectorsEnabled:$false `
                -ErrorAction Stop

            $configuredGroup = Get-UnifiedGroup -Identity $Name -ErrorAction SilentlyContinue
            if ($configuredGroup) {
                Write-Debug -Message ("[New-UnifiedGroup] Post-creation Set-UnifiedGroup verification:")
                Write-Debug -Message (" Identity : '{0}'" -f $configuredGroup.Identity)
                Write-Debug -Message (" HiddenFromAddressListsEnabled: '{0}'" -f $configuredGroup.HiddenFromAddressListsEnabled)
                Write-Debug -Message (" AccessType : '{0}'" -f $configuredGroup.AccessType)
                Write-Debug -Message (" HiddenGroupMembershipEnabled : '{0}'" -f $configuredGroup.HiddenGroupMembershipEnabled)
                Write-Debug -Message (" SubscriptionEnabled : '{0}'" -f $configuredGroup.SubscriptionEnabled)
                Write-Debug -Message (" ConnectorsEnabled : '{0}'" -f $configuredGroup.ConnectorsEnabled)
                Write-Debug -Message (" ManagedBy : '{0}'" -f (($configuredGroup.ManagedBy | Where-Object { $_ }) -join ', '))
                Write-Debug -Message (" MemberCount : '{0}'" -f $configuredGroup.GroupMemberCount)
                return [pscustomobject]@{
                    Name           = $Name
                    DisplayName    = $configuredGroup.DisplayName
                    OwnerRequested = $ManagedBy
                    OwnerAdded     = $resolvedOwner
                    AlreadyExisted = $false
                    Group          = $configuredGroup
                }
            }
            else {
                Write-Debug -Message ("[New-UnifiedGroup] WARNING: Get-UnifiedGroup returned null after Set-UnifiedGroup for '{0}'. Post-create verification skipped." -f $Name)
            }

            return [pscustomobject]@{
                Name           = $Name
                DisplayName    = $nug.DisplayName
                OwnerRequested = $ManagedBy
                OwnerAdded     = $resolvedOwner
                AlreadyExisted = $false
                Group          = $nug
            }
        }
    }
}