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

Discussion: Lack of resource referencing is a pain #15653

Closed
aidan-harding opened this issue Nov 21, 2024 · 2 comments
Closed

Discussion: Lack of resource referencing is a pain #15653

aidan-harding opened this issue Nov 21, 2024 · 2 comments
Labels
Needs: Author Feedback Awaiting feedback from the author of the issue

Comments

@aidan-harding
Copy link

I was moaning about this on Bluesky and encouraged to raise an issue here to explain it. Apologies if I have an X-Y problem here or some other mistake. But maybe it's simply a case for improving resource referencing. I'd also say that bicep is very elegant, which is why I'm struggling here because it feels inelegant in this use case.

What I'm trying to do is create a resource group containing a number of resources, and then allow them access to each other using Managed Identities and RBAC with standard roles.

Importantly, I want the role assignment to be scoped to just the resource I'm granting access to. e.g. I want to grant a managed identity access for to a specific storage account, scoped at the level of that account. I don't want it at the subscription level because it may not be allowed access to other storage resources.

To give a small example, say we create an Azure Data Factory and a Storage Account. Then try to give the ADF managed identity access to the Storage Account.

It would look like this:

main.bicep

targetScope='subscription'

param locationName string
@allowed([
  'dev'
  'prd'
  'sbx'
  'shd'
  'stg'
  'tst'
  'uat'
])
param environmentName string
param projectName string
param location string
param instance int

resource newResourceGroup 'Microsoft.Resources/resourceGroups@2024-03-01' = {
  name: 'rg-${projectName}-${environmentName}-${locationName}-${instance}'
  location: location
}

module storageAccount 'storage.bicep' = {
  name: 'st${projectName}${environmentName}${locationName}${instance}'
  scope: newResourceGroup
  params: {
    storageLocation: location
    storageName: 'st${projectName}${environmentName}${locationName}${instance}'
  }
}

module dataFactory 'data-factory.bicep' = {
  name: 'adf-${projectName}-${environmentName}-${locationName}-${instance}'
  scope: newResourceGroup
  params: {
    location: location
    name: 'adf-${projectName}-${environmentName}-${locationName}-${instance}'
    dfsStorageUrl: storageAccount.outputs.dfsUri
    storageName: storageAccount.name
  }
}

storage.bicep

param storageLocation string
param storageName string

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-04-01' = {
  name: storageName
  location: storageLocation
  sku: {
    name: 'Standard_RAGRS'
  }
  kind: 'StorageV2'
  properties: { 
    minimumTlsVersion: 'TLS1_2'
    encryption: {
      requireInfrastructureEncryption:true
    }
  }
}

output dfsUri string = storageAccount.properties.primaryEndpoints.dfs

data-factory.bicep

param location string
param name string
param storageName string
param dfsStorageUrl string

resource dataFactory 'Microsoft.DataFactory/factories@2018-06-01' = {
  name: name
  location: location
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    publicNetworkAccess: 'Disabled'
  }
}

resource storageService 'Microsoft.DataFactory/factories/linkedservices@2018-06-01' = {
  name: 'storageService'
  parent: dataFactory
  properties: {
    type:'AzureBlobFS'
    typeProperties: {
      url: dfsStorageUrl
    }
  }
}

I read the docs: https://github.com/MicrosoftDocs/azure-docs/blob/main/articles/azure-resource-manager/bicep/scenarios-rbac.md

But getting the standard role definitions seemed super janky. So I read more here. And also this blog post https://yourazurecoach.com/2023/02/02/my-developer-friendly-bicep-module-for-role-assignments/.

They addressed the problem of reading in the standard role ids by listing them all out in a neat module. But, the scope becomes tricky e.g. if we try to assign like this:

param storageAccountName string
param principalId string

@allowed[
    'Device'
    'ForeignGroup'
    'Group'
    'ServicePrincipal'
    'User'
    ''
]
param principalType string = ''

@allowed([
    'Storage Blob Data Contributor'
    'Storage Blob Data Reader'
])
param roleDefinition string

var roles = {
    // See https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles for these mappings and more.
    'Storage Blob Data Contributor': '/providers/Microsoft.Authorization/roleDefinitions/ba92f5b4-2d11-453d-a403-e96b0029c9fe'
    'Storage Blob Data Reader': '/providers/Microsoft.Authorization/roleDefinitions/2a2b9908-6ea1-4ae2-8e65-a410df84e7d1'
}

