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.
An app registration with a client secret (credential). Grant it:
- the built-in Reader role (
*/read, which includesMicrosoft.Authorization/roleAssignments/read,roleDefinitions/readanddenyAssignments/read) at the root management group, or at every subscription toolpass is asked about. Assignments at a scope the app cannot read answerresource_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 permissionUser.Read.Allon 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.
- 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) |
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}').
| 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.
| 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.
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 ABACconditioncounts only as a conditional grant.- When something grants (or might),
GET {scope}/providers/Microsoft.Authorization/denyAssignments?$filter=assignedTo('{oid}'). Each deny whose scope is the target or an ancestor (honouringdoNotApplyToChildScopes) and whoseactionsminusnotActions(or data equivalents) match the operation denies, unlessexcludePrincipalsnames the user. A deny with acondition, one excluding a group (toolpass does not read group membership), or one whose scope cannot be placed, makes the answerunsupportedinstead of allowed. - 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. - 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
unsupportedsince the hierarchy is not read. An assignment without a scope is treated as uncertain.
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.
| 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 |
- ABAC conditions on role or deny assignments are not evaluated.
- Group membership for
excludePrincipalson 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.
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 appliesexcludePrincipals(toolpass applies them again, which is safe). - The tenant-root listing
GET /providers/Microsoft.Authorization/roleDefinitionsused by the probe is not in the specification file, thoughaz role definition listuses it.
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.