Automating Azure Resource Elevated Role Requests with Power Automate and Azure Automation

Automating Azure Resource Elevated Role Requests with Power Automate and Azure Automation

Adam MaurerTechnical Leave a Comment

Image
1. Inside an Azure subscription of your choice, create an Automation Account resource by visiting portal.azure.com, select All Resources and click Add.

a. Search for “Automation” and click on Create.

Automation

Fill out all the necessary fields on the Automation creation form and click Create

Image

b. Import the AzureAD modules
c. To do this, open the Automation Account we’ve just created, select Modules located under Shared Resources, select Browse gallery, and search for AzureAD.

Modules

d. Import the AzureAD module and wait until the process has completed successfully.

2. Create Runbooks

a. Navigate to your Automation Account, select Runbooks, and click Create a Runbook.

Rubooks

b. When the Runbook is created, click Edit and paste your PowerShell script then click on Save and Publish.


3. The script below can be used to elevate the access of a User to the Contributor role and triggers emails should any errors occur. The script first validates if the user has a Reader role already assigned to the subscription they wish to gain elevated access to. If they do not, it will error out. If the user already has a Contributor role on the requested resource, this too will error out.

The highlighted areas should be adapted to fit your environment details.

param (
    [Parameter(Mandatory = $true)]
    [string] $subscriptionId,
    [Parameter(Mandatory = $true)]
    [string] $resourceGroupName,
    [Parameter(Mandatory = $true)]
    [string] $resourceName,
    [Parameter(Mandatory = $true)]
    [string] $resourceType,
    [Parameter(Mandatory = $true)]
    [string] $userPrincipalName,
    [Parameter(Mandatory = $false)]
    [string] $linkUrl
)
$ErrorActionPreference = "Continue" $errors = @() function Send-Email($subject, $body, $to, $cc = $null, $isBodyHtml = $false) {     $credential = Get-AutomationPSCredential -Name 'SMTP Relay'     $smtpServer =  'smtp.example.com'     $smtpPort =  <Int32>     $from =  'example@examplecompany.com'     $mailParams = @{         To         = $to         Subject    = $subject         Body       = $body         SmtpServer = $smtpServer         Credential = $credential         Port       = $smtpPort         UseSsl     = $true         From       = $from         BodyAsHtml = $isBodyHtml     }     if ($cc) {         $mailParams['Cc'] = $cc     }     Send-MailMessage @mailParams -ErrorAction Stop } try {     $connection = Connect-AzAccount -Identity } catch {     $errors += "Failed to authenticate with Azure using Managed Identity: $_" } try {     Set-AzContext -SubscriptionId $subscriptionId } catch {     $errors += "Failed to set Azure subscription context: $_" } $roleDefinitionName = 'Contributor' $readerRoleDefinitionName = 'Reader' try {     $user = Get-AzADUser -UserPrincipalName $userPrincipalName -ErrorAction Stop } catch {     $errors += "Failed to retrieve user object for UPN '$userPrincipalName': $_" } # Check if the user has the Reader role at the subscription level $readerRoleAssignment = Get-AzRoleAssignment -ObjectId $user.Id -RoleDefinitionName $readerRoleDefinitionName -Scope "/subscriptions/$subscriptionId" -ErrorAction SilentlyContinue if (-not $readerRoleAssignment) { $noReaderRoleHtml = @" <h2>Not in Reader Role</h2>
<p>User <strong>$userPrincipalName</strong> does not have the <strong>$readerRoleDefinitionName</strong> role at the subscription level and cannot be added to the <strong>$roleDefinitionName</strong> role for the resource.</p> "@ $noReaderRoleHtml += $signature Send-Email -subject "Missing Reader Role" -body $noReaderRoleHtml -to $userPrincipalName -cc "itadministrator@examplecompany.com" -isBodyHtml $true return } # Check if the resource group exists $resourceGroupExists = $true $resourceGroup = Get-AzResourceGroup -Name $resourceGroupName -ErrorAction SilentlyContinue if (-not $resourceGroup) { $errors += "Resource group '$resourceGroupName' does not exist." $resourceGroupExists = $false } # Check if the resource exists within the resource group $domain = "yourdomain.onmicrosoft.com" # replace this with your actual AAD tenant domain # Check if the resource exists within the resource group using both name and type $resourceExists = $false $resourceUrl = "" if ($resourceGroupExists) { $resources = Get-AzResource -ResourceGroupName $resourceGroupName -ResourceType $resourceType -Name $resourceName -ErrorAction SilentlyContinue $resource = $resources | Where-Object { $_.ResourceType -eq $resourceType -and $_.Name -eq $resourceName } if ($resource) { $resourceExists = $true # Construct the URL using the domain, resource group, resource type, and resource name $resourceUrl = "https://portal.azure.com/#@$domain/resourcegroups/$resourceGroupName/providers/$($resource.ResourceType)/$resourceName" } else { $errors += "Resource of type '$resourceType' with name '$resourceName' does not exist in resource group '$resourceGroupName'." } } # Attempt role assignment only if resource group and resource exist if ($resourceGroupExists -and $resourceExists) { try { $roleAssignment = New-AzRoleAssignment -ObjectId $user.Id -RoleDefinitionName $roleDefinitionName -Scope $resource.ResourceId -ErrorAction Stop } catch { if ($_.Exception -match 'Conflict') { # User already has the Contributor role, send a specific email $alreadyContributorHtml = @" <h2>User Already a Contributor</h2>             <p>User <strong>$userPrincipalName</strong> already has the <strong>$roleDefinitionName</strong> role for resource <strong>$resourceName</strong>.</p>             <p>Resource URL: <a href='$linkUrl'>$linkUrl</a></p> "@             $alreadyContributorHtml += $signature Send-Email -subject "User Already Has Contributor Role" -body $alreadyContributorHtml -to $userPrincipalName -cc "itadministrator@examplecompany.com" -isBodyHtml $true         } else {             $errors += "Failed to assign role '$roleDefinitionName' to user '$userPrincipalName': $_"         }     } } $signature = @" <p>Best regards,</p> <p><strong>Your IT Team</strong></p> <p><em>This is an automated message, please do not reply directly to this email.</em></p> "@ if ($errors) {     $errorMessageHtml = "<h2>Issues Detected in Azure Role Assignment</h2><ul>"     foreach ($errorItem in $errors) {         $errorMessageHtml += "<li>$errorItem</li>"     }     $errorMessageHtml += "</ul>$signature" Send-Email -subject "Azure Role Assignment Issues" -body $errorMessageHtml -to $userPrincipalName -cc "itadministrator@examplecompany.com" -isBodyHtml $true } elseif ($roleAssignment) {     # Role assignment was successful, send a success email     $successMessageHtml = @"     <h2>Role Assignment Success</h2>     <p>User <strong>$userPrincipalName</strong> was successfully assigned the <strong>$roleDefinitionName</strong> role for resource <strong>$resourceName</strong>.</p>     <p>Resource URL: <a href='$linkUrl'>$linkUrl</a></p> "@     $successMessageHtml += $signature Send-Email -subject "Azure Role Assignment Success" -body $successMessageHtml -to $userPrincipalName -cc "itadministrator@examplecompany.com" -isBodyHtml $true }

