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

azurerm_role_assignment support conditional ABAC for Microsoft.Authorization/roleAssignments #23364

Closed
1 task done
audunsolemdal opened this issue Sep 22, 2023 · 4 comments
Closed
1 task done

Comments

@audunsolemdal
Copy link
Contributor

audunsolemdal commented Sep 22, 2023

Is there an existing issue for this?

  • I have searched the existing issues

Community Note

  • Please vote on this issue by adding a 👍 reaction to the original issue to help the community and maintainers prioritize this request
  • Please do not leave "+1" or "me too" comments, they generate extra noise for issue followers and do not help prioritize the request
  • If you are interested in working on this issue or have submitted a pull request, please leave a comment and review the contribution guide to help.

Description

This functionality came out recently in preview https://learn.microsoft.com/en-us/azure/role-based-access-control/delegate-role-assignments-overview?tabs=template#a-more-secure-method-delegate-role-assignments-with-conditions-preview

Via the Azure portal it works as expected, but via the azurerm provider it seems like the new ABAC constraints are not accounted for properly. I am not sure what the reason is.

The below code describes an example in detail. "User A" has unconstrained owner permissions and is attempting to delegate user access administrator with some constraints to the group "contributors" - which User B is a member of.

User B is able to grant role assignments within the constraints if using the Azure portal, but not if using terraform.

New or Affected Resource(s)/Data Source(s)

azurerm_role_assignment

Potential Terraform Configuration

locals {
  # Map of roles which the "contributors" group should be able to assign to Service Principals and specific Groups
  delegated_roles = {
    "Storage Queue Data Contributor"       = "974c5e8b-45b9-4653-ba55-5f855dd0fb88"
    "Storage Queue Data Message Sender"    = "c6a89b2d-59bc-44d0-9896-0f6e12d7b80a"
    "Storage Queue Data Reader"            = "19e7f393-937e-4f77-808e-94535e297925"
    "Storage Queue Data Message Processor" = "974c5e8b-45b9-4653-ba55-5f855dd0fb88"
  }
  delegated_role_ids_string = join(", ", values(local.delegated_roles))
  # map of groups which can be assigned the roles listed above
  delegated_groups = {
    "contributors" = data.azuread_group.contributors.object_id
  }
  delegated_group_ids_string = join(", ", values(local.delegated_groups))
}

# This code is run by "User A" - which has unrestricted owner access
# role assignment and condition are created as expected.

