One of the things that makes Azure DevOps so great is the REST API that comes with it. This API allows you to do almost all the things that you can do through the interface. Unfortunately, it is sometimes a bit behind in functionality when comparing it to the interface. Especially in edge cases or when looking at the newest features, support for these features has sometimes not lighted up in the REST API yet. Or the functionality is available, but it is not yet documented.
One example where I ran into this were the new Environments, that can be used for supporting YAML pipelines. If you are working with tens or hundreds of pipelines, automation is key to doing so effectively so I needed that API!
To work with environments, three types of operations need to be available:
- Management (get, create, update, delete) of environment themselves;
- Management (get, add, remove) of user permissions on those environments;
- Management (get, add, remove) of checks on those environments. Checks are rules that are enforced on every deployment that goes into that environment.
The first type of operation has recently been made available in the preview of the next version of API and can be found here. However, managing user permissions or checks is not yet documented. For a recent project, I went ahead with reverse engineering these calls. In this post I will share how I reverse engineered managing user permissions on environments.
Disclaimer: this is al reverse engineered, so no guarantees whatsoever.
Tip: The approach outlined here works for many of the newer functionalities added to Azure DevOps, which seem to often use calls to URLs that start with _apis that are quite stable in my experience.
Managing user permissions
Finding the call for listing user permissions was rather straight forward. To get the API, I went through the following steps:
- Open the details of an environment and navigate to the security settings (visible on the left in the screenshot below);
- Next I opened up the developer tools, went to the network tab and filtered the list down to XHR requests only and refreshed the page (visible on the right in the screenshot below).
In the list of executed XHR requests, I selected the request that returns the different user permissions. I found this request by first looking at the request below it (roledefinitions), but quickly saw that this only listed the different roles and their names, descriptions and meaning. Inspecting the results visible on the far right will show the active permissions as JSON. I marked the corresponding sections left and right with different colors for the ease of reading.
The URL that was being called for this result was: https://dev.azure.com/azurespecialist/_apis/securityroles/scopes/distributedtask.environmentreferencerole/roleassignments/resources/b6f84576-4e8f-4754-b006-8bd4e735558a_1. Inspecting this URL in detail shows that the 1 at the end corresponds with the id of the environment as it is visible in the URL of the screenshot before. The guid in front of the environmentId took a bit more investigation, but after looking around for a bit, this came out to be the id of the project (formerly Team Project) that the environment is in. From here the call for listing the current user permissions on any environment can be generalized to:
GET https://dev.azure.com/{organizationName}/_apis/securityroles/scopes/distributedtask.environmentreferencerole/roleassignments/resources/{projectId}_{environmentId}
If you are not familiar with your project id(s), you can find those using a GET call to https://dev.azure.com/azurespecialist/_apis/projects.
Adding a user permission
Now that we can view the current set of permissions, let’s see if we can add a new user permission. To get the details of this operation, I did the following:
- Cleared the recent list of captured network operations;
- Make any change to the list on the left (note that there are no XHR requests being made);
- Press the save button in the user interface. This results in the following:
In this second screenshot we see that a PUT request has been made to https://dev.azure.com/azurespecialist/_apis/securityroles/scopes/distributedtask.environmentreferencerole/roleassignments/resources/b6f84576-4e8f-4754-b006-8bd4e735558a_1 with the following content:
[ { "userId":"60aac053-6937-6e07-9a3f-296202a3dfff", "roleName":"Administrator" } ]
This shows that adding permissions can be done by PUTTING an entry to the same URL as we have seen before. The valid values for theĀ roleNames property are Administrator, Reader and User. (They can be retrieved and verified using the roledefinitions call we discovered earlier.) But what do we put in for the user id? To find the user id, we have to do two things.
- Look the user up using the Graph API
- Decode the user descriptor into the correct guid.
The graph API can be accessed through a GET call to https://vssps.dev.azure.com/azurespecialist/_apis/graph/users?api-version=5.1-preview.1, yielding the following response:
[ { "subjectKind": "user", "directoryAlias": "henry", "domain": "c570bc0b-9ef3-4b15-98fc-9d7ca9b22afe", "principalName": "henry@azurespecialist.nl", "mailAddress": "henry@azurespecialist.nl", "origin": "aad", "originId": "186167cb-63ab-4ef9-a221-0398c9ab6bba", "displayName": "Henry Been", "_links": { "self": { "href": "https://vssps.dev.azure.com/azurespecialist/_apis/Graph/Users/aad.NjBhYWMwNTMtNjkzNy03ZTA3LTlhM2YtMjk2MjAyYTNkZmZm" }, "memberships": { "href": "https://vssps.dev.azure.com/azurespecialist/_apis/Graph/Memberships/aad.NjBhYWMwNTMtNjkzNy03ZTA3LTlhM2YtMjk2MjAyYTNkZmZm" }, "membershipState": { "href": "https://vssps.dev.azure.com/azurespecialist/_apis/Graph/MembershipStates/aad.NjBhYWMwNTMtNjkzNy03ZTA3LTlhM2YtMjk2MjAyYTNkZmZm" }, "storageKey": { "href": "https://vssps.dev.azure.com/azurespecialist/_apis/Graph/StorageKeys/aad.NjBhYWMwNTMtNjkzNy03ZTA3LTlhM2YtMjk2MjAyYTNkZmZm" }, "avatar": { "href": "https://dev.azure.com/azurespecialist/_apis/GraphProfile/MemberAvatars/aad.NjBhYWMwNTMtNjkzNy03ZTA3LTlhM2YtMjk2MjAyYTNkZmZm" } }, "url": "https://vssps.dev.azure.com/azurespecialist/_apis/Graph/Users/aad.NjBhYWMwNTMtNjkzNy03ZTA3LTlhM2YtMjk2MjAyYTNkZmZm", "descriptor": "aad.NjBhYWMwNTMtNjkzNy03ZTA3LTlhM2YtMjk2MjAyYTNkZmZm" } ]
From this response we take the descriptor, strip of the prefix of aad. and BASE64 decode the remainder. This yields the guid we need.
Note: updating an entry is done the same way, the PUT operation acts as an upsert.
Deleting a user permission
Deleting user permissions can be done by making two changes:
- Sending a PATCH operation instead of an PUT
- Leaving out the roleName
Happy coding!
Hey Henry
Thanks for this great post! One simplification: if you use “az devops security group list” you can find a group or user and use the “originId” property instead of using the graph API and Base64 decoding.
Thanks for your tip!
How did you authenticalte with this API? If PAT, then what scopes did you use? I’m trying to update permissions but get 401 all the time even with several scopes
Hey Arunkumar! I know for a fact that I’ve been using PATs for authentication when working with environments. As for scopes, I’m sorry but I do not recall which scopes I authorized to get this to working.
It might very well be that this was with ‘all scopes’ allowed, as I encountered this when working with a highly privileged account that performed a lot of automations.