Azure Automation Runbook RBAC Delegation

Often it can be useful for other teams and departments to edit and run their own Azure automation runbooks, however, this creates a challenge – permissions.

Runbooks live and are managed from within Azure Automation accounts which are in turn connected to log analytics workspaces and their associated agents.

Both automation accounts and log analytics workspaces have RBAC controls built in to the portal via the standard “Access Control (IAM)” blade, however, individual runbooks do not.

We don’t want to give other teams / departments permissions to our main automation account with the ability to create and edit all our runbooks! So that gives us a couple of potential options, lets explore each of them:

A: We create an automation account (and thus a log analytics workspace) per team / department that wishes to use runbooks.

Bad idea – lots of overhead to manage all those extra automation accounts, log analytics workspaces and in turn the agents that come with them. Plus this wouldn’t allow for shared hybrid worker groups, schedules etc which may be common between runbooks.

OR

B: We use powershell to set RBAC permissions on the individual runbooks.

This is the correct answer. Runbooks do actually support RBAC assignments, just not through the portal. Fear not, I have done the hard work and created a script to allow the adding, removing and viewing of individual runbook RBAC permissions.

So, lets break down the script:


# *******************************************
# VARIABLES
# *******************************************

$subid = ""

# Optional override user inputs

$mode = ''

$automationaccountname = ''

$runbookname = ''

$objectid = ''

$rolename = ''

First we set some variables, with the most important required variable being the subscription ID which must be set. Any variable set here the user will not be prompted for.

# *******************************************
# FUNCTIONS
# *******************************************

function creatergifnotexist($rg){

    # Check if RG exists

    if((Get-AzResourceGroup -Name $rg -Erroraction Ignore) -eq $null){

        # If does not exist we create

        # But we need to ask the user what location to create it in

        $rglocation = Read-Host -Prompt "Resource group $rg does not exist, type region name to create in [e.g uksouth]?"

        New-AzResourceGroup -Name $rg -Location $rglocation | Out-Null

    }

}

Next we define a function we will call later to create a resource group if it does not exist.

# *******************************************
# CODE
# *******************************************

# //////////////////////////////////////////
# AUTH AND SUB
# //////////////////////////////////////////

# Auth to Azure

Connect-AzAccount -WarningAction silentlyContinue | Out-Null

# Set subscription
 
Set-AzContext -Subscription $subid | Out-Null

# //////////////////////////////////////////
# USER INPUTS
# //////////////////////////////////////////

# Mode

While ("view","set","delete" -notcontains $mode){

    $mode = Read-Host -Prompt "Mode [view/set/delete]"

}

# Automation Account Name

While ($automationaccountname.Length -eq 0){

    $automationaccountname = Read-Host -Prompt "Automation Account Name"

}

# Runbook Name

While ($runbookname.Length -eq 0){

    $runbookname = Read-Host -Prompt "Runbook Name"

}

# Object ID

While (($objectid.Length -eq 0) -and ("set","delete" -contains $mode)){

    $objectid = Read-Host -Prompt "User OR Group Object ID"

    # Very crude but we assume that if the user entered an @ then its an email
    # This is because an object ID would never contain an @ symbol

    if($objectid -match "@"){

        # Convert to object ID

        $objectid = Get-AzADUser -UserPrincipalName $objectid | select-object -ExpandProperty Id

    }

}

# Role Name

While (($rolename.Length -eq 0) -and ("set","delete" -contains $mode)){

    if($mode -eq "delete"){

        $rolename = Read-Host -Prompt "Role Name e.g Reader OR * for ALL"

    }
    else{

        $rolename = Read-Host -Prompt "Role Name e.g Reader"

    }

}

Next we do some standard auth and prompt users for inputs. Note when requesting the user / group object ID we allow an email to be entered and then converted to an object ID.

# //////////////////////////////////////////
# PRE REQS
# //////////////////////////////////////////

# Get automation account RG

