Fill out all the necessary fields on the Automation creation form and click Create
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.
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.
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.
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)
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.
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)
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
-
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.
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
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:
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.
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:
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.
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']
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:
- Use your internal ticketing system as the trigger as opposed to Microsoft Forms
- 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.
- Add an Approval Flow sent to a person or group of people that have the ability to approve or deny the request.
COO at Connecting Software
Author:
I am the Chief Operating Officer for Connecting Software, managing the day-to-day operations throughout our various locations. I am passionate about continuous improvement and increasing efficiency. If you want to join our amazing team either in Slovakia or Madeira, please reach out.
Is There More Information Online?
Check other articles on our blog about Exchange Server synchronization: