My next challenge in my TerraForm journey is to assign some RBAC roles (some custom) to users on specific resources.

Custom roles

Roles need to be assigned to a scope and have a list of scopes to which they can be assigned. For these we may need the subscription id, something that up until now I have not needed in my TerraForm files. Fortunately, there are several Azure data providers with the TerraForm integration including azurerm_subscription which will give the current Azure Resource Manager provider’s subscription id if no other arguments are provided:

data "azurerm_subscription" "current" {
}

After this, a custom role definition is fairly straight forward to create:

resource "azurerm_role_definition" "vmuser" {
  name        = "Virtual Machine User"
  scope       = data.azurerm_subscription.current.id
  description = "Can deallocate and start virtual machines"

  permissions {
    actions     = [
        "Microsoft.Compute/virtualMachines/start/action",
        "Microsoft.Compute/virtualMachines/deallocate/action",
        "Microsoft.Network/publicIPAddresses/read",
        "Microsoft.Network/virtualNetworks/read",
        "Microsoft.Network/loadBalancers/read",
        "Microsoft.Network/networkInterfaces/read",
        "Microsoft.Compute/virtualMachines/*/read",
        "Microsoft.Compute/virtualMachines/restart/action"
    ]
    data_actions = ["Microsoft.Compute/virtualMachines/login/action"]
    not_actions = []
    not_data_actions = []
  }

  assignable_scopes = [
    data.azurerm_subscription.current.id, # /subscriptions/00000000-0000-0000-0000-000000000000
  ]
}

Users and groups

In order to access user and group information, we need to use the azuread provider.

Firstly we need to add the azuread provider to TerraForm:

terraform {
  required_providers {
    #...
    azuread = {
      source ="hashicorp/azuread"
      version = ">=2.12.0"
    }
    #...
  }
}

Once added to the configuration, we need to re-run terraform init.

Then, groups and users can be looked up like this:

data "azuread_group" "rdpusers" {
  display_name     = "RDPUsers-Production-G"
  security_enabled = true
}

data "azuread_user" "someuser" {
  user_principal_name = "some.user@domain.tld"
}

What I found useful is that the traditional login for the user is available via the onpremises_sam_account_name property (e.g. data.azuread_user.someuser.onpremises_sam_account_name). This can be useful if that login was used to generate other things, like file-share names.

Assigning the role

resource "azurerm_role_assignment" "role-assignment" {
  scope              = data.azurerm_management_group.primary.id
  role_definition_id = azurerm_role_definition.vmuser.id
  principal_id       = data.azuread_user.someuser.id
}

In-built roles can be assigned by putting role_definition_name = "Reader" (for example) instead of using role_definition_id or looking up the role definition with the azurerm_role_definition data source and using the returned id property:

data "azurerm_role_definition" "reader" {
  name = "Reader"
}

I prefer the latter as using the id consistently works better when you start modularising and doing for_each loops with a mix of custom and built-in roles (using the ids in the loop variables).

Adding to VM module

Expanding my previous work modularising the creation of VDIs to assign a role to a list of principals is relatively straight-forward (with a little reworking, this could probably be generalised to allow combinations of roles but I only need to assign one at the VM level).

Firstly, add 2 new variables - one for the role id to assign and one for the list of principals to assign to (this could be turned into a map of role to principals to generalise it):

variable "vm_user_role_id" {
    type = string
    description = "Scoped resource ID of the role to assign to principals for the VM"
}

variable "vm_user_principal_ids" {
    type = set(string)
    description = "IDs of principals to assign the vm_user_role_id to for this VM"
}

Then adding the assignment is very easy:

resource "azurerm_role_assignment" "vm_role" {
    scope = azurerm_linux_virtual_machine.vdi.id
    role_definition_id = var.vm_user_role_id
    principal_id = each.value
    for_each = var.vm_user_principal_ids
}

Importing these roles is a bit finicky. There is no easy way I have found to see the assignment’s full id in the portal so the best route seems to be to use the Azure CLI to look it up:

$sub_id="$(az account show --query id -o tsv)"
$vdi_users = @{
  VDI01 = "some.other.user@domain.tld"
  VDI27 = "some.user@domain.tld"
  VDI99 = "someone.else@domain.tld"
}
$vdi_groups = @{
    VDI24 = "RDPUsers-Production-G"
}

foreach ( $entry in $vdi_users.GetEnumerator()) {
    $user_id="$(az ad user show --id $($entry.Value) --query objectId -o tsv)"
    terraform import "module.vdis[\""$($entry.Name)\""].azurerm_role_assignment.vm_role[\""$user_id\""]" "$(az role assignment list --scope /subscriptions/$sub_id/resourceGroups/RG-VDI-001/providers/Microsoft.Compute/virtualMachines/$($entry.Name) --assignee $user_id --query [].id -o tsv)"
}
foreach ( $entry in $vdi_groups.GetEnumerator()) {
    $group_id="$(az ad group show --group $($entry.Value) --query objectId -o tsv)"
    terraform import "module.vdis[\""$($entry.Name)\""].azurerm_role_assignment.vm_role[\""$group_id\""]" "$(az role assignment list --scope /subscriptions/$sub_id/resourceGroups/RG-VDI-001/providers/Microsoft.Compute/virtualMachines/$($entry.Name) --assignee $group_id --query [].id -o tsv)"
}