Azure RBAC and PIM assignments from one map, with a secure-by-default guard against re-delegating the privileged roles.
Role assignments keyed by a logical label you choose, plus custom Azure RBAC role definitions
keyed by role name. Each assignment entry is expanded over the cartesian product of its
principal_ids and its role_names + role_ids + role_definition_keys, so one entry can grant
several roles to several principals at one scope. The module does four things a bare
azurerm_role_assignment cannot:
- Secure-by-default delegation guard. When an assignment grants a privileged role (Owner, User
Access Administrator, or Role Based Access Control Administrator), the module attaches a Microsoft
ABAC condition that denies the assignee the ability to create or delete role assignments for those
same privileged roles. So a granted Owner cannot hand Owner to anyone else. It is on by default for
privileged roles, opt out per assignment with
constrain_delegation = false, and the privileged set is caller-overridable viaconstrained_delegation_role_ids. - PIM in the same interface. Set
assignment_typetopim_active(active, PIM managed, typically time-bound) orpim_eligible(the principal activates the role just-in-time) and the module creates the matching PIM resource.role_namesare resolved to the role definition id PIM requires (via a data source), so you keep naming roles rather than pasting GUIDs. The guard also covers privilegedpim_eligiblegrants. - Stable, plan-known keys. Instance keys are index based (
label|rN|pN), so assigning a role to a freshly created identity (a computedprincipal_id) never trips the "for_each argument must be known" error, and reordering inputs never churns unrelated assignments. - Define-then-assign custom roles.
role_definitionscreates custom role definitions (actions/not_actions/data_actions,assignable_scopesdefaulting to the definition's own scope) at a management group, subscription, or resource group, and assignments (permanent or PIM) reference them by key throughrole_definition_keysin the same call. Creating a definition needsroleDefinitions/writeon the scope (Owner; User Access Administrator is not enough), and a check block flags wildcard-*custom roles (Owner-equivalents in disguise).
Assignments carry no tags and are global to their scope, so there is no resource_group_id, location,
or tags input. Pairs naturally with the user-assigned-identity, keyvault, and storage-account
modules to grant a workload identity exactly the access it needs.
module "role_assignment" {
source = "libre-devops/role-assignment/azurerm"
version = "~> 4.0"
role_assignments = {
# Reader for a workload identity on a resource group.
app_reader = {
scope = module.rg.ids["rg-ldo-uks-prd-001"]
principal_ids = [module.identity.principal_ids["id-ldo-uks-prd-001"]]
role_names = ["Reader"]
}
# Owner for the platform group: the delegation guard is applied automatically.
platform_owners = {
scope = module.rg.ids["rg-ldo-uks-prd-001"]
principal_ids = [var.platform_group_object_id]
role_names = ["Owner"]
}
# Just-in-time Contributor via PIM eligibility, activated for up to 8 hours.
break_glass = {
scope = module.rg.ids["rg-ldo-uks-prd-001"]
principal_ids = [var.oncall_group_object_id]
role_names = ["Contributor"]
assignment_type = "pim_eligible"
justification = "On-call break-glass access"
schedule = { expiration = { duration_hours = 8 } }
}
}
}examples/minimal- grant the running principal Reader on a resource group (required inputs only).examples/complete- the full permanent surface: cartesian expansion over two principals (one with a computed id), the guard on and opted out,principal_type, and a role given by definition id. PIM active and eligible are exercised intestswith a mocked provider, since PIM needs Entra ID P2 that a test tenant may not carry.
Local work needs PowerShell 7+ and just, because the recipes
wrap the LibreDevOpsHelpers
PowerShell module (the same engine the libre-devops/terraform-azure action runs in CI). Install
just with brew install just, or uv tool add rust-just then uv run just <recipe>.
Run just to list recipes: just update-ldo-pwsh (install or force-update LibreDevOpsHelpers from
PSGallery), just validate, just scan (Trivy only), just pwsh-analyze (PSScriptAnalyzer only),
just plan, just apply, just destroy, just e2e, just test, and just docs (the
plan/apply/destroy recipes mirror the action, including the storage firewall dance; just e2e
applies an example then always destroys it, defaulting to minimal, so nothing is left running).
Releasing is also just:
just increment-release [patch|minor|major] bumps, tags, and publishes a GitHub release, and the
Terraform Registry picks up the tag.
This module is scanned with Trivy; HIGH and CRITICAL
findings fail the build. Any waiver is a deliberate, reviewed decision, never a way to quiet a
finding that should be fixed. Waivers live in .trivyignore.yaml (the
machine-applied source of truth, passed to Trivy with --ignorefile) and are mirrored in a table
here so the reason is auditable.
There are currently no exceptions: the module and its examples scan clean. A role assignment grants access rather than deploying infrastructure, and the module's whole purpose is to make that grant safer, so there is nothing to waive.
To add an exception: add an entry to .trivyignore.yaml (id, optional paths to scope it, and a
statement recording why), then add a matching row here recording the reason. Both the file and
the table are reviewed in the pull request.
The Requirements, Providers, Inputs, Outputs, and Resources below are generated by terraform-docs.
| Name | Version |
|---|---|
| terraform | >= 1.9.0, < 2.0.0 |
| azurerm | >= 4.0.0, < 5.0.0 |
| Name | Version |
|---|---|
| azurerm | >= 4.0.0, < 5.0.0 |
No modules.
| Name | Type |
|---|---|
| azurerm_pim_active_role_assignment.this | resource |
| azurerm_pim_eligible_role_assignment.this | resource |
| azurerm_role_assignment.this | resource |
| azurerm_role_definition.this | resource |
| azurerm_role_definition.pim | data source |
| azurerm_subscription.current | data source |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| constrained_delegation_role_ids | The privileged role definition ids (name => GUID) whose re-delegation is denied by the ABAC condition this module applies to privileged assignments. Defaults to the three roles that can grant Azure RBAC (Owner, User Access Administrator, Role Based Access Control Administrator). Extend this to treat more roles as privileged. The guard denies the assignee the ability to create or delete role assignments for any of these roles, so a granted Owner cannot hand Owner (or the other listed roles) to anyone else. |
map(string) |
{ |
no |
| pim_role_definition_lookup_scope | The scope at which PIM role_names are resolved to role definition ids (built-in roles resolve at anyscope). Defaults to the current subscription. Set this when a PIM assignment uses a custom role defined at a different scope, or pass the full role definition id via role_ids instead. |
string |
null |
no |
| role_assignments | The role assignments to create, keyed by a logical label you choose (the label is only for readable plan output and stable addressing, it is not the Azure resource name). Each entry is expanded over the cartesian product of principal_ids and the combined set of role_names + role_ids, so one entrycan assign several roles to several principals at one scope. assignment_type selects how the role is granted:- permanent (default): a standard azurerm_role_assignment (a persistent, always-active grant).- pim_active: a PIM active assignment (active now, but managed by PIM and typically time-bound).- pim_eligible: a PIM eligible assignment (the principal can activate the role just-in-time).role_names are resolved to a role definition id automatically for the PIM types (which require theid, not the name); role_ids (full role definition resource ids) are used as-is; androle_definition_keys reference custom roles created in this call via role_definitions, sodefine-then-assign works in one module call. The privileged delegation guard (see constrained_delegation_role_ids) only applies to permanent and pim_eligible assignments, becausepim_active has no condition argument. |
map(object({ |
{} |
no |
| role_definitions | Custom Azure RBAC role definitions to create, keyed by role name (custom role names are unique per tenant, so make them distinctive). scope is where the definition is stored (a management group,subscription, or resource group id); assignable_scopes defaults to just that scope when empty.permissions holds the control-plane actions/not_actions and data-planedata_actions/not_data_actions. Assignments in the same call reference a definition throughrole_definition_keys. Creating a definition needs Microsoft.Authorization/roleDefinitions/writeon the scope (Owner; User Access Administrator is NOT enough). A freshly created definition can take a short while to become assignable (Azure RBAC replication), so a first apply may need a retry when the definition and a same-call assignment race. |
map(object({ |
{} |
no |
| Name | Description |
|---|---|
| conditions_applied | The permanent assignments that carry an ABAC condition, with their principal, role, scope, and condition version. |
| guarded_assignments | The keys of assignments that received the privileged-delegation deny condition. |
| pim_active_role_assignments | The PIM active assignments, keyed by "label|rN|pN". Full resource objects. |
| pim_eligible_role_assignments | The PIM eligible assignments, keyed by "label|rN|pN". Full resource objects. |
| principal_ids | The distinct principal ids that received an assignment. |
| role_assignment_ids | All assignment ids across the three types, keyed by "label|rN|pN". |
| role_assignment_ids_zipmap | key => { name, id } across all assignment types, for easy composition with other modules. |
| role_assignments | The permanent (azurerm_role_assignment) assignments, keyed by "label|rN|pN". Full resource objects (all attributes). |
| role_definition_guids | Map of custom role name to the role definition GUID (the name portion of the id). |
| role_definition_ids | Map of custom role name to the full role definition resource id (feed this to role_ids in other calls or modules). |
| role_definition_ids_zipmap | Map of custom role name to an object of its name and full resource id, for easy composition. |
| role_definitions | Map of custom role name to its useful attributes. |
| scopes | The distinct scopes assignments were created at. |