hashicorp / terraform-provider-azurerm

Terraform provider for Azure Resource Manager
https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs
Mozilla Public License 2.0
4.53k stars 4.6k forks source link

Role assignment does not work for a subscription resource but for data source #15212

Open stephan2012 opened 2 years ago

stephan2012 commented 2 years ago

Community Note

Terraform (and AzureRM Provider) Version

Terraform v1.1.4
on linux_amd64
+ provider registry.terraform.io/hashicorp/azuread v2.16.0
+ provider registry.terraform.io/hashicorp/azurerm v2.94.0
+ provider registry.terraform.io/hashicorp/random v3.1.0

Affected Resource(s)

Terraform Configuration Files

resource "azurerm_subscription" "asa_dev" {
  subscription_name = "***redacted***"
  alias             = "asa-dev"
  # Provide either billing_scope_id for a new subscription or the
  # subscription_id for importing an existing
  # billing_scope_id  = ""
  subscription_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

data "azuread_client_config" "current" {}
data "azurerm_client_config" "current" {}

resource "azuread_application" "asa_dev" {
  display_name     = "ASA API User"
  owners           = [data.azuread_client_config.current.object_id]
  sign_in_audience = "AzureADMyOrg"

  web {
  }
}

resource "azuread_application_password" "asa_dev_cicd" {
  application_object_id = azuread_application.asa_dev.object_id
  display_name          = "ASA CI/CD"
}

resource "azuread_service_principal" "asa_tf_user" {
  description                  = "ASA CI/CD"
  application_id               = azuread_application.asa_dev.application_id
  owners                       = [data.azuread_client_config.current.object_id]
}

resource "azurerm_role_assignment" "asa_dev" {
  scope                = azurerm_subscription.asa_dev.id
  role_definition_name = "Reader"
  principal_id         = azuread_service_principal.asa_tf_user.id
}

Debug Output

Panic Output

N/A

Expected Behaviour

Terraform should process the role assignment scope whether it comes from a resource or data.

Actual Behaviour

Execution of the above configuration code failed:

╷
│ Error: ID was missing the `enrollmentAccounts` element
│ 
│   with azurerm_role_assignment.asa_dev,
│   on subscriptions.tf line 51, in resource "azurerm_role_assignment" "asa_dev":
│   51:   scope                = azurerm_subscription.asa_dev.id
│ 
╵
╷
│ Error: parsing "/providers/Microsoft.Subscription/aliases/asa-dev": parsing segment "resourceProvider": expected the segment "Microsoft.Subscription" to be "Microsoft.Management"
│ 
│   with azurerm_role_assignment.asa_dev,
│   on subscriptions.tf line 51, in resource "azurerm_role_assignment" "asa_dev":
│   51:   scope                = azurerm_subscription.asa_dev.id
│ 
╵
╷
│ Error: parsing "/providers/Microsoft.Subscription/aliases/asa-dev": parsing segment "subscriptions": expected the segment "providers" to be "subscriptions"
│ 
│   with azurerm_role_assignment.asa_dev,
│   on subscriptions.tf line 51, in resource "azurerm_role_assignment" "asa_dev":
│   51:   scope                = azurerm_subscription.asa_dev.id
│ 
╵
╷
│ Error: parsing "/providers/Microsoft.Subscription/aliases/asa-dev": parsing segment "subscriptions": expected the segment "providers" to be "subscriptions"
│ 
│   with azurerm_role_assignment.asa_dev,
│   on subscriptions.tf line 51, in resource "azurerm_role_assignment" "asa_dev":
│   51:   scope                = azurerm_subscription.asa_dev.id
│ 
╵
╷
│ Error: Can not parse "scope" as a resource id: No subscription ID found in: "providers/Microsoft.Subscription/aliases/asa-dev"
│ 
│   with azurerm_role_assignment.asa_dev,
│   on subscriptions.tf line 51, in resource "azurerm_role_assignment" "asa_dev":
│   51:   scope                = azurerm_subscription.asa_dev.id
│ 
╵

However, adding a data source (yielding the very same information) and refering to it in the role assignment makes things work:

data "azurerm_subscription" "asa_dev" {
  subscription_id = azurerm_subscription.asa_dev.subscription_id
}

resource "azurerm_role_assignment" "asa_dev" {
  # scope                = azurerm_subscription.asa_dev.id
  scope                = data.azurerm_subscription.asa_dev.id
  role_definition_name = "Reader"
  principal_id         = azuread_service_principal.asa_tf_user.id
}

A closer look to the id shows different values for resource.azurerm_subscription..id (alias, /providers/Microsoft.Subscription/aliases/asa-dev) and data.azurerm_subscription..id (actual id, /subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx).

Steps to Reproduce

  1. See HCL above
  2. terraform apply

Important Factoids

N/A

References

N/A

Lucero7919 commented 2 years ago

Expected Behaviour Terraform should process the role assignment scope whether it comes from a resource or data.

That's not how this works. You can't expect TF to create a subscription and deploy an RBAC policy to it in the same config. It needs to obtain that information from a data call. Your alias effectively utilised a pseudonym ID which is why you see different values. Keep the RBAC separate to the creation of the subscription

stephan2012 commented 2 years ago

@Lucero7919, thanks for your feedback.

Anyway, I think having two identical identifier with different semantics is at least confusing. Is there any documentation that I missed?

jakubigla commented 2 years ago

@Lucero7919 This is not an expected behaviour and yes you can expect TF to create a subscription and deploy an RBAC policy. This is how CAF Terraform modules work as well - after the subscription is created they update the token so the subscription is visible by the principle. This means you can deploy anything you want in the same config - it's also called a landing zone vending machine.

@stephan2012 Try

provider "azurerm" {
  alias = "asa_dev"
  features {}
  subscription_id = azurerm_subscription.asa_dev.subscription_id
}

data "azurerm_subscription" "asa_dev" {
  provider = azurerm.asa_dev
}

resource "azurerm_role_assignment" "asa_dev" {
  scope                = data.azurerm_subscription.asa_dev.id
  role_definition_name = "Reader"
  principal_id         = azuread_service_principal.asa_tf_user.id

  provider = azurerm.asa_dev
}
mb-northwave commented 2 years ago

I would like to add something here. In Azure, you can actually configure Role Assignments on the Subscription, but also on the Alias. Even better: you will have to set role assignments on the Alias, otherwise other people cannot read the alias. If I create a subscription using Terraform, and my colleague tries to run Terraform on the same code, he will get the following error message:

Error: reading Subscription Alias "REDACTED": subscription.AliasClient#Get: Failure responding to request: StatusCode=401 -- Original Error: autorest/azure: Service returned an error. Status=401 Code="UserNotAuthorized" Message="User does not have access Microsoft.Subscription/aliases/read over scope providers/Microsoft.Subscription/aliases/REDACTED"

This is despite having Reader access on the subscription itself. Using az role assignment create --role "Reader" --scope providers/Microsoft.Subscription/aliases/REDACTED --assignee <COLLEAGUE>, I can manually create a role assignment on the Alias, which allows my colleague to see the alias.

However, Terraform will not allow you to do that. If you try to create a role assignment on the Alias, it will set it on the Subscription, not on the Alias. This is despite the fact that the subscription id is the alias.

resource "azurerm_role_assignment" "sub-readers" {
  scope                = data.azurerm_subscription.sub.id
  role_definition_name = "Reader"
  principal_id         = data.azuread_group.my_group.id
}
❯ terraform state show module.REDACTED-subscription.azurerm_subscription.sub
# module.test-incident1-subscription.azurerm_subscription.sub:
resource "azurerm_subscription" "sub" {
    alias             = "7c7f69e2-BBBB-BBBB-BBBB-BBBBBBBBBB"
    billing_scope_id  = "/providers/Microsoft.Billing/billingAccounts/REDACTED/billingProfiles/REDACTED/invoiceSections/REDACTED"
    id                = "/providers/Microsoft.Subscription/aliases/7c7f69e2-BBBB-BBBB-BBBB-BBBBBBBBBB"
    subscription_id   = "fac33ee6-AAAA-AAAA-AAAA-AAAAAAAAA"
    subscription_name = "REDACTED subscription"
    tags              = {}
    tenant_id         = "REDACTED"
}

So I can make Terraform-created subscriptions work with co-workers, but not with the native Terraform azurerm_role_assignment resource, despite the fact that I just need a simple role assignment. Which seems to be because the scope parameter is interpolated and not used literally.

fleetwoodstack commented 2 years ago

@mb-northwave did you find a workaround to that error? I've started getting it and can't get around it!

mb-northwave commented 1 year ago

Hello

I found a workaround for this issue. What we see, is that the data resource does not contain the alias reference. The original azurerm_subscription resource does contain the alias. Compare the below outputs:

❯ terraform state show module.SUBSCRIPTION.data.azurerm_subscription.sub
# module.SUBSCRIPTION.data.azurerm_subscription.sub:
data "azurerm_subscription" "sub" {
    display_name          = "My first subscription"
    id                    = "/subscriptions/<SUB_GUID>"
    location_placement_id = "Public_2014-09-01"
    quota_id              = "PayAsYouGo_2014-09-01"
    spending_limit        = "Off"
    state                 = "Enabled"
    subscription_id       = "<SUB_GUID>"
    tags                  = {}
    tenant_id             = "<TENANT_ID>"
}
❯ terraform state show module.SUBSCRIPTION.azurerm_subscription.sub
# module.SUBSCRIPTION.azurerm_subscription.sub:
resource "azurerm_subscription" "sub" {
    alias             = "<SUB_GUID_OR_ALIAS>"
    billing_scope_id  = "/providers/Microsoft.Billing/billingAccounts/<ID>/billingProfiles/<ID>invoiceSections<ID>"
    id                = "/providers/Microsoft.Subscription/aliases/<ALIAS>"
    subscription_id   = "<SUB_GUID>"
    subscription_name = "My first subscription"
    tags              = {}
    tenant_id         = "<TENANT_ID>"
}

To work around the problem, I created the following resource:

resource "null_resource" "set_TEAM_as_Alias_Reader" {
  provisioner "local-exec" {
    command = "az role assignment create --role \"Reader\" --scope \"${azurerm_subscription.sub.id}\" --assignee ${data.azuread_group.myteam.id}"
  }
}

Note that this works against the azurerm_subscription resource, not against the data field, as the data field does not contain the alias itself.

This creates the role assignment on the Alias scope instead of the Subscription scope, and allows my colleagues to use the subscription alias using Terraform.

Another note, that trying to set the role_assignment using the azurerm_role_assignment resource does not work, as that tries to be cleverer than it should have been. The following code references the resource, not the data block, as scope:

resource "azurerm_role_assignment" "sub-readers" {
  scope                = azurerm_subscription.sub.id
  role_definition_name = "Reader"
  principal_id         = data.azuread_group.my_group.id
}

And that generates the following errors:

╷
│ Error: ID was missing the `enrollmentAccounts` element
│ 
│   with module.SUBSCRIPTION.azurerm_role_assignment.sub-alias-readers,
│   on modules/AzureSubscriptionMCA/main.tf line 83, in resource "azurerm_role_assignment" "sub-alias-readers":
│   83:   scope                = azurerm_subscription.sub.id
│ 
╵
╷
│ Error: parsing "/providers/Microsoft.Subscription/aliases/<ALIAS>": parsing segment "resourceProvider": expected the segment "Microsoft.Subscription" to be "Microsoft.Management"
│ 
│   with module.SUBSCRIPTION.azurerm_role_assignment.sub-alias-readers,
│   on modules/AzureSubscriptionMCA/main.tf line 83, in resource "azurerm_role_assignment" "sub-alias-readers":
│   83:   scope                = azurerm_subscription.sub.id
│ 
╵
╷
│ Error: parsing "/providers/Microsoft.Subscription/aliases/<ALIAS>": parsing segment "subscriptions": expected the segment "providers" to be "subscriptions"
│ 
│   with module.SUBSCRIPTION.azurerm_role_assignment.sub-alias-readers,
│   on modules/AzureSubscriptionMCA/main.tf line 83, in resource "azurerm_role_assignment" "sub-alias-readers":
│   83:   scope                = azurerm_subscription.sub.id
│ 
╵
╷
│ Error: parsing "/providers/Microsoft.Subscription/aliases/<ALIAS>": parsing segment "subscriptions": expected the segment "providers" to be "subscriptions"
│ 
│   with module.SUBSCRIPTION.azurerm_role_assignment.sub-alias-readers,
│   on modules/AzureSubscriptionMCA/main.tf line 83, in resource "azurerm_role_assignment" "sub-alias-readers":
│   83:   scope                = azurerm_subscription.sub.id
│ 
╵
╷
│ Error: Can not parse "scope" as a resource id: No subscription ID found in: "providers/Microsoft.Subscription/aliases/<ALIAS>"
│ 
│   with module.SUBSCRIPTION.azurerm_role_assignment.sub-alias-readers,
│   on modules/AzureSubscriptionMCA/main.tf line 83, in resource "azurerm_role_assignment" "sub-alias-readers":
│   83:   scope                = azurerm_subscription.sub.id
│ 

So the azurerm_role_assignment resource is actively being unhelpful here. @fleetwoodstack FYI.

kvakulo commented 1 year ago

Ran into this issue as well... Another workaround is to use the azapi provider for the role assignment:

resource "azapi_resource" "role_assignment" {
  type      = "Microsoft.Authorization/roleAssignments@2022-04-01"
  name      = var.guid
  parent_id = azurerm_subscription.sub.id
  body = jsonencode({
    properties = {
      principalId      = var.principal_id
      roleDefinitionId = "/providers/Microsoft.Authorization/roleDefinitions/${var.role_assignment_id}"
      principalType    = "User"
    }
  })
}
CarolineChiari commented 1 year ago

I just ran into this issue today, and here is my fix in case anyone needs it:

resource "azurerm_subscription" "subscription" {
  subscription_name = var.subscriptionName
  billing_scope_id  = data.azurerm_billing_mca_account_scope.billingScope.id
}

data "azurerm_subscriptions" "available" {
  display_name_contains = var.subscriptionName
  depends_on = [
    azurerm_subscription.subscription
  ]
}

resource "azurerm_role_assignment" "SPNOwnership" {
  scope    = data.azurerm_subscriptions.available.subscriptions[0].id
  role_definition_name = "Owner"
  principal_id         = var.SubscriptionOwnerId
  depends_on = [
    azurerm_subscription.subscription
  ]
}

Note: the data provider is azurerm_subscriptions and the resource provider is azurerm_subscription.

Note2: This only works if display_name_contains can return unique subscription names. For example, if you have 2 subscriptions named My subscription and My subscription - The Return, and you set display_name_contains = "My subscription", both will be returned, and I'm not sure in what order, so taking value 0 may not work.

mb-northwave commented 1 year ago

Looks like https://github.com/hashicorp/terraform-provider-azurerm/pull/20895 has fixed this problem, released in 3.49.0.

jwthanh commented 6 months ago

This issue still happens in azurerm version 3.96.0

We can use the format function to assign directly to subscription_id instead of the alias.

resource "azurerm_subscription" "subscription_1" {
  alias             = "00000000-0000-0000-0000-000000000000"
  subscription_name = "Subscription 1"
  subscription_id   = "00000000-0000-0000-0000-000000000000"
}

resource "azurerm_role_assignment" "user_1" {
  scope = format(
    "/subscriptions/%s",
    azurerm_subscription.subscription_1.subscription_id
  )
  role_definition_name = "Contributor"
  principal_id         = azuread_user.user_1.id
}