var roleDefinitionId = roles[roleDefinition]

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

resource roleAuthorization 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
    // Generate a unique but deterministic resource name
    name: guid('storage-rbac', storageAccount.id, resourceGroup().id, principalId, roleDefinitionId)
    scope: storageAccount
    properties: {
        principalId: principalId
        roleDefinitionId: roleDefinitionId
        principalType: empty(principalType) ? null : principalType
    }
}

Then this module can only handle storage accounts because retrieving the existing resource requires a string literal to specify the resource type, 'Microsoft.Storage/storageAccounts@2021-06-01'.

If you could pass in an actual resource, then it would not be restricted like that.

Having poked this around a whole load, I'VE FOUND A WAY THAT I LIKE!

Instead of having a module that does the role assignment, I have a module which encapsulates retrieving the role id. And then everything is not so ugly. So, that means adding the following to data-factory.bicep:

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

module storageAccountContributorRole 'standard-role.bicep' = {
  name: 'storageAccountContributorRole'
  params: {standardRoleName:'Storage Account Contributor'}
}

resource roleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
  name: guid(storageName, 'Storage Account Contributor', dataFactory.id)
  scope: storageAccount
  properties: {
    roleDefinitionId: storageAccountContributorRole.outputs.standardRoleId
    principalId: dataFactory.identity.principalId
    principalType: 'ServicePrincipal'
  }
}

Which feels reasonably elegant. standard-role.bicep looks like this:

@allowed([
  'Access Review Operator Service Role'
  'AcrDelete'
  'AcrImageSigner'
  'AcrPull'
  'AcrPush'
...
  ])
  param standardRoleName string
  
  var roleIds = {
  'Access Review Operator Service Role': resourceId('Microsoft.Authorization/roleAssignments', '76cc9ee4-d5d3-4a45-a930-26add3d73475')
  AcrDelete: resourceId('Microsoft.Authorization/roleAssignments', 'c2f4ef07-c644-48eb-af81-4b1b4947fb11')
  AcrImageSigner: resourceId('Microsoft.Authorization/roleAssignments', '6cef56e8-d556-48e5-a04f-b8e64114680f')
  AcrPull: resourceId('Microsoft.Authorization/roleAssignments', '7f951dda-4ed3-4680-a7ca-43fe172d538d')
  AcrPush: resourceId('Microsoft.Authorization/roleAssignments', '8311e382-0749-4cb8-b61a-304f252e45ec')
...
  }

output standardRoleId string = roleIds[standardRoleName]

I ended up putting the whole thing into a public repo https://github.com/processity/bicep-role-assignment

This feels OK now. One thing that I think it shows is that this use case of assigning roles in a way where bicep helps you with auto-complete for the standard roles might be worth adding to the docs.

@alex-frankel
Copy link
Collaborator

alex-frankel commented Nov 22, 2024

+1 on this being one of the jankier parts of bicep today for the two reasons you identified:

Once that is done you should be able to write a module like the following (I left the data factory references alone, but the data factory resource could also be passed in):

generic-role-assignment.bicep

param res resource

resource roleAssignment 'Microsoft.Authorization/roleAssignments@2020-04-01-preview' = {
  scope: res

  name: guid(res.name, 'Storage Account Contributor', dataFactory.id)
  properties: {
    roleDefinitionId: builtInRoleDefinition('Storage Account Contributor')// storageAccountContributorRole.outputs.standardRoleId
    principalId: dataFactory.identity.principalId
    principalType: 'ServicePrincipal'
  }
}

Does that get you closer to what you are looking for?

@alex-frankel alex-frankel added Needs: Author Feedback Awaiting feedback from the author of the issue and removed Needs: Triage 🔍 labels Nov 22, 2024
@aidan-harding
Copy link
Author

Hi Alex,

Yes - that's brilliant. I think that those three things would make a really big difference. And each one of them would chip away at the friction individually, so it's great to hear that bicep is heading that way already.

The idea of a function to return standard roles occurred to me after making the post above. But, honestly, it was long enough already and I appreciate that you read through it all.

The code sample you gave is pretty much what I was hoping to write before I went down this rabbit-hole, so I look forward to deleting some code and doing it like that as bicep evolves 😃

@github-project-automation github-project-automation bot moved this from Todo to Done in Bicep Nov 25, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs: Author Feedback Awaiting feedback from the author of the issue
Projects
Archived in project
Development

No branches or pull requests

2 participants