paint-brush
Automation of Rights, Permissions, and Access Levels in Azure DevOpsby@socialdiscoverygroup
13,398 reads
13,398 reads

Automation of Rights, Permissions, and Access Levels in Azure DevOps

by Social Discovery GroupNovember 29th, 2023
Read on Terminal Reader
Read this story w/o Javascript

Too Long; Didn't Read

When the business is growing rapidly, a DevOps engineer faces the challenge of automating processes in all the systems used for CI/CD, including manual access management. Such routine tasks slow down the resolution of customer requests dramatically. In the article, the Social Discovery Group team shares customized solutions to automate rights, permissions, and access levels in Azure DevOps.
featured image - Automation of Rights, Permissions, and Access Levels in Azure DevOps
Social Discovery Group HackerNoon profile picture

When the business is growing rapidly, a DevOps engineer faces the challenge of automating processes in all the systems used for CI/CD, including manual access management. Such routine tasks may be acceptable when you have a small team, up to 10 releases with 3 stages each, and 10 variable groups.


However, imagine if your team grows to hundreds of people who need to manually operate with up to 500 releases, each with 10 stages, and 200 variable groups. In this case, manual access management slows down the resolution of customer requests dramatically.


At this stage, every business faces a need for efficient automatization.


In Social Discovery Group, we rely on Azure DevOps as the primary cloud technology. However, currently, it lacks the functionality to automate rights, permissions, and access levels. To address the user needs promptly, we decided to find some custom solutions and automate assigning permissions.

How to Automate in Azure DevOps

In this article, we will share some good hucks on how to automate certain routine tasks in Azure DevOps, focusing on:


  • Assigning permissions to a group or user for variable groups based on a mask using Azure Pipeline.


  • Assigning group or user permissions to specific stages for entire release groups, categorized into catalog groups using Azure Pipeline.

The Technology Stack Used: Azure Services, Azure DevOps, Bash, Azure CLI

Currently, Azure offers 3 options for assigning permissions: using the site's graphical interface, using the az DevOps CLI, and the API requests. The first option is the most straightforward and fast; however, as the company scales and the number of CI/CD pipelines and variables increases, manually assigning permissions becomes time-consuming.


The second option for granting permissions is limited in functionality; in other words, the user cannot go beyond what a specific team offers. The third option through the REST API is the most versatile at the moment. Let's consider a specific example where we have numerous release pipelines with multiple stages:


Release pipelines with multiple stages


Each deployment stage requires permission from a specific user/group (the so-called pre-deployment approvals function). A problem appears here when a specific user or a group needs to be added or removed from a given list of approvers.


As the company expands, when the number of pipelines and stages increases by N times, the routine case becomes a problem, and the solution to this issue is only possible through API requests. Here is how the implementation looks from our side:


" 
pool:
vmImage: ubuntu-latest

parameters:
- name: folder_names
type: string
default: '\'
- name: stage_names
type: string
default: 'Dev'
- name: group_names
type: string
default: 'Devops'

variables:
- name: organization
value: ORGANIZATION
- name: project
value: PROJECT
- name: pat
Value: PAT

steps:
- bash: |
export organization="$(organization)"
export project="$(project)"
export pat="$(pat)"
export folder_names='${{ parameters.folder_names }}'
export stage_names='${{ parameters.stage_names }}'
export group_names='${{ parameters.group_names }}'
export pipeline_name_array=()
export stage_name_array=()
export group_name_array=()
IFS=',' read -ra folder_name_array <<< "$folder_names"
IFS=',' read -ra stage_name_array <<< "$stage_names"
IFS=',' read -ra group_name_array <<< "$group_names"

# Make a GET request to retrieve the release pipelines within the specified project
pipeline_url="https://vsrm.dev.azure.com/$organization/$project/_apis/release/definitions?api-version=7.0"
pipeline_response=$(curl -s -u ":$pat" "$pipeline_url")

# Check for errors in the response
if [[ "$pipeline_response" == *"error"* ]]; then
echo "Error fetching release pipeline data."
exit 1
fi

pipeline_names=($(echo "$pipeline_response" | jq -r '.value[].name'))
pipeline_ids=($(echo "$pipeline_response" | jq -r '.value[].id'))

