"Reverse engineering" the Azure REST API
The Azure REST API is well documented and thanks to the REST API browser you can quickly try something out. However, there are moments in the Azure portal that lead to puzzled faces and in these cases the “Developer tools” of your favorite browser help to shed light into the dark.
The scenario
A user with “Read Only” rights on the subscription should be able to see if and where a virtual machine is backed up. The user can see the virtual machine without problems, but after clicking on “Backup” the request to set up the backup appears.
So for the inclined user, this virtual machine is not backed up.
Search for solutions
More permissions
More is always better and “Read Only” is not enough for many services (e.g. storage accounts) to see more than just the resource. What is in the resource is invisible for the user.
So I added the user to the group “Backup Reader” and checked if the Backup Vault can be accessed.
Works fine … Clicked back on the Backup tab in the virtual machine and the same error message appears as before. Because with the Backup Vault, “Reader” is also sufficient to see the individual Protected Items. Back to the drawing board.
A look under the hood
Return to the overview page of the virtual machines and activate the Chrome “Developer tools” using “CTRL + SHIFT + I”.
Then switch to the “Network” page and record only XMLHttpRequest (XHR) under Filter. Also make sure that the log is not lost (Preserve log).
Now click on Backup and the two red “403 - Forbidden” error messages immediately catch your eye.
You can find out more about this when you select the request in question. The headers show that, contrary to expectations, no GET was performed here, but a POST.
At the very bottom you can also find the called request
Request Header:
....
x-ms-path-query: /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/providers/Microsoft.RecoveryServices/locations/westeurope/backupStatus?api-version=2016-06-01
Request Payload:
{
"resourceId": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/RESOURCEGROUPNAME/providers/Microsoft.Compute/virtualMachines/VMNAME",
"resourceType": "VM"
}
In " Preview" the parsed JSON output can be displayed and this gives even more information about the performed action “Microsoft.RecoveryServices/locations/backupStatus/action”.
{
"error": {
"code": "AuthorizationFailed",
"message": "The client 'user@bader.cloud' with object id 'd73b4dd6-0666-449c-aad2-nnnnnnnnnn' does not have authorization to perform action 'Microsoft.RecoveryServices/locations/backupStatus/action' over scope '/subscriptions/9c80de97-daff-4246-aa54-nnnnnnnnn'."
}
}
In Microsoft’s RBAC documentation, this action is described as “Check Backup Status for Recovery Services Vaults”.
The solution
Custom RBAC Role
The solution is quickly implemented. Since the RBAC documentation was very revealing, I create a separate role with the same rights as the Reader
role plus the action Microsoft.RecoveryServices/locations/backupStatus/action
.
{
"Name": "Reader including BackupStatus",
"IsCustom": true,
"Description": "Lets you view everything including the backup status, but not make any changes.",
"Actions": \[
"\*/read",
"Microsoft.RecoveryServices/locations/backupStatus/action"
\],
"AssignableScopes": \[
"/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
\]
}
Now create the new role in the Azure Portal and assign it to the user / user group.
New-AzureRmRoleDefinition -InputFile '.\AzureReaderIncludingBackup.json'
The result
Looks good. No more 403 error and the user sees immediately that his virtual machine is backed up. But he is not allowed to restore anything, because he lacks further rights.
Bonus
While checking the executed REST requests, the following request caught my eye
https://management.azure.com/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/RESOURCEGROUPNAME/providers/Microsoft.RecoveryServices/vaults/BACKUPVAULTNAME/backupFabrics/Azure/protectionContainers/iaasvmcontainer;iaasvmcontainerv2;RESOURCEGROUPNAME;VMNAME/protectedItems/vm;iaasvmcontainerv2;RESOURCEGROUPNAME;VMNAME?$filter=expand%20eq%20%27ExtendedInfo%27&api-version=2017-07-01
The actual GET request is documented, but not the filter used
$filter=expand+eq+'ExtendedInfo'&api-version=2017-07-01
Through this, the normal response…
{
"id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/RESOURCEGROUPNAME/providers/Microsoft.RecoveryServices/vaults/BACKUPVAULTNAME/backupFabrics/Azure/protectionContainers/IaasVMContainer;iaasvmcontainerv2;RESOURCEGROUPNAME;VMNAME/protectedItems/VM;iaasvmcontainerv2;RESOURCEGROUPNAME;VMNAME",
"name": "VM;iaasvmcontainerv2;RESOURCEGROUPNAME;VMNAME",
"type": "Microsoft.RecoveryServices/vaults/backupFabrics/protectionContainers/protectedItems",
"properties": {
"friendlyName": "VMNAME",
"virtualMachineId": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/RESOURCEGROUPNAME/providers/Microsoft.Compute/virtualMachines/VMNAME",
"protectionStatus": "Healthy",
"protectionState": "Protected",
"healthStatus": "Passed",
"healthDetails": \[
{
"code": 400239,
"title": "IaasVmHealthGreenDefault",
"message": "Backup pre-check status of this virtual machine is OK.",
"recommendations": \[\]
}
\],
"lastBackupStatus": "Completed",
"lastBackupTime": "2018-07-23T02:36:32.2674417Z",
"protectedItemDataId": "140738223649118",
"protectedItemType": "Microsoft.Compute/virtualMachines",
"backupManagementType": "AzureIaasVM",
"workloadType": "VM",
"containerName": "iaasvmcontainerv2;RESOURCEGROUPNAME;VMNAME",
"sourceResourceId": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/RESOURCEGROUPNAME/providers/Microsoft.Compute/virtualMachines/VMNAME",
"policyId": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/RESOURCEGROUPNAME/providers/Microsoft.RecoveryServices/vaults/BACKUPVAULTNAME/backupPolicies/AQI-PRO-DailyPolicy",
"policyName": "DailyPolicy",
"lastRecoveryPoint": "2018-07-23T02:36:35.6143732Z"
}
}
… becomes a somewhat more detailed one.
{
"id": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/RESOURCEGROUPNAME/providers/Microsoft.RecoveryServices/vaults/BACKUPVAULTNAME/backupFabrics/Azure/protectionContainers/IaasVMContainer;iaasvmcontainerv2;RESOURCEGROUPNAME;VMNAME/protectedItems/VM;iaasvmcontainerv2;RESOURCEGROUPNAME;VMNAME",
"name": "VM;iaasvmcontainerv2;RESOURCEGROUPNAME;VMNAME",
"type": "Microsoft.RecoveryServices/vaults/backupFabrics/protectionContainers/protectedItems",
"properties": {
"friendlyName": "VMNAME",
"virtualMachineId": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/RESOURCEGROUPNAME/providers/Microsoft.Compute/virtualMachines/VMNAME",
"protectionStatus": "Healthy",
"protectionState": "Protected",
"healthStatus": "Passed",
"healthDetails": \[
{
"code": 400239,
"title": "IaasVmHealthGreenDefault",
"message": "Backup pre-check status of this virtual machine is OK.",
"recommendations": \[\]
}
\],
"lastBackupStatus": "Completed",
"lastBackupTime": "2018-07-23T02:36:32.2674417Z",
"protectedItemDataId": "140738223649118",
"extendedInfo": {
"oldestRecoveryPoint": "2018-07-17T02:39:57.9021763Z",
"recoveryPointCount": 7,
"policyInconsistent": false
},
"protectedItemType": "Microsoft.Compute/virtualMachines",
"backupManagementType": "AzureIaasVM",
"workloadType": "VM",
"containerName": "iaasvmcontainerv2;RESOURCEGROUPNAME;VMNAME",
"sourceResourceId": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/RESOURCEGROUPNAME/providers/Microsoft.Compute/virtualMachines/VMNAME",
"policyId": "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/RESOURCEGROUPNAME/providers/Microsoft.RecoveryServices/vaults/BACKUPVAULTNAME/backupPolicies/AQI-PRO-DailyPolicy",
"policyName": "DailyPolicy",
"lastRecoveryPoint": "2018-07-23T02:36:35.6143732Z"
}
}
Helpful are e.g. the values oldestRecoveryPoint
and recoveryPointCount
to find orphaned and no longer needed protected items.