Enhance examples and documentation for role assignments, adding scenarios for multiple principals and role constraints

This commit is contained in:
2026-02-27 19:30:42 +01:00
parent 6b6615b7d3
commit b7594f4a5f
8 changed files with 366 additions and 12 deletions

View File

@@ -1,19 +1,21 @@
# Azure RM Simple IAM module
This module creates Azure RBAC role assignments for a given scope and principal.
This module creates a set of Azure RBAC role assignments for a given scope and principal. To use it, the granting principal must have the **Owner**, **User Access Administrator** or **Role Based Access Control Administrator** role at the scope.
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.
It also optionally assigns the **Role Based Access Control Administrator** role with an ABAC condition that limits role assignments write/delete using allow-list (`delegable_roles`) and/or deny-list (`restricted_roles`) role constraints.
The constrained RBAC Administrator assignment is created only when `delegable_roles` is non-empty.
The constrained RBAC Administrator assignment is created when `delegable_roles` or `restricted_roles` is non-empty.
## Usage
```hcl
module "iam" {
source = "../modules/simple-iam"
# This is only an example, use the actual path to the module or an URL
source = "./modules/terraform-azurerm-simple-iam"
scope = data.azurerm_subscription.current.id
principal_id = azuread_service_principal.sp.object_id
scope = data.azurerm_subscription.current.id
principal_id = azuread_service_principal.sp.object_id
principal_type = "ServicePrincipal"
roles = [
"Contributor",
@@ -25,18 +27,56 @@ module "iam" {
"Key Vault Certificates Officer",
]
restricted_roles = [
"Owner",
"User Access Administrator",
"Role Based Access Control Administrator"
]
# Optional
principal_type = "ServicePrincipal"
delegable_roles_to_sp_only = true
}
```
> **Note:** The above example is for demonstration of every input variable. **restricted_roles** and **delegable_roles** will not make sense to use together in most scenarios.
### Scenarios
#### Assging roles to user and group principals
This is a common scenario when you want to assign roles to users and groups in Azure AD. Infrastructure users will not need any delegation permissions, so the `delegable_roles` and `restricted_roles` lists can be left empty. The module will only create the unconditional role assignments.
**Best practice:** Assign the Reader role at broad scope and add job function roles at more narrow scopes. Work with the end users to identify the right roles and scopes for their needs. Avoid assigning high privilege roles like Owner or User Access Administrator.
On production environments, use even more restrictive roles and scopes, for example by avoiding generic roles like Contributor.
#### Assigning roles to devops engineers
Devops engineers often need permissions to create resources and assign roles to service principals and managed identities. They often request Owner role for convenience, but this is not a recommended practice for any environment other than lab/sandbox.
**Best practice:** Assign the unconditional **Contributor** role and use restricted roles to stop them from assigning **Owner**, **User Access Administrator** and **Role Based Access Control Administrator** roles to any principal on relaxed environments. On production environments, use only Job Function roles and delegable roles limited to the specific needs of the project, for example **Storage Blob Data Contributor** and **Key Vault Secrets Officer** roles. Additionally limit their ability to assign roles to service principals only, so they will be able to configure Managed Identities and Service Principals for their applications, but not assign any roles to themselves or others.
#### Assigning roles to managed identities and service principals
Managed identities and service principals are often used by applications to access other resources and IaC pipelines to create and manage resources.
**Best practice:** Application dedicated identities should have only the permissions they need at the narrowest scope possible. IaC pipelines should have permissions to assign only the roles for services that are being deployed. For example, a pipeline deploying a web application that needs to access a storage account and a key vault should have the **Storage Blob Data Contributor**, **Key Vault Secrets User** and **Key Vault Certificates User** delegable roles.
## Examples
The `examples` folder contains example usage of the module for different scenarios. The examples are not meant to be deployed as-is, but rather to provide guidance on how to use the module in different ways.
1. Multiple principals with different role assignments at the same scope.
2. A single principal with unconditional roles at different scopes.
3. Multiple principals given roles at multiple scopes.
## 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`.
- `roles` (list(string)): Unconditional role definition names to assign.
- `delegable_roles` (list(string)): Role definition names allowed by the constrained RBAC Admin condition.
- `restricted_roles` (list(string)): Role definition names denied by the constrained RBAC Admin condition.
- `delegable_roles_to_sp_only` (bool): When true, RBAC Admin delegation can only assign/delete roles to principals of type ServicePrincipal.
## Outputs

