Skip to content

Latest commit

 

History

History
183 lines (153 loc) · 11.3 KB

azure.md

File metadata and controls

183 lines (153 loc) · 11.3 KB

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

  - 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.