0
votes

I am new to Azure Devops and currently migrating to it. I want to add team members for my azure project via REST API. I referred the following documentation, but there is no mention of it. 'Teams' API has no functionality to add Members to it, rather only to create a Team with the Team Name of your choice.

https://docs.microsoft.com/en-us/rest/api/azure/devops/?view=azure-devops-rest-5.1

I encountered another problem in the Group Entitlements API:

https://docs.microsoft.com/en-us/rest/api/azure/devops/memberentitlementmanagement/group%20entitlements/list?view=azure-devops-rest-5.1

I am unable to hit this particular URL: https://vsaex.dev.azure.com. In the other API examples, they have used only https://dev.azure.com which works perfectly fine for me. I do not understand what the vsaex stands for. Adding 'vsaex' or ignoring it did not work either. I could not find any documentation regarding this. Same problem arises for vsaex.dev.azure.com for Users API.

Solutions to any of these would be helpful. Thanks in advance :)

4
Did you get a chance to try out below answer? Please let me know how did it go? We can discuss it and work out a better solution together.Levi Lu-MSFT

4 Answers

2
votes

I recently write a PowerShell Script to solve your first problem, but it is only tested on a local azure devops server.

class REST {
    #PROPERTIES
    [string]$ContentType = "application/json;charset=utf-8"
    [string]$PAT
    [System.Collections.IDictionary]$Headers
    [string]$Url
    [string]$Collection
    [string]$_Project

    #STATIC PROPERTIES
    static [int]$Timeout = 30

    #CONSTRUCTOR
    REST([string]$PAT, [string]$Url, [string]$Collection, [string]$Project) { $this.Init($PAT, $Url, $Collection, $Project) }
    REST([string]$PAT, [string]$Url, [string]$Collection) { $this.Init($PAT, $Url, $Collection, $null) }
    REST([string]$PAT, [string]$Url) { $this.Init($PAT, $Url, $null, $null) }
    REST([string]$PAT) { $this.Init($PAT, $null, $null, $null) }

    #INITIALIZE
    [void]Init([string]$PAT, [string]$Url, [string]$Collection, [string]$Project) {
        $this.PAT = $PAT
        $this.Url = $Url
        $this.Collection = $Collection
        $this._Project = $Project
        $this.Headers = $(Headers -PAT $PAT) 
    }

    #GET
    [PSCustomObject]Get([string]$Uri) { return Invoke-RestMethod -Uri $Uri -Method GET -ContentType $this.ContentType -Headers $this.Headers -TimeoutSec $([REST]::Timeout) -Verbose }

    #PUT
    [PSCustomObject]Put([string]$Uri, $Body) { return Invoke-RestMethod -Uri $Uri -Method PUT -ContentType $this.ContentType -Headers $this.Headers -Body $Body -TimeoutSec $([REST]::Timeout) -Verbose }

    #POST
    [PSCustomObject]Post([string]$Uri, $Body) { return Invoke-RestMethod -Uri $Uri -Method POST -ContentType $this.ContentType -Headers $this.Headers -Body $Body -TimeoutSec $([REST]::Timeout) -Verbose }

    #DELETE
    [PSCustomObject]Delete([string]$Uri) { return Invoke-RestMethod -Uri $Uri -Method DELETE -ContentType $this.ContentType -Headers $this.Headers -TimeoutSec $([REST]::Timeout) -Verbose }


    #TEAMS
    [PSCustomObject]Teams([string]$Url, [string]$Collection, [string]$Project) { return $($this.Get($(Combine @($Url, $Collection, $Project, "_settings/teams?__rt=fps&__ver=2")))).fps.dataProviders.data.'ms.vss-tfs-web.team-data' }
    [PSCustomObject]Teams([string]$Collection, [string]$Project) { return $this.Teams($this.Url, $Collection, $Project) }
    [PSCustomObject]Teams([string]$Project) { return $this.Teams($this.Url, $this.Collection, $Project) }
    [PSCustomObject]Teams() { return $this.Teams($this.Url, $this.Collection, $this._Project) }

    #TEAM MEMBERS
    [PSCustomObject]TeamMembers([string]$Url, [string]$Collection, [string]$Project, [string]$TeamId) { return $this.Get($(Combine @($Url, $Collection, $Project, "_api/_identity/ReadGroupMembers?__v=5&scope=$($TeamId)&readMembers=true&scopedMembershipQuery=1"))) }
    [PSCustomObject]TeamMembers([string]$Collection, [string]$Project, [string]$TeamId) { return $this.TeamMembers($this.Url, $Collection, $Project, $TeamId) }
    [PSCustomObject]TeamMembers([string]$Project, [string]$TeamId) { return $this.TeamMembers($this.Url, $this.Collection, $Project, $TeamId) }
    [PSCustomObject]TeamMembers([string]$TeamId) { return $this.TeamMembers($this.Url, $this.Collection, $this._Project, $TeamId) }