48
examples/scenario-1.tf Normal file
View File

@@ -0,0 +1,48 @@
# Scenario: Multiple principals with different role assignments at the same scope
variable "principals" {
type = map(object({
principal_name = string
principal_type = string
roles = list(string)
delegable_roles = optional(list(string))
restricted_roles = optional(list(string))
}))
default = {
principal1 = {
principal_name = "sp-principal1"
principal_type = "User"
roles = ["Reader"]
}
principal2 = {
principal_name = "sg-admins"
principal_type = "Group"
roles = ["Contributor"]
}
principal3 = {
principal_name = "john.doe@example.com"
principal_type = "User"
roles = ["Owner"]
restricted_roles = [
"Owner",
"User Access Administrator",
"Role Based Access Control Administrator"
]
}
}
}
module "simple_iam" {
source = "../modules/terraform-azurerm-simple-iam"
scope = data.azurerm_subscription.current.id
principal_id = each.value.principal_id
principal_type = each.value.principal_type
roles = each.value.roles
delegable_roles = try(each.value.delegable_roles, [])
restricted_roles = try(each.value.restricted_roles, [])
for_each = var.principals
}

View File

@@ -0,0 +1,30 @@
{
"principals": {
"principal1": {
"principal_name": "sp-principal1",
"principal_type": "User",
"roles": [
"Reader"
]
},
"principal2": {
"principal_name": "sg-admins",
"principal_type": "Group",
"roles": [
"Contributor"
]
},
"principal3": {
"principal_name": "john.doe@example.com",
"principal_type": "User",
"roles": [
"Owner"
],
"restricted_roles": [
"Owner",
"User Access Administrator",
"Role Based Access Control Administrator"
]
}
}
}

48
examples/scenario-2.tf Normal file
View File

@@ -0,0 +1,48 @@
# Scenario: A single principal with unconditional roles at different scopes.
variable "principal" {
type = object({
principal_name = string
principal_id = string
principal_type = string
})
default = {
principal_name = "sp-platform-ops"
principal_id = "00000000-0000-0000-0000-000000000001"
principal_type = "ServicePrincipal"
}
}
variable "role_assignments" {
type = map(object({
scope = string
roles = list(string)
}))
default = {
subscription = {
scope = "/subscriptions/00000000-0000-0000-0000-000000000000"
roles = ["Reader"]
}
rg_platform = {
scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-platform"
roles = ["Contributor"]
}
rg_security = {
scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-security"
roles = ["Log Analytics Contributor", "Monitoring Reader"]
}
}
}
module "simple_iam" {
source = "../modules/terraform-azurerm-simple-iam"
scope = each.value.scope
principal_id = var.principal.principal_id
principal_type = var.principal.principal_type
roles = each.value.roles
for_each = var.role_assignments
}

View File

@@ -0,0 +1,28 @@
{
"principal": {
"principal_name": "sp-platform-ops",
"principal_id": "00000000-0000-0000-0000-000000000001",
"principal_type": "ServicePrincipal"
},
"role_assignments": {
"subscription": {
"scope": "/subscriptions/00000000-0000-0000-0000-000000000000",
"roles": [
"Reader"
]
},
"rg_platform": {
"scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-platform",
"roles": [
"Contributor"
]
},
"rg_security": {
"scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-security",
"roles": [
"Log Analytics Contributor",
"Monitoring Reader"
]
}
}
}

100
examples/scenario-3.tf Normal file
View File

