Setting default repository permissions on your Azure DevOps Organization

There is no UI to set the security at the Organization level. This is fine if you're happy with the default security settings in Azure DevOps, but if you want certain settings to apply to all projects, then it's sometimes useful to set the permissions at the Organization level.

In Azure Repos there are a lot of places where you can set security:

  • At the Branch level (develop, master)
  • At the Branches level (default for all branches)
  • At the Tag level
  • At the Tags level (default for all tags)
  • At the Repository level (PartsUnlimited-GDBC)
  • At the Git Repositories level (for all repositories in a project)
Permissions can be managed at each level in the tree. Many permissions will cascade down.

But there is no UI to set the security at the Organization level. This is fine if you're happy with the default security settings in Azure DevOps, but if you want certain settings to apply to all projects (also newly created projects), then it's sometimes useful to set the permissions at the Organization level.

For the Global DevOps Bootcamp we have a few challenges that require changes to be committed to Git through an automated process in order to cause a disruption.

To ensure the changes are able to bypass any branch policies and protected branches, we needed to make sure the service account that makes the change is able to bypass policies.

If you've dug into the security innards of Azure DevOps in the past, you'll have found out that certain permissions are granted to persons or groups and are linked to a token. This token is usually built up out of a root object and a bunch of GUIDs. For example, this is the token for a specific Git Repository:

repoV2/daec401a-49b6-4758-adb5-3f65fd3264e3/f59f38e0-e8c4-45d5-8dee-0d20e7ada1b7
^      ^                                    ^
|      |                                    |
|      |                                    -- The Git Repository
|      -- The Team Project Guid
|
-- The root object (Repositories)
Scope: specific repository.

Simplest way I know of to find these details, is to capture the web request made when a permission is changed:

You can use the Web Developer tools in your favorite browser to find the token you need.

Once you understand this, it's easy to find the token for the "All Repositories in a Team Project" token. Just take off the Git Repository GUID at the end:

repoV2/daec401a-49b6-4758-adb5-3f65fd3264e3b7/
^      ^                                    
|      |                                    
|      -- The Team Project Guid
|
-- The root object (Repositories)
Scope: all repositories in a project.

And, using the same reasoning, to get to the token for "All repositories in the Project Collection/Organization" token. Just take off the Team Project GUID at the end:

repoV2/
^                                          
|
-- The root object (Repositories)
Scope: all repositories in an azure DevOps Organization or Azure DevOps Server Project Collection

And now that we have this token, we can use tfssecurity to set Organization level git permissions:

tfssecurity /a+ "Git Repositories" repoV2/ "PullRequestBypassPolicy" adm: ALLOW /collection:https://dev.azure.com/org
            ^   ^                  ^       ^                         ^    ^
            |   |                  |       |                         |    -- Allow or Deny the permission 
            |   |                  |       |                         -- The Group (in this case "Project Collection Administrators")
            |   |                  |       -- The Permission we want to set
            |   |                  -- The Token we found above
            |   -- The Secuity Namespace
            -- Add  (a+) or Remove (a-) this permission

And, as you can see below, this trick actually works :).

Before: Bypass policies are not set at the Team Project level.
After: Bypass policies are inherited from the Organization level.

You can use the same technique to secure branches. The token of a branch uses the token of the Repository as a basis and adds the branch to that. The token for permissions is a strange numeric branch identifier at first look, but upon closer inspection it turns out to be a hex representation of the branch name:

function hexify($string) {
     return ($string | Format-Hex -Encoding Unicode | Select-Object -Expand Bytes | ForEach-Object { '{0:x2}' -f $_ }) -join ''
}

$branch = "feature/mine"
$split = $branch.Split("/")
$hexBranch = ($split | ForEach-Object { hexify -string $_ }) -join "/"
$token = "refs/heads/$hexBranch"

refs/heads/6600650061007400750072006500/6d0069006e006500
Transform branch name to token (Thanks Pickle Rick!).
repoV2/daec401a-49b6-4758-adb5-3f65fd3264e3/f59f38e0-e8c4-45d5-8dee-0d20e7ada1b7/refs/heads/6600650061007400750072006500/6d0069006e006500
^      ^                                    ^                                    ^
|      |                                    |                                    |
|      |                                    |                                    -- The branch
|      |                                    -- The Git Repository
|      -- The Team Project Guid
|
-- The root object (Repositories)
This was very useful for the Global DevOps Bootcamp. Instead of having to customize the permissions for 3000 Team Projects, we could now simply set this permission in the 7 organizations that were setup for the event.
$orgs = @("gdbc2019-westeurope", "gdbc2019-westeurope2", "gdbc2019-india", "gdbc2019-centralus", "gdbc2019-australia", "gdbc2019-southamerica", "gdbc2019-canada")

$orgs | %{ 
    $org = $_
    & tfssecurity /a+ "Git Repositories" repoV2/ "PullRequestBypassPolicy" adm: ALLOW /collection:https://dev.azure.com/$org
    & tfssecurity /a+ "Git Repositories" repoV2/ "PolicyExempt" adm: ALLOW /collection:https://dev.azure.com/$org
} 
Set default org level permissions.

Note: You can use the REST API to manage security as well, but it requires a little more work to look up the correct identifiers for the Group or User, the Namespace Identifier and more. While generally more complete, the REST API is even harder to understand.

Instead of tfssecurity you can now also use az devops permissions to set permissions. You still need most of the intricate knowledge of the tokens:

# tfssecurity /a+ "Git Repositories" repoV2/ "PullRequestBypassPolicy" adm: ALLOW /collection:https://dev.azure.com/$org

az login
az extension add --name "azure-devops"

# Find the group identifier of the group you want to set permissions for

$org = "gdbc2019-westeurope"

# There is a weird edge case here when an Azure DevOps Organization has a Team Project with the same name as the org.
# In that case you must also add a query to filter on the right domain property `?@.domain == '?'`	

$subject = az devops security group list `
    --org "https://dev.azure.com/$org/" `
    --scope organization `
    --subject-types vssgp `
    --query "graphGroups[?@.principalName == '[$org]\Project Collection Administrators'].descriptor | [0]"
    
$namespaceId = az devops security permission namespace list `
    --org "https://dev.azure.com/$org/" `
    --query "[?@.name == 'Git Repositories'].namespaceId | [0]"

$bit = az devops security permission namespace show `
    --namespace-id $namespaceId `
    --org "https://dev.azure.com/$org/" `
    --query "[0].actions[?@.name == 'PullRequestBypassPolicy'].bit | [0]"

az devops security permission update `
    --id $namespaceId `
    --subject $subject `
    --token "repoV2/" `
    --allow-bit $bit `
    --merge true `
    --org https://dev.azure.com/$org/
Use az devops as a modern alternative to tfssecurity

More information:

Photo credit: Stephen Edmonds.

Leave a comment.