$automationaccountRG = Get-AzResource -ResourceType Microsoft.Automation/automationAccounts -Name $automationaccountname | select-object -ExpandProperty ResourceGroupName

# Set scope

$scope = "/subscriptions/$subid/resourcegroups/$automationaccountRG/Providers/Microsoft.Automation/automationAccounts/$automationaccountname/runbooks/$runbookname"

Then we do some pre requisites which include setting the scope of the automation account runbook where we will run all our commands against.

# //////////////////////////////////////////
# VIEW
# //////////////////////////////////////////

if($mode -eq "view"){

    write-host ""

    # Filter to get only direct assignments on the given scope

    write-host "** DIRECT ASSIGNMENTS **"

    Get-AzRoleAssignment -Scope $scope | Where-Object {$_.Scope -eq $scope} | Format-Table -Property DisplayName, RoleDefinitionName -AutoSize
   
    # Filter to show inherited assingments e.g not for the given scope

    write-host "** INHERITED ASSIGNMENTS **"

    Get-AzRoleAssignment -Scope $scope | Where-Object {$_.Scope -ne $scope} | Format-Table -Property DisplayName, RoleDefinitionName -AutoSize

}

Now we write the view code which will show both direct assignments and inherited assignments separately.

# //////////////////////////////////////////
# SET
# //////////////////////////////////////////

if($mode -eq "set"){

    New-AzRoleAssignment -ObjectId $objectid -RoleDefinitionName $rolename -Scope $scope | Out-Null

}

Then the set code, which is very simple.

# //////////////////////////////////////////
# DELETE
# //////////////////////////////////////////

if($mode -eq "delete"){

    # Check if user wants to remove all assignments for given object

    if($rolename -eq "*"){

        # If wildcard meaning all roles

        # Get all role defs for given object ID at current scope e.g not inherited

        $roledefs = Get-AzRoleAssignment -Scope $scope | Where-Object {$_.Scope -eq $scope} | Where-Object {$_.ObjectId -eq $objectid}

        # Loop through each of those role definitions
           
        foreach($roledef in $roledefs){

            # And remove them individually

            Remove-AzRoleAssignment -ObjectId $objectid -RoleDefinitionName $roledef.RoleDefinitionName -Scope $scope | Out-Null

        }

    }
    else{

        # Else just remove the single role given
       
        Remove-AzRoleAssignment -ObjectId $objectid -RoleDefinitionName $rolename -Scope $scope | Out-Null

    }

}

And finally the delete code which supports both deleting a single role for a user and all roles for a user.

So, how does this interface for the user when running it? First, lets look at viewing permissions.

As you can see, all that is required is the automation account name and runbook name. The script will then display a list of both inherited assignments and direct assignments on that runbook.

Next, lets see how we would create a new role assignment.

This time we need to provide a user or group object ID and the role name we want to assign. Also note, we can, as seen in the screenshot, provide an email address and this will be converted to an object ID.

Finally, lets look at removing a role assignment.

Here we need to select which role we wish to remove from the user OR we can use a wildcard * to remove all directly assigned roles for that user on the runbook.

So if we put all of the above together, here is the final code:

 <#

 Name:         Runbook RBAC Delegation
 Description:  Delegate RBAC permissions to an individual runbook
 Author:       Mike Hosker - mikehosker.net

#>

# *******************************************
# VARIABLES
# *******************************************

$subid = ""

# Optional override user inputs

$mode = ''

$automationaccountname = ''

$runbookname = ''

$objectid = ''

$rolename = ''

# *******************************************
# FUNCTIONS
# *******************************************

function creatergifnotexist($rg){

    # Check if RG exists

    if((Get-AzResourceGroup -Name $rg -Erroraction Ignore) -eq $null){

        # If does not exist we create

        # But we need to ask the user what location to create it in

        $rglocation = Read-Host -Prompt "Resource group $rg does not exist, type region name to create in [e.g uksouth]?"

        New-AzResourceGroup -Name $rg -Location $rglocation | Out-Null

    }

}

# *******************************************
# CODE
# *******************************************

