Export the members of a project level group from all the projects in an Azure DevOps


How many project administrators do you have on an average per project? Though there is no real mandate or best practice on the number, I think you don’t want too many people with project admin privileges. This could also be an auditing question to figure out who can do what. Same can be the case with Build and Release admin group, other default groups or any of the custom security groups that you may have created at the project level.

If you are trying to export such a list, here is a script that can help you.
You can provide any project level group as the input to the script, and it will export a list containing the members of the group, across all the projects in your organization to a csv file.  The script lists both users and groups that are members of the group given as input, but do not expand groups further.
You can then filter by project to look at the details of individual projects.

How to run the script:

  1. The script takes three parameters:
    • Name of the Azure DevOps organization.
    • PAT token with collection level permissions.
    • Name of the project level group. If you need to export the project admin list, give "Project Administrators" as the group name. The complete name of a project level group will be [ProjectName]\GroupName, you just have to provide GroupName as the input.

  1. Invoke the script with the above parameters. If the provided group name exists in at least one project in the organization, as csv file will be generated at the same location from where you are running the script.


Hope this helps!




param (
[Parameter(Mandatory=$true)]
[string] $accountName = "",
[Parameter(Mandatory=$true)]
[string] $pat = "",
[Parameter(Mandatory=$true)]
[string] $groupName ="" #name of the group for which memebers are to be listed
)
#initialize
$groupMembersList =@()
$groupExists = $false
$memberExists = $false
# Create the AzureDevOps auth header
$base64authinfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$pat"))
$vstsAuthHeader = @{"Authorization"="Basic $base64authinfo"}
$allHeaders = $vstsAuthHeader + @{"Content-Type"="application/json"; "Accept"="application/json"}
try
{
#get list of projects in the org
$projectsJson=@()
$continuationToken=$null
do{
$getProjectsUrl= "https://dev.azure.com/$accountName/_apis/projects?continuationtoken=$continuationToken&api-version=5.1"
$projectsResult = Invoke-WebRequest -Headers $allHeaders -Method GET "$getProjectsUrl"
if ($projectsResult.StatusCode -ne 200)
{
Write-Output $projectsResult.Content
throw "Failed to query projects"
}
$continuationToken = $projectsResult.Headers.'x-ms-continuationtoken'
$projectsJson += ConvertFrom-Json $projectsResult.Content
}
while($continuationToken)
#get all account level groups
$groupsJson = @()
$continuationToken=$null
do{
$getGroupsUrl= "https://vssps.dev.azure.com/$accountName/_apis/graph/groups?continuationtoken=$continuationToken&api-version=6.0-preview.1"
$groupsResult = Invoke-WebRequest -Headers $allHeaders -Method GET "$getGroupsUrl"
if ($groupsResult.StatusCode -ne 200)
{
Write-Output $groupsResult.Content
throw "Failed to get account level groups"
}
$continuationToken = $groupsResult.Headers.'x-ms-continuationtoken'
$groupsJson += ConvertFrom-Json $groupsResult.Content
}
while($continuationToken)
#Ffilter out matching groups - case insensitive
for($i=0; $i -lt $groupsJson.Count; $i++)
{
$groupsJson[$i] = ($groupsJson[$i].value | Where-Object {$_.principalName.ToLower().Contains($groupName.ToLower())})
}
#get users from all project level groups
$processingCount=0
for($p=0; $p -lt $projectsJson.Count; $p++)
{
foreach($project in $projectsJson[$p].value)
{
$processingCount++
Clear-Host
Write-Output ("Processimg project " + $processingCount + " of " + $projectsJson.value.count)
for($i=0; $i -lt $groupsJson.Count; $i++)
{
$groupFound =$false
foreach($group in $groupsJson[$i])
{
if (($group.principalName.ToLower()) -eq (("["+$project.name+"]\$groupName").ToLower()))
{
#region FindMemberGroups
#check if the group has other groups within.
$groupDescriptor= $group.descriptor
$getGroupsInGroupUrl="https://vssps.dev.azure.com/$accountName/_apis/graph/Memberships/"+$groupDescriptor+"?direction=down&api-version=5.1-preview.1"
$groupsInGroup=Invoke-WebRequest -Headers $allHeaders -Method GET "$getGroupsInGroupUrl"
if ($groupsInGroup.StatusCode -ne 200)
{
Write-Output $groupMembers.Content
throw "Failed to get member groups"
}
$groupsInGroupJson = ConvertFrom-Json $groupsInGroup.Content
#extract only Azure DevOps or AAD groups from the list
$groupsInGroupJson = $groupsInGroupJson.value | Where-Object {$_.memberDescriptor -like "vssgp.*" -or $_.memberDescriptor -like "aadgp.*"}
foreach($memberGroup in $groupsInGroupJson)
{
if($memberGroup.memberDescriptor -like "vssgp.*")
{
$memberType = "Azure DevOps Group"
}
else
{
$memberType = "AAD Group"
}
$getGroupDetailsUrl=$memberGroup._links.member.href
$memberGroupDetails=Invoke-WebRequest -Headers $allHeaders -Method GET "$getGroupDetailsUrl"
if ($memberGroupDetails.StatusCode -ne 200)
{
Write-Output $groupMembers.Content
throw "Failed to get member groups"
}
$memberGroupDetailsJson = ConvertFrom-Json $memberGroupDetails.Content
$memberDetailsObject=@{
ProjectName = $project.name
MemberName = $memberGroupDetailsJson.principalName
MemberEmail = $memberGroupDetailsJson.mailaddress
MemberType = $memberType
}
$obj = New-Object -Type PSObject -Prop $memberDetailsObject
$groupMembersList += $obj
$memberExists = $true
}
#endregion FindMemberGroups
#region FindDirectGroupMembers
#get members of the provided group
$getGroupMembersUrl = "https://vsaex.dev.azure.com/$accountName/_apis/GroupEntitlements/"+$group.originid+"/members?api-version=5.1-preview.1"
$groupMembers = Invoke-WebRequest -Headers $allHeaders -Method GET "$getGroupMembersUrl"
if ($groupMembers.StatusCode -ne 200)
{
Write-Output $groupMembers.Content
throw "Failed to get group members"
}
$groupMembersJson = ConvertFrom-Json $groupMembers.Content
foreach($member in $groupMembersJson.members)
{
#build and object with the member details and add to the list
$memberDetailsObject=@{
ProjectName = $project.name
MemberName = $member.user.displayName
MemberEmail = $member.user.mailaddress
MemberType = "User"
}
$obj = New-Object -Type PSObject -Prop $memberDetailsObject
$groupMembersList += $obj
$memberExists = $true
}
$groupExists= $true
$groupFound= $true
break #project would have only one group since the group names are unique.
#endregion FindDirectGroupMembers
}
}
if($groupFound)
{
break #project would have only one group since the group names are unique.
}
}
}
}
if($groupExists) #if atleast one group exists with the given name
{
if($memberExists){ #if atleast one memeber exists in any of the groups
#export to a csv file in the same location as the script
$groupMembersList | Select-Object -Property ProjectName, MemberName, MemberEmail, MemberType | Export-CSV -Path (".\$groupName"+"_$accountName.csv") -NoTypeInformation
Write-Output("Processed successfully")
}
else {
Write-Output("The group exists, but there are no members for the provided group across all projects.")
}
}
else{
Write-Output "No matching groups found, please check the group name you have provided"
}
}
catch
{
throw "Failed to export members for the group. Details : $_"
}

Comments

Popular posts from this blog

Azure DevOps - getting project stats for all projects

Do you really need so many Basic + Test Plans licenses?