Azure Role Assignment

I recently came into some issues with assigning Azure roles through a Bicep template and pipeline deployment. I was looking to assign ‘Storage Blob Data Reader’ to a service principal, and refine their access to only the container of the storage account. The three main issues that I ran into were:

  1. What are Role Assignment Conditions and how can I use them in my template?
  2. I am trying to assign a built in role, what is the roleDefinitionId that I should be using?
  3. I am trying to assign the role to a service principal user, what id should I be referencing in the template?

For reference, the Bicep template for role assignment1 that I am using is shown below:

resource symbolicname 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: 'string'
  scope: resourceSymbolicName or tenant()
  properties: {
    condition: 'string'
    conditionVersion: 'string'
    delegatedManagedIdentityResourceId: 'string'
    description: 'string'
    principalId: 'string'
    principalType: 'string'
    roleDefinitionId: 'string'
  }
}

Just in case you are new to this and require a bit more information on role assignments, Azure role assignments are used to provide a security principal access to Azure resources. Assignments are built up of three main components2 :

  1. Security Principal - this is the user, group, service principal, or managed identity that the role is going to be assigned to.
  2. Role Definition - this is the built in or custom defined collection of permissions that is to be assigned to the security principal.
  3. Scope - this is the set of resources that the permissions apply to. You can assign roles at four different scope levels:
    • Management Group
      • Subscription
        • Resource group
          • Resource

With that in mind, lets look into the problems I was having.

As per Microsoft Documentation, ‘a condition is an additional check that you can optionally add to your role assignment to provide more fine-grained access control. For example, you can add a condition that requires an object to have a specific tag to read the object.’ Or in my case, as mentioned above, I would like to refine my service principals access to a specific container on the storage account.

The Bicep template takes your defined condition as a string, so lets look at the syntax and format of the condition that makes up this string.

Conditions are made up of a mixture of actions and expressions.

  • An action is an operation that a user can perform on a resource type.
  • An expression is a statement that evaluates to true or false, which determines whether the action is allowed to be performed.

Syntax for an action condition:

(
  (
    ActionMatches{'<action>'}
  )
)

The value that you replace ‘<action>’ with is the action namespace such as ‘Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read’. For a storage account, you can find a list of these defined actions and examples here.

Syntax for an expression condition:

(
  (
    <attribute> <operator> <value>
  )
)

The values that you replace ‘<attribute>’ with can be one of the following:

  • Resource - Indicates that the attribute is on the resource, such as a container name.
    • Example @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:name]
  • Request - Indicates that the attribute is part of the action request, such as setting the blob index tag.
    • Example @Request[Microsoft.Storage/storageAccounts/blobServices/containers/blobs:snapshot]
  • Principal - Indicates that the attribute is an Azure AD custom security attribute on the principal, such as a user, enterprise application (service principal), or managed identity.
    • Example @Principal[Microsoft.Directory/CustomSecurityAttributes/Id:Engineering_Project]

The values that you replace ‘<operator>’ indicate how the attribute is to be evaluated. These are split between:

Lastly ‘<value>’ will be replaced with what you are expecting the attribute to equate to.

These action and attribute conditions can be combined to create simple and far more complex conditions to suit your needs, see documentation for more.

In my case, I would like a simple condition that will only allow my service principal access to read blobs within a specific storage account container. Therefore, using the syntax above, my condition will look like the following:

( <-- Condition
  @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:name] <-- Attribute
  StringEqualsIgnoreCase <-- Operation
  'nameOfContainer' <-- Value
)

To place this within a Bicep template, make sure to escape your single quotes around the value, such as:

'(@Resource[Microsoft.Storage/storageAccounts/blobServices/containers:name] StringEqualsIgnoreCase \'nameOfContainer\')'

The role definition id is what is used to identify the role that is to be applied to your security principal. Whether you create a custom role definition or use a standard built-in role definition, each one will have an id, and finding them is exactly the same.

Methods to find your Definition Id:

  1. Azure Portal
  2. Azure PowerShell
  3. Azure CLI
  4. REST API

Whichever method you choose, retrieve the Id of the role. The role definition id (guid) by itself cannot be utilised to reference the role definition in your template. To reference the Id correctly, you will need to reference the role definition as a resource. Such as:

roleDefinitionId: '/subscriptions/{subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/{Guid role definition Id}'

In my case, I would like to reference the ‘Storage Blob Data Reader’ built-in role definition.

Using Azure PowerShell:

  1. Using Connect-AzAccount to Login
  2. Then using this command to get the Role Definition Get-AzRoleDefinition 'Storage Blob Data Reader'
  3. From the output, I have identified the Id which is 2a2b9908-6ea1-4ae2-8e65-a410df84e7d1

Therefore the Bicep template property will appear as follows:

roleDefinitionId: '/subscriptions/${subscription().subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/2a2b9908-6ea1-4ae2-8e65-a410df84e7d1'

If you are like me, and you followed standard practice for creating a Service Principal user, you followed these steps:

  1. Create an App Registration for your Service Principal.
  2. Create a Secret on the App Registration.
  3. Make note of the secret, and the Client Id.

In previous API versions of the role assignment template, you were able to reference the Client Id as the Principal Id. However, in the 2022-04-01 template API version, this does not work.

When you create an App Registration for your service principal, you also get a linked Enterprise Application.

From what appears to be an act of decoupling, role assignments now use the ObjectId of the Enterprise Application of which then references your App Registration. You can obtain the Enterprise Application ObjectId using Azure PowerShell:

  1. Using Connect-AzAccount to Login
  2. Then using this command to get the Enterprise Application details Get-AzADServicePrincipal -DisplayName '{Name Of App Reg}' | fl
  3. From the output, identify the Id - this is the ObjectId

The method of authentication, if using the App Registration Client Id and Secret will certainly stay the same.

For the original version of this post see Andrew Wilson's personal blog at Azure Role Assignment