Feature Request: Make Bicep authoritative for role assignments — adopt/replace an existing assignment created outside Bicep instead of failing with `RoleAssignmentExists`
- Dominant language
- Bicep
- Stars
- 3.6k
- Forks
- 830
- Avg merge
- 1d 21m
- Merged PRs (30d)
- 79
Description
## Summary
When a `Microsoft.Authorization/roleAssignments` resource is deployed via Bicep and an **equivalent** role assignment already exists that was created **outside Bicep** (Azure Portal, `az role assignment create`, PowerShell, PIM, etc.), the deployment fails with a `RoleAssignmentExists` conflict.
This happens because Azure RBAC enforces uniqueness on the **(scope, principalId, roleDefinitionId)** tuple — **not** on the role assignment *name* (GUID). Bicep generates a **deterministic** GUID name (the documented best practice, `guid(scope, principalId, roleDefinitionId)`), whereas a manual/portal creation uses a **random** GUID name. The two assignments are semantically identical but have different names, so the platform rejects the Bicep PUT.
I would like a first-class way to tell Bicep: **"this role assignment is now managed by IaC — adopt or replace whatever already exists for this principal/role/scope."** Essentially, let Bicep become the authority for that assignment.
## Request type
- [x] Feature / enhancement
- [ ] Bug
## Current behavior
Given a principal that **already** has a role at a scope (assignment created manually with a random GUID name):
```bicep
resource ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(resourceGroup().id, principalId, roleDefinitionId) // deterministic name
scope: resourceGroup()
properties: {
principalId: principalId
roleDefinitionId: roleDefinitionId
principalType: 'ServicePrincipal'
}
}
```
Deployment fails:
```text
Status: Conflict
Code: RoleAssignmentExists
Message: The role assignment already exists.
```
> Note: this is distinct from `RoleAssignmentUpdateNotPermitted`. That error is about changing the immutable properties of an assignment that shares the *same name*. The case here is a **different name, same (scope, principal, role) tuple**, which returns `RoleAssignmentExists`.
## Why this is painful
This blocks the pipeline at scale. Our current workaround for **every** colliding assignment is fully manual:
1. Pipeline fails on `RoleAssignmentExists`.
2. An engineer opens the Azure Portal (or runs CLI), finds the duplicate assignment that was created outside Bicep.
3. Manually deletes it.
4. Re-runs the pipeline so Bicep can recreate it under the deterministic name.
For a large RBAC change (dozens or hundreds of assignments across many scopes), this is slow, error-prone, requires elevated access at deploy time, and defeats the point of declarative IaC. We want Bicep to be the single source of truth, but today any out-of-band assignment silently sabotages the deployment.
## Proposed solutions (any one of these would solve it)
### Option A — A `force` / `replace` behavior on the resource
A decorator or property that, on a `RoleAssignmentExists` conflict, instructs ARM/Bicep to look up the existing assignment by **(scope, principalId, roleDefinitionId)**, delete it, and recreate it under the deterministic Bicep name — making Bicep authoritative.
```bicep
@replaceExisting() // proposed
resource ra 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(resourceGroup().id, principalId, roleDefinitionId)
scope: resourceGroup()
properties: { ... }
}
```
### Option B — Idempotent "adopt existing" / treat-as-success
If an assignment with the same **(scope, principalId, roleDefinitionId)** tuple already exists (under any name), treat the deployment as a **no-op success** rather than a conflict. The desired end state (principal has the role at the scope) is already met, so the deployment should converge, not fail.
### Option C — Stabilize `onlyIfNotExists`, but key it on the tuple (not the name)
The experimental `onlyIfNotExists` decorator was removed in [Azure/bicep#17996](https://github.com/Azure/bicep/pull/17996). The core problem reported in [Azure/bicep#18454](https://github.com/Azure/bicep/issues/18454) was that it only checked by **name**, so it never detected a manually-created assignment with a different (random) name. A version that checks existence by the real uniqueness key — **(scope, principalId, roleDefinitionId)** — would directly solve this.
### Option D — A deployment-level `existingResourceBehavior` for role assignments
A deployment/stack-level setting such as `existingResourceBehavior: adopt | replace | fail` scoped to role assignments, so an entire RBAC rollout can be made authoritative without annotating each resource. (Deployment Stacks already explore some adoption semantics; aligning role assignments with that would be ideal.)
## Why the existing options don't cover this
- **Deterministic GUID names** (the documented best practice) already make Bicep-to-Bicep deployments idempotent — but they do **nothing** for assignments created *outside* Bicep with random names, because the platform key is the tuple, not the name.
- **`existing` reference** can read an assignment by name, but you cannot reference the out-of-band assignment because you don't know its random name, and you cannot look it up by principal/role/scope. (See [Azure/bicep#4917](https://github.com/Azure/bicep/issues/4917) — "Look up existing resources by properties other than name".)
- **Reproducing the portal's GUID** isn't possible — the portal uses a random GUID. (See [Azure/bicep#5694](https://github.com/Azure/bicep/issues/5694).)
- **Pre-deploy cleanup scripts** (enumerate + delete duplicates before deploy) work but require elevated `Microsoft.Authorization/roleAssignments/delete` at deploy time, add a brittle imperative step, and momentarily remove access.
## Related issues
- [Azure/bicep#18454](https://github.com/Azure/bicep/issues/18454) — `@onlyIfNotExists()` doesn't check for existing role assignments (open)
- [Azure/bicep#18226](https://github.com/Azure/bicep/issues/18226) — onlyIfNotExists roleAssignment deployment fails despite existing (closed)
- [Azure/bicep#17996](https://github.com/Azure/bicep/pull/17996) — Remove `onlyIfNotExists` experimental feature (merged)
- [Azure/bicep#8774](https://github.com/Azure/bicep/issues/8774) — Provide a mechanism to maintain role assignments with idempotent names (closed)
- [Azure/bicep#5694](https://github.com/Azure/bicep/issues/5694) — Ability to generate the same GUID value the portal does for roleAssignments (open)
- [Azure/bicep#4917](https://github.com/Azure/bicep/issues/4917) — Look up existing resources by properties other than name (open)
- [Azure/bicep#4023](https://github.com/Azure/bicep/issues/4023) — Check if resource exists (open)
## Environment
- Bicep CLI version:
- Deployment scope: resource group / subscription / management group
- API version in use: `Microsoft.Authorization/roleAssignments@2022-04-01`
- Deployment method: Azure DevOps pipeline (`az deployment group create`)
## Acceptance criteria
- A deployment of a `roleAssignments` resource succeeds (converges) when an equivalent assignment for the same **(scope, principalId, roleDefinitionId)** already exists, regardless of that assignment's GUID name.
- The mechanism is opt-in and explicit (decorator / property / deployment setting), so default behavior is unchanged.
- No manual portal/CLI cleanup is required to make Bicep authoritative.
Contributor guide
Assessment
This issue has not been assessed yet.