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 of DoPurchaseTwoStep)
Microsoft.Capacity/reservationOrders/read
Microsoft.Capacity/reservationOrders/write
Microsoft.Capacity/reservationOrders/purchase/action (step 2 of DoPurchaseTwoStep, 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/action
Microsoft.Capacity/reservationOrders/read
Microsoft.Capacity/reservationOrders/write
Microsoft.Capacity/reservationOrders/purchase/action
Microsoft.Capacity/calculateExchange/action (compute exchange)
Microsoft.Capacity/exchange/action (compute exchange)
Savings plans / billing benefits:
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 validate against 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".
Symptom
User-reported purchase failure (Azure two-step
POST .../reservationOrders/{id}/purchase):Root cause
arm/CUDly-CrossSubscription/template.jsongrants the CUDly SP the built-in role "Reservation Purchaser" (f7b75c60-3036-4b75-91c3-6b41c27c1689) at both subscription scope and/providers/Microsoft.Capacitytenant scope.Verified via
az role definition list --name "Reservation Purchaser", that role'spermissions[0].actionsis: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 PurchaserincludesMicrosoft.BillingBenefits/savingsPlanOrders/writebut is missing thevalidate/actionused byAzureSPProber.ValidatePurchase(PR #724) and likely other narrower actions.Fix direction
Define a custom role in
arm/CUDly-CrossSubscription/template.json(subscription-scoped viaMicrosoft.Authorization/roleDefinitions) that explicitly enumerates the actions the CUDly executors actually call, then assign that custom role plus the built-inReservation Purchaser(still needed forreservationRecommendationsreads).Minimum custom action set (verify each by tracing the SDK calls in
providers/azure/services/internal/reservations/purchase.go, the seven executors inproviders/azure/services/{cache,compute,cosmosdb,database,managedredis,search,synapse}/client.go, the exchange flow inproviders/azure/services/compute/exchange.go, and the SP probe ininternal/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
assignableScopesshould 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 Purchaserassignment — it carriesMicrosoft.Consumption/reservationRecommendations/readwhich we still need for recommendation discovery, and removing it would be a regression for users already in production.Tests required
az deployment sub validateagainst a sandbox subscription).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 this403on every Azure purchase.Source of finding
User report on the live
Standard_D2d_v4purchase attempt + verification of the role'sactionsarray viaaz role definition list --name "Reservation Purchaser".