    #TEAM MEMBER POST
    [PSCustomObject]TeamMemberPost([string]$Url, [string]$Collection, [string]$Project, [string]$TeamId, [string]$Domain, [string]$Name) { $body = '{{''newUsersJson'':''[\''{0}\\\\{1}\'']'',''existingUsersJson'':''[]'',''groupsToJoinJson'':''[\''{2}\'']'',''aadGroupsJson'':''[]''}}' -f ($Domain, $Name, $TeamId); return $this.Post($(Combine @($Url, $Collection, $Project, "_api/_identity/AddIdentities?__v=5")), $body) }
    [PSCustomObject]TeamMemberPost([string]$Collection, [string]$Project, [string]$TeamId, [string]$Domain, [string]$Name) { return $this.TeamMemberPost($this.Url, $Collection, $Project, $TeamId, $Domain, $Name) }
    [PSCustomObject]TeamMemberPost([string]$Project, [string]$TeamId, [string]$Domain, [string]$Name) { return $this.TeamMemberPost($this.Url, $this.Collection, $Project, $TeamId, $Domain, $Name) }
    [PSCustomObject]TeamMemberPost([string]$TeamId, [string]$Domain, [string]$Name) { return $this.TeamMemberPost($this.Url, $this.Collection, $this._Project, $TeamId, $Domain, $Name) }
}

These are the REST-API calls I used for.

  • #TEAMS returns all teams of a project as json. The call also gives you the $TeamId
  • #TEAM MEMBERS give you all members of a team
  • #TEAM MEMBER POST allows you to add you new members. Important: the members must be known by Azure DevOps, that means they need to be in your domain (I don't know how it is organized in azure devops service)

How to use: (but this in the same file like the REST class or load the REST class as module or file before)

#ADD = LIST OF VALID AND KNOWN MEMBERS OF YOUR AZURE DEVOPS SERVICE (STORE IT IN A .TXT FILE OR SOMETHING)
$ADD = @("[email protected]", "[email protected]")

#INITIALIZE REST API
$REST = [REST]::new($PAT, $Uri, $Collection, $Project) #$PAT ~ "atfghfrhfdgdwnx6jnyrculcmaas2g5j6rrogpmn7aza266hrudsahq"; $Uri = https://server.com

#REQUEST TEAMS
$result = $REST.Teams()
$team = $result.team

#REQUEST TEAM MEMBERS
$result = $REST.TeamMembers($team.id)
$members = $result.identities.MailAddress

#ADD MISSING MEMBERS TO TEAM
foreach ($item in $ADD) {

    if (-not $members.Contains($item)) {
        Write-Host "[ps1] add: '$item'" -ForegroundColor Yellow

        #POST ADD MEMBER
        $name = $item.Replace($mail, "")
        $result = $REST.TeamMemberPost($team.id, $domain, $name)
        if ("AddedIdentities" -in $result.PSobject.Properties.Name) { Write-Host "[ps1] successful added: $($result.AddedIdentities.DisplayName) ($($result.AddedIdentities.TeamFoundationId))" -ForegroundColor Green } 
        else { Write-Host "[ps1] fail to add: '$name'" -ForegroundColor Red }
    }
}

I take the snippts from my script. I don't have the time to test this stuff, so please expect errors.


How to find out the correct URLs by your self:

  • Open Browser (I used Edge)
  • Press F12
  • Go to Network
  • Navigate to the event you want to observe

enter image description here

  • Clear the list
  • Execute the event (click button)
  • Check out the GET/POST with application/json like in sceen shot:

enter image description here

If it is a GET/POST Event you can display the the transfered json under text

{
  "newUsersJson": "[\"Domain\\\\user\"]",
  "existingUsersJson": "[]",
  "groupsToJoinJson": "[\"2d1dfa03-a108-4421-958a-bdsfdsf161696\"]",
  "aadGroupsJson": "[]"
}

Hope this helps.

2
votes

You can use member add api to user to team members.

PUT https://vsaex.dev.azure.com/{organization}/_apis/GroupEntitlements/{groupId}/members/{memberId}?api-version=5.1-preview.1

When you go to the Permissions under Project Settings, You will find the team is actually listed as a group. So i tried using team Id for the groupId in above api. And it worked.

After testing,the memeberId is actually the user id.

You can get the user id with below Get User Entitlements api: check here for details.

