Repository navigation
Azure ARM template grants role missing purchase/calculatePrice actions -- all reservation purchases 403 #731
Description
Symptom
User-reported purchase failure (Azure two-step POST .../reservationOrders/{id}/purchase):
some purchases failed: [Standard_D2d_v4: purchase failed: reservation purchase failed with status 403:
{"error":{"code":"AuthorizationFailed",
"message":"The client '0c2390de-60fd-4df3-bb60-3e6a8b6821e4'
with object id '8158bcef-a8f2-4aca-8d16-b513a57d2214'
does not have authorization to perform action
'Microsoft.Capacity/reservationOrders/purchase/action'
over scope '/providers/Microsoft.Capacity/reservationOrders/5205aaa2-18e3-44ec-bebb-f3e9df66d2e4'
or the scope is invalid. If access was recently granted, please refresh your credentials."}}]
Root cause
arm/CUDly-CrossSubscription/template.json grants the CUDly SP the built-in role "Reservation Purchaser" (f7b75c60-3036-4b75-91c3-6b41c27c1689) at both subscription scope and /providers/Microsoft.Capacity tenant scope.
Verified via az role definition list --name "Reservation Purchaser", that role's permissions[0].actions is:
Microsoft.Authorization/roleAssignments/read
Microsoft.Capacity/catalogs/read
Microsoft.Capacity/register/action
Microsoft.Compute/register/action
Microsoft.Consumption/register/action
Microsoft.Consumption/reservationRecommendationDetails/read
Microsoft.Consumption/reservationRecommendations/read
Microsoft.Resources/subscriptions/read
Microsoft.Resources/subscriptions/resourceGroups/read
Microsoft.SQL/register/action
Microsoft.Support/supporttickets/write
The name is misleading. The role only lets the SP read recommendations and register the provider. None of the actions actually required to execute a two-step purchase are present:
Microsoft.Capacity/calculatePrice/action(step 1 ofDoPurchaseTwoStep)Microsoft.Capacity/reservationOrders/readMicrosoft.Capacity/reservationOrders/writeMicrosoft.Capacity/reservationOrders/purchase/action(step 2 ofDoPurchaseTwoStep, the one that 403's)
Same shape for savings plans: Savings plan Purchaser includes Microsoft.BillingBenefits/savingsPlanOrders/write but is missing the validate/action used by AzureSPProber.ValidatePurchase (PR #724) and likely other narrower actions.
Fix direction
Define a custom role in arm/CUDly-CrossSubscription/template.json (subscription-scoped via Microsoft.Authorization/roleDefinitions) that explicitly enumerates the actions the CUDly executors actually call, then assign that custom role plus the built-in Reservation Purchaser (still needed for reservationRecommendations reads).
Minimum custom action set (verify each by tracing the SDK calls in providers/azure/services/internal/reservations/purchase.go, the seven executors in providers/azure/services/{cache,compute,cosmosdb,database,managedredis,search,synapse}/client.go, the exchange flow in providers/azure/services/compute/exchange.go, and the SP probe in internal/commitmentopts/probe_azure.go):
Reservations:
Microsoft.Capacity/calculatePrice/actionMicrosoft.Capacity/reservationOrders/readMicrosoft.Capacity/reservationOrders/writeMicrosoft.Capacity/reservationOrders/purchase/actionMicrosoft.Capacity/calculateExchange/action(compute exchange)Microsoft.Capacity/exchange/action(compute exchange)
Savings plans / billing benefits:
Microsoft.BillingBenefits/savingsPlanOrders/readMicrosoft.BillingBenefits/savingsPlanOrders/writeMicrosoft.BillingBenefits/savingsPlanOrders/savingsPlans/readMicrosoft.BillingBenefits/validate/action(RPClient.ValidatePurchase from PR feat(commitmentopts/azure): probe Savings Plans offerings for live validation #724)
The custom role's assignableScopes should be [/subscriptions/{subscriptionId}] so it's tenant-portable. Tag the role with version metadata so future bumps are visible in role assignment history.
Keep the existing built-in Reservation Purchaser assignment — it carries Microsoft.Consumption/reservationRecommendations/read which we still need for recommendation discovery, and removing it would be a regression for users already in production.
Tests required
- ARM template lint passes (
az deployment sub validateagainst a sandbox subscription). - A new acceptance/integration test (or doc verifying manual repro) confirms that with only the new ARM-granted set of roles, a purchase succeeds end-to-end.
- known-issues.md entry updated to flag the upgrade requirement for existing CUDly tenants (they MUST redeploy the ARM template to refresh their role grants).
Impact
P0 — this is the blocker for any Azure reservation purchase across all seven executors. The error has been hit in production right now. Mark priority/p0, severity/critical. Users with the old ARM template will see this 403 on every Azure purchase.
Source of finding
User report on the live Standard_D2d_v4 purchase attempt + verification of the role's actions array via az role definition list --name "Reservation Purchaser".
Activity
fix(azure-arm): grant SP the actual purchase actions via custom role (c…
fix(iac/azure): mirror reservation-purchaser custom role on host SP +…
fix(iac/azure): custom role grants Microsoft.Capacity/reservationOrde…
cristim commentedon May 28, 2026
Verified shipped: PRs #732 (commit ce05942, merged 2026-05-26) and #744 (commit f34b036, merged 2026-05-27) are both in feat/multicloud-web-frontend. PR #732 added the custom role with calculatePrice/reservationOrders/purchase/action to arm/CUDly-CrossSubscription/template.json; PR #744 added shared Terraform module terraform/modules/iam/azure/cudly-reservation-role/.
fix(providers/azure): send canonical reservedResourceType enum values
fix(providers/azure): send canonical reservedResourceType enum values
fix(providers/azure): send canonical reservedResourceType enum values
sec(iac): scope Azure purchase role to onboarded subscription
sec(iac): scope Azure purchase role to onboarded subscription (#1658)