Enhance the module to allow multiple scope assignments.

This commit is contained in:
2026-02-23 21:05:08 +01:00
parent 7c641d5e5c
commit b20b9c4494
4 changed files with 77 additions and 25 deletions

View File

@@ -12,7 +12,7 @@ The constrained RBAC Administrator assignment is created only when `delegable_ro
module "iam" { module "iam" {
source = "../modules/simple-iam" source = "../modules/simple-iam"
scope = data.azurerm_subscription.current.id scopes = [data.azurerm_subscription.current.id]
principal_id = azuread_service_principal.sp.object_id principal_id = azuread_service_principal.sp.object_id
roles = [ roles = [
@@ -32,14 +32,15 @@ module "iam" {
## Inputs ## Inputs
- `scope` (string): Scope ID at which to assign roles. - `scopes` (list(string)): Scope IDs at which to assign roles.
- `principal_id` (string): Object ID of the principal. - `principal_id` (string): Object ID of the principal.
- `roles` (list(string)): Unconditional role definition names to assign. - `roles` (list(string)): Unconditional role definition names to assign at each scope in `scopes`.
- `delegable_roles` (list(string)): Role definition names allowed by the constrained RBAC Admin condition. When empty, RBAC Admin is not assigned. - `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`. - `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.
## Outputs ## Outputs
- `role_assignment_ids` (map(string)) - `role_assignment_ids` (map(string))
- `rbac_admin_role_assignment_id` (string|null) - `rbac_admin_role_assignment_id` (map(string))
- `rbac_admin_condition` (string|null) - `rbac_admin_condition` (string|null)

58
main.tf
View File

@@ -1,9 +1,45 @@
locals { locals {
lookup_scope = var.scopes[0]
allowed_role_definition_ids_list = join(", ", [ allowed_role_definition_ids_list = join(", ", [
for name in var.delegable_roles : for name in var.delegable_roles :
basename(data.azurerm_role_definition.allowed_for_rbac_admin_condition[name].id) basename(data.azurerm_role_definition.allowed_for_rbac_admin_condition[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_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 rbac_admin_condition = <<-EOT
( (
( (
@@ -11,7 +47,7 @@ locals {
) )
OR OR
( (
@Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAnyValues:GuidEquals {${local.allowed_role_definition_ids_list}} ${local.rbac_admin_write_constraint}
) )
) )
AND AND
@@ -21,17 +57,17 @@ locals {
) )
OR OR
( (
@Resource[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAnyValues:GuidEquals {${local.allowed_role_definition_ids_list}} ${local.rbac_admin_delete_constraint}
) )
) )
EOT EOT
} }
data "azurerm_role_definition" "rbac_admin" { data "azurerm_role_definition" "rbac_admin" {
count = length(var.delegable_roles) > 0 ? 1 : 0 for_each = length(var.delegable_roles) > 0 ? { this = true } : {}
name = "Role Based Access Control Administrator" name = "Role Based Access Control Administrator"
scope = var.scope scope = local.lookup_scope
} }
data "azurerm_role_definition" "allowed_for_rbac_admin_condition" { data "azurerm_role_definition" "allowed_for_rbac_admin_condition" {
@@ -39,15 +75,15 @@ data "azurerm_role_definition" "allowed_for_rbac_admin_condition" {
for_each = toset(var.delegable_roles) for_each = toset(var.delegable_roles)
name = each.value name = each.value
scope = var.scope scope = local.lookup_scope
} }
resource "azurerm_role_assignment" "role" { resource "azurerm_role_assignment" "role" {
for_each = toset(var.roles) for_each = local.role_assignments
scope = var.scope scope = each.value.scope
role_definition_name = each.value role_definition_name = each.value.role
principal_id = var.principal_id principal_id = var.principal_id
principal_type = var.principal_type principal_type = var.principal_type
skip_service_principal_aad_check = true skip_service_principal_aad_check = true
@@ -55,10 +91,10 @@ resource "azurerm_role_assignment" "role" {
resource "azurerm_role_assignment" "rbac_admin" { resource "azurerm_role_assignment" "rbac_admin" {
count = length(var.delegable_roles) > 0 ? 1 : 0 for_each = length(var.delegable_roles) > 0 ? toset(var.scopes) : toset([])
scope = var.scope scope = each.value
role_definition_id = data.azurerm_role_definition.rbac_admin[0].id # Role Based Access Control Administrator role_definition_id = data.azurerm_role_definition.rbac_admin["this"].id # Role Based Access Control Administrator
principal_id = var.principal_id principal_id = var.principal_id
principal_type = var.principal_type principal_type = var.principal_type
skip_service_principal_aad_check = true skip_service_principal_aad_check = true

View File

@@ -1,14 +1,14 @@
output "role_assignment_ids" { output "role_assignment_ids" {
value = { for role, ra in azurerm_role_assignment.role : role => ra.id } value = { for key, ra in azurerm_role_assignment.role : key => ra.id }
description = "IDs of unconditional role assignments, keyed by role definition name." description = "IDs of unconditional role assignments, keyed by '${scope}:${role_definition_name}'."
} }
output "rbac_admin_role_assignment_id" { output "rbac_admin_role_assignment_id" {
value = length(azurerm_role_assignment.rbac_admin) > 0 ? azurerm_role_assignment.rbac_admin[0].id : null value = { for scope, ra in azurerm_role_assignment.rbac_admin : scope => ra.id }
description = "ID of the constrained RBAC Administrator role assignment, or null when delegable_roles is empty." description = "IDs of constrained RBAC Administrator role assignments, keyed by scope. Empty when delegable_roles is empty."
} }
output "rbac_admin_condition" { output "rbac_admin_condition" {
value = length(azurerm_role_assignment.rbac_admin) > 0 ? local.rbac_admin_condition : null value = length(var.delegable_roles) > 0 ? local.rbac_admin_condition : null
description = "Rendered condition used for the constrained RBAC Administrator assignment, or null when not created." description = "Rendered condition used for constrained RBAC Administrator assignments, or null when delegable_roles is empty."
} }

View File

@@ -1,6 +1,15 @@
variable "scope" { variable "scopes" {
type = string type = list(string)
description = "Scope ID at which to assign roles (subscription, resource group, resource, etc.)." description = "Scope IDs 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."
}
} }
variable "principal_id" { variable "principal_id" {
@@ -11,7 +20,7 @@ variable "principal_id" {
variable "roles" { variable "roles" {
type = list(string) type = list(string)
default = [] default = []
description = "Unconditional role definition names to assign to principal_id at scope." description = "Unconditional role definition names to assign to principal_id at each scope in scopes."
validation { validation {
condition = length(distinct(var.roles)) == length(var.roles) condition = length(distinct(var.roles)) == length(var.roles)
@@ -35,3 +44,9 @@ variable "principal_type" {
default = "ServicePrincipal" default = "ServicePrincipal"
description = "Value for azurerm_role_assignment.principal_type (e.g., ServicePrincipal, User, Group)." description = "Value for azurerm_role_assignment.principal_type (e.g., ServicePrincipal, User, Group)."
} }
variable "delegable_roles_to_sp_only" {
type = bool
default = false
description = "When true, the RBAC Admin conditional delegation allows roleAssignments write/delete only when the target principal type is ServicePrincipal."
}