Azure DevOps Security API demystified

Developer Support

App Dev Manager Art Garcia demystifies Azure DevOps Security APIs to extract nearly any information about your projects and teams.

When you think of DevOps, you probably think of methodologies and platforms, builds and pipelines, and all sorts of cool industry buzzwords. DevOps is about providing structure and process to how development done, while giving the end user what they want and need the most: Value. Security plays a primary role in that value proposition. In a complex project, we can lose track of what groups have what rights within the project. We can go thru the DevOps project and go to each team and look at each component in Azure DevOps, but that does not give us a unified view of security for the project. In this article, I will show how to use the Azure DevOps REST API to get a picture of all rights for all teams in the project.

In Azure DevOps, you can manage your security for a given team or group using the Permissions module. In this example, the API New Team has inherited and granted permissions. The “Allow permissions to view project level information” has been granted explicitly, while the permissions to delete, edit and manage projects has been inherited from the org settings.

This is great, but what other permissions does the API New team have? Well, you can go thru the entire site looking for each security role, but that would be very time consuming and inefficient. That’s where the Azure DevOps REST API comes into the picture. We can use the API to view and manage permissions for all resources within the project. Sounds great, right? Well, it’s not always as easy as it seems. The API is very powerful but also very complex .Let’s start with the current documentation.

The documentation states that the security namespace is where we house the permissions to resources. Each security resource has resources associated to it. Each namespace contains access control lists and each access control list contains a token, an inherit flag, and zero to multiple access control entries. Each access control entry contains a descriptor, an allowed permissions bitmask and a deny permission bitmask. Whew! that’s a lot of detail in a single paragraph! Let’s try to simplify it some…

First, what exactly is all this namespace, control lists, and control entries business and why should I care? The security namespace is where each family of resources (Git repositories, Build, etc.) is secured. Remember that I said each resource will have a security namespace associated with it. And that each namespace will have zero or more access control lists associated with it. An access control list is where we find the inherit flag which tells us if the permission is inherited from another group. A token and the access control entry. The token is an identifier for the resource. The access control entry is where we will find the security information we are looking for. Even though it’s here we find the information, it’s not that easy to decipher. This is probably for security purposes, to keep your settings locked down.

The access control entry has a descriptor, along with an allow and deny bitmask. Here is where the fun starts! This bitmask must be decoded to get the rights. You must convert the bitmask to a binary number and use that to determine allow and deny rights. Because of the masked security, this extra step ensures your information stays secure, but you still get to the date you’re looking for.

Below is an example of an access control list and the accompanying access control entries. You’re probably thinking that all this is very interesting, but how do I find what my specific team or group in my specific project has rights to do? How do I associate the team or group to this access control list and how do I find the permissions my group or team has access to? Trust me, I was there, too!

So, here is where the Azure DevOps Rest API comes to save the day. With some PowerShell, the API calls, some help from the Azure DevOps Product Group, and a great deal of luck, I will attempt to make sense of all of this.

So, lets start with the team or group. If I use the Graph API call to get a list of all teams, I will get something like this. I highlighted the descriptor because that is what we need to find the access control lists. The first problem is this value is base 64 encoded. I have a function that takes the group descriptor and returns the decoded descriptor to use in finding the access control list.

