Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unclear how to assign a role assignment to an arbitrary resource type/name in module #4607

Closed
WhitWaldo opened this issue Sep 25, 2021 · 6 comments
Labels
enhancement New feature or request Needs: Triage 🔍

Comments

@WhitWaldo
Copy link

WhitWaldo commented Sep 25, 2021

Is your feature request related to a problem? Please describe.
I have the GUID of a principal as a parameter to which I need to assign several roles to resources I'm otherwise creating. I would like to scope the role assignment to a specific resource, but I'd also like to put this role assignment in a module so as not to duplicate it all over the place.

What I can't figure out is how to identify the resource in a manner that's acceptable to Bicep.

Here's what I'm starting with:

param principalId string
@minLength(1)
param roleIds array

resource roleAssignment 'Microsoft.Authorization@roleAssignments@2020-10-01-preview = [for (roleId, index) in roleIds: {
  name: '${principalId}-assignments-${roleId}'
  scope: ???
  properties: {
    roleDefinitionId: '/subscriptions/${subscription().subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/${roleId}'
    principalId: principalId
  }
}]

So the question is how to set the scope. This might be a load balancer, it might be a key vault - any number of things.

I've tried doing something like the following:

param scopeResourceName
param scopeTypeAndVersion
resource myScopedResource '${scopeTypeAndVersion}' existing = {
  name: scopeResourceName
}

But this didn't work because "String interpolation isn't supported for specifying the resource type". Neither does this work:

param scopeResourceName
param scopeTypeAndVersion
resource myScopedResource scopeTypeAndVersion existing = {
  name: scopeResourceName
}

This one gives the error "Expected a resource type string. Specify a valid resource type of format..."

So then I tried just passing in the resource as a parameter, but that doesn't work since 'resource' isn't a valid type. Given all that, how can I make a generic resource-typed role assignments module?

Thanks!

@WhitWaldo WhitWaldo added the enhancement New feature or request label Sep 25, 2021
@ghost ghost added the Needs: Triage 🔍 label Sep 25, 2021
@brwilkinson
Copy link
Collaborator

If you are using this dynamically you can fall back to the native way to retrieve the resourceid, which is via resourceid() method.

https://github.com/brwilkinson/AzureDeploymentFramework/blob/0aa5f1500b29fc7327956d3af621514625bf371b/ADF/bicep/WAF-WAF.bicep#L24

https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/bicep-functions-resource#resourceid

It takes the resource type and name as arguments.

resourceId([subscriptionId], [resourceGroupName], resourceType, resourceName1, [resourceName2], ...)

@WhitWaldo
Copy link
Author

WhitWaldo commented Sep 26, 2021

So looking at my roleAssignments module then:

@minLength(1)
param roleIds array

@description('The name of the managed identity to assign the roles to')
param managedIdentityName string

@description('The type of the resource to scope to')
param resourceType string
@description('The ID of the resource to scope to')
param resourceId string

resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' existing = {
  name: managedIdentityName
}

resource roleAssignments 'Microsoft.Authorization/roleAssignments@2020-10-01-preview' = [for roleId in roleIds: {
  name: guid(subscription().subscriptionId, resourceGroup().location, roleId)
  scope:  ???
  properties: {
    roleDefinitionId: roleId
    principalId: managedIdentity.id
  }
}]

roleAssignments.scope indicates that it accepts a resource or a tenant (and not a resourceId).

The docs hereonly indicate how to do this with the name of the resource, but I can't use the name because I can't dynamically construct the resource type/API version from the parameters passed in.

Looking at the docs you linked, it indicates the syntax for using resourceId, but the examples don't actually show using it, especially not in a context of creating a resource I can pass into the scope.

Finally, I tried using reference:

var dynamicResource = reference(resourceId)

But this isn't allowed either because I'm once again just passing an object into scope where it expects to see a tenant or resource.

So when I need the resource reference and not just the ID, how can I get it when I have a string of the type/version and the string with the resource's ID?

@brwilkinson
Copy link
Collaborator

brwilkinson commented Sep 26, 2021

