In a project where develop a provisioning engine for Azure DevOps at DB Systel we currently have to deal with the not so in intuitive security and permission handling on the API level.
The naive approach
First, we thought to just check the documentation for the default permissions and then define which permissions we want to have according to the permissions given by the UI.
So we just choose what to set and then use pulumi and create the groups with our chosen permissions. The following problems we had:
- Permission reflected on the UI can really represent a bundle of different permissions (e.g. Edit project-level information)
- Permission on API level work completely different that expected on UI level
- In many cases you have to handle with a lot of Guids that do not tell you anything
So how to handle now to create a group / resource with the permission you intend to have.
The not so naive solution
I post a short script from the PowerShell module VSTeam which gives you quickly an idea how to get the construct your permissions for a new project-level group beware it is hard stuff…
I tried to put explanations in the comments but here I reference the most important lines again:
- Line 16-20: Here we can find out what kind of permissions in which security namespace (e.g. Project) actually exists. This includes the security bit, the internal name AND the display name that you see on the Azure DevOps GUI
- Line 26: Here we basically get the unique key from our existing project that we need to add out permissions to the right project. This key is for security purposes only (I think)
- Line 59-64: We are putting it all together – Our permissions, our project security namespace and our security references to the created group.
#uncomment below if you don't have the module
#Install-Module -Name VSTeam
$org = "#Org#"
$pat = "#Pat#"
$projectName = "#Project#"
$GroupName = "#GroupName#"
$Description = "#GroupDescription#"
Set-VSTeamAccount -Account $org -PersonalAccessToken $pat
##### Get the permissions we want to Deny here in these calls #####
#get namespaces for different security permissions and their needed bit masks
$namespaces = (Get-VSTeamSecurityNamespace) | Sort-Object Name
#permission bit mask / namespace for projects
$projectNameSpace = $namespaces | Where-Object Name -eq Project
#say which permissions we want to have denied. So called 'Actions' can be vied by looking into the contents of $projectNameSpace
$permissionsDeny = @("UPDATE_VISIBILITY", "MANAGE_PROPERTIES", "GENERIC_WRITE", "DELETE", "WORK_ITEM_PERMANENTLY_DELETE")
#handle everything on a project
$project = Get-VSTeamProject -Name $projectName
#get the descriptor for this projects. This is they key for connecting the permission bit masks to the project in the ACL (Access Control Lists)
$projectDescriptor = Get-VSTeamDescriptor -StorageKey $project.ID
#create the group
$groupBody = @{
displayName = $GroupName
description = $Description
}
$jsonBody = $groupBody | ConvertTo-Json -Compress -Depth 100
$group = Invoke-VSTeamRequest `
-NoProject `
-method POST `
-subDomain vssps -area Graph -resource Groups `
-version "5.1-preview.1" `
-body $jsonBody `
-QueryString @{scopeDescriptor = $projectDescriptor.Descriptor } `
-contentType "application/json"
$projectPermissions = $projectNameSpace.Actions | Where-Object { $permissionsDeny.Contains($_["Name"]) }
$projectPermissions | ForEach-Object {
$permission = $_
#to get the correct descriptor we have to take the group descriptor which is base64 encoded.
# for PowerShell I have to add '==' and remove the non Base64 character 'vssgp.' (meaning it is a descriptor for a AzD native group... not AAD)
$decodedDescriptor = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("$($group.descriptor -replace "vssgp.",'')=="))
#if this is not odd enough I have to add certain other strings to token and the descriptor reference for the ACE
# if you want 'Deny' you need to ad the given bit to 'DenyMask'
# if you want 'Allow' you need to ad the given bit to 'AllowMask'
Add-VSTeamAccessControlEntry `
-SecurityNamespaceId $projectNameSpace.ID `
-Token "`$PROJECT:$($group.domain):" `
-Descriptor "Microsoft.TeamFoundation.Identity;$decodedDescriptor" `
-AllowMask 0 `
-DenyMask $permission.Bit
}
The result:
Conclusion
The Azure DevOps API is a hell of a complex mess, but I think I understood after creating the script.
So this means. You are not attaching permissions on the project or group. You rather create new security related data entries into Azure DevOps that link the permission and the resources.
In Database terms resources and permissions are linked as foreign keys uniquely together in a relationship database… At least this is how I try to understand this!
Also published on Medium.
Kevin M Sampson
Great article! I’m learning this myself and found there are ways to give users admin-like access to manage team members and most everything in the project without giving them the ability to delete the project or do anything an auditor might not like