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.
You can then filter by project to look at the details of individual projects.
How to run the
script:
- 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.
- 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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
Post a Comment