Please note that the above and below PowerShell scripts typically take approximately 5 minutes before they complete, as the AzureAD modules take time to load.

Also note, that when adding a new role such as Contributor, the user should log out and back into Azure or else use an Incognito/Private window for the new role to take effect.

4. A second Runbook should be created to remove the Contributor role when a defined period of time has elapsed.

An example of a PowerShell script for this is:

  param ( 
    [Parameter(Mandatory = $true)] 
    [string] $subscriptionId, 
    [Parameter(Mandatory = $true)] 
    [string] $resourceGroupName, 
    [Parameter(Mandatory = $true)] 
    [string] $resourceName, 
    [Parameter(Mandatory = $true)] 
    [string] $resourceType, 
    [Parameter(Mandatory = $true)] 
    [string] $userPrincipalName, 
    [Parameter(Mandatory = $false)] 
    [string] $linkUrl 
) 

$ErrorActionPreference = "Continue" 
$errors = @() 
$signature = @" 

<p>Best regards,</p> <p><strong>Your IT Team</strong></p> <p><em>This is an automated message, please do not reply directly to this email.</em></p> "@ function Send-Email($subject, $body, $to, $cc = $null, $isBodyHtml = $false) {     $credential = Get-AutomationPSCredential -Name 'SMTP Relay' $smtpServer = 'smtp.example.com' $smtpPort = <int 32> $from = 'example@examplecompany.com'     $mailParams = @{         To         = $to         Subject    = $subject         Body       = $body         SmtpServer = $smtpServer         Credential = $credential         Port       = $smtpPort         UseSsl     = $true         From       = $from         BodyAsHtml = $isBodyHtml     }     if ($cc) {         $mailParams['Cc'] = $cc     }     Send-MailMessage @mailParams -ErrorAction Stop } try {     $connection = Connect-AzAccount -Identity } catch {     $errors += "Failed to authenticate with Azure using Managed Identity: $_" } try {     Set-AzContext -SubscriptionId $subscriptionId } catch {     $errors += "Failed to set Azure subscription context: $_" } $roleDefinitionName = 'Contributor' try {     $user = Get-AzADUser -UserPrincipalName $userPrincipalName -ErrorAction Stop } catch {     $errors += "Failed to retrieve user object for UPN '$userPrincipalName': $_" }

