Help to create Azure role assignment with several roles

Hi everione!

I hope you are well.

I need build a terraform module where I could provider several roles for one principal ID.

I already built one module what provider one role for one principal id:

In the mdoule I provide ons list of object:

main.tf

module "role" {
  source = "/mnt/c/Users/lmoreira/OneDrive/Documents/projetos/tf-modules/Azure/tf-module-azure-role-assignment"
  
  role_assignment = [
    {
      scope = module.storage.id
      role_definition_name = var.role_definition_name_developer
      principal_id = data.azuread_group.developer.object_id 
    },
    {
      scope = module.storage.id
      role_definition_name = var.role_definition_name_operation
      principal_id = data.azuread_group.operation.object_id 
    },
    {
      scope = module.storage.id
      role_definition_name = var.role_definition_name_reader
      principal_id = data.azuread_group.reader.object_id 
      
    }

  ]
  depends_on = [ module.storage ]
}

terraform.tfvars.json

{
  "location": "East US",
  "account_tier": "Standard",  
  "account_replication_type": "GRS",
  "container_access_type": "private",
  "storage_account_name": "demobackstagelms",
  "resource_group_name": "rg-demo",

  "tags": {
    "terraform": true,
    "environment": "staging"
  },
  
  "role_definition_name_developer": "",
  "role_definition_name_operation": "Storage Blob Data Owner",
  "role_definition_name_reader": "Storage Blob Data Reader"
}

In the module:
main.tf

resource "azurerm_role_assignment" "default" {

  for_each = { 
    for role in var.role_assignment : role.principal_id => role
    if role.role_definition_name != ""
  }
  
  name               = lookup(each.value, "name", null)
  description        = lookup(each.value, "description", "Role Assignment")
  scope              = each.value["scope"]
  principal_id       = each.value["principal_id"]
  principal_type     = lookup(each.value, "principal_type", "ServicePrincipal")
  role_definition_id = lookup(each.value, "role_definition_name", null) == null ? each.value["role_definition_id"] : null
  role_definition_name = lookup(each.value, "role_definition_id", null) == null ? each.value["role_definition_name"] : null
  condition = lookup(each.value, "condition", null)
  condition_version = lookup(each.value, "condition_version", null)
  delegated_managed_identity_resource_id = lookup(each.value, "delegated_managed_identity_resource_id", null)
  skip_service_principal_aad_check = lookup(each.value, "skip_service_principal_aad_check", null)
}

variables.tf

variable "role_assignment" {
  type = list(object({

    name = optional(string)
    description = optional(string)
    scope = string
    role_definition_id = optional(string)
    role_definition_name = optional(string)
    principal_id = string
    principal_type = optional(string)
    condition = optional(string)
    condition_version = optional(string)
    delegated_managed_identity_resource_id = optional(string)
    skip_service_principal_aad_check = optional(string)
  }))
  description = <<EOF
    (Required) Create Role role Assignments.

    (Optional) name - The name of the Private DNS Zone. Must be a valid domain name.
    (Optional) description - Description for this Role Assignment.
    (Required) scope - Scope at which the Role Assignment applies to.
    (Optional) role_definition_id - Scoped-ID of the Role Definition.
    (Optional) role_definition_name - Name of a built-in Role.
    (Required) principal_id -  ID of the Principal (User, Group or Service Principal) to assign the Role Definition to.
    (Optional) principal_type - Type of the principal_id. Possible values are `User`, `Group` and `ServicePrincipal`.
    (Optional) condition - Condition that limits the resources that the role can be assigned to.
    (Optional) condition_version - Version of the condition
    (Optional) delegated_managed_identity_resource_id - Delegated Azure Resource Id which contains a Managed Identity.
    (Optional) skip_service_principal_aad_check |
    If the principal_id is a newly provisioned Service Principal set this value to true to skip the Azure Active Directory check which may fail due to replication lag. This argument is only valid if the principal_id is a Service Principal.
  EOF
  default = []
}

This module is working fine.

But now I need provider a list ofthe string in the terraform.tfvars.json, but I already test several possibilities, but don’t work

{
  "location": "East US",
  "account_tier": "Standard",  
  "account_replication_type": "GRS",
  "container_access_type": "private",
  "storage_account_name": "demobackstagelms",
  "resource_group_name": "rg-demo",

  "tags": {
    "terraform": true,
    "environment": "staging"
  },
  
  "role_definition_name_developer": "",
  "role_definition_name_operation": ["Storage Blob Data Owner", "Storage Blob Data Reader"],
  "role_definition_name_reader": ["Storage Blob Data Reader"]
}

Can anyone please give me some advice on how to implement this change?

I realized some test this simple test.

I created a locals.tf file woth following code:

locals.tf

locals {
  role = [
    {
      scope = "meu-escopo"
      role_definition_name = ["Storage Blob Data Owner"]
      principal_id = "meu-storage"
    },
    {
      scope = "meu-escopo"
      role_definition_name = ["Storage Blob Data Owner", "Storage Blob Data Reader"]
      principal_id = "meu-storage"
    },
    {
      scope = "meu-escopo"
      role_definition_name = [""]
      principal_id = "meu-storage"
      
    }
  ]

   test = { for role in local.role : role.principal_id => role... }
}

