Search code examples
azureazure-resource-managerazure-cliazure-rm-templateazure-managed-identity

Azure Managed ID - How to assign Multiple RBAC Roles via shell


Background Information

I have the following arm template that assigns a RBAC to a managed ID;

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "principalId": {
      "type": "string",
      "metadata": {
        "description": "The principal to assign the role to"
      }
    },
    "builtInRoleType": {
      "type": "string",
      "allowedValues": [
        "Owner",
        "Contributor",
        "Reader",
        "StorageQueueDataContributor",
        "StorageTableDataContributor"
      ],
      "metadata": {
        "description": "Built-in role to assign"
      }
    },
    "roleNameGuid": {
      "type": "string",
      "defaultValue": "[newGuid()]",
      "metadata": {
        "description": "A new GUID used to identify the role assignment"
      }
    }
  },
  "variables": {
    "Owner": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]",
    "Contributor": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]",
    "Reader": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]",
    "StorageQueueDataContributor": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/','974c5e8b-45b9-4653-ba55-5f855dd0fb88')]",
    "StorageTableDataContributor": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/','0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')]"
  },
  "resources": [
    {
      "type": "Microsoft.Authorization/roleAssignments",
      "apiVersion": "2018-09-01-preview",
      "name": "[parameters('roleNameGuid')]",
      "properties": {
        "roleDefinitionId": "[variables(parameters('builtInRoleType'))]",
        "principalId": "[parameters('principalId')]"
      }
    }
  ]
}

and I'm calling it from a powershell script like this:

az deployment group create `
    --resource-group $RESOURCE_GROUP `
    --template-file "./rbac-role.json" `
    --parameters principalId=$objectid builtInRoleType=StorageTableDataContributor roleNameGuid=0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3

So far so good. But now I'd like to be able to assign mutiple RBAC roles instead of just one. I tried to call it twice, once per role type but that fails with an error:

{"status":"Failed","error":{"code":"DeploymentFailed","message":"At least one resource deployment operation failed. Please list deployment operations for details. Please see https://aka.ms/DeployOperations for usage details.","details":[{"code":"BadRequest","message":"{\r\n  \"error\": {\r\n    \"code\": \"RoleAssignmentUpdateNotPermitted\",\r\n    \"message\": \"Tenant ID, application ID, principal ID, and scope are not allowed to be updated.\"\r\n  }\r\n}"}]}}

What I've Tried So far

I've tried to pass arrays as parameters. In reading the docs, it seems that ARM Template parameters can accept arrays.
So I've updated my powershell code to look like this:

$RoleTypeArray = @(
    'StorageQueueDataContributor'
    'StorageTableDataContributor'
)
$RoleGuidArray = @(
    '974c5e8b-45b9-4653-ba55-5f855dd0fb88'
    '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3'
)

az deployment group create `
    --resource-group $RESOURCE_GROUP `
    --template-file "./rbac-role.json" `
    --parameters principalId=$objectid builtInRoleType=$RoleTypeArray roleNameGuid=$RoleGuidArray

And the template like this:

{
  "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "principalId": {
      "type": "string",
      "metadata": {
        "description": "The principal to assign the role to"
      }
    },
    "builtInRoleType": {
      "type": "array",
      "defaultValue": [
        "Owner",
        "Contributor",
        "Reader",
        "StorageQueueDataContributor",
        "StorageTableDataContributor"
      ],
      "metadata": {
        "description": "Built-in role to assign"
      }
    },
    "roleNameGuid": {
      "type": "array",
      "defaultValue": [
        "8e3af657-a8ff-443c-a75c-2fe8c4bcb635",
        "b24988ac-6180-42a0-ab88-20f7382dd24c",
        "acdd72a7-3385-48ef-bd42-f606fba81ae7",
        "974c5e8b-45b9-4653-ba55-5f855dd0fb88",
        "0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3"
      ],
      "metadata": {
        "description": "A new GUID used to identify the role assignment"
      }
    }
  },
  "variables": {
    "Owner": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]",
    "Contributor": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]",
    "Reader": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]",
    "StorageQueueDataContributor": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/','974c5e8b-45b9-4653-ba55-5f855dd0fb88')]",
    "StorageTableDataContributor": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/','0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')]"
  },
  "resources": [
    {
      "type": "Microsoft.Authorization/roleAssignments",
      "apiVersion": "2018-09-01-preview",
      "name": "[parameters('roleNameGuid')[0]]",
      "properties": {
        "roleDefinitionId": "[variables(parameters('builtInRoleType')[0])]",
        "principalId": "[parameters('principalId')]"
      }
    }
  ]
}

For now, I've just hardcoded the index to 0 but if this is the right tree to bark up, I'll have to figure out how to loop? Or I can create the same section twice in the JSON ARM template file maybe - once for each of the two roles?

For now, the error I'm getting is:

Failed to parse JSON: StorageQueueDataContributor StorageTableDataContributor
Error detail: Expecting value: line 1 column 1 (char 0)

It's not clear to me at what point its dying. Adding some debug statements but any tips would be appreciated.

EDIT 1