for ((i=0; i<${#pipeline_names[@]}; i++)); do
pipeline_name="${pipeline_names[i]}"
pipeline_id="${pipeline_ids[i]}"

# Make a GET request to retrieve the release pipeline details
pipeline_detail_url="https://vsrm.dev.azure.com/$organization/$project/_apis/release/definitions/$pipeline_id?api-version=7.0"
pipeline_detail_response=$(curl -s -u ":$pat" "$pipeline_detail_url")

# Extract the releaseDefinition.path from the pipeline details
pipeline_path=$(echo "$pipeline_detail_response" | jq -r '.path')

# Check if the pipeline_path matches any of the specified folder_names
for folder_name in "${folder_name_array[@]}"; do
if [[ "$pipeline_path" == *"$folder_name"* ]]; then
# Make a GET request to retrieve all releases for the specified pipeline
releases_url="https://vsrm.dev.azure.com/$organization/$project/_apis/release/releases?api-version=7.0&definitionId=$pipeline_id"
releases_response=$(curl -s -u ":$pat" "$releases_url")

# Extract the release names
release_names=$(echo "$releases_response" | jq -r '.value[].id')

release_definition_url="https://vsrm.dev.azure.com/$organization/$project/_apis/release/definitions/$pipeline_id?api-version=7.0"
release_definition_response=$(curl -s -u ":$pat" "$release_definition_url")

# Iterate through each group name
for group_name in "${group_name_array[@]}"; do
# Make a GET request to retrieve the list of groups
groups_response=$(curl -s -u ":$pat" "https://vssps.dev.azure.com/$organization/_apis/graph/groups?api-version=7.1-preview.1")

# Find the origin_id for the specified group name
origin_id=$(echo "$groups_response" | jq -r ".value[] | select(.displayName == \"$group_name\") | .originId")

if [ -z "$origin_id" ]; then
echo "Group '$group_name' not found or origin_id not available for release '$release_name'."
else
# Iterate through each stage name
for stage_name in "${stage_name_array[@]}"; do
# Construct the JSON structure for the new approval
new_approval='{
"rank": 1,
"isAutomated": false,
"isNotificationOn": false,
"approver": {
"id": "'"$origin_id"'"
}
}'

# Use jq to update the JSON structure for the specified stage
updated_definition=$(echo "$release_definition_response" | jq --argjson new_approval "$new_approval" '.environments |= map(if .name == "'"$stage_name"'" then .preDeployApprovals.approvals += [$new_approval] else . end)')

# Make a PUT request to update the release definition for the specified stage
put_response=$(curl -s -u ":$pat" -X PUT -H "Content-Type: application/json" -d "$updated_definition" "$release_definition_url")

release_definition_response=$(curl -s -u ":$pat" "$release_definition_url")

# Check if the update was successful
if [[ "$put_response" == *"The resource could not be found."* ]]; then
echo "Error updating release definition for stage '$stage_name' and group '$group_name'."
else
echo "Pre-deployment approval added successfully to stage '$stage_name' for group '$group_name' in '$pipeline_id'."
fi
done
fi
done
fi
done
done

displayName: 'PreDeployApprovals' 
"


A similar issue arises with the Library section when numerous variable groups are created, where a specific word is used to indicate affiliation with something. Let's break down an example where multiple variable groups are created containing the word "SERVICES" to denote their affiliation with service-related variables.


And also examine a scenario where a specific group needs access to all variables with certain permissions.


Library section when numerous variable groups


The solution to this common case is also possible only via the REST API:


"
variables:
- name: organization
value: ORGANIZATION
- name: project
value: PROJECT
- name: pat
value: PAT
parameters:
- name: searchWord
type: string
default: 'SERVICES'
- name: UserOrGroupName
type: string
default: 'Devops'
- name: UserRights
type: string
default: 'Administrator'

steps:
- bash: |
export organization="$(organization)"
export project="$(project)"
export pat="$(pat)"
export userOrGroupName='${{ parameters.UserOrGroupName }}'
export UserRights='${{ parameters.UserRights }}'
export searchWord='${{ parameters.searchWord }}'

# Perform the API request and store the JSON response in a variable
response=$(curl -s -u ":$pat" "https://dev.azure.com/$organization/$project/_apis/distributedtask/variablegroups?api-version=6.0")

# Initialize an empty array to store matching JSON objects
matching_json=()

# Loop through the JSON objects and append matching objects to the array
while read -r json; do
if [[ $(echo "$json" | jq -r '.name' | grep "$searchWord") ]]; then
matching_json+=("$json")
fi
done < <(echo "$response" | jq -c '.value[]')


# Iterate through the matching variable groups and assign permissions
for group in "${matching_json[@]}"; do
# Extract the variable group ID and name
variableGroupId=$(echo "$group" | jq -r '.id')
variableGroupName=$(echo "$group" | jq -r '.name')

# Determine the type of userOrGroupName (username or group name)
if [[ $myString != *'@'* ]]; then
# If userOrGroupName matches the username format, it's treated as a username
roleName=$UserRights
assignType="group"
groupsResponse=$(curl -s -u ":$pat" "https://vssps.dev.azure.com/$organization/_apis/graph/groups?api-version=6.0-preview.1")
#groupOriginId=$(curl -s -u ":$pat" "https://vssps.dev.azure.com/$organization/_apis/identities?searchFilter=$userOrGroupName&api-version=6.0" | jq -r '.value[0].originId')
groupOriginId=$(echo "$groupsResponse" | jq -r ".value[] | select(.displayName == \"$userOrGroupName\") | .originId")

# Get the user's originId using Azure DevOps REST API
#userOriginId=$(curl -s -u ":$pat" "https://vssps.dev.azure.com/$organization/_apis/identities?searchFilter=$userOrGroupName&api-version=6.0" | jq -r '.value[0].principalName')
else
# Otherwise, it's treated as a group name
roleName=$UserRights
assignType="user"
userOriginId=$(curl -s -u ":$pat" "https://vssps.dev.azure.com/$organization/_apis/identities?searchFilter=$userOrGroupName&api-version=6.0" | jq -r '.value[0].principalName')
# Get the group's originId using Azure DevOps REST API
#groupOriginId=$(curl -s -u ":$pat" "https://vssps.dev.azure.com/$organization/_apis/identities?searchFilter=$userOrGroupName&api-version=6.0" | jq -r '.value[0].originId')
fi

# Construct the API URL for assigning permissions
apiUrl="https://dev.azure.com/$organization/_apis/securityroles/scopes/distributedtask.variablegroup/roleassignments/resources/19802563-1b7b-43d6-81e0-16cf29d68c0d$"$variableGroupId"?api-version=5.1-preview"

# Assign permissions based on the userOrGroupName type
if [ "$assignType" == "group" ]; then
# Assign permissions to a group
# Construct the request body with group's originId
requestBody="[{
\"roleName\": \"$roleName\",
\"userId\": \"$groupOriginId\"
}]"
else
# Assign permissions to a user
# Construct the request body with the user's uniqueName
requestBody="[{
\"roleName\": \"$roleName\",
\"uniqueName\": \"$userOrGroupName\"
}]"
fi

# Execute the REST API call to assign permissions
response=$(curl -s -u ":$pat" -X PUT -H "Content-Type: application/json" -d "$requestBody" "$apiUrl")

# Check if the permissions were successfully assigned
if [[ "$response" == *"error"* ]]; then
echo "Error assigning permissions to $assignType '$userOrGroupName' in variable group '$variableGroupName'."
else
echo "Permissions assigned successfully to $assignType '$userOrGroupName' in variable group '$variableGroupName'."
fi
done

displayName: 'variable-groups' "


By automating these processes, we gained a more detailed understanding of rights and permission structures in Azure DevOps, enhancing the project's security.


We now have stable and versatile pipelines for a wide range of tasks, speeding up the processing time for user requests and improving their execution quality.


Additionally, our overall system development speed and quality have improved because we allocate more time to other CI/CD tasks, moving away from routine actions.


We have put in significant effort to automate daily tasks, explore and analyze various implementation options, debug, and refine. Having ready-made pipelines and scripts is a tremendous convenience, as they can accomplish tasks in a matter of seconds.