Here is your parent bicep

param managedIdentityName string = 'myUAIName123'
param storageRoleIds array = [
  //Storage Blob Data Contributor
  'ba92f5b4-2d11-453d-a403-e96b0029c9fe'
]

var storageAccountName = [
  {
    name: 'acu1brwaoad2sadiag'
  }
]

module roleAssignments 'roleAssignments.bicep' = [for (account, index) in storageAccountName: {
  name: 'storageRoleAssignments-${account.name}'
  params: {
    roleIds: storageRoleIds
    managedIdentityName: managedIdentityName
    resourceType: 'microsoft.storage/storageAccounts'
    name: account.name
  }
}]

calls this module which has the official way to do this by using resource storageAccount which can then be used for the scope.

param roleIds array
param managedIdentityName string
param resourceType string
param name string

resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' existing = {
  name: name
}

resource mi 'Microsoft.ManagedIdentity/userAssignedIdentities@2018-11-30' existing = {
  name: managedIdentityName
}

var guids = [for (roleId, index) in roleIds: guid(subscription().subscriptionId, resourceGroup().location, roleId, resource,name)]

resource rd 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = [for (roleId, index) in roleIds: {
  name: roleId
}]

resource roleAssignments 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = [for (roleId, index) in roleIds: {
  name: guids[index]
  scope: storageAccount
  properties: {
    roleDefinitionId: rd[index].id
    principalId: mi.properties.principalId
  }
}]

if you build that you will see it shows exactly what is in the docs

ARM:
https://docs.microsoft.com/en-us/azure/azure-resource-manager/templates/scope-extension-resources?tabs=azure-powershell#apply-to-resource

BICEP:
https://docs.microsoft.com/en-us/azure/azure-resource-manager/bicep/scope-extension-resources

Obviously above works great, however it's not dynamic.

Role assignments are actually extension types, that is like saying they extend the resources and can be applied at many scopes.
https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/extension-resource-types#microsoftauthorization

e.g.
extend on a management group scope:
/providers/Microsoft.Management/managementGroups/WCBMG/providers/Microsoft.Authorization/roleAssignments/b83a8ee1-cae2-463f-bcd8-db1a94162948

extend on a subscription:
/subscriptions/b8f402aa-20f7-4888-b45c-3cf086dad9c3/providers/Microsoft.Authorization/roleAssignments/1e82fa5a-c93b-56f8-a5e0-35e0c90b7783

extend on a resource group scope:
/subscriptions/b8f402aa-20f7-4888-b45c-3cf086dad9c3/resourcegroups/ACU1-BRW-AOA-RG-P0/providers/Microsoft.Authorization/roleAssignments/3eeb8ce2-8cc9-514f-89b3-2f75ee65af4e

extend on a resource scope e.g. keyvault:
/subscriptions/b8f402aa-20f7-4888-b45c-3cf086dad9c3/resourcegroups/ACU1-BRW-AOA-RG-T5/providers/Microsoft.KeyVault/vaults/akvr3vdqwnxd23hy/providers/Microsoft.Authorization/roleAssignments/5fa9ac3b-d2ab-55ab-bfad-cc5b43d30594

So those long resourceID's contain types and associated values for the types. values in bold below

Mirosoft.KeyVault/vaults/akvr3vdqwnxd23hy/providers/Microsoft.Authorization/roleAssignments/5fa9ac3b-d2ab-55ab-bfad-cc5b43d30594

when we deploy these via ARM, we split these into the name and type (plus api version), so this becomes:

name: akvr3vdqwnxd23hy/Microsoft.Authorization/5fa9ac3b-d2ab-55ab-bfad-cc5b43d30594
type: Microsoft.KeyVault/vaults/providers/roleAssignments

  • you can always count the number of slashes on the type and you will know you need that many associated values, in this case 3 values, that we pass in via the name.

so what's next