function Get-DescriptorFromGroup()
        [Parameter(Mandatory = $true)]

    $b64 = $dscriptor.Split('.')[1]
    $rem = [math]::ieeeremainder( $b64.Length, 4 ) 
    $str = ""
    $ln1 = 0
    $dscrpt = ""

    if($rem -ne 0)
        $ln1 = (4 - [math]::Abs($rem))
        if ($ln1 -gt 2)
            $ln1 = 2
        $str = ("=" * $ln1)
        $b64 +=  $str
    try {
        Write-Host $b64
        $dscrpt = [System.Text.Encoding]::ASCII.GetString([System.Convert]::FromBase64String($b64))
    catch {
          $ErrorMessage = $_.Exception.Message
          $FailedItem = $_.Exception.ItemName
          Write-Host "Security Error : " + $ErrorMessage + " iTEM : " + $FailedItem
    return $dscrpt

This will return the group descriptor as shown below.

Group: Build Administrators
Descriptor : Microsoft.TeamFoundation.Identity;S-1-9-1551374245-2275748270-3080019534-3196173461-3388768270-0-0-0-1-2

I append the “Microsoft.TeamFoundation.Identity;” and that is what we use as the descriptor. There is also an undocumented API call that will take the descriptor and return the group name that can be found here. I use that to verify that we decoded the descriptor correctly.

Next, I find all the security namespaces for the given project. This call returns the security namespace and the list of available actions. These actions are the allowed or deny permissions.

"namespaceId":  "52d39943-cb85-4d7f-8fa8-c6baac873819",
        "name":  "Project",
        "displayName":  "Project",
        "separatorValue":  ":",
        "elementLength":  -1,
        "writePermission":  2,
        "readPermission":  1,
        "dataspaceCategory":  "Default",
        "actions":  [
                            "bit":  1,
                            "name":  "GENERIC_READ",
                            "displayName":  "View project-level information",
                            "namespaceId":  "52d39943-cb85-4d7f-8fa8-c6baac873819"
                            "bit":  2,
                            "name":  "GENERIC_WRITE",
                            "displayName":  "Edit project-level information",
                            "namespaceId":  "52d39943-cb85-4d7f-8fa8-c6baac873819"
                            "bit":  4,
                            "name":  "DELETE",
                            "displayName":  "Delete team project",
                            "namespaceId":  "52d39943-cb85-4d7f-8fa8-c6baac873819"
                            "bit":  8,
                            "name":  "PUBLISH_TEST_RESULTS",
                            "displayName":  "Create test runs",
                            "namespaceId":  "52d39943-cb85-4d7f-8fa8-c6baac873819"
                            "bit":  16,
                            "name":  "ADMINISTER_BUILD",
                            "displayName":  "Administer a build",
                            "namespaceId":  "52d39943-cb85-4d7f-8fa8-c6baac873819"
                            "bit":  32,
                            "name":  "START_BUILD",
                            "displayName":  "Start a build",
                            "namespaceId":  "52d39943-cb85-4d7f-8fa8-c6baac873819"
                            "bit":  64,
                            "name":  "EDIT_BUILD_STATUS",
                            "displayName":  "Edit build quality",
                            "namespaceId":  "52d39943-cb85-4d7f-8fa8-c6baac873819"
                            "bit":  128,
                            "name":  "UPDATE_BUILD",
                            "displayName":  "Write to build operational store",
                            "namespaceId":  "52d39943-cb85-4d7f-8fa8-c6baac873819"
                            "bit":  256,
                            "name":  "DELETE_TEST_RESULTS",
                            "displayName":  "Delete test runs",
                            "namespaceId":  "52d39943-cb85-4d7f-8fa8-c6baac873819"
                            "bit":  512,
                            "name":  "VIEW_TEST_RESULTS",
                            "displayName":  "View test runs",
                            "namespaceId":  "52d39943-cb85-4d7f-8fa8-c6baac873819"
                            "bit":  2048,
                            "name":  "MANAGE_TEST_ENVIRONMENTS",
                            "displayName":  "Manage test environments",
                            "namespaceId":  "52d39943-cb85-4d7f-8fa8-c6baac873819"
                            "bit":  4096,
                            "name":  "MANAGE_TEST_CONFIGURATIONS",
                            "displayName":  "Manage test configurations",

Now that I have the security namespace and the descriptor for the group, I need to find the allow and deny permissions. I need to put them all together and get the access control list for the given security namespace and given descriptor. This will give the access control list. I have formatted it for clarity

      inheritPermissions: True
     Descriptor :Microsoft.TeamFoundation.Identity;S-1-9-1551374245-2275748270-3080019534-3196173461-3388768270-0-0-0-1-2
     Group Name :[AzureDevOpsDemo]\Build Administrators
     Allow      :112
     Deny       :0

The next step is to decode the bitmask. This is relatively straight forward , but I was I was never able to find this documented, I ended up going to some of my internal resources to find the answer. So, to decode it, I used the ToString operator in PowerShell.

 $permAllow = [convert]::ToString($_.Value.allow,2)

For the above allow bit of 112, I get 1110000. To determine the allow or deny permissions, I go to the security namespace and find the action list. I take the action list and associate it to the binary number. Once I decode the bitmask, I can look up the action and find the allow or deny permission as illustrated below. Finally adding up the values gives us the original bit and verifies I decoded it correctly. This also works on the deny bitmask and the inherited permissions.

20 - 0
21 - 0
22 - 0
23 - 0
25 - 1 = 32 START_BUILD
16 + 32 + 64 = 112

I have a PowerShell project here that takes a given Azure DevOps project and finds all the permissions for each team or group by security namespace. Feel free to use what I have learned to find what permissions exist in your project.

Using the Azure DevOps Security API is challenging and not for the faint of heart, but the power it gives you is well worth the investment. With a little education and research, you can harness the full power of Azure DevOps API to extract nearly any information about your projects and teams. This will help you understand how your teams are configured, ensure the proper security is being enforced, and keep your DevOps projects running smoothly.

1 comment

Discussion is closed. Login to edit/delete existing comments.

  • Neil Fraser 0

    Thank you Art, this blog is fantastic.

    I was about half way through the process of unpicking the security API, and was starting to get disheartened by the complexity of the task.
    Your blog has saved me a huge amount of time and distress.

    As an update the ‘Identity’ API is now an official API as of version 6:


Feedback usabilla icon