Modules/AzureDevOpsDsc.Common/Api/Functions/Private/Api/Team/Set-DevOpsTeamAdministrator.ps1

<#
    .SYNOPSIS
        Grants or revokes team administrator rights for an identity on an Azure DevOps team.
 
    .DESCRIPTION
        Team administrator rights are granted by setting the 'ManageMembership' bit (resolved
        dynamically from the cached 'Identity' security namespace — never hardcoded) in the
        'allow' mask of the identity's ACE on the team's own token ('{ProjectId}\{TeamId}').
 
        The Azure DevOps 'accesscontrollists' POST always replaces the entire ACL for the token(s)
        submitted (merge=false) — any ACE left out of the payload is dropped from the live ACL. A
        team's admin ACEs belong to potentially many identities (one per admin), so this function
        always reads the live ACL for the token first and carries every other identity's existing
        ACE through unchanged, touching only the ManageMembership bit for the one target
        MemberDescriptor. Submitting only the target ACE — as a naive reuse of the
        AzDoGroupPermission serialization pipeline would do — would silently strip every other
        team admin's rights on the same token.
 
        Revoking the right removes the ACE entirely when doing so leaves it with no allow and no
        deny bits, rather than leaving a bare, meaningless zero-permission entry behind.
 
    .PARAMETER OrganizationName
        The name of the Azure DevOps organization.
 
    .PARAMETER ProjectId
        The id of the project that owns the team.
 
    .PARAMETER TeamId
        The id of the team (its group origin id), used as the second half of the Identity ACL token.
 
    .PARAMETER MemberDescriptor
        The ACL (identity) descriptor of the member to grant or revoke team administrator rights
        for — e.g. 'Microsoft.TeamFoundation.Identity;S-1-9-...'.
 
    .PARAMETER IsTeamAdmin
        Whether the member should hold team administrator rights ($true) or not ($false).
 
    .EXAMPLE
        Set-DevOpsTeamAdministrator -OrganizationName 'contoso' -ProjectId $project.id -TeamId $team.id -MemberDescriptor $memberAclDescriptor -IsTeamAdmin $true
#>

Function Set-DevOpsTeamAdministrator
{
    [CmdletBinding(SupportsShouldProcess)]
    param
    (
        [Parameter(Mandatory = $true)]
        [string]$OrganizationName,

        [Parameter(Mandatory = $true)]
        [string]$ProjectId,

        [Parameter(Mandatory = $true)]
        [string]$TeamId,

        [Parameter(Mandatory = $true)]
        [string]$MemberDescriptor,

        [Parameter(Mandatory = $true)]
        [bool]$IsTeamAdmin
    )

    Write-Verbose "[Set-DevOpsTeamAdministrator] Started."

    $namespace = Get-CacheItem -Key 'Identity' -Type 'SecurityNamespaces'
    if (-not $namespace)
    {
        Throw "[Set-DevOpsTeamAdministrator] The 'Identity' security namespace was not found in the cache."
    }

    $manageMembershipBit = ($namespace.actions | Where-Object { $_.name -eq 'ManageMembership' }).bit
    if (-not $manageMembershipBit)
    {
        Throw "[Set-DevOpsTeamAdministrator] The 'ManageMembership' action was not found on the 'Identity' security namespace."
    }

    $token = '{0}\{1}' -f $ProjectId, $TeamId

    $acl = Get-DevOpsACL -OrganizationName $OrganizationName -SecurityDescriptorId $namespace.namespaceId -Token $token
    $matchingAcl = $acl | Where-Object { $_.token -eq $token } | Select-Object -First 1

    # Carry every existing ACE on this token through as-is; only the target member's entry changes.
    $acesDictionary = @{}
    if ($matchingAcl)
    {
        $matchingAcl.acesDictionary.psobject.Properties | ForEach-Object {
            $acesDictionary[$_.Name] = @{
                allow      = [int]$_.Value.allow
                deny       = [int]$_.Value.deny
                descriptor = $_.Name
            }
        }
    }

    if ($IsTeamAdmin)
    {
        $existing = $acesDictionary[$MemberDescriptor]
        $allow = if ($existing) { [int]$existing.allow -bor $manageMembershipBit } else { $manageMembershipBit }
        $deny  = if ($existing) { [int]$existing.deny -band (-bnot $manageMembershipBit) } else { 0 }
        $acesDictionary[$MemberDescriptor] = @{ allow = $allow; deny = $deny; descriptor = $MemberDescriptor }
    }
    elseif ($acesDictionary.ContainsKey($MemberDescriptor))
    {
        $existing = $acesDictionary[$MemberDescriptor]
        $allow = [int]$existing.allow -band (-bnot $manageMembershipBit)
        if ($allow -eq 0 -and [int]$existing.deny -eq 0)
        {
            $acesDictionary.Remove($MemberDescriptor)
        }
        else
        {
            $acesDictionary[$MemberDescriptor] = @{ allow = $allow; deny = [int]$existing.deny; descriptor = $MemberDescriptor }
        }
    }

    $serializedAcl = @{
        Count = 1
        value = @(
            [PSCustomObject]@{
                token              = $token
                inheritPermissions = if ($matchingAcl) { [bool]$matchingAcl.inheritPermissions } else { $true }
                acesDictionary     = $acesDictionary
            }
        )
    }

    if ($PSCmdlet.ShouldProcess($token, 'Set team administrator ACE'))
    {
        Set-AzDoPermission -OrganizationName $OrganizationName -SecurityNamespaceID $namespace.namespaceId -SerializedACLs $serializedAcl
    }

    Write-Verbose "[Set-DevOpsTeamAdministrator] Token '$token' — member '$MemberDescriptor' IsTeamAdmin set to $IsTeamAdmin."
}