Add user to Azure DevOps Team and set as Administrator via REST API

PR Code - Christopher Pateman
Version 1
Published in
8 min readJun 10, 2024

When using the Azure DevOps REST API to create Teams and add users, you may want to set the administrators as well at the same time. However, this REST API is not well documented on the Microsoft documentation site for the REST API. With some searching and mapping of resources, I have below the method on how to add users to an Azure DevOps team and then set them as Administrators.

Photo by Mikhail Fesenko on Unsplash

Assumptions

During this post, there will be some assumptions through the code, as all of them will be using the REST API and authenticating in the same way.

Authentication

When Authenticating with the API we are going to use a `Basic` authentication, which is using a Personal Access Token (PAT). This can be generated by an Azure DevOps user, or if using this through a pipeline, you can use the `System.AccessToken` variable. With this token, we will encrypt it in the correct format using the below PowerShell code. The `$user` can be any text and is used to form the access token. This token will be used throughout the API calls.

$user = "[anything]" 
$accessToken = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $user, $PersonalAccessToken)))

Code Formatting

We will also have a standard format for all the API requests and the code on how we request them. Following the underlined components below, we will have:

  • NAME: the name of the function.
  • $PARAMETERS: all parameters being passed into the function.
  • ACTION: This will print what it is doing for each request and to what.
  • REST API URL: This is the Azure DevOps REST API URL, made up of parameters.
  • $ENCRYPTED_PAT: The access token generated with the above code.
  • ERROR: This is a print of the issue if the API was not successful.
  • $ITEM: The response will be in JSON, so we convert it to a PowerShell Object for us to handle later.

The Solution

Below I will go through each request required to add the user to the team and then set them as an administrator. Finally, at the end, I will show you the complete script that could be used to add a user.

Get Team ID

If you know the team ID(GUID) already, then you could save resources bypassing this request, but for most, you would just have the team’s name to request. With this section, we would use the Team GET

Although the parameter names for this request are `projectId` and `teamId` you can, for both, pass the name of each into the request. This makes it easier not to need to know the technical GUID of the items.

Get Descriptor

Descriptors are unique identifiers for security-related resources like users, groups, and scopes. They are used to represent and reference these items within Azure DevOps.

Later in the request to add the user to the team, we are required to get the team/group descriptor. For this, we will use the Descriptor GET request.

This request requires the `Storage Key`, which is the target items GUID. This would be in our case the Azure DevOps team ID, which we would get from the request above.

Get user ID

This request is a little harder as you cannot get a user just by their name, and we need to get more than just the user ID for later. What we require here is to use the User Entitlements GET request, with a filter.

This request will return many users and without the filter, it will return all of them, but we can use the query parameter `filter` with the value `name –eq {Users Name}`. This will search for the user with a matching name that we can pass in. For the use case, I had users called `Firstname.Surname`, which made it easier to search for them.

This will, if successful, return a single user in an array that we can source out. Within the user’s object, you will then have access to the `id` and the `originId`. The `id` is the GUID representation of the user in Azure DevOps, whereas the `originId` is the GUID representation from the source the user is stored in, e.g. Active Directory. These will be used later for distinctively different requests.

Add user to team

This request will seem strange as we are going to use a graph request to create a user in a group, even though our aim is to add a user to a team. This request creates the user’s reference in Azure DevOps, or if existing it will update, then using the query parameters, we scope the adding of the user to the team. The request is documented here as Create Users.

As mentioned in the query parameter `groupDescriptor` we enter the team’s descriptor, sourced from the request above to scope the adding to the desired team. Within the body of the POST request, we can reference the Origin and Origin ID sourced from the user GET request from above as well. The `storageKey` can just be left blank to save some complexity.

Set User as Admin

Once the above request is successful, we can then set the user as an administrator. Technically, you don’t need the user to be a member of the team to make them an administrator, but it is a good practice. For this API call, I could not find any official documentation, but from various other community questions, I found the URL to be this.
`POST https://dev.azure.com/{OrganisationName}/{ProjectName}/_api/_identity/AddTeamAdmins?api-version=5.1-preview.1`

The body then consists of:

The Team ID, we would have got from the GET Team API call earlier and because we have already added the user into the Team, we don’t need to use the `newUserJson` parameter. For the `existingUsersJson` it is an Array of the Azure DevOps User IDs in JSON format, but then escaped and put into a string. For example:

"[""3b42bb8a-9c9e-441f-9f83–935eab97563e"",""13d79ee8–7e92–4923-a710-c327ef556ef4"]"

For this parameter, you only need to add the IDs you are adding, so there is no reason to get the current administrators.

Complete Script

