docs/integrations/azure.md
Rama main · réplica de lectura del repositorio original.
# 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 '<email>' or userPrincipalName eq '<email>'`,
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:<name>` | `/providers/Microsoft.Management/managementGroups/<name>` |
| `subscription:<guid>` | `/subscriptions/<guid>` |
| `resourcegroup:<guid>/<name>` | `/subscriptions/<guid>/resourceGroups/<name>` |
| `resource:/subscriptions/.../providers/<ns>/<type>/<name>[/<type>/<name>]` | 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:<operation>` | any control-plane operation | control |
| `data:<operation>` | 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:<id>` 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 `*`, `*.<name>` (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.