From 84faa4d027a6c1f25a9d0e14fade50fadcc2f2e6 Mon Sep 17 00:00:00 2001 From: Slawomir Koszewski Date: Mon, 23 Feb 2026 21:16:04 +0100 Subject: [PATCH] Reverted multiple-scope due to complexity introduced. Added ability to restrict assignments to ServicePrincipal only. --- README.md | 8 +++---- main.tf | 64 +++++++++++++++------------------------------------- outputs.tf | 10 ++++---- variables.tf | 16 +++++-------- 4 files changed, 33 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index ce6d7f7..bc489ae 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ The constrained RBAC Administrator assignment is created only when `delegable_ro module "iam" { source = "../modules/simple-iam" - scopes = [data.azurerm_subscription.current.id] + scope = data.azurerm_subscription.current.id principal_id = azuread_service_principal.sp.object_id roles = [ @@ -32,9 +32,9 @@ module "iam" { ## Inputs -- `scopes` (list(string)): Scope IDs at which to assign roles. +- `scope` (string): Scope ID at which to assign roles. - `principal_id` (string): Object ID of the principal. -- `roles` (list(string)): Unconditional role definition names to assign at each scope in `scopes`. +- `roles` (list(string)): Unconditional role definition names to assign. - `delegable_roles` (list(string)): Role definition names allowed by the constrained RBAC Admin condition. When empty, RBAC Admin is not assigned. - `principal_type` (string): Passed to `azurerm_role_assignment.principal_type`. - `delegable_roles_to_sp_only` (bool): When true, RBAC Admin delegation can only assign/delete roles to principals of type ServicePrincipal. @@ -42,5 +42,5 @@ module "iam" { ## Outputs - `role_assignment_ids` (map(string)) -- `rbac_admin_role_assignment_id` (map(string)) +- `rbac_admin_role_assignment_id` (string|null) - `rbac_admin_condition` (string|null) diff --git a/main.tf b/main.tf index faa363f..b37f7f8 100644 --- a/main.tf +++ b/main.tf @@ -1,45 +1,12 @@ locals { - lookup_scope = var.scopes[0] - allowed_role_definition_ids_list = join(", ", [ for name in var.delegable_roles : - basename(data.azurerm_role_definition.allowed_for_rbac_admin_condition[name].id) + basename(data.azurerm_role_definition.delegable[name].id) ]) - role_assignments = { - for entry in flatten([ - for scope in var.scopes : [ - for role in var.roles : { - key = "${scope}:${role}" - scope = scope - role = role - } - ] - ]) : - entry.key => { - scope = entry.scope - role = entry.role - } - } - - rbac_admin_write_constraint_role_definition_ids = "@Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAnyValues:GuidEquals {${local.allowed_role_definition_ids_list}}" - rbac_admin_delete_constraint_role_definition_ids = "@Resource[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAnyValues:GuidEquals {${local.allowed_role_definition_ids_list}}" - - rbac_admin_write_constraint_principal_type = "@Request[Microsoft.Authorization/roleAssignments:PrincipalType] ForAnyOfAnyValues:StringEquals {'ServicePrincipal'}" + rbac_admin_write_constraint_principal_type = "@Request[Microsoft.Authorization/roleAssignments:PrincipalType] ForAnyOfAnyValues:StringEquals {'ServicePrincipal'}" rbac_admin_delete_constraint_principal_type = "@Resource[Microsoft.Authorization/roleAssignments:PrincipalType] ForAnyOfAnyValues:StringEquals {'ServicePrincipal'}" - rbac_admin_write_constraint = ( - var.delegable_roles_to_sp_only ? - "(${local.rbac_admin_write_constraint_role_definition_ids} AND ${local.rbac_admin_write_constraint_principal_type})" : - "(${local.rbac_admin_write_constraint_role_definition_ids})" - ) - - rbac_admin_delete_constraint = ( - var.delegable_roles_to_sp_only ? - "(${local.rbac_admin_delete_constraint_role_definition_ids} AND ${local.rbac_admin_delete_constraint_principal_type})" : - "(${local.rbac_admin_delete_constraint_role_definition_ids})" - ) - rbac_admin_condition = <<-EOT ( ( @@ -47,7 +14,10 @@ locals { ) OR ( - ${local.rbac_admin_write_constraint} + ( + @Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAnyValues:GuidEquals {${local.allowed_role_definition_ids_list}} + ) + ${var.delegable_roles_to_sp_only ? "AND\n (${local.rbac_admin_write_constraint_principal_type})" : ""} ) ) AND @@ -57,7 +27,10 @@ locals { ) OR ( - ${local.rbac_admin_delete_constraint} + ( + @Resource[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAnyValues:GuidEquals {${local.allowed_role_definition_ids_list}} + ) + ${var.delegable_roles_to_sp_only ? "AND\n (${local.rbac_admin_delete_constraint_principal_type})" : ""} ) ) EOT @@ -67,23 +40,22 @@ data "azurerm_role_definition" "rbac_admin" { for_each = length(var.delegable_roles) > 0 ? { this = true } : {} name = "Role Based Access Control Administrator" - scope = local.lookup_scope + scope = var.scope } -data "azurerm_role_definition" "allowed_for_rbac_admin_condition" { - +data "azurerm_role_definition" "delegable" { for_each = toset(var.delegable_roles) name = each.value - scope = local.lookup_scope + scope = var.scope } resource "azurerm_role_assignment" "role" { - for_each = local.role_assignments + for_each = toset(var.roles) - scope = each.value.scope - role_definition_name = each.value.role + scope = var.scope + role_definition_name = each.value principal_id = var.principal_id principal_type = var.principal_type skip_service_principal_aad_check = true @@ -91,9 +63,9 @@ resource "azurerm_role_assignment" "role" { resource "azurerm_role_assignment" "rbac_admin" { - for_each = length(var.delegable_roles) > 0 ? toset(var.scopes) : toset([]) + for_each = length(var.delegable_roles) > 0 ? { this = true } : {} - scope = each.value + scope = var.scope role_definition_id = data.azurerm_role_definition.rbac_admin["this"].id # Role Based Access Control Administrator principal_id = var.principal_id principal_type = var.principal_type diff --git a/outputs.tf b/outputs.tf index 68178b6..9e7ff46 100644 --- a/outputs.tf +++ b/outputs.tf @@ -1,14 +1,14 @@ output "role_assignment_ids" { - value = { for key, ra in azurerm_role_assignment.role : key => ra.id } - description = "IDs of unconditional role assignments, keyed by '${scope}:${role_definition_name}'." + value = { for role, ra in azurerm_role_assignment.role : role => ra.id } + description = "IDs of unconditional role assignments, keyed by role definition name." } output "rbac_admin_role_assignment_id" { - value = { for scope, ra in azurerm_role_assignment.rbac_admin : scope => ra.id } - description = "IDs of constrained RBAC Administrator role assignments, keyed by scope. Empty when delegable_roles is empty." + value = length(azurerm_role_assignment.rbac_admin) > 0 ? azurerm_role_assignment.rbac_admin["this"].id : null + description = "ID of the constrained RBAC Administrator role assignment, or null when delegable_roles is empty." } output "rbac_admin_condition" { value = length(var.delegable_roles) > 0 ? local.rbac_admin_condition : null - description = "Rendered condition used for constrained RBAC Administrator assignments, or null when delegable_roles is empty." + description = "Rendered condition used for constrained RBAC Administrator assignment, or null when delegable_roles is empty." } diff --git a/variables.tf b/variables.tf index bcd0810..d19daba 100644 --- a/variables.tf +++ b/variables.tf @@ -1,14 +1,10 @@ -variable "scopes" { - type = list(string) - description = "Scope IDs at which to assign roles (subscription, resource group, resource, etc.)." +variable "scope" { + type = string + description = "Scope ID at which to assign roles (subscription, resource group, resource, etc.)." validation { - condition = ( - length(var.scopes) > 0 && - alltrue([for scope in var.scopes : scope != null && trimspace(scope) != ""]) && - length(distinct(var.scopes)) == length(var.scopes) - ) - error_message = "scopes must be a non-empty list of unique, non-empty strings." + condition = var.scope != null && trimspace(var.scope) != "" + error_message = "scope must be a non-empty string." } } @@ -20,7 +16,7 @@ variable "principal_id" { variable "roles" { type = list(string) default = [] - description = "Unconditional role definition names to assign to principal_id at each scope in scopes." + description = "Unconditional role definition names to assign to principal_id at scope." validation { condition = length(distinct(var.roles)) == length(var.roles)