@@ -0,0 +1,100 @@
# Scenario: Multiple principals given roles at multiple scopes.
variable "principals" {
type = map(object({
principal_name = string
principal_id = string
principal_type = string
role_assignments = map(object({
scope = string
roles = list(string)
delegable_roles = optional(list(string))
restricted_roles = optional(list(string))
}))
}))
default = {
principal1 = {
principal_name = "sp-app-ops"
principal_id = "00000000-0000-0000-0000-000000000011"
principal_type = "ServicePrincipal"
role_assignments = {
subscription = {
scope = "/subscriptions/00000000-0000-0000-0000-000000000000"
roles = ["Reader"]
delegable_roles = [
"Reader",
"Contributor"
]
}
rg_app = {
scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-app"
roles = ["Contributor"]
delegable_roles = [
"Reader",
"Contributor"
]
}
}
}
principal2 = {
principal_name = "sg-security-reviewers"
principal_id = "00000000-0000-0000-0000-000000000022"
principal_type = "Group"
role_assignments = {
rg_security = {
scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-security"
roles = ["Owner"]
restricted_roles = [
"Owner",
"User Access Administrator",
"Role Based Access Control Administrator"
]
}
rg_logs = {
scope = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-logs"
roles = ["Role Based Access Control Administrator"]
restricted_roles = [
"Owner",
"User Access Administrator",
"Role Based Access Control Administrator"
]
}
}
}
}
}
locals {
role_assignments = {
for item in flatten([
for principal_key, principal in var.principals : [
for assignment_key, assignment in principal.role_assignments : {
key = "${principal_key}_${assignment_key}"
value = {
scope = assignment.scope
roles = assignment.roles
principal_id = principal.principal_id
principal_type = principal.principal_type
delegable_roles = try(assignment.delegable_roles, [])
restricted_roles = try(assignment.restricted_roles, [])
}
}
]
]) : item.key => item.value
}
}
module "simple_iam" {
source = "../modules/terraform-azurerm-simple-iam"
scope = each.value.scope
principal_id = each.value.principal_id
principal_type = each.value.principal_type
roles = each.value.roles
delegable_roles = each.value.delegable_roles
restricted_roles = each.value.restricted_roles
for_each = local.role_assignments
}

View File

@@ -0,0 +1,60 @@
{
"principals": {
"principal1": {
"principal_name": "sp-app-ops",
"principal_id": "00000000-0000-0000-0000-000000000011",
"principal_type": "ServicePrincipal",
"role_assignments": {
"subscription": {
"scope": "/subscriptions/00000000-0000-0000-0000-000000000000",
"roles": [
"Reader"
],
"delegable_roles": [
"Reader",
"Contributor"
]
},
"rg_app": {
"scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-app",
"roles": [
"Contributor"
],
"delegable_roles": [
"Reader",
"Contributor"
]
}
}
},
"principal2": {
"principal_name": "sg-security-reviewers",
"principal_id": "00000000-0000-0000-0000-000000000022",
"principal_type": "Group",
"role_assignments": {
"rg_security": {
"scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-security",
"roles": [
"Owner"
],
"restricted_roles": [
"Owner",
"User Access Administrator",
"Role Based Access Control Administrator"
]
},
"rg_logs": {
"scope": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-logs",
"roles": [
"Role Based Access Control Administrator"
],
"restricted_roles": [
"Owner",
"User Access Administrator",
"Role Based Access Control Administrator"
]
}
}
}
}
}

View File

@@ -36,12 +36,12 @@ variable "delegable_roles" {
}
variable "restricted_roles" {
type = list(string)
default = []
type = list(string)
default = []
description = "Role definitions names that RBAC Administrator is not allowed to assign/delete via ABAC condition."
validation {
condition = length(distinct(var.restricted_roles)) == length(var.restricted_roles)
condition = length(distinct(var.restricted_roles)) == length(var.restricted_roles)
error_message = "restricted_roles must not contain duplicates."
}
}