GET https://vsaex.dev.azure.com/{organization}/_apis/userentitlements?top={top}&skip={skip}&filter={filter}&sortOption={sortOption}&api-version=5.1-preview.2

Then you can call above member add api to add user to teams.

$uri ="https://vsaex.dev.azure.com/{ORG}/_apis/GroupEntitlements/{teamid}/members/{userid}?api-version=5.1-preview.1"

$connectionToken="PAT"

$base64AuthInfo= [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes(":$($connectionToken)"))

# Invoke the REST call and capture the results (notice this uses the PATCH methodg
$result = Invoke-RestMethod -Uri $group -Headers @{Authorization=("Basic {0}" -f $base64AuthInfo)} -Method put

If you cannot hit https://vsaex.dev.azure.com. You may need to check if your {PAT} has the all the permission scopes to perform add member action. Check here for more information about PAT.

There is a lack of information about vsaex. but i guess vsaex is the server domain for user ad data. As Microsoft manage user ad data information in a separate server from other data.

2
votes

Not 100% an answer to your question, but maybe it can help you or other people.

Recently I had to add user to a team in an AzDo project. I had the email for the users and the name of the team.

I used the following Powershell code in AzDo version M183_20210320.1:

$PAT = "my-path"; # get your Personal access token https://docs.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&tabs=preview-page
$Headers += @{Authorization = 'Basic ' + [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(":$($PAT)")) };
 
$Collection = 'my-organisation';
$Project = 'my-project';
$Timeout = 30;
$TeamToAddUser = "my-team-name>";
$EmailToAddToTeam = "[email protected]";
# get project id
$ProjectId = (Invoke-RestMethod -Uri "https://dev.azure.com/$($Collection)/_apis/projects/$($Project)?api-version=6.0" -Method GET -ContentType "application/json;charset=utf-8" -Headers $Headers -TimeoutSec $Timeout).id;
Write-Host "ProjectId: $ProjectId";
# get project descriptor
$ProjecDescriptor = (Invoke-RestMethod -Uri "https://vssps.dev.azure.com/$($Collection)/_apis/graph/descriptors/$($ProjectId)?api-version=6.0-preview" -Method GET -ContentType "application/json;charset=utf-8" -Headers $Headers -TimeoutSec $Timeout).value;
Write-Host "ProjecDescriptor: $ProjecDescriptor";
   
# get all teams in project
$TeamsInProject = (Invoke-RestMethod -Uri "https://vssps.dev.azure.com/$($Collection)/_apis/graph/groups?scopeDescriptor=$($ProjecDescriptor)&api-version=6.0-preview" -Method GET -ContentType "application/json;charset=utf-8" -Headers $Headers -TimeoutSec $Timeout).value;
Write-Host "TeamsInProject: $($TeamsInProject | forEach { "`n - $($_.displayName) : $($_.descriptor)" }) `n";

# get the team 
$Team = $TeamsInProject | Where-Object { $_.displayName -eq $TeamToAddUser }
Write-Host "Team: $($Team.displayName) : $($Team.descriptor)";

# get user id from email
$User = (Invoke-RestMethod -Uri "https://vsaex.dev.azure.com/$($Collection)/_apis/userentitlements?api-version=6.0-preview.3&`$filter=name eq '$(${EmailToAddToTeam})'" -Method GET -ContentType "application/json;charset=utf-8" -Headers $Headers -TimeoutSec $Timeout).members[0];
Write-Host "User to add user: $($User.user.displayName) : $($User.user.originId)";

# add user to team
$body = @{
   "originId" = $User.user.originId
};
$result = (Invoke-RestMethod -Uri "https://vssps.dev.azure.com/$($Collection)/_apis/graph/Users?groupDescriptors=$($Team.descriptor)&api-version=6.0-preview" -Method POST -ContentType "application/json;charset=utf-8" -Headers $Headers -Body $($body | ConvertTo-Json -Depth 10 -Compress) -TimeoutSec $Timeout)
0
votes

After spending hours to do this via API I found a solution, You can use

POST   https://vssps.dev.azure.com/{organization}/_apis/graph/users?groupDescriptors={groupDescriptor}&api-version=6.0-preview.1

you can add email in a requestbody, JSON will look like

{
  "principalName": "[email protected]"
}

For getting the group descriptor use below GET call

GET https://vssps.dev.azure.com/{organization}/_apis/graph/groups?api-version=6.0-preview.1

Check for group with "displayName" as "{your project name} Team" in the response and take group descriptor of that group.

If you are using Postman to make this call, select basic Auth as authorization and give username empty and password as the PAT token.