Given we need to be dynamic with generating the scope (we can't use the normal Bicep Scope) ... we can fall back to a 'deployment', which is just calling ARM directly, via bypassing Bicep.

resource roleAssignments 'Microsoft.Resources/deployments@2021-04-01' = [for (roleId, index) in roleIds: {
  name: 'roleassign-${roleId}'
  properties: {
    mode: 'Incremental'
    template: {
      '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#'
      contentVersion: '1.0.0.0'
      resources: [
        {
          name: '${name}/Microsoft.Authorization/${guids[index]}'
          type: '${resourceType}/providers/roleAssignments'
          apiVersion: '2021-04-01-preview'
          properties: {
            roleDefinitionId: rd[index].id
            principalId: mi.properties.principalId
          }
        }
      ]
    }
  }
}]

OR using scope... which is a shortcut to create the name/type combination. Essentially the same.

resource roleAssignments 'Microsoft.Resources/deployments@2021-04-01' = [for (roleId, index) in roleIds: {
  name: 'roleassign-${roleId}'
  properties: {
    mode: 'Incremental'
    template: {
      '$schema': 'https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#'
      contentVersion: '1.0.0.0'
      resources: [
        {
          name: guids[index]
          type: 'Microsoft.Authorization/roleAssignments'
          scope: '${resourceType}/${name}'
          apiVersion: '2021-04-01-preview'
          properties: {
            roleDefinitionId: rd[index].id
            principalId: mi.properties.principalId
          }
        }
      ]
    }
  }
}]

review the deployment in the portal

image

Get-AzRoleAssignment -ObjectId 067e90f2-ea05-48f3-b9ed-d03cc39dc64c

RoleAssignmentId   : /subscriptions/b8f402aa-20f7-4888-b45c-3cf086dad9c3/resourcegroups/ACU1-BRW-AOA-RG-D2/providers/microsoft.storage/storageAccounts/acu1brwaoad2sadiag/providers/Microsoft.Authorization/roleAssignments/3159335b-597d-5f30-b61a-18c8f9d6270d
Scope              : /subscriptions/b8f402aa-20f7-4888-b45c-3cf086dad9c3/resourcegroups/ACU1-BRW-AOA-RG-D2/providers/microsoft.storage/storageAccounts/acu1brwaoad2sadiag
DisplayName        : ACU1-BRW-AOA-D2-uaiStorageAccountOperator
SignInName         :
RoleDefinitionName : Storage Blob Data Contributor
RoleDefinitionId   : ba92f5b4-2d11-453d-a403-e96b0029c9fe
ObjectId           : 067e90f2-ea05-48f3-b9ed-d03cc39dc64c
ObjectType         : ServicePrincipal
CanDelegate        : False

@WhitWaldo
Copy link
Author

Thank you very much for the detailed look at how to do this!

It seems like there might be an opportunity then for a more dynamic component in Bicep so as to avoid bypassing it in favor of an ARM stand-in.

Is something like this anywhere on the roadmap? The initial two ways I see of doing this would be:

  • Re-using the resource keyword to reference by name or by ID instead of only by name
  • Support interpolated strings in type/version string

@anthony-c-martin
Copy link
Member

Proposal #2246 will allow you to author a reusable module which applies a roleAssignment to a resource which is passed in as a parameter. Would that provide enough flexibility here?

e.g.

param inputResource resource

resource roleAssignments 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
  name: ...
  scope: inputResource
  properties: {
    roleDefinitionId: ...
    principalId: ...
  }
}

So you could then call it with:

resource storageAccount 'Microsoft.Storage/storageAccounts@2021-04-01' existing = {
  name: name
}

module 'addAssignments.bicep' = {
  name: 'addAssignments'
  params: {
    inputResource: storageAccount
  }
}

Proposal #2245 would allow you to simplify this further to:

module 'addAssignments.bicep' = {
  name: 'addAssignments'
  params: {
    inputResource: resource('Microsoft.Storage/storageAccounts@2021-04-01', name)
  }
}

@alex-frankel
Copy link
Collaborator

Closing and tracking with #2245/#2246

@ghost ghost locked as resolved and limited conversation to collaborators May 26, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
enhancement New feature or request Needs: Triage 🔍
Projects
None yet
Development

No branches or pull requests

4 participants