---
cuppy_hub: azure
cuppy_path: guides/rbac/assign-roles.md
cuppy_section: docs
cuppy_doc: 447T-DE9Z
cuppy_doc_uuid: b1a8c2d1-f3af-42dc-bb6c-ba7329debad6
cuppy_permalink: https://cuppy.io/azure/d/447T-DE9Z
cuppy_version: 1
cuppy_updated: 2026-10-23T16:16:52.575Z
cuppy_canonical: https://cuppy.io/azure/docs/guides/rbac/assign-roles
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: c688a1786f157bc8c961ba7c314f34da425a21ae8ba94d85d577a4448d30228d
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---


# Assign Azure roles — idempotently

**Use the principal's object ID, the role's definition ID, the narrowest scope, and a name derived from all three.** That combination avoids every common assignment failure: directory lookups, replication delays, duplicate assignments and `RoleAssignmentUpdateNotPermitted`.

## 1. Collect the three inputs

| Input | Get it with |
|---|---|
| Principal object ID | Managed identity: `az identity show -n <id> -g <rg> --query principalId -o tsv` (system-assigned: `az webapp identity show … --query principalId`). Service principal: `az ad sp show --id <client-id> --query id -o tsv`. Group: `az ad group show --group <name> --query id -o tsv` |
| Role definition ID | `az role definition list --name "Key Vault Secrets User" --query "[0].name" -o tsv` (the GUID; built-in IDs are listed in `reference/rbac/role-assignments-and-scopes.md`) |
| Scope | The target resource's `id`: `az keyvault show -n <kv> --query id -o tsv` |

## 2. Azure CLI

```
az role assignment create \
  --assignee-object-id <object-id> \
  --assignee-principal-type ServicePrincipal \
  --role 4633458b-17de-408a-b874-0445c86b69e6 \
  --scope <resource-id>
```

`--assignee <name-or-upn>` makes the CLI look the principal up in Microsoft Graph first, which fails for service principal callers without directory read rights (`Insufficient privileges to complete the operation`) and for principals that haven't replicated yet. `--assignee-object-id` with `--assignee-principal-type` skips both. Use `User`, `Group` or `ServicePrincipal` (managed identities are service principals).

## 3. Bicep

```bicep
param principalId string
param keyVaultName string

var secretsUserRoleId = '4633458b-17de-408a-b874-0445c86b69e6'

resource kv 'Microsoft.KeyVault/vaults@2026-05-15' existing = {
  name: keyVaultName
}

resource kvSecretsUser 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(kv.id, principalId, secretsUserRoleId)
  scope: kv
  properties: {
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', secretsUserRoleId)
    principalId: principalId
    principalType: 'ServicePrincipal'
  }
}
```

- `scope: kv` puts the assignment on the vault, not the resource group.
- `guid(scope, principal, role)` is stable across redeploys and changes when any input changes, so a recreated identity gets a new assignment instead of failing with `RoleAssignmentUpdateNotPermitted`.
- The deploying identity needs `Microsoft.Authorization/roleAssignments/write` on that scope: Contributor isn't enough.

## 4. TypeScript

```ts
import { createHash } from "node:crypto";
import { DefaultAzureCredential } from "@azure/identity";
import { AuthorizationManagementClient } from "@azure/arm-authorization";

// Deterministic GUID from the three inputs, like Bicep's guid().
function assignmentName(scope: string, principalId: string, roleId: string): string {
  const h = createHash("sha256").update(`${scope}|${principalId}|${roleId}`).digest("hex");
  return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
}

export async function grant(subscriptionId: string, scope: string, principalId: string, roleId: string) {
  const client = new AuthorizationManagementClient(new DefaultAzureCredential(), subscriptionId);
  try {
    await client.roleAssignments.create(scope, assignmentName(scope, principalId, roleId), {
      roleDefinitionId: `/subscriptions/${subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/${roleId}`,
      principalId,
      principalType: "ServicePrincipal",
    });
  } catch (e) {
    // Same principal, role and scope already assigned under another name: treat as done.
    if ((e as { code?: string }).code !== "RoleAssignmentExists") throw e;
  }
}
```

The REST error is `"code": "RoleAssignmentExists", "message": "The role assignment already exists."` (seen in a Microsoft Q&A thread); the SDK's `RestError.code` carrying it is the hub's reading, so log `e` once to confirm.

## 5. Verify effective access

```
az role assignment list --assignee <object-id> --all --include-inherited --include-groups \
  --query "[].{role:roleDefinitionName, scope:scope}" -o table
```

`--all` searches every scope in the subscription; `--include-inherited` adds assignments from parent scopes and `--include-groups` those through groups. A change can take about 10 minutes to apply. If access still fails, `guides/identity/fix-access-denied.md` goes through the other layers (deny assignments, policy, network, data-plane models).

## Next

Errors: `reference/rbac/errors.md`. The model and built-in role IDs: `reference/rbac/role-assignments-and-scopes.md`.


---
<!-- cuppy -->
You are reading one file. The same content is free over MCP at https://cuppy.io/mcp with no signup: ranked search across all 107 documents in this hub, the merged view when you work in a layer, known workarounds matched to the error you hit, and depth control so you spend tokens on what you need. Start with the `_hello` tool. How to connect: https://cuppy.io/azure/ Something wrong in this document? Report it with the `session feedback` tool on the same endpoint.
<!-- /cuppy -->