Below is a complete script on how to do the above action. This discovery was made while doing an Azure DevOps migration between Organization, which uses some of the components of this script to move Team members and set up their Team, permissions, and administrative rights.

<#
.SYNOPSIS
Add a user to an Azure DevOps Team and make an Admin if required

.PARAMETER OrganisationName
Azure DevOps Organisation Name

.PARAMETER projectName
Azure DevOps Organisation Name

.PARAMETER personalAccessToken
Azure DevOps Personal Access Token

.PARAMETER teamName
Azure DevOps Team Name

.PARAMETER userName
Azure DevOps Username

.PARAMETER isAdmin
Boolean if user is Admin or not

.EXAMPLE
Add-UserToTeam -OrganisationName myOrg -projectName myProject -teamName myTeam -userName chris.pateman -personalAccessToken *** -isAdmin $true

.NOTES

#>
function Add-UserToTeam {
param (
[Parameter(Mandatory = $true)]
[string]
$OrganisationName,
[Parameter(Mandatory = $true)]
[string]
$projectName,
[Parameter(Mandatory = $true)]
[string]
$personalAccessToken,
[Parameter(Mandatory = $true)]
[string]
$teamName,
[Parameter(Mandatory = $true)]
[string]
$userName,

[Parameter(Mandatory = $false)]
[bool]
$isAdmin = $false
)

$user = "[anything]"
$accessToken = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $user, $personalAccessToken)))

$team = Get-Team -accessToken $accessToken -orgName $OrganisationName -projectName $projectName -teamName $teamName
$user = Get-User -accessToken $accessToken -orgName $OrganisationName -userName $userName

if ($null -eq $user -or $nul -eq $team){
throw "##[error]Unable to get User and/or Team"
}

Add-TeamUser -accessToken $accessToken -orgName $OrganisationName -teamId $team.id -userId $user.id -userOriginId $user.user.originId -userOriginType $user.user.origin -projectName $projectName -isAdmin $isAdmin

}

<#
.SYNOPSIS
Get an Azure DevOps Team

.PARAMETER accessToken
Encrypted PAT

.PARAMETER orgName
Azure DevOps Organisation Name

.PARAMETER projectName
Azure DevOps Project Name

.PARAMETER teamName
Azure DevOps Team Name to get

.NOTES
The names can be the ID
#>
function Get-Team($accessToken, $orgName, $projectName, $teamName) {

Write-Host ""
Write-Host "-- Get Team $teamName from $orgName/$projectName --"

$url = "https://dev.azure.com/$orgName/_apis/projects/$projectName/teams/$teamName`?api-version=7.1-preview.3"

Write-Host "Requesting: $url"
$response = Invoke-WebRequest -Uri $url -Headers @{"Authorization" = "Basic $accessToken"; 'X-VSS-ForceMsaPassThrough' = $true; }

if ($response.StatusCode -ne "200") {
Write-Host "##[error]API Response Error (Status Code: $($response.StatusCode))" -ForegroundColor Red
throw $response.content
}

$team = $($response.content | ConvertFrom-Json)
Write-Host "##[success]$($team.name)(ID: $($team.id)) found" -ForegroundColor Green

return $team
}

<#
.SYNOPSIS
Get User from Azure DevOps Organisation

.PARAMETER accessToken
Encrypted PAT

.PARAMETER orgName
Azure DevOps Organisation Name

.PARAMETER userName
Users name to search for in Azure DevOps

.NOTES

#>
function Get-User($accessToken, $orgName, $userName) {

Write-Host ""
Write-Host "-- Get $userName from $orgName --"

$url = "https://vsaex.dev.azure.com/$orgName/_apis/userentitlements?api-version=5.1-preview.2&filter=name eq $userEmail"

Write-Host "Requesting: $url"
$response = Invoke-WebRequest -Uri $url -Headers @{"Authorization" = "Basic $accessToken"; 'X-VSS-ForceMsaPassThrough' = $true; }

if ($response.StatusCode -ne "200") {
Write-Host "##[error]API Response Error (Status Code: $($response.StatusCode))" -ForegroundColor Red
throw $response.content
}

$user = $($response.content | ConvertFrom-Json).members[0]
Write-Host "##[success]$($user.user.displayName)(ID: $($tuser.user.originId)) found" -ForegroundColor Green

return $team
}

<#
.SYNOPSIS
Get any items Descriptor ID

.PARAMETER accessToken
Encrypted PAT

.PARAMETER orgName
Azure DevOps Organisation Name