# //////////////////////////////////////////
# AUTH AND SUB
# //////////////////////////////////////////

# Auth to Azure

Connect-AzAccount -WarningAction silentlyContinue | Out-Null

# Set subscription
 
Set-AzContext -Subscription $subid | Out-Null

# //////////////////////////////////////////
# USER INPUTS
# //////////////////////////////////////////

# Mode

While ("view","set","delete" -notcontains $mode){

    $mode = Read-Host -Prompt "Mode [view/set/delete]"

}

# Automation Account Name

While ($automationaccountname.Length -eq 0){

    $automationaccountname = Read-Host -Prompt "Automation Account Name"

}

# Runbook Name

While ($runbookname.Length -eq 0){

    $runbookname = Read-Host -Prompt "Runbook Name"

}

# Object ID

While (($objectid.Length -eq 0) -and ("set","delete" -contains $mode)){

    $objectid = Read-Host -Prompt "User OR Group Object ID"

    # Very crude but we assume that if the user entered an @ then its an email
    # This is because an object ID would never contain an @ symbol

    if($objectid -match "@"){

        # Convert to object ID

        $objectid = Get-AzADUser -UserPrincipalName $objectid | select-object -ExpandProperty Id

    }

}

# Role Name

While (($rolename.Length -eq 0) -and ("set","delete" -contains $mode)){

    if($mode -eq "delete"){

        $rolename = Read-Host -Prompt "Role Name e.g Reader OR * for ALL"

    }
    else{

        $rolename = Read-Host -Prompt "Role Name e.g Reader"

    }

}

# //////////////////////////////////////////
# PRE REQS
# //////////////////////////////////////////

# Get automation account RG

$automationaccountRG = Get-AzResource -ResourceType Microsoft.Automation/automationAccounts -Name $automationaccountname | select-object -ExpandProperty ResourceGroupName

# Set scope

$scope = "/subscriptions/$subid/resourcegroups/$automationaccountRG/Providers/Microsoft.Automation/automationAccounts/$automationaccountname/runbooks/$runbookname"

# //////////////////////////////////////////
# VIEW
# //////////////////////////////////////////

if($mode -eq "view"){

    write-host ""

    # Filter to get only direct assignments on the given scope

    write-host "** DIRECT ASSIGNMENTS **"

    Get-AzRoleAssignment -Scope $scope | Where-Object {$_.Scope -eq $scope} | Format-Table -Property DisplayName, RoleDefinitionName -AutoSize
   
    # Filter to show inherited assingments e.g not for the given scope

    write-host "** INHERITED ASSIGNMENTS **"

    Get-AzRoleAssignment -Scope $scope | Where-Object {$_.Scope -ne $scope} | Format-Table -Property DisplayName, RoleDefinitionName -AutoSize

}

# //////////////////////////////////////////
# SET
# //////////////////////////////////////////

if($mode -eq "set"){

    New-AzRoleAssignment -ObjectId $objectid -RoleDefinitionName $rolename -Scope $scope | Out-Null

}

# //////////////////////////////////////////
# DELETE
# //////////////////////////////////////////

if($mode -eq "delete"){

    # Check if user wants to remove all assignments for given object

    if($rolename -eq "*"){

        # If wildcard meaning all roles

        # Get all role defs for given object ID at current scope e.g not inherited

        $roledefs = Get-AzRoleAssignment -Scope $scope | Where-Object {$_.Scope -eq $scope} | Where-Object {$_.ObjectId -eq $objectid}

        # Loop through each of those role definitions
           
        foreach($roledef in $roledefs){

            # And remove them individually

            Remove-AzRoleAssignment -ObjectId $objectid -RoleDefinitionName $roledef.RoleDefinitionName -Scope $scope | Out-Null

        }

    }
    else{

        # Else just remove the single role given
       
        Remove-AzRoleAssignment -ObjectId $objectid -RoleDefinitionName $rolename -Scope $scope | Out-Null

    }

}

Save as .ps1 and enjoy!