functions/azdo/Assert-AzdoGroupMembership.ps1

# <copyright file="Assert-AzdoGroupMembership.ps1" company="Endjin Limited">
# Copyright (c) Endjin Limited. All rights reserved.
# </copyright>

<#
.SYNOPSIS
Ensures that an Azure DevOps project group has the specified members.

.DESCRIPTION
Synchronises the membership of an existing Azure DevOps project group to match those provided in the
members configuration parameter.

.PARAMETER Name
The name of the group to process.

.PARAMETER Project
The name of the Azure DevOps project.

.PARAMETER Organisation
The name of the Azure DevOps organisation.

.PARAMETER RequiredMembers
An array of hashtables, each representing a required member of the specified group.
@(
    @{
        name = "<member-name>"
        type = "<member-type>" # valid values are 'user' or 'group'
    }
    ...
)

#>

function Assert-AzdoGroupMembership
{
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter()]
        [string] $Name,

        [Parameter()]
        [string] $Project,

        [Parameter()]
        [string] $Organisation,

        [Parameter()]
        [hashtable[]] $RequiredMembers
    )

    $output = @{
        Added = @()
        Flagged = @()
        Removed = @()
    }

    $orgUrl = Get-AzdoOrganisationUrl $Organisation
    
    # Lookup the group
    $listGroupArgs = @(
        "devops security group list"
        "--organization $orgUrl"
        "--project `"$Project`""
        "--query `"graphGroups[?displayName == '$Name']`""
    )
    $existingGroup = Invoke-AzCli -Command $listGroupArgs -AsJson

    if ($existingGroup) {
        # Lookup the current group members
        $getMemberArgs = @(
            "devops security group membership list"
            "--organization $orgUrl"
            "--id $($existingGroup.descriptor)"
        )
        $existingMembers = Invoke-AzCli -Command $getMemberArgs -AsJson
  
        # add missing members
        foreach ($member in $RequiredMembers) {
            $alreadyMember = _IsAzdoGroupMember -ExistingGroupMembers $existingMembers `
                                                -NewMemberEntry $member
            if (!$alreadyMember) {
                if ( !(_AddGroupMember -WhatIf:$WhatIfPreference) ) {
                    # When _AddGroupMember fails, stop processing this iteration and continue to the next member
                    continue
                }
                $output.Added += $member
            }
            else {
                Write-Verbose "Already a member: $($member.name) [$($member.type)]"
            }
        }

        
        # Audit existing group members who are not in the configuration file
        
        # Project the existing group members into a structure that
        # we can easily compare with the set of required members
        $existingMembersProjection = $existingMembers.Keys | ForEach-Object {
            $memberId = $existingMembers[$_].descriptor
            # The field used for the comparison is different depending on whether we're considering a user or group
            $memberType = $existingMembers[$_].subjectKind
            $memberName = $memberType -eq 'group' ? $existingMembers[$_].displayName : $existingMembers[$_].principalName
            @{ id = $memberId; name = $memberName; type = $memberType }
        }

        # Log a warning for unexpected group members
        $extraMembers = $existingMembersProjection | Where-Object { 
            $_.name -notin ([array]($RequiredMembers | Select-Object -ExpandProperty name))
        }
        foreach ($extraMember in $extraMembers) {
            Write-Warning ("[UNEXPECTED-USER] {0} '{1}' is a member of '{2}'" -f $extraMember.type, $extraMember.name, $Name)
            $output.Flagged += $extraMember
        }
    }

    return $output
}