.NOTES
example descriptor is 'aad.MjhfMGI4YzktMTA2Yi03GjcxLTk13DktMTdmYWE2ZDgwNGUz'
#>
function Get-Descriptor($accessToken, $orgName, $itemId) {

Write-Host ""
Write-Host "-- Get Descriptor for $itemId --"

$url = "https://vssps.dev.azure.com/$orgName/_apis/graph/descriptors/$itemId`?api-version=7.1-preview.1"

Write-Host "Requesting: $url"
$response = Invoke-WebRequest -Uri $url -Headers @{"Authorization" = "Basic $accessToken"; 'X-VSS-ForceMsaPassThrough' = $true; 'Content-Type' = 'application/json' }

if ($response.StatusCode -ne "200") {
Write-Host "##[error]API Response Error (Status Code: $($response.StatusCode))" -ForegroundColor Red
throw $response.content
}

$descriptor = $($response.content | ConvertFrom-Json).value
Write-Host "##[success]Descriptor Found $descriptor" -ForegroundColor Green

return $descriptor
}

<#
.SYNOPSIS
Add a User to ADO Team

.PARAMETER accessToken
Encrypted PAT

.PARAMETER orgName
Azure DevOps Organisation Name

.PARAMETER teamId
Azure DevOps Team GUID

.PARAMETER userId
Azure DevOps User ID GUID

.PARAMETER userOriginId
Users Origin ID GUID

.PARAMETER userOriginType
User Origin source shorthand e.g. aad

.PARAMETER projectName
Azure DevOps Project Name

.PARAMETER isAdmin
Should be added as Admin or not

#>
function Add-TeamUser($accessToken, $orgName, $teamId, $userId, $userOriginId, $userOriginType, $projectName, $isAdmin) {

Write-Host ""
Write-Host "-- Add $userOriginId into Team $teamId --"

$teamDescriptor = Get-Descriptor -accessToken $accessToken -orgName $orgName -itemId $teamId

$url = "https://vssps.dev.azure.com/$orgName/_apis/Graph/Users?groupDescriptors=$teamDescriptor&api-version=7.1-preview.1"

$body = @{
storageKey = ""
originId = $userOriginId
origin = $userOriginType
}

Write-Host "Requesting: $url"
$response = Invoke-WebRequest -Method Post -Body $($body | ConvertTo-Json -Compress -Depth 100) -Uri $url -Headers @{"Authorization" = "Basic $accessToken"; 'X-VSS-ForceMsaPassThrough' = $true; 'Content-Type' = 'application/json' }

if ($response.StatusCode -ne "200" -and $response.StatusCode -ne "201") {
Write-Host "##[error]API Response Error $($response.StatusCode)" -ForegroundColor Red
throw $response.content
}
else {
Write-Host "##[success]Added user to team" -ForegroundColor Green
if ($isAdmin) {
Add-TeamAdmin -accessToken $accessToken -orgName $orgName -teamId $teamId -userId $userId -projectName $projectName
}
}

}

<#
.SYNOPSIS
Add a User as Admin to ADO Team

.PARAMETER accessToken
Encrypted PAT

.PARAMETER orgName
Azure DevOps Organisation Name

.PARAMETER projectName
Azure DevOps Project Name

.PARAMETER teamId
Azure DevOps Team GUID

.PARAMETER userId
Azure DevOps User ID GUID

.NOTES
It is the Users ID not Origin ID
#>
function Add-TeamAdmin($accessToken, $orgName, $projectName, $teamId, $userId) {

Write-Host ""
Write-Host "-- Add $userId as Admin into Team $teamId --"

$url = "https://dev.azure.com/$orgName/$projectName/_api/_identity/AddTeamAdmins?api-version=5.1-preview.1"

$body = @{
teamId = $teamId
newUsersJson = "[]"
existingUsersJson = "[""$userId""]"
}

Write-Host "Requesting: $url"
$response = Invoke-WebRequest -Method Post -Uri $url -Body $($body | ConvertTo-Json -Compress -Depth 100) -Headers @{"Authorization" = "Basic $accessToken"; 'X-VSS-ForceMsaPassThrough' = $true; 'Content-Type' = 'application/json' }

if ($response.StatusCode -ne "200" -and $response.StatusCode -ne "201") {
Write-Host "##[error]API Response Error $($response.StatusCode)" -ForegroundColor Red
throw $response.content
}

#$admin = $response.content | ConvertFrom-Json
Write-Host "##[success]Added user as Admin of Team" -ForegroundColor Green
}

If you’re interested in professional DevOps services, you might find Version 1’s DevOps services helpful.

About the Author

Christopher Pateman is an Azure Team Lead here at Version 1.

--

--

PR Code - Christopher Pateman
Version 1

I’m a Azure DevOps Engineer with a wide knowledge of the digital landscape. I enjoy sharing hard to find fixes and solutions for the wider community to use.