resource "azurerm_role_assignment" "tstsp1_constrained" {
  scope                = "/subscriptions/xxxxxxxxxxxxxxxxxxxx"
  role_definition_name = "User Access Administrator"
  principal_id         = data.azuread_group.contributors.object_id
  description          = "This role assignment is used to allow the owners group to assign the delegated roles to the delegated groups"
  condition_version    = "2.0"
  condition            = <<-EOT
(
 (
  !(ActionMatches{'Microsoft.Authorization/roleAssignments/write'})
 )
 OR 
 (
  @Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAnyValues:GuidEquals {${local.delegated_role_ids_string}}
  AND
  (
   @Request[Microsoft.Authorization/roleAssignments:PrincipalType] StringEqualsIgnoreCase 'ServicePrincipal'
   OR
   @Request[Microsoft.Authorization/roleAssignments:PrincipalType] StringEqualsIgnoreCase 'Group'
   AND
   @Request[Microsoft.Authorization/roleAssignments:PrincipalId] ForAnyOfAnyValues:GuidEquals {${local.delegated_group_ids_string}}
  )
 )
)
AND
(
 (
  !(ActionMatches{'Microsoft.Authorization/roleAssignments/delete'})
 )
 OR 
 (
  @Resource[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAnyValues:GuidEquals {${local.delegated_role_ids_string}}
  AND
  (
   @Resource[Microsoft.Authorization/roleAssignments:PrincipalType] StringEqualsIgnoreCase 'ServicePrincipal'
   OR
   @Resource[Microsoft.Authorization/roleAssignments:PrincipalType] StringEqualsIgnoreCase 'Group'
   AND
   @Resource[Microsoft.Authorization/roleAssignments:PrincipalId] ForAnyOfAnyValues:GuidEquals {${local.delegated_group_ids_string}}
  )
 )
)
EOT
}

# The below code is run by "User B" - which is a member of the "contributors" group
# This works as expected (403- ... does not have authorization to perform action 'Microsoft.Authorization/roleAssignments/write')
# Modifying the ABAC condition may give another error code referencing ABAC constraints, which is fine

resource "azurerm_role_assignment" "example_expected_not_to_work" {
  scope                = "/subscriptions/xxxxxxxxxxxxxxxxxxxx"
  role_definition_name = "Owner"
  principal_id         = data.azuread_group.contributors.object_id
}


# The below code is run by "User B" - which is a member of the "contributors" group
# This example currently does not work as expected, returns (403- ... does not have authorization to perform action 'Microsoft.Authorization/roleAssignments/write')
# Assigning the same role in the azure portal does work, so it is likely a missing feature in the azurerm provider
resource "azurerm_role_assignment" "example_expected_to_work" {
  scope                = "/subscriptions/xxxxxxxxxxxxxxxxxxxx"
  role_definition_name = "Storage Queue Data Contributor"
  principal_id         = data.azuread_group.contributors.object_id
}

References

No response

@rcskosir
Copy link
Contributor

@audunsolemdal Thank you for taking the time to open this feature request!

@audunsolemdal
Copy link
Contributor Author

audunsolemdal commented Jan 16, 2024

This now works since azurem provider version 3.87 was released which included this PR #24271 Thanks @pemcne !

It is necessary to explicitly set the principal_type attribute when creating role assignments given that the principal creating the assignment is constrained by ABAC rules that filters on the PrincipalType attribute.

Example below

resource "azurerm_role_assignment" "example_expected_to_work" {
  scope                = "/subscriptions/xxxxxxxxxxxxxxxxxxxx"
  role_definition_name = "Storage Queue Data Contributor"
  principal_id         = data.azuread_group.contributors.object_id
  principal_type       = "Group" # Need to Explicitly set "Group", "ServicePrincipal" or "User" depending on which principal id is to be assigned
}

@audunsolemdal
Copy link
Contributor Author

Module quick start with dynamic logic if anyone is interested

resource "azuread_group" "readers" {
  display_name            = "az rbac sub ${var.subscription_kv.display_name} readers"
  security_enabled        = true
  prevent_duplicate_names = true
}

resource "azuread_group" "owners" {
  display_name            = "az rbac sub ${var.subscription_kv.display_name} owners"
  security_enabled        = true
  prevent_duplicate_names = true
}

resource "azuread_group" "contributors" {
  display_name            = "az rbac sub ${var.subscription_kv.display_name} contributors"
  security_enabled        = true
  prevent_duplicate_names = true
}


locals {
  # Map of roles which the "owner" group should be able to assign to Service Principals and Groups
  delegated_roles = {
    "Azure Service Bus Data Receiver"                  = "4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0"
    "Azure Service Bus Data Sender"                    = "69a216fc-b8fb-44d8-bc22-1f3c2cd27a39"
    "Key Vault Secrets Officer"                        = "b86a8fe4-44ce-4948-aee5-eccb2c155cd7"
    "Key Vault Secrets User"                           = "4633458b-17de-408a-b874-0445c86b69e6"
    "Key Vault Administrator"                          = "00482a5a-887f-4fb3-b363-3b7fe8e74483"
    "Key Vault Crypto User"                            = "12338af0-0e69-4776-bea7-57ae8d297424"
    "App Configuration Data Owner"                     = "5ae67dd6-50cb-40e7-96ff-dc2bfa4b606b"
    "App Configuration Data Reader"                    = "516239f1-63e1-4d78-a4de-a74fb236a071"
    "Website Contributor"                              = "de139f84-1756-47ae-9be6-808fbbe84772"
    "Cosmos DB Account Reader Role"                    = "fbdf93bf-df7d-467e-a4d2-9458aa1360c8"
    "Cosmos DB Operator"                               = "230815da-be43-4aae-9cb4-875f7bd000aa"
    "Storage Account Contributor"                      = "17d1049b-9a84-46fb-8f53-869881c3d3ab"
    "Storage Blob Data Owner"                          = "b7e6dc6d-f1e8-4753-8033-0f276bb0955b"
    "Storage Blob Data Contributor"                    = "ba92f5b4-2d11-453d-a403-e96b0029c9fe"
    "Storage Blob Data Reader"                         = "2a2b9908-6ea1-4ae2-8e65-a410df84e7d1"
    "Storage File Data SMB Share Elevated Contributor" = "a7264617-510b-434b-a828-9731dc254ea7"
    "Storage File Data SMB Share Reader"               = "aba4ae5f-2193-4029-9191-0cb91df5e314"
    "Storage Queue Data Contributor"                   = "974c5e8b-45b9-4653-ba55-5f855dd0fb88"
    "Storage Queue Data Message Sender"                = "c6a89b2d-59bc-44d0-9896-0f6e12d7b80a"
    "Storage Queue Data Reader"                        = "19e7f393-937e-4f77-808e-94535e297925"
    "Storage Queue Data Message Processor"             = "974c5e8b-45b9-4653-ba55-5f855dd0fb88"
  }
  delegated_role_ids_string = join(", ", values(local.delegated_roles))
  # map of groups which can be assigned the roles listed above
  delegated_groups = {
    "readers"      = azuread_group.readers.id
    "contributors" = azuread_group.contributors.id
    "owners"       = azuread_group.owners.id
  }
  delegated_group_ids_string = join(", ", concat(values(local.delegated_groups), var.rbac_extra_allowed_group_principal_ids))
  delegated_sp_ids_string    = join(", ", var.rbac_allowed_service_principal_ids)
  delegated_user_ids_string  = join(", ", var.rbac_allowed_user_principal_ids)

  allowed_group_ids                  = var.rbac_allow_any_group_assignment == true ? "" : "AND @Request[Microsoft.Authorization/roleAssignments:PrincipalId] ForAnyOfAnyValues:GuidEquals {${local.delegated_group_ids_string}}"
  allow_user_assignment              = var.rbac_allow_user_assignment == true || local.delegated_user_ids_string != "" ? "OR @Request[Microsoft.Authorization/roleAssignments:PrincipalType] StringEqualsIgnoreCase 'User'" : ""
  allowed_user_ids                   = local.delegated_user_ids_string != "" ? "AND @Request[Microsoft.Authorization/roleAssignments:PrincipalId] ForAnyOfAnyValues:GuidEquals {${local.delegated_user_ids_string}}" : ""
  allow_service_principal_assignment = var.rbac_allow_service_principal_assignment == true || local.delegated_sp_ids_string != "" ? "OR @Request[Microsoft.Authorization/roleAssignments:PrincipalType] StringEqualsIgnoreCase 'ServicePrincipal'" : ""
  limit_sp_user_assignment           = local.delegated_sp_ids_string != "" ? "AND @Request[Microsoft.Authorization/roleAssignments:PrincipalId] ForAnyOfAnyValues:GuidEquals {${local.delegated_sp_ids_string}}" : ""

  allowed_group_ids_delete                  = var.rbac_allow_any_group_assignment == true ? "" : "AND @Resource[Microsoft.Authorization/roleAssignments:PrincipalId] ForAnyOfAnyValues:GuidEquals {${local.delegated_group_ids_string}}"
  allow_user_assignment_delete              = var.rbac_allow_user_assignment == true || local.delegated_user_ids_string != "" ? "OR @Resource[Microsoft.Authorization/roleAssignments:PrincipalType] StringEqualsIgnoreCase 'User'" : ""
  allowed_user_ids_delete                   = local.delegated_user_ids_string != "" ? "AND @Resource[Microsoft.Authorization/roleAssignments:PrincipalId] ForAnyOfAnyValues:GuidEquals {${local.delegated_user_ids_string}}" : ""
  allow_service_principal_assignment_delete = var.rbac_allow_service_principal_assignment == true || local.delegated_sp_ids_string != "" ? "OR @Resource[Microsoft.Authorization/roleAssignments:PrincipalType] StringEqualsIgnoreCase 'ServicePrincipal'" : ""
  limit_sp_user_assignment_delete           = local.delegated_sp_ids_string != "" ? "AND @Resource[Microsoft.Authorization/roleAssignments:PrincipalId] ForAnyOfAnyValues:GuidEquals {${local.delegated_sp_ids_string}}" : ""
}

resource "azurerm_role_assignment" "ua_admins" {
  role_definition_name = "User Access Administrator"
  principal_id         = azuread_group.owners.id
  scope                = var.subscription_kv.id
  condition_version    = "2.0"
  principal_type       = "Group"
  provider             = azurerm.spoke
  description          = "User Access Admin role assignment for the ${var.subscription_kv.display_name} subscription. This role can grant role assignments to certain roles and groups under specific constraints."
  condition            = <<-EOT
(
 (
  !(ActionMatches{'Microsoft.Authorization/roleAssignments/write'})
 )
 OR 
 (
  @Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAnyValues:GuidEquals {${local.delegated_role_ids_string}}
  AND
  (
   @Request[Microsoft.Authorization/roleAssignments:PrincipalType] StringEqualsIgnoreCase 'Group'
   ${local.allowed_group_ids}
   ${local.allow_service_principal_assignment}
   ${local.limit_sp_user_assignment}
   ${local.allow_user_assignment}
   ${local.allowed_user_ids}
  )
 )
)
AND
(
 (
  !(ActionMatches{'Microsoft.Authorization/roleAssignments/delete'})
 )
 OR 
 (
  @Resource[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAnyValues:GuidEquals {${local.delegated_role_ids_string}}
  AND
  (
   @Resource[Microsoft.Authorization/roleAssignments:PrincipalType] StringEqualsIgnoreCase 'Group'
   ${local.allowed_group_ids_delete}
   ${local.allow_service_principal_assignment_delete}
   ${local.limit_sp_user_assignment_delete}
   ${local.allow_user_assignment_delete}
   ${local.allowed_user_ids_delete}
  )
 )
)
EOT
}

variables.tf

variable "subscription_kv" {
  type = object({
    display_name = string
    id           = string
  })
  description = "object pair for /subscriptions/subscription_id and subscription display_name"
}


variable "rbac_allow_user_assignment" {
  type        = bool
  description = "Allows terraform managed identity to assign RBAC roles to user principals, not just service principals or groups"
  default     = false
}

variable "rbac_allow_any_group_assignment" {
  type        = bool
  description = "Allows terraform managed identity to assign RBAC roles to any group in the tenant, not just a limited set of groups"
  default     = false
}

variable "rbac_allow_service_principal_assignment" {
  type        = bool
  description = "Allows terraform managed identity to assign RBAC roles to service principals"
  default     = false
}

variable "rbac_allowed_service_principal_ids" {
  type        = list(string)
  description = "IDs of service principals allowed to be assigned RBAC roles. Leave empty to allow all service principals"
  default     = []
}

variable "rbac_allowed_user_principal_ids" {
  type        = list(string)
  description = "Only relevant if rbac_allow_user_assignment == true. IDs of user principals allowed to be assigned RBAC roles. Leave empty to allow all user principals"
  default     = []
}

variable "rbac_extra_allowed_group_principal_ids" {
  type        = list(string)
  description = "IDs of groups allowed to be assigned RBAC roles. Reader, owner and contributor groups for the subscription are always allowed."
  default     = []
}

terraform {
  required_version = "~> 1.5"

  required_providers {
    azurerm = {
      source                = "hashicorp/azurerm"
      version               = "~> 3.87"
      configuration_aliases = [azurerm.spoke]
    }
    azuread = {
      source  = "hashicorp/azuread"
      version = "~> 2.38"
    }
  }
}

Copy link

I'm going to lock this issue because it has been closed for 30 days ⏳. This helps our maintainers find and focus on the active issues.
If you have found a problem that seems similar to this, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Apr 27, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

3 participants