output "role" {
  value = local.test
}

This test prouce the following resulte:

Changes to Outputs:
  + role = {
      + meu-storage = [
          + {
              + principal_id         = "meu-storage"
              + role_definition_name = [
                  + "Storage Blob Data Owner",
                ]
              + scope                = "meu-escopo"
            },
          + {
              + principal_id         = "meu-storage"
              + role_definition_name = [
                  + "Storage Blob Data Owner",
                  + "Storage Blob Data Reader",
                ]
              + scope                = "meu-escopo"
            },
          + {
              + principal_id         = "meu-storage"
              + role_definition_name = [
                  + "",
                ]
              + scope                = "meu-escopo"
            },
        ]
    }

Now I need iterate over this output and get I need get the role_definition_name list and send as string for create a role assignment, but all attempts and tests what I executed falilure.

I’m executing new test, if any success I post in this tread.

Thanks for help.

Hey guys!

I achieved solved this problem, don’t was the solution what I would like, but was the solution what I found in this moment.

I changed the variable role assignment to string for list(object), and criated on interation in locals file made it possible add 2 roles for one user group.

variable "role_assignment" {
  type = list(object({
    name = optional(string)
    description = optional(string)
    scope = string
    role_definition_id = optional(string)
    role_definition_name = list(object({
      role_definition_name = optional(string)
    }))
    principal_id = string
    principal_type = optional(string)
    condition = optional(string)
    condition_version = optional(string)
    delegated_managed_identity_resource_id = optional(string)
    skip_service_principal_aad_check = optional(string)
  }))
  description = <<EOF
    (Required) Create Role role Assignments.

    (Optional) name - The name of the Private DNS Zone. Must be a valid domain name.
    (Optional) description - Description for this Role Assignment.
    (Required) scope - Scope at which the Role Assignment applies to.
    (Optional) role_definition_id - Scoped-ID of the Role Definition.
    (Optional) role_definition_name - Name of a built-in Role.
    (Required) principal_id -  ID of the Principal (User, Group or Service Principal) to assign the Role Definition to.
    (Optional) principal_type - Type of the principal_id. Possible values are `User`, `Group` and `ServicePrincipal`.
    (Optional) condition - Condition that limits the resources that the role can be assigned to.
    (Optional) condition_version - Version of the condition
    (Optional) delegated_managed_identity_resource_id - Delegated Azure Resource Id which contains a Managed Identity.
    (Optional) skip_service_principal_aad_check |
    If the principal_id is a newly provisioned Service Principal set this value to true to skip the Azure Active Directory check which may fail due to replication lag. This argument is only valid if the principal_id is a Service Principal.
  EOF
  default = []
}
    role_definition_name = list(object({
      role_definition_name = optional(string)
    }))

I’m sharing the solution becouse it can be useful for other person.

locals.tf

locals {
  role = flatten([
    for k, v in var.role_assignment : [
      for role, role_value in v.role_definition_name : {
        name = v.name
        description = v.description
        scope = v.scope
        principal_id = v.principal_id
        principal_type = v.principal_type
        role_definition_id = v.role_definition_id
        role_definition_name = role_value["role_definition_name"]
        condition = v.condition
        condition_version = v.condition_version
        delegated_managed_identity_resource_id = v.delegated_managed_identity_resource_id
        skip_service_principal_aad_check = v.skip_service_principal_aad_check
      }
    ]
  ])
}

On the role assigment resource:

resource "azurerm_role_assignment" "default" {
  for_each = { 
    for role in local.role : role.role_definition_name => role
    if role.role_definition_name != ""
  }

  name               = lookup(each.value, "name", null)
  description        = lookup(each.value, "description", "Role Assignment")
  scope              = each.value["scope"]
  principal_id       = each.value["principal_id"]
  #principal_type     = "Group"
  principal_type     = lookup(each.value, "principal_type", "ServicePrincipal")
  role_definition_id = lookup(each.value, "role_definition_name", null) == null ? each.value["role_definition_id"] : null
  role_definition_name = lookup(each.value, "role_definition_id", null) == null ? each.value["role_definition_name"] : null
  #role_definition_name = element(each.value["role_definition_name"], length(each.key))
  #role_definition_name = [for role in each.value["role_definition_name"] : role.role_definition_name ]
  #role_definition_name = var.role_assignment[each.value]["role_definition_name"]
  #role_definition_name = each.value.role_definition_id
  #role_definition_name = lookup(each.value, "role_definition_id", null) == null ? each.value["role_definition_name"] : null
  condition = lookup(each.value, "condition", null)
  condition_version = lookup(each.value, "condition_version", null)
  delegated_managed_identity_resource_id = lookup(each.value, "delegated_managed_identity_resource_id", null)
  skip_service_principal_aad_check = lookup(each.value, "skip_service_principal_aad_check", null)

  lifecycle {
    ignore_changes = [principal_id]
  }
}

Note: I create one condition on the resource:

if role.role_definition_name != ""

If role_definition_name be empty, it don’t be create this role assignment.