diff --git a/README.md b/README.md
index 25321a0..c0b32d1 100644
--- a/README.md
+++ b/README.md
@@ -116,6 +116,7 @@ The following requirements are needed by this module:
The following resources are used by this module:
- [azapi_resource.hierarchy_settings](https://registry.terraform.io/providers/azure/azapi/latest/docs/resources/resource) (resource)
+- [azapi_resource.management_group_role_assignments](https://registry.terraform.io/providers/azure/azapi/latest/docs/resources/resource) (resource)
- [azapi_resource.management_groups_level_0](https://registry.terraform.io/providers/azure/azapi/latest/docs/resources/resource) (resource)
- [azapi_resource.management_groups_level_1](https://registry.terraform.io/providers/azure/azapi/latest/docs/resources/resource) (resource)
- [azapi_resource.management_groups_level_2](https://registry.terraform.io/providers/azure/azapi/latest/docs/resources/resource) (resource)
@@ -260,6 +261,38 @@ object({
Default: `null`
+### [management\_group\_role\_assignments](#input\_management\_group\_role\_assignments)
+
+Description: A map of role assignments to create. The map key is deliberately arbitrary to avoid issues where map keys maybe unknown at plan time.
+
+ - `management_group_name` - The name of the management group to assign the role to.
+ - `role_definition_id_or_name` - The ID or name of the role definition to assign to the principal.
+ - `principal_id` - The ID of the principal to assign the role to.
+ - `description` - (Optional) The description of the role assignment.
+ - `skip_service_principal_aad_check` - (Optional) No effect when using AzAPI.
+ - `condition` - (Optional) The condition which will be used to scope the role assignment.
+ - `condition_version` - (Optional) The version of the condition syntax. Leave as `null` if you are not using a condition, if you are then valid values are '2.0'.
+ - `delegated_managed_identity_resource_id` - (Optional) The delegated Azure Resource Id which contains a Managed Identity. Changing this forces a new resource to be created. This field is only used in cross-tenant scenario.
+ - `principal_type` - (Optional) The type of the `principal_id`. Possible values are `User`, `Group` and `ServicePrincipal`. It is necessary to explicitly set this attribute when creating role assignments if the principal creating the assignment is constrained by ABAC rules that filters on the PrincipalType attribute.
+
+Type:
+
+```hcl
+map(object({
+ management_group_name = string
+ role_definition_id_or_name = string
+ principal_id = string
+ description = optional(string, null)
+ skip_service_principal_aad_check = optional(bool, false)
+ condition = optional(string, null)
+ condition_version = optional(string, null)
+ delegated_managed_identity_resource_id = optional(string, null)
+ principal_type = optional(string, null)
+ }))
+```
+
+Default: `{}`
+
### [override\_policy\_definition\_parameter\_assign\_permissions\_set](#input\_override\_policy\_definition\_parameter\_assign\_permissions\_set)
Description: This list of objects allows you to set the [`assignPermissions` metadata property](https://learn.microsoft.com/azure/governance/policy/concepts/definition-structure-parameters#parameter-properties) of the supplied definition and parameter names.
@@ -612,6 +645,15 @@ object({
multiplier = optional(number, null)
randomization_factor = optional(number, null)
}), {})
+ role_assignments = optional(object({
+ error_message_regex = optional(list(string), [
+ "AuthorizationFailed", # Avoids a eventual consistency issue where a recently created management group is not yet available for a GET operation.
+ ])
+ interval_seconds = optional(number, null)
+ max_interval_seconds = optional(number, null)
+ multiplier = optional(number, null)
+ randomization_factor = optional(number, null)
+ }), {})
policy_definitions = optional(object({
error_message_regex = optional(list(string), [
"AuthorizationFailed" # Avoids a eventual consistency issue where a recently created management group is not yet available for a GET operation.
@@ -671,6 +713,15 @@ object({
Default: `{}`
+### [role\_assignment\_definition\_lookup\_enabled](#input\_role\_assignment\_definition\_lookup\_enabled)
+
+Description: A control to disable the lookup of role definitions when creating role assignments.
+If you disable this then all role assignments must be supplied with a `role_definition_id_or_name` that is a valid role definition ID.
+
+Type: `bool`
+
+Default: `true`
+
### [subscription\_placement](#input\_subscription\_placement)
Description: A map of subscriptions to place into management groups. The key is deliberately arbitrary to avoid issues with known after apply values. The value is an object:
@@ -721,6 +772,13 @@ object({
read = optional(string, "5m")
}), {}
)
+ role_assignment = optional(object({
+ create = optional(string, "5m")
+ delete = optional(string, "5m")
+ update = optional(string, "5m")
+ read = optional(string, "5m")
+ }), {}
+ )
policy_definition = optional(object({
create = optional(string, "5m")
delete = optional(string, "5m")
@@ -788,7 +846,13 @@ Description: A map of role definition names to their resource ids.
## Modules
-No modules.
+The following Modules are called:
+
+### [avm\_interfaces](#module\_avm\_interfaces)
+
+Source: Azure/avm-utl-interfaces/azure
+
+Version:
## Data Collection
diff --git a/examples/default/README.md b/examples/default/README.md
index 8ce042e..db0edfe 100644
--- a/examples/default/README.md
+++ b/examples/default/README.md
@@ -7,6 +7,20 @@ This example shows how to deploy the ALZ reference architecture.
# This allows us to get the tenant id
data "azapi_client_config" "current" {}
+# Include the additional policies and override archetypes
+provider "alz" {
+ library_overwrite_enabled = true
+ library_references = [
+ {
+ path = "platform/alz",
+ ref = "2025.02.0"
+ },
+ {
+ custom_url = "${path.root}/lib"
+ }
+ ]
+}
+
module "alz_architecture" {
source = "../../"
architecture_name = "alz"
diff --git a/examples/default/main.tf b/examples/default/main.tf
index 0da3820..04142ad 100644
--- a/examples/default/main.tf
+++ b/examples/default/main.tf
@@ -1,6 +1,20 @@
# This allows us to get the tenant id
data "azapi_client_config" "current" {}
+# Include the additional policies and override archetypes
+provider "alz" {
+ library_overwrite_enabled = true
+ library_references = [
+ {
+ path = "platform/alz",
+ ref = "2025.02.0"
+ },
+ {
+ custom_url = "${path.root}/lib"
+ }
+ ]
+}
+
module "alz_architecture" {
source = "../../"
architecture_name = "alz"
diff --git a/examples/default/terraform.tf b/examples/default/terraform.tf
index 71af668..4a39851 100644
--- a/examples/default/terraform.tf
+++ b/examples/default/terraform.tf
@@ -11,10 +11,3 @@ terraform {
}
}
}
-
-provider "alz" {
- library_references = [{
- path = "platform/alz"
- ref = "2025.01.0"
- }]
-}
diff --git a/examples/management/.e2eignore b/examples/management/.e2eignore
new file mode 100644
index 0000000..e69de29
diff --git a/examples/policy-assignment-modification-with-custom-lib/.e2eignore b/examples/policy-assignment-modification-with-custom-lib/.e2eignore
new file mode 100644
index 0000000..e69de29
diff --git a/examples/privatednszones/.e2eignore b/examples/privatednszones/.e2eignore
new file mode 100644
index 0000000..e69de29
diff --git a/examples/role-assignments/README.md b/examples/role-assignments/README.md
new file mode 100644
index 0000000..1e66410
--- /dev/null
+++ b/examples/role-assignments/README.md
@@ -0,0 +1,81 @@
+
+# role-assignments
+
+This simplified example shows how to assign roles, both built-in and custom.
+
+```hcl
+# This allows us to get the tenant id
+data "azapi_client_config" "current" {}
+
+# Include the additional policies and override archetypes
+provider "alz" {
+ library_references = [
+ {
+ custom_url = "${path.root}/lib"
+ }
+ ]
+}
+
+module "alz_architecture" {
+ source = "../../"
+ architecture_name = "test"
+ parent_resource_id = data.azapi_client_config.current.tenant_id
+ location = "northeurope"
+ management_group_role_assignments = {
+ test1 = {
+ principal_type = "User"
+ role_definition_id_or_name = "Storage Blob Data Contributor"
+ principal_id = data.azapi_client_config.current.object_id
+ management_group_name = "test123"
+ }
+ test2 = {
+ principal_type = "User"
+ role_definition_id_or_name = "Security-Operations (test456)"
+ principal_id = data.azapi_client_config.current.object_id
+ management_group_name = "test456"
+ }
+ }
+}
+```
+
+
+## Requirements
+
+The following requirements are needed by this module:
+
+- [terraform](#requirement\_terraform) (>= 1.9, < 2.0)
+
+- [alz](#requirement\_alz) (~> 0.17)
+
+- [azapi](#requirement\_azapi) (~> 2.0, >= 2.0.1)
+
+## Resources
+
+The following resources are used by this module:
+
+- [azapi_client_config.current](https://registry.terraform.io/providers/azure/azapi/latest/docs/data-sources/client_config) (data source)
+
+
+## Required Inputs
+
+No required inputs.
+
+## Optional Inputs
+
+No optional inputs.
+
+## Outputs
+
+No outputs.
+
+## Modules
+
+The following Modules are called:
+
+### [alz\_architecture](#module\_alz\_architecture)
+
+Source: ../../
+
+Version:
+
+
\ No newline at end of file
diff --git a/examples/role-assignments/_footer.md b/examples/role-assignments/_footer.md
new file mode 100644
index 0000000..e69de29
diff --git a/examples/role-assignments/_header.md b/examples/role-assignments/_header.md
new file mode 100644
index 0000000..dd4fe67
--- /dev/null
+++ b/examples/role-assignments/_header.md
@@ -0,0 +1,3 @@
+# role-assignments
+
+This simplified example shows how to assign roles, both built-in and custom.
diff --git a/examples/role-assignments/lib/security_operations.alz_role_definition.json b/examples/role-assignments/lib/security_operations.alz_role_definition.json
new file mode 100644
index 0000000..f61cba7
--- /dev/null
+++ b/examples/role-assignments/lib/security_operations.alz_role_definition.json
@@ -0,0 +1,34 @@
+{
+ "name": "d3584a79-4f0d-5980-aa3c-7a76ba783b76",
+ "type": "Microsoft.Authorization/roleDefinitions",
+ "apiVersion": "2018-01-01-preview",
+ "properties": {
+ "roleName": "Security-Operations",
+ "description": "Security Administrator role with a horizontal view across the entire Azure estate and the Azure Key Vault purge policy.",
+ "type": "CustomRole",
+ "permissions": [
+ {
+ "actions": [
+ "*/read",
+ "*/register/action",
+ "Microsoft.KeyVault/locations/deletedVaults/purge/action",
+ "Microsoft.PolicyInsights/*",
+ "Microsoft.Authorization/policyAssignments/*",
+ "Microsoft.Authorization/policyDefinitions/*",
+ "Microsoft.Authorization/policyExemptions/*",
+ "Microsoft.Authorization/policySetDefinitions/*",
+ "Microsoft.Insights/alertRules/*",
+ "Microsoft.Resources/deployments/*",
+ "Microsoft.Security/*",
+ "Microsoft.Support/*"
+ ],
+ "notActions": [],
+ "dataActions": [],
+ "notDataActions": []
+ }
+ ],
+ "assignableScopes": [
+ "${current_scope_resource_id}"
+ ]
+ }
+}
diff --git a/examples/role-assignments/lib/test.alz_archetype_definition.yml b/examples/role-assignments/lib/test.alz_archetype_definition.yml
new file mode 100644
index 0000000..25ff84d
--- /dev/null
+++ b/examples/role-assignments/lib/test.alz_archetype_definition.yml
@@ -0,0 +1,3 @@
+name: test
+role_definitions:
+ - Security-Operations
diff --git a/examples/role-assignments/lib/test.alz_architecture_definition.yml b/examples/role-assignments/lib/test.alz_architecture_definition.yml
new file mode 100644
index 0000000..e05d92d
--- /dev/null
+++ b/examples/role-assignments/lib/test.alz_architecture_definition.yml
@@ -0,0 +1,14 @@
+management_groups:
+ - archetypes:
+ - empty
+ display_name: test123
+ exists: false
+ id: test123
+ parent_id: null
+ - archetypes:
+ - test
+ display_name: test456
+ exists: false
+ id: test456
+ parent_id: null
+name: test
diff --git a/examples/role-assignments/main.tf b/examples/role-assignments/main.tf
new file mode 100644
index 0000000..d3e5d81
--- /dev/null
+++ b/examples/role-assignments/main.tf
@@ -0,0 +1,32 @@
+# This allows us to get the tenant id
+data "azapi_client_config" "current" {}
+
+# Include the additional policies and override archetypes
+provider "alz" {
+ library_references = [
+ {
+ custom_url = "${path.root}/lib"
+ }
+ ]
+}
+
+module "alz_architecture" {
+ source = "../../"
+ architecture_name = "test"
+ parent_resource_id = data.azapi_client_config.current.tenant_id
+ location = "northeurope"
+ management_group_role_assignments = {
+ test1 = {
+ principal_type = "User"
+ role_definition_id_or_name = "Storage Blob Data Contributor"
+ principal_id = data.azapi_client_config.current.object_id
+ management_group_name = "test123"
+ }
+ test2 = {
+ principal_type = "User"
+ role_definition_id_or_name = "Security-Operations (test456)"
+ principal_id = data.azapi_client_config.current.object_id
+ management_group_name = "test456"
+ }
+ }
+}
diff --git a/examples/role-assignments/terraform.tf b/examples/role-assignments/terraform.tf
new file mode 100644
index 0000000..4a39851
--- /dev/null
+++ b/examples/role-assignments/terraform.tf
@@ -0,0 +1,13 @@
+terraform {
+ required_version = ">= 1.9, < 2.0"
+ required_providers {
+ alz = {
+ source = "azure/alz"
+ version = "~> 0.17"
+ }
+ azapi = {
+ source = "azure/azapi"
+ version = "~> 2.0, >= 2.0.1"
+ }
+ }
+}
diff --git a/examples/template-architecture-definition/lib/custom.alz_architecture_definition.json.tftpl b/examples/template-architecture-definition/lib/custom.alz_architecture_definition.json.tftpl
index d961625..8c4b585 100644
--- a/examples/template-architecture-definition/lib/custom.alz_architecture_definition.json.tftpl
+++ b/examples/template-architecture-definition/lib/custom.alz_architecture_definition.json.tftpl
@@ -8,7 +8,7 @@
],
"display_name": "${prefix} ALZ root",
"exists": false,
- "id": "${prefix}-alzroot",
+ "id": "${prefix}-alz",
"parent_id": null
},
{
@@ -49,11 +49,11 @@
},
{
"archetypes": [
- "sandboxes"
+ "sandbox"
],
- "display_name": "${prefix} Sandboxes",
+ "display_name": "${prefix} Sandbox",
"exists": false,
- "id": "${prefix}-sandboxes",
+ "id": "${prefix}-sandbox",
"parent_id": "${prefix}-alzroot"
},
{
diff --git a/main.role_assignments.tf b/main.role_assignments.tf
new file mode 100644
index 0000000..e0af0c9
--- /dev/null
+++ b/main.role_assignments.tf
@@ -0,0 +1,46 @@
+module "avm_interfaces" {
+ for_each = var.management_group_role_assignments
+ source = "Azure/avm-utl-interfaces/azure"
+ role_assignment_definition_lookup_enabled = var.role_assignment_definition_lookup_enabled
+ role_assignment_definition_scope = provider::azapi::tenant_resource_id("Microsoft.Management/managementGroups", [each.value.management_group_name])
+ role_assignments = {
+ this = {
+ role_definition_id_or_name = each.value.role_definition_id_or_name
+ principal_id = each.value.principal_id
+ description = each.value.description
+ skip_service_principal_aad_check = each.value.skip_service_principal_aad_check
+ condition = each.value.condition
+ condition_version = each.value.condition_version
+ delegated_managed_identity_resource_id = each.value.delegated_managed_identity_resource_id
+ principal_type = each.value.principal_type
+ }
+ }
+ depends_on = [azapi_resource.role_definitions]
+}
+
+resource "azapi_resource" "management_group_role_assignments" {
+ for_each = module.avm_interfaces
+
+ type = each.value.role_assignments_azapi.this.type
+ body = each.value.role_assignments_azapi.this.body
+ name = each.value.role_assignments_azapi.this.name
+ parent_id = provider::azapi::tenant_resource_id("Microsoft.Management/managementGroups", [var.management_group_role_assignments[each.key].management_group_name])
+ retry = {
+ error_message_regex = var.retries.role_assignments.error_message_regex
+ interval_seconds = var.retries.role_assignments.interval_seconds
+ max_interval_seconds = var.retries.role_assignments.max_interval_seconds
+ multiplier = var.retries.role_assignments.multiplier
+ randomization_factor = var.retries.role_assignments.randomization_factor
+ }
+
+ timeouts {
+ create = var.timeouts.role_assignment.create
+ delete = var.timeouts.role_assignment.delete
+ read = var.timeouts.role_assignment.read
+ update = var.timeouts.role_assignment.update
+ }
+
+ depends_on = [
+ azapi_resource.role_definitions,
+ ]
+}
diff --git a/variables.role_assignments.tf b/variables.role_assignments.tf
new file mode 100644
index 0000000..b4436f5
--- /dev/null
+++ b/variables.role_assignments.tf
@@ -0,0 +1,37 @@
+variable "management_group_role_assignments" {
+ type = map(object({
+ management_group_name = string
+ role_definition_id_or_name = string
+ principal_id = string
+ description = optional(string, null)
+ skip_service_principal_aad_check = optional(bool, false)
+ condition = optional(string, null)
+ condition_version = optional(string, null)
+ delegated_managed_identity_resource_id = optional(string, null)
+ principal_type = optional(string, null)
+ }))
+ default = {}
+ nullable = false
+ description = <