I've modified my ARM Template to look like this:

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "principalId": {
      "type": "string",
      "metadata": {
        "description": "The principal to assign the role to"
      }
    },
    "builtInRoleType": {
      "type": "array",
      "defaultValue": [
        "Owner",
        "Contributor",
        "Reader",
        "StorageQueueDataContributor",
        "StorageTableDataContributor"
      ],
      "metadata": {
        "description": "Built-in role to assign"
      }
    }
  },
  "variables": {
    "Owner": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')]",
    "Contributor": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/', 'b24988ac-6180-42a0-ab88-20f7382dd24c')]",
    "Reader": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/', 'acdd72a7-3385-48ef-bd42-f606fba81ae7')]",
    "StorageQueueDataContributor": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/','974c5e8b-45b9-4653-ba55-5f855dd0fb88')]",
    "StorageTableDataContributor": "[concat('/subscriptions/', subscription().subscriptionId, '/providers/Microsoft.Authorization/roleDefinitions/','0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')]"
  },
  "resources": [
    {
      "copy": {
        "name": "roleAssignment",
        "count": "[length(parameters('builtInRoleType'))]"
      },
      "type": "Microsoft.Authorization/roleAssignments",
      "apiVersion": "2018-09-01-preview",
      "name": "[guid(subscription().subscriptionId, resourceGroup().name, parameters('builtInRoleType')[copyIndex()], parameters('principalId'))]",
      "properties": {
        "roleDefinitionId": "[parameters('builtInRoleType')[copyIndex()]]",
        "principalId": "[parameters('principalId')]"
      }
    }
  ]
}

This is how I'm calling it:

az deployment group create `
    --resource-group $RESOURCE_GROUP_NAME `
    --template-file "./rbac-role.json" `
    --parameters `
    principalId=$objectid `
    builtInRoleType="['StorageQueueDataContributor', 'StorageTableDataContributor']"

But the error I'm getting is

{
    "status": "Failed",
    "error": {
        "code": "BadRequestFormat",
        "message": "The request was incorrectly formatted."
    }
}

I think it could be related to the roleDefinitionID. This is the second time I'm testing the refactored code. The first time through though, the roleDefinitionID still looked like this:

    "roleDefinitionId": "[variables(parameters('builtInRoleType')[0])]",
    "principalId": "[parameters('principalId')]"

And I ended up with the StorageQueueDataContributor role being assigned twice.


Solution

  • Multiple things here.

    1. The role assignment name has to be unique. Once an assignment has been created you can't change it so you can't use the same guid otherwise it looks like you're trying to update the role assignment which is not allowed.

    2. You can't use powershell array as input parameter. Using az cli, a valid array parameter would look like that:

      az deployment group create `
        --resource-group $RESOURCE_GROUP `
        --template-file "./rbac-role.json" `
        --parameters `
        principalId=$objectid `
        builtInRoleType="['StorageQueueDataContributor', 'StorageTableDataContributor']" `
        roleNameGuid="['974c5e8b-45b9-4653-ba55-5f855dd0fb88', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3']"     
      

    To remove the need of generating unique role name, you could use the guid() function:

    Creates a value in the format of a globally unique identifier based on the values provided as parameters.

    So this will generate a unique string but you can run the same template multiple time, it will always be the same unique string:

    "name": "[guid(subscription().subscriptionId, resourceGroup().name, parameters('builtInRoleType')[0]], parameters('principalId'))]",
    

    If you want to create multiple assignment at the same time, you could use the copy() function:

    {
      "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
      "contentVersion": "1.0.0.0",
      "parameters": {
        "principalId": {
          "type": "string"
        },
        "builtInRoleType": {
          "type": "array",
          "allowedValues":[
            "Owner",
            "Contributor",
            "Reader",
            "StorageQueueDataContributor",
            "StorageTableDataContributor"
          ]
        }
      },
      "variables": {
        "Owner": "8e3af657-a8ff-443c-a75c-2fe8c4bcb635",
        "Contributor": "'b24988ac-6180-42a0-ab88-20f7382dd24c",
        "Reader": "acdd72a7-3385-48ef-bd42-f606fba81ae7",
        "StorageQueueDataContributor": "974c5e8b-45b9-4653-ba55-5f855dd0fb88",
        "StorageTableDataContributor": "0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3"
      },
      "resources": [
        {
          "copy": {
            "name": "roleAssignment",
            "count": "[length(parameters('builtInRoleType'))]"
          },
          "type": "Microsoft.Authorization/roleAssignments",
          "apiVersion": "2020-04-01-preview",
          "name": "[guid(subscription().subscriptionId, resourceGroup().name, parameters('builtInRoleType')[copyIndex()], parameters('principalId'))]",
          "properties": {
            "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleAssignments', variables(parameters('builtInRoleType')[copyIndex()]))]",
            "principalId": "[parameters('principalId')]"
          }
        }
      ]
    }
    

    then you could just invoke the template like that:

    az deployment group create `
      --resource-group $RESOURCE_GROUP `
      --template-file "./rbac-role.json" `
      --parameters `
      principalId=$objectid `
      builtInRoleType="['StorageQueueDataContributor', 'StorageTableDataContributor']"