diff --git a/README.md b/README.md index b1d3dbd..36481f5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,47 @@ # Azure RM Simple IAM module -This module create IAM role assignments for a given scope and principal. It can also assign RBAC Admininator role with conditions to assignable roles. +This module creates Azure RBAC role assignments for a given scope and principal. + +It also optionally assigns the **Role Based Access Control Administrator** role with an ABAC condition that limits roleAssignments write/delete to a selected set of delegable roles. + +The constrained RBAC Administrator assignment is created only when `delegable_roles` is non-empty. + +## Usage + +```hcl +module "iam" { + source = "../modules/simple-iam" + + scope = data.azurerm_subscription.current.id + principal_id = azuread_service_principal.sp.object_id + + roles = [ + "Contributor", + ] + + delegable_roles = [ + "Storage Blob Data Contributor", + "Key Vault Secrets Officer", + "Key Vault Certificates Officer", + ] + + # Optional + principal_type = "ServicePrincipal" + skip_service_principal_aad_check = true +} +``` + +## Inputs + +- `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. +- `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`. +- `skip_service_principal_aad_check` (bool): Passed to `azurerm_role_assignment.skip_service_principal_aad_check`. + +## Outputs + +- `role_assignment_ids` (map(string)) +- `rbac_admin_role_assignment_id` (string|null) +- `rbac_admin_condition` (string|null) diff --git a/main.tf b/main.tf index cee07df..4678adf 100644 --- a/main.tf +++ b/main.tf @@ -1 +1,62 @@ -data "azurerm_client_config" "current" {} +locals { + + allowed_role_definition_ids_list = join(", ", [ + for name in var.delegable_roles : + basename(data.azurerm_role_definition.allowed_for_rbac_admin_condition[name].id) + ]) + + rbac_admin_condition = <<-EOT + ( + ( + !(ActionMatches{'Microsoft.Authorization/roleAssignments/write'}) + ) + OR + ( + @Request[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAnyValues:GuidEquals {${local.allowed_role_definition_ids_list}} + ) + ) + AND + ( + ( + !(ActionMatches{'Microsoft.Authorization/roleAssignments/delete'}) + ) + OR + ( + @Resource[Microsoft.Authorization/roleAssignments:RoleDefinitionId] ForAnyOfAnyValues:GuidEquals {${local.allowed_role_definition_ids_list}} + ) + ) + EOT +} + +data "azurerm_role_definition" "allowed_for_rbac_admin_condition" { + + for_each = toset(var.delegable_roles) + + name = each.value + scope = var.scope +} + +resource "azurerm_role_assignment" "role" { + + for_each = toset(var.roles) + + scope = var.scope + role_definition_name = each.value + principal_id = var.principal_id + principal_type = var.principal_type + skip_service_principal_aad_check = var.skip_service_principal_aad_check +} + +resource "azurerm_role_assignment" "rbac_admin" { + + count = length(var.delegable_roles) > 0 ? 1 : 0 + + scope = var.scope + role_definition_name = "Role Based Access Control Administrator" + principal_id = var.principal_id + principal_type = var.principal_type + skip_service_principal_aad_check = var.skip_service_principal_aad_check + + condition_version = "2.0" + condition = local.rbac_admin_condition +} diff --git a/outputs.tf b/outputs.tf new file mode 100644 index 0000000..1bce009 --- /dev/null +++ b/outputs.tf @@ -0,0 +1,14 @@ +output "role_assignment_ids" { + 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 = length(azurerm_role_assignment.rbac_admin) > 0 ? azurerm_role_assignment.rbac_admin[0].id : null + description = "ID of the constrained RBAC Administrator role assignment, or null when delegable_roles is empty." +} + +output "rbac_admin_condition" { + value = length(azurerm_role_assignment.rbac_admin) > 0 ? local.rbac_admin_condition : null + description = "Rendered condition used for the constrained RBAC Administrator assignment, or null when not created." +} diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..ecccb27 --- /dev/null +++ b/variables.tf @@ -0,0 +1,43 @@ +variable "scope" { + type = string + description = "Scope ID at which to assign roles (subscription, resource group, resource, etc.)." +} + +variable "principal_id" { + type = string + description = "Object ID of the principal (service principal, user, group, managed identity)." +} + +variable "roles" { + type = list(string) + default = [] + description = "Unconditional role definition names to assign to principal_id at scope." + + validation { + condition = length(distinct(var.roles)) == length(var.roles) + error_message = "roles must not contain duplicates." + } +} + +variable "delegable_roles" { + type = list(string) + default = [] + description = "Role definition names that RBAC Administrator is allowed to assign/delete via ABAC condition. When empty, RBAC Administrator assignment is not created." + + validation { + condition = length(distinct(var.delegable_roles)) == length(var.delegable_roles) + error_message = "delegable_roles must not contain duplicates." + } +} + +variable "principal_type" { + type = string + default = "ServicePrincipal" + description = "Value for azurerm_role_assignment.principal_type (e.g., ServicePrincipal, User, Group)." +} + +variable "skip_service_principal_aad_check" { + type = bool + default = true + description = "Whether to skip the Azure AD check for service principals." +} diff --git a/versions.tf b/versions.tf index e69de29..98391a2 100644 --- a/versions.tf +++ b/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">= 4.0.0, < 5.0.0" + } + } + + required_version = ">= 1.0.0" +}