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 = <