# azure One connection is one Entra tenant's Azure Resource Manager. toolpass authenticates as an app registration, resolves the user to an Entra object id, lists the role assignments and deny assignments that apply to that user at a scope (inherited from parent scopes and through group membership, which Azure's `assignedTo()` filter expands), reads the role definitions and evaluates `actions` minus `notActions` (or `dataActions` minus `notDataActions`) the way Azure Resource Manager does. A deny assignment wins over any grant. Nothing is written. ## Credential An **app registration** with a **client secret** (`credential`). Grant it: - the built-in **Reader** role (`*/read`, which includes `Microsoft.Authorization/roleAssignments/read`, `roleDefinitions/read` and `denyAssignments/read`) at the root management group, or at every subscription toolpass is asked about. Assignments at a scope the app cannot read answer `resource_not_visible`. - for identity, either a `microsoft365_connection` (the microsoft365 integration's app resolves the user; nothing else is needed on this app), or the Graph application permission `User.Read.All` on this app so toolpass can look the user up itself. `toolpass probe` fetches an ARM token, lists the built-in role definitions and, without a `microsoft365_connection`, lists one Graph user. ## Connection ```yaml - id: azure-corp integration: azure tenant_id: 11111111-2222-3333-4444-555555555555 client_id: 66666666-7777-8888-9999-000000000000 credential: env:AZURE_CLIENT_SECRET # microsoft365_connection: m365-corp # url: https://management.azure.com # https://management.usgovcloudapi.net for Azure Government # graph_url: https://graph.microsoft.com # authority_url: https://login.microsoftonline.com ``` | Key | Meaning | |---|---| | `tenant_id` | the tenant id or domain | | `client_id` | the app registration's application id | | `credential` | the client secret, `env:` or `file:` | | `microsoft365_connection` | optional: resolve users through that connection | | `url`, `graph_url`, `authority_url` | the ARM, Graph and token endpoints (sovereign clouds) | ### Identity With a `microsoft365_connection`, that connection's identity (object id, `account_enabled`) is used. Otherwise `GET /v1.0/users?$filter=mail eq '' or userPrincipalName eq ''`, compared exactly; none is `user_not_found`, two are `user_ambiguous`. A disabled account is denied every action. Groups sent by the caller are ignored: Azure expands the user's transitive group memberships itself through `$filter=assignedTo('{objectId}')`. ## Resources | Resource | Scope | |---|---| | `managementgroup:` | `/providers/Microsoft.Management/managementGroups/` | | `subscription:` | `/subscriptions/` | | `resourcegroup:/` | `/subscriptions//resourceGroups/` | | `resource:/subscriptions/.../providers///[//]` | the full ARM id | Every segment is limited to letters, digits, `-`, `_`, `.`, `(` and `)`, with no dot-only segments, so the scope toolpass builds is the one ARM spells in assignment scopes and needs no escaping in the URL. ## Actions | Action | Operation | Plane | |---|---|---| | `vm.read` | `Microsoft.Compute/virtualMachines/read` | control | | `vm.start` / `vm.restart` / `vm.deallocate` | `Microsoft.Compute/virtualMachines/{start,restart,deallocate}/action` | control | | `vm.delete` | `Microsoft.Compute/virtualMachines/delete` | control | | `storage.listkeys` | `Microsoft.Storage/storageAccounts/listkeys/action` | control | | `storage.blob.read` / `storage.blob.write` | `Microsoft.Storage/storageAccounts/blobServices/containers/blobs/{read,write}` | data | | `keyvault.secret.read` / `keyvault.secret.write` | `Microsoft.KeyVault/vaults/secrets/{getSecret,setSecret}/action` | data (RBAC permission model) | | `aks.admin_credentials` / `aks.user_credentials` | `Microsoft.ContainerService/managedClusters/listCluster{Admin,User}Credential/action` | control | | `rbac.write` | `Microsoft.Authorization/roleAssignments/write` | control | | `resourcegroup.delete` | `Microsoft.Resources/subscriptions/resourceGroups/delete` | control | | `deployment.write` | `Microsoft.Resources/deployments/write` | control | | `raw:` | any control-plane operation | control | | `data:` | any data-plane operation | data | Wildcards are not accepted in `raw:` or `data:`; role definitions carry them. ### Evaluation 1. `GET {scope}/providers/Microsoft.Authorization/roleAssignments?$filter=assignedTo('{oid}')`. Each assignment whose scope is the target or an ancestor is read (`GET {roleDefinitionId}`, cached five minutes) and its permissions evaluated. When none grants the operation the answer is **denied** and nothing else is read. An assignment with an ABAC `condition` counts only as a conditional grant. 2. When something grants (or might), `GET {scope}/providers/Microsoft.Authorization/denyAssignments?$filter=assignedTo('{oid}')`. Each deny whose scope is the target or an ancestor (honouring `doNotApplyToChildScopes`) and whose `actions` minus `notActions` (or data equivalents) match the operation **denies**, unless `excludePrincipals` names the user. A deny with a `condition`, one excluding a **group** (toolpass does not read group membership), or one whose scope cannot be placed, makes the answer `unsupported` instead of allowed. 3. Otherwise the first unconditional grant **allows**, naming the role, whether it was assigned directly or through a group, and the scope; only conditional grants answer `unsupported`. 4. Assignments below the target scope do not apply. Management-group and tenant-root assignments are ancestors of every subscription; for a management-group target, an assignment at a different management group is `unsupported` since the hierarchy is not read. An assignment without a scope is treated as uncertain. ## Requests a tool makes The route table (`toolpass routes azure`) maps a toolpass tool's requests to the connection's `url` to the operation ARM authorizes them as, read from the path: `{ns}/{type}[/{type}...]/read` for GET and HEAD, `/write` for PUT and PATCH, `/delete` for DELETE, `/{action}/action` for a POST to `.../{name}/{action}`; a GET of a collection is the type's read on its parent. Paths without a provider are Microsoft.Resources' (`Microsoft.Resources/subscriptions/resourceGroups/delete`); an extension resource such as `{resource}/providers/Microsoft.Authorization/roleAssignments/{id}` is its own provider's. The scope is the narrowest the integration takes: `resource:` inside a resource group, `resourcegroup:`, `subscription:` or `managementgroup:` for those themselves. An object directly under a subscription or management group (a role assignment there, a subscription deployment) is checked on that subscription or management group, so an assignment on the object alone is not seen. A DNS record set named `@` (the zone apex) or `*`, `*.` (a wildcard), the only names in a path that may hold those characters, is checked on its zone in the same way. Refused (`unresolved_resource`): a path that does not parse exactly (percent-encoded characters, dot or empty segments, a subscription that is not a GUID), `/batch`, tenant-level writes (`elevateAccess`, Resource Graph queries), resource moves, moving a subscription between management groups, creating or changing a template deployment or deployment stack however the path spells it (`.../providers/Microsoft.Resources/deployments/{name}`, `.../resourcegroups/{rg}/deployments/{name}`, `/subscriptions/{id}/deployments/{name}`; its template can create anything, as the agent), and a write whose JSON body names another Azure resource by id: ARM checks the linked permission (joining a subnet, attaching a disk, assigning an identity) as the agent. A role assignment's own `roleDefinitionId`, the target itself and the scopes above it are allowed in the body. Tenant-level listings (subscriptions, tenants, management groups) list only what the agent can see and provider metadata is public, so they are not checked; any other tenant-level path is refused. Every POST action is treated as sending data out (it may run a Logic App or send a notification), and its output as untrusted. The data planes (blob storage, Key Vault) are other hosts and not routed. ## Decisions | Code | When | |---|---| | `allowed` | an unconditional assignment at or above the scope grants the operation and no deny blocks it | | `denied` | a deny assignment blocks it; no assignment at or above the scope grants it; the account is disabled | | `unsupported` | only conditional grants; an uncertain deny (condition, excluded group, unplaceable scope); a management-group assignment whose place in the hierarchy is unknown; Graph did not report whether the account is enabled | | `resource_not_visible` | ARM answers 404, or 403 `AuthorizationFailed` (no Reader at the scope) | | `user_not_found` / `user_ambiguous` | the Graph lookup | | `credential_rejected` | the token endpoint rejects the client; 401; 403 other than `AuthorizationFailed` | | `invalid_request` | a malformed scope or operation; a resource type or action the integration does not take | | `upstream_*` | 5xx, 429, timeouts, a `nextLink` off the ARM endpoint, a role definition that cannot be read | ## What it cannot see - **ABAC conditions** on role or deny assignments are not evaluated. - **Group membership** for `excludePrincipals` on deny assignments. - **The management-group hierarchy**: which management group holds a subscription. - **Classic administrators**, **Azure Lighthouse** delegations (not returned by the listing), **Elevate access** for Global Administrators, and resource-provider-specific checks (Key Vault access policies when the vault is not in the RBAC permission model, storage account keys). - **Service principals and managed identities** as the subject: identity is resolved from a user email. ## Unverified Written from the authorization REST specification (2022-04-01), the Azure RBAC documentation source and the Graph documentation; not run against a live tenant. Marked `UNVERIFIED` in the code where it matters: - Operation matching is case-insensitive and `*` spans slashes, as the documentation's examples imply. - Whether `assignedTo()` on deny assignments already applies `excludePrincipals` (toolpass applies them again, which is safe). - The tenant-root listing `GET /providers/Microsoft.Authorization/roleDefinitions` used by the probe is not in the specification file, though `az role definition list` uses it. ## Test `python -m pytest tests/integrations/azure` (in `toolpass-py`) runs a fake token endpoint, Graph and ARM validated against `authorization-RoleAssignmentsCalls`, `authorization-RoleDefinitionsCalls`, `authorization-DenyAssignmentCalls` and the Graph description when `TOOLPASS_SPECS_DIR` holds them (`test/specs/fetch.sh`). The fake has Reader, Contributor, Owner, a data-plane role and a custom role, assignments direct and through a group at management-group, subscription, resource-group and resource scopes, a conditional assignment, and deny assignments with exclusions, conditions and `doNotApplyToChildScopes`. A property test (`test_fuzz_parse_target`) checks that only well-formed scopes and operations reach the API. `test_routes.py` checks the route table against the same three descriptions and that every scope it builds is one `parse_target` takes.