Not in Reader Role

# Check if the resource group exists and get the resource 
$resourceGroupExists = $resourceExists = $false 
if ((Get-AzResourceGroup -Name $resourceGroupName -ErrorAction SilentlyContinue)) { 
    $resourceGroupExists = $true 
    $resources = Get-AzResource -ResourceGroupName $resourceGroupName -ResourceType $resourceType -Name $resourceName -ErrorAction SilentlyContinue 
    $resource = $resources | Where-Object { $_.ResourceType -eq $resourceType -and $_.Name -eq $resourceName } 

    if ($resource) { 

        $resourceExists = $true 

    } else { 
        $errors += "Resource of type '$resourceType' with name '$resourceName' does not exist in resource group '$resourceGroupName'." 
    } 
} else { 

    $errors += "Resource group '$resourceGroupName' does not exist." 
} 

# Attempt to remove the role assignment if resource group and resource exist 

if ($resourceGroupExists -and $resourceExists) { 
    try { 

        $roleAssignments = Get-AzRoleAssignment -ObjectId $user.Id -RoleDefinitionName $roleDefinitionName -Scope $resource.ResourceId -ErrorAction SilentlyContinue 

        foreach ($roleAssignment in $roleAssignments) { 

            # Note: Confirmation suppression will work if the cmdlet supports it.  
            Remove-AzRoleAssignment -ObjectId $user.Id -RoleDefinitionName $roleDefinitionName -Scope $resource.ResourceId -Confirm:$false 

        } 

        # Check if role assignments are successfully removed 
        if (-not (Get-AzRoleAssignment -ObjectId $user.Id -RoleDefinitionName $roleDefinitionName -Scope $resource.ResourceId -ErrorAction SilentlyContinue)) { 

            # If removal is successful, send a success email 
            $successMessageHtml = @" 
            <h2>Role Removal Success</h2> 
            <p>User <strong>$userPrincipalName</strong> was successfully removed from the <strong>$roleDefinitionName</strong> role for resource <strong>$resourceName</strong>.</p>
            <p>Resource URL: <a href='$linkUrl'>$linkUrl</a></p> 
"@ 
            $successMessageHtml += $signature 
  Send-Email -subject "Azure Role Removal Success" -body $successMessageHtml -to $userPrincipalName -cc "itadministrator@examplecompany.com " -isBodyHtml $true 
        } else { 
            $errors += "Role '$roleDefinitionName' still exists for user '$userPrincipalName' after removal attempt." 
        } 

    } catch { 
        $errors += "Failed to remove role '$roleDefinitionName' from user '$userPrincipalName': $_" 
    } 

} 

if ($errors) { 
    $errorMessageHtml = "<h2>Issues Detected in Azure Role Removal</h2><ul>" 
    foreach ($errorItem in $errors) { 
        $errorMessageHtml += "<li>$errorItem</li>" 
    } 
    $errorMessageHtml += "</ul>$signature" 
  Send-Email -subject "Azure Role Removal Issues" -body $errorMessageHtml -to $userPrincipalName -cc "itadministrator@examplecompany.com" -isBodyHtml $true 

} 

5. Give the Automation Account the User Access Administrator role per subscription

a. For the PowerShell script to be able to run, the Automation Account must have the User Access Administrator role.
b. Add a role in each subscription where you want to allow elevated access to be requested for.
c. In the Role section, Select User Access Administrator then click Next.

add-role-assignment

d. In the Members section, select Managed identity then + Select members, select the Subscription where the Automation Account is running, select the Managed identity and the member in the Select field.

e. The selected member will populate at the bottom of the screen and then click Select and Review + assign.

Image

f. The Automation Account Enterprise Application will now be visible in the subscription(s) as having the user Access Administrator role under Access control (IAM)

Image

6. Create a Microsoft Form requesting the URL of the resource and any other information you require as per your business needs. In the below example, we will ask for the business reason and the number of hours one requires elevated access to the resource, with a maximum of 8 hours possible for them to select from the dropdown list.

azure-subscription-access-request

7. Setup a Security Group in Office Admin

a. Assign the Members that are allowed to make the elevated access request. (Optional but highly recommended – If you don’t want to perform this security check, proceed to the Flow creation below – Step 9)

active-teams-and-groups

b.Once the Security Group is created, make note of the Security Group ID located in the URL. For example, the security group ID is the GUID located here:

https://admin.microsoft.com/Adminportal/Home#/groups/:/GroupDetails/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/1

8. Register an application in portal.azure.com

a. Grant Admin Consent to the following Microsoft Graph API Permissions with Type Application:

      • Directory.Read.All
      • Group. Read.All
      • User.Read.All
config-permissions

b. Create a Client secret for the Application and make note of the Tenant ID, the Application (client) ID and the Secret Value for the Flow.

client-secrets

8. Now setup a Flow to process the form data and execute the PowerShell scripts:

a. Microsoft Forms trigger – When a new response is submitted

b. Microsoft Forms action – Get response details from the trigger responseId

c. If you skipped the security group recommendation, proceed to step 10 below

I. Get user profile (v2) using the trigger responder as the User (UPN)

II. To validate that the user is part of the security group, create an HTTP GET request to URI: https://graph.microsoft.com/v1.0/groups/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/transitiveMembers?$filter=id eq 'outputs('Get_user_profile_(V2) ')?['body/id']'&$select=id

graphic-part01

III. In the HTTP GET request, expand the Authentication and ensure Authentication Type is Active Directory OAuth, Audience is https://graph.microsoft.com/, and that the notated values when you registered the Application in Azure are added to the respective fields:

form

IV. Send an email to IT Admin if request rejected is set to run if the Security Group validation fails to obtain a record. And an email is then sent to the requester notifying them they are not part of the security group to be able to make such a request.

Image

V. It is worth noting that the above security check can also be managed directly in the access of the Microsoft Form, but the security group is the safer solution that requires less maintenance:

settings

10. Create a condition to check if the URLs provided to gain elevated access are in the list of subscriptions you will allow them to request elevated access to.

Image

a. To parse the subscription from the provided URL, us the following expression, replacing the highlighted Get_response_details value pointing to the URL field of your form:

first(split(last(split(body('Get_response_details')?['URLfieldID'],'/resource/subscriptions/')),'/resourceGroups/'))

b. If the URL is for a subscription not listed, send a rejection email to the submitter

c. If the URL contains a subscription in the list, proceed to Creating an Azure Automation job

11. In the Azure Automation action, ensure you fill in all the highlighted values, pointing to the Runbook which grants the elevated access of Contributor.

a. Subscription = The subscription where the Runbook is

b. Resource Group = Resource Group where the Runbook is

c. Automation Account = The Automation Account which was setup previously b

d. Runbook Name = Name of the Runbook to grant the Contributor role

e. Once the Runbook is selected, the Parameters from the PowerShell script will populate for you to add the required details. To parse this information from the URL that was provided at form submission:

I. SubscriptionId

first(split(last(split(body('Get_response_details')?['URLfieldID'],'/resource/subscriptions/')),'/resourceGroups/'))

II. ResourceGroupName

first(split(last(split(body('Get_response_details')?['URLfieldID'],'/resourceGroups/')),'/providers/'))

III. ResourceGroupName

first(split(first(split(last(split(body('Get_response_details')?['URLfieldID'],'/providers/')),concat('/',last(split(body('Get_response_details')?['URLfieldID'],'/'))))),concat('/',last(split(first(split(last(split(body('Get_response_details')?['URLfieldID'],'/providers/')),concat('/',last(split(body('Get_response_details')?['URLfieldID'],'/'))))),'/')))))

IV. UserPrincipalName

body('Get_response_details')?['responder']

V. ResourceName

last(split(first(split(last(split(body('Get_response_details')?['URLfieldID'],'/providers/')),concat('/',last(split(body('Get_response_details')?['URLfieldID'],'/'))))),'/'))

VI. LinkUrl

body('Get_response_details')?['URLfieldID']

form02
Note that ResourceType is technically not required for the script to run only if you have a naming convention that ensures each resource name within a resource group is unique. To avoid errors in the granting of the Contributor role, it is recommended to keep it in the script.

12. In this example, we allow the user to define how long they require elevated access. In the Delay the Count is defined by the number provided in the form field for hours while the Unit is Hour.

13. After the Delay, we Create a new Azure Automation action, this time pointing to the Runbook which removes the Contributor role for the user on that particular resource.

a. The fields are identical as the fields to grant the Contributor role as well as the parsing for the respective parameters.

Several ideas that can be adapted or added to this Flow that we will not go into are:

  1. Use your internal ticketing system as the trigger as opposed to Microsoft Forms
  2. Allow the user to select Resource Group and/or Subscription level access, based on their needs. Understand some parsing expressions will change when doing this.
  3. Add an Approval Flow sent to a person or group of people that have the ability to approve or deny the request.

Leave a Reply

Your email address will not be published. Required fields are marked *

For security, use of Google's reCAPTCHA service is required which is subject to the Google Privacy Policy and Terms of Use.