From f12423a6b7692991a7e357fd22fad3dd3b74df77 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Wed, 12 Aug 2026 00:49:13 +0200 Subject: [PATCH] fix(iac/azure): remove nonexistent Microsoft.Capacity action from reservation-purchaser role Microsoft.Capacity/reservationOrders/purchase/action does not exist in Azure's operation catalog. Validated all 12 actions in the custom reservation-purchaser role against the live catalog (az provider operation show, both namespace-level and resourceType-level operations): 11 valid, this one absent. Azure rejects the entire role definition with InvalidActionOrNotAction when it appears in the actions list, so the role was never created, which is why the Azure deploy has failed on every main run since 2026-07-19 (the bootstrap stack's data.azurerm_role_definition lookup finds nothing). It is also why the customer-facing ARM template in arm/CUDly-CrossSubscription fails for anyone deploying it to onboard a subscription. The role's actual purpose is legitimate: the built-in Reservation Purchaser role genuinely lacks Microsoft.Capacity/reservationOrders/write, Microsoft.Capacity/calculatePrice/action, and every Microsoft.BillingBenefits action, which is what causes 403s on production purchases. Only the one action name was wrong; the role's description and every comment citing it are corrected to name the real gap instead. Purchasing a reservation is Microsoft.Capacity/reservationOrders/write, already present in the list. Removed the action from every occurrence: the Terraform module, the customer-facing ARM template (both the actions list and two descriptions), the two comments in consuming modules, and all 34 scripts/testdata/role-parity/*-arm.json fixtures plus the TF baseline fixture that also carried it. The fixtures are replaced with a real, previously-absent action (Microsoft.BillingBenefits/register/action) rather than deleted outright, since the role-parity checker compares TF and ARM actions as a set (sort -u then diff) and most fixtures use a single shared TF baseline: a bare deletion would desync the actions count across 33 of the 35 fixtures and mask the scope/casing/collision axis each one actually exists to test. Full fixture suite verified green after the change (36/36). The parity checker itself never caught this because both the ARM and TF sides carried the same invalid action: parity is not validity, and neither side of the checker validates actions against Azure's actual catalog. That is a separate, larger effort (needs network access in CI) and is not attempted here. Also corrects the "Explain a missing bootstrap role" CI diagnostic in deploy-azure.yml, which assumed the bootstrap stack had simply never been applied. It had been applied and failed, so the role was rejected rather than skipped; the message now points at InvalidActionOrNotAction in the bootstrap stack's own apply log as the first thing to check. Verified by a real terraform apply against a live subscription: the role definition now creates successfully. --- .github/workflows/deploy-azure.yml | 20 +++++++++++++------ arm/CUDly-CrossSubscription/template.json | 5 ++--- iac/federation/azure-target/terraform/main.tf | 5 +++-- .../canonical-scope-variants-arm.json | 2 +- ...ion-assignablescopes-benign-first-arm.json | 2 +- ...ision-assignablescopes-evil-first-arm.json | 2 +- .../collision-properties-evil-first-arm.json | 2 +- ...ision-roledefinitionid-evil-first-arm.json | 2 +- ...llision-root-resources-evil-first-arm.json | 4 ++-- .../collision-type-evil-first-arm.json | 2 +- .../role-parity/dataactions-wildcard-arm.json | 2 +- .../role-parity/decorative-variables-arm.json | 2 +- .../deploymentscript-scope-escape-arm.json | 2 +- .../deploymentstack-scope-escape-arm.json | 2 +- ...bscription-literal-with-canonical-arm.json | 2 +- ...ild-keyvault-roleassignment-owner-arm.json | 2 +- ...legacy-child-roleassignment-owner-arm.json | 2 +- .../lowercase-tenant-scope-arm.json | 2 +- .../lowercase-type-wildcard-actions-arm.json | 2 +- .../testdata/role-parity/matching-arm.json | 2 +- .../role-parity/matching-tf.tf.fixture | 2 +- .../role-parity/mgmt-group-schema-arm.json | 2 +- .../miscased-assignablescopes-arm.json | 4 ++-- .../role-parity/miscased-properties-arm.json | 2 +- .../miscased-roledefinitionid-arm.json | 2 +- .../role-parity/miscased-scope-arm.json | 2 +- .../role-parity/nested-deployment-arm.json | 2 +- .../obfuscated-tenant-scope-arm.json | 2 +- .../role-parity/other-subscription-arm.json | 2 +- ...pim-roleassignment-schedule-owner-arm.json | 2 +- .../pim-roleeligibility-owner-arm.json | 2 +- .../second-permissions-entry-arm.json | 2 +- .../role-parity/space-inside-literal-arm.json | 2 +- .../role-parity/tenant-scope-arm.json | 2 +- .../unallowed-roledefinitionid-arm.json | 2 +- ...ppercase-allowed-roledefinitionid-arm.json | 2 +- .../uppercase-canonical-scope-arm.json | 2 +- .../uppercase-guid-literal-arm.json | 2 +- .../compute/azure/container-apps/main.tf | 3 ++- .../iam/azure/cudly-reservation-role/main.tf | 6 +++--- 40 files changed, 61 insertions(+), 52 deletions(-) diff --git a/.github/workflows/deploy-azure.yml b/.github/workflows/deploy-azure.yml index f541f174f..387e4b7c3 100644 --- a/.github/workflows/deploy-azure.yml +++ b/.github/workflows/deploy-azure.yml @@ -237,9 +237,12 @@ jobs: # The provider fails the data read outright when the custom role is absent, # so a Terraform precondition can never run for this case. Issue #1794: the - # Azure deploy failed on every main run for three weeks because the raw - # provider error names neither the missing prerequisite nor the stack that - # creates it, and the explanation lived only in a source comment. + # Azure deploy failed on every main run since 2026-07-19 because the + # bootstrap stack's role definition carried an action that does not exist + # in Azure's operation catalog, so Azure rejected the whole role definition + # with InvalidActionOrNotAction and the role was never created. That action + # is removed in this PR. The message below still covers the general "role + # missing" case for any future recurrence. - name: Explain a missing bootstrap role if: failure() run: | @@ -276,11 +279,16 @@ jobs: pipeline holds roleAssignments/write only. If the bootstrap HAS been applied, check in this order: - 1. the deploy SP has Microsoft.Authorization/roleDefinitions/read. + 1. the bootstrap stack's own apply log for InvalidActionOrNotAction. + That means the role definition itself was rejected by Azure and + never created, not merely unassigned (issue #1794's root cause: + a nonexistent action in the role's actions list). Make sure the + bootstrap module is on a commit that includes that fix. + 2. the deploy SP has Microsoft.Authorization/roleDefinitions/read. Without it the lookup returns empty, which is indistinguishable from the role being absent. - 2. the role was not renamed or deleted out of band. - 3. the name suffix still matches. The runtime module looks up + 3. the role was not renamed or deleted out of band. + 4. the name suffix still matches. The runtime module looks up "CUDly Reservation Purchaser (custom) - "; the bootstrap builds the name from its own var.name_suffix. EOT diff --git a/arm/CUDly-CrossSubscription/template.json b/arm/CUDly-CrossSubscription/template.json index 8cdbdb03d..7a7a0c33e 100644 --- a/arm/CUDly-CrossSubscription/template.json +++ b/arm/CUDly-CrossSubscription/template.json @@ -39,7 +39,7 @@ "name": "[variables('customRoleName')]", "properties": { "roleName": "CUDly Reservation Purchaser (custom)", - "description": "Custom role granting CUDly exactly the Microsoft.Capacity and Microsoft.BillingBenefits actions required by the calculatePrice -> purchase flow. Replaces the built-in Reservation Purchaser, which lacks reservationOrders/purchase/action.", + "description": "Custom role granting CUDly exactly the Microsoft.Capacity and Microsoft.BillingBenefits actions required by the calculatePrice -> purchase flow. Replaces the built-in Reservation Purchaser, which lacks reservationOrders/write, calculatePrice/action and every Microsoft.BillingBenefits action.", "type": "CustomRole", "permissions": [ { @@ -49,7 +49,6 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/register/action", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", @@ -79,7 +78,7 @@ "roleDefinitionId": "[variables('customRoleDefinitionId')]", "principalId": "[parameters('servicePrincipalObjectId')]", "principalType": "ServicePrincipal", - "description": "CUDly — subscription-scope assignment of custom role; grants calculatePrice + purchase/action required by the two-step reservation purchase flow" + "description": "CUDly — subscription-scope assignment of custom role; grants calculatePrice/action and reservationOrders/write required by the two-step reservation purchase flow" } }, diff --git a/iac/federation/azure-target/terraform/main.tf b/iac/federation/azure-target/terraform/main.tf index 8d8c0b7e3..4dd428c4a 100644 --- a/iac/federation/azure-target/terraform/main.tf +++ b/iac/federation/azure-target/terraform/main.tf @@ -60,8 +60,9 @@ resource "azuread_application_federated_identity_credential" "cudly" { # Custom role: grants the exact Microsoft.Capacity and Microsoft.BillingBenefits actions # used by the calculatePrice -> purchase flow (introduced in PR #680). The built-in -# Reservation Purchaser role lacks reservationOrders/purchase/action, which causes 403 on -# production reservation purchases. +# Reservation Purchaser role lacks reservationOrders/write and calculatePrice/action, plus +# every Microsoft.BillingBenefits action, which is what caused 403s on production +# reservation purchases. # # The role definition is factored into a shared module so the customer-side (here) and # host-side (terraform/modules/compute/azure/container-apps) definitions stay in lockstep. diff --git a/scripts/testdata/role-parity/canonical-scope-variants-arm.json b/scripts/testdata/role-parity/canonical-scope-variants-arm.json index a79f213c2..4e08264ca 100644 --- a/scripts/testdata/role-parity/canonical-scope-variants-arm.json +++ b/scripts/testdata/role-parity/canonical-scope-variants-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/collision-assignablescopes-benign-first-arm.json b/scripts/testdata/role-parity/collision-assignablescopes-benign-first-arm.json index a4793ccc8..1e333e24d 100644 --- a/scripts/testdata/role-parity/collision-assignablescopes-benign-first-arm.json +++ b/scripts/testdata/role-parity/collision-assignablescopes-benign-first-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/collision-assignablescopes-evil-first-arm.json b/scripts/testdata/role-parity/collision-assignablescopes-evil-first-arm.json index 06c03d9ca..fa2d62bcf 100644 --- a/scripts/testdata/role-parity/collision-assignablescopes-evil-first-arm.json +++ b/scripts/testdata/role-parity/collision-assignablescopes-evil-first-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/collision-properties-evil-first-arm.json b/scripts/testdata/role-parity/collision-properties-evil-first-arm.json index ea1fac29b..42dad6db5 100644 --- a/scripts/testdata/role-parity/collision-properties-evil-first-arm.json +++ b/scripts/testdata/role-parity/collision-properties-evil-first-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/collision-roledefinitionid-evil-first-arm.json b/scripts/testdata/role-parity/collision-roledefinitionid-evil-first-arm.json index e617dba27..311da5abc 100644 --- a/scripts/testdata/role-parity/collision-roledefinitionid-evil-first-arm.json +++ b/scripts/testdata/role-parity/collision-roledefinitionid-evil-first-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/collision-root-resources-evil-first-arm.json b/scripts/testdata/role-parity/collision-root-resources-evil-first-arm.json index 28d5bdff4..7caab38a4 100644 --- a/scripts/testdata/role-parity/collision-root-resources-evil-first-arm.json +++ b/scripts/testdata/role-parity/collision-root-resources-evil-first-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", @@ -58,7 +58,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/collision-type-evil-first-arm.json b/scripts/testdata/role-parity/collision-type-evil-first-arm.json index 76b630cfb..476c138f3 100644 --- a/scripts/testdata/role-parity/collision-type-evil-first-arm.json +++ b/scripts/testdata/role-parity/collision-type-evil-first-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/dataactions-wildcard-arm.json b/scripts/testdata/role-parity/dataactions-wildcard-arm.json index 29af37e51..8e2bb2169 100644 --- a/scripts/testdata/role-parity/dataactions-wildcard-arm.json +++ b/scripts/testdata/role-parity/dataactions-wildcard-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/decorative-variables-arm.json b/scripts/testdata/role-parity/decorative-variables-arm.json index bfced043b..dcf94c453 100644 --- a/scripts/testdata/role-parity/decorative-variables-arm.json +++ b/scripts/testdata/role-parity/decorative-variables-arm.json @@ -33,7 +33,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/deploymentscript-scope-escape-arm.json b/scripts/testdata/role-parity/deploymentscript-scope-escape-arm.json index 6c474de5b..acef6a34b 100644 --- a/scripts/testdata/role-parity/deploymentscript-scope-escape-arm.json +++ b/scripts/testdata/role-parity/deploymentscript-scope-escape-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/deploymentstack-scope-escape-arm.json b/scripts/testdata/role-parity/deploymentstack-scope-escape-arm.json index 738a409a5..99761ca7e 100644 --- a/scripts/testdata/role-parity/deploymentstack-scope-escape-arm.json +++ b/scripts/testdata/role-parity/deploymentstack-scope-escape-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/foreign-subscription-literal-with-canonical-arm.json b/scripts/testdata/role-parity/foreign-subscription-literal-with-canonical-arm.json index 03730c27d..842c74f15 100644 --- a/scripts/testdata/role-parity/foreign-subscription-literal-with-canonical-arm.json +++ b/scripts/testdata/role-parity/foreign-subscription-literal-with-canonical-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/legacy-child-keyvault-roleassignment-owner-arm.json b/scripts/testdata/role-parity/legacy-child-keyvault-roleassignment-owner-arm.json index 5065983d9..f2b59630a 100644 --- a/scripts/testdata/role-parity/legacy-child-keyvault-roleassignment-owner-arm.json +++ b/scripts/testdata/role-parity/legacy-child-keyvault-roleassignment-owner-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/legacy-child-roleassignment-owner-arm.json b/scripts/testdata/role-parity/legacy-child-roleassignment-owner-arm.json index 2bce079e1..158c13a2a 100644 --- a/scripts/testdata/role-parity/legacy-child-roleassignment-owner-arm.json +++ b/scripts/testdata/role-parity/legacy-child-roleassignment-owner-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/lowercase-tenant-scope-arm.json b/scripts/testdata/role-parity/lowercase-tenant-scope-arm.json index 34884bdbd..7f4d2f5b0 100644 --- a/scripts/testdata/role-parity/lowercase-tenant-scope-arm.json +++ b/scripts/testdata/role-parity/lowercase-tenant-scope-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/lowercase-type-wildcard-actions-arm.json b/scripts/testdata/role-parity/lowercase-type-wildcard-actions-arm.json index dc24c3b00..7e66df61b 100644 --- a/scripts/testdata/role-parity/lowercase-type-wildcard-actions-arm.json +++ b/scripts/testdata/role-parity/lowercase-type-wildcard-actions-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/matching-arm.json b/scripts/testdata/role-parity/matching-arm.json index 516dc12cd..cfe1173c0 100644 --- a/scripts/testdata/role-parity/matching-arm.json +++ b/scripts/testdata/role-parity/matching-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/matching-tf.tf.fixture b/scripts/testdata/role-parity/matching-tf.tf.fixture index 2a51ab4d9..39d7f81a2 100644 --- a/scripts/testdata/role-parity/matching-tf.tf.fixture +++ b/scripts/testdata/role-parity/matching-tf.tf.fixture @@ -11,7 +11,7 @@ resource "azurerm_role_definition" "cudly_reservation_purchaser" { "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/mgmt-group-schema-arm.json b/scripts/testdata/role-parity/mgmt-group-schema-arm.json index c91eb6d8d..64365cc54 100644 --- a/scripts/testdata/role-parity/mgmt-group-schema-arm.json +++ b/scripts/testdata/role-parity/mgmt-group-schema-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/miscased-assignablescopes-arm.json b/scripts/testdata/role-parity/miscased-assignablescopes-arm.json index 0598ec9a1..4a4a440d1 100644 --- a/scripts/testdata/role-parity/miscased-assignablescopes-arm.json +++ b/scripts/testdata/role-parity/miscased-assignablescopes-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", @@ -46,7 +46,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/miscased-properties-arm.json b/scripts/testdata/role-parity/miscased-properties-arm.json index 22dd483c0..d8989aae1 100644 --- a/scripts/testdata/role-parity/miscased-properties-arm.json +++ b/scripts/testdata/role-parity/miscased-properties-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/miscased-roledefinitionid-arm.json b/scripts/testdata/role-parity/miscased-roledefinitionid-arm.json index 6796472df..d1701e7df 100644 --- a/scripts/testdata/role-parity/miscased-roledefinitionid-arm.json +++ b/scripts/testdata/role-parity/miscased-roledefinitionid-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/miscased-scope-arm.json b/scripts/testdata/role-parity/miscased-scope-arm.json index 819979f7f..af0a959f7 100644 --- a/scripts/testdata/role-parity/miscased-scope-arm.json +++ b/scripts/testdata/role-parity/miscased-scope-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/nested-deployment-arm.json b/scripts/testdata/role-parity/nested-deployment-arm.json index 4fd6f4ae5..112be4f3c 100644 --- a/scripts/testdata/role-parity/nested-deployment-arm.json +++ b/scripts/testdata/role-parity/nested-deployment-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/obfuscated-tenant-scope-arm.json b/scripts/testdata/role-parity/obfuscated-tenant-scope-arm.json index 96c952326..c0ca9d492 100644 --- a/scripts/testdata/role-parity/obfuscated-tenant-scope-arm.json +++ b/scripts/testdata/role-parity/obfuscated-tenant-scope-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/other-subscription-arm.json b/scripts/testdata/role-parity/other-subscription-arm.json index 96d5c9ece..0862d5615 100644 --- a/scripts/testdata/role-parity/other-subscription-arm.json +++ b/scripts/testdata/role-parity/other-subscription-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/pim-roleassignment-schedule-owner-arm.json b/scripts/testdata/role-parity/pim-roleassignment-schedule-owner-arm.json index c23c90b9a..245fb60ee 100644 --- a/scripts/testdata/role-parity/pim-roleassignment-schedule-owner-arm.json +++ b/scripts/testdata/role-parity/pim-roleassignment-schedule-owner-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/pim-roleeligibility-owner-arm.json b/scripts/testdata/role-parity/pim-roleeligibility-owner-arm.json index 0893d8ee2..42c25eb38 100644 --- a/scripts/testdata/role-parity/pim-roleeligibility-owner-arm.json +++ b/scripts/testdata/role-parity/pim-roleeligibility-owner-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/second-permissions-entry-arm.json b/scripts/testdata/role-parity/second-permissions-entry-arm.json index 9891bf4ba..41dbbb979 100644 --- a/scripts/testdata/role-parity/second-permissions-entry-arm.json +++ b/scripts/testdata/role-parity/second-permissions-entry-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/space-inside-literal-arm.json b/scripts/testdata/role-parity/space-inside-literal-arm.json index d56de7492..938da0a1d 100644 --- a/scripts/testdata/role-parity/space-inside-literal-arm.json +++ b/scripts/testdata/role-parity/space-inside-literal-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/tenant-scope-arm.json b/scripts/testdata/role-parity/tenant-scope-arm.json index aabccaf64..416664fc1 100644 --- a/scripts/testdata/role-parity/tenant-scope-arm.json +++ b/scripts/testdata/role-parity/tenant-scope-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/unallowed-roledefinitionid-arm.json b/scripts/testdata/role-parity/unallowed-roledefinitionid-arm.json index 9189f0ae7..4e2314553 100644 --- a/scripts/testdata/role-parity/unallowed-roledefinitionid-arm.json +++ b/scripts/testdata/role-parity/unallowed-roledefinitionid-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/uppercase-allowed-roledefinitionid-arm.json b/scripts/testdata/role-parity/uppercase-allowed-roledefinitionid-arm.json index 2fdcdfed0..072b4eba1 100644 --- a/scripts/testdata/role-parity/uppercase-allowed-roledefinitionid-arm.json +++ b/scripts/testdata/role-parity/uppercase-allowed-roledefinitionid-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/uppercase-canonical-scope-arm.json b/scripts/testdata/role-parity/uppercase-canonical-scope-arm.json index 7e0acee8f..b2d122506 100644 --- a/scripts/testdata/role-parity/uppercase-canonical-scope-arm.json +++ b/scripts/testdata/role-parity/uppercase-canonical-scope-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/scripts/testdata/role-parity/uppercase-guid-literal-arm.json b/scripts/testdata/role-parity/uppercase-guid-literal-arm.json index 0ae919a83..c5d3858d1 100644 --- a/scripts/testdata/role-parity/uppercase-guid-literal-arm.json +++ b/scripts/testdata/role-parity/uppercase-guid-literal-arm.json @@ -16,7 +16,7 @@ "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", + "Microsoft.BillingBenefits/register/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", "Microsoft.BillingBenefits/savingsPlanOrders/read", diff --git a/terraform/modules/compute/azure/container-apps/main.tf b/terraform/modules/compute/azure/container-apps/main.tf index 433d49349..04cfcc69b 100644 --- a/terraform/modules/compute/azure/container-apps/main.tf +++ b/terraform/modules/compute/azure/container-apps/main.tf @@ -261,7 +261,8 @@ resource "azurerm_role_assignment" "subscription_reader" { # Custom reservation-purchaser role: same role as customer-side but scoped to # the host subscription. The built-in Reservation Purchaser lacks -# reservationOrders/purchase/action (same gap fixed customer-side by PR #744). +# reservationOrders/write and calculatePrice/action, plus every +# Microsoft.BillingBenefits action (same gap fixed customer-side by PR #744). # # BOOTSTRAP-vs-RUNTIME SPLIT: the role *definition* is created by the # human-applied bootstrap module diff --git a/terraform/modules/iam/azure/cudly-reservation-role/main.tf b/terraform/modules/iam/azure/cudly-reservation-role/main.tf index e9acc56f0..ef4407a14 100644 --- a/terraform/modules/iam/azure/cudly-reservation-role/main.tf +++ b/terraform/modules/iam/azure/cudly-reservation-role/main.tf @@ -15,7 +15,8 @@ terraform { # Custom role: grants the exact Microsoft.Capacity and Microsoft.BillingBenefits # actions used by the calculatePrice -> purchase flow. The built-in Reservation -# Purchaser role lacks reservationOrders/purchase/action, which causes 403 on +# Purchaser role lacks reservationOrders/write and calculatePrice/action, plus +# every Microsoft.BillingBenefits action, which is what caused 403s on # production reservation purchases. # # Used by both: @@ -35,7 +36,7 @@ locals { resource "azurerm_role_definition" "cudly_reservation_purchaser" { name = local.role_definition_name scope = var.scope - description = "Custom role granting CUDly exactly the Microsoft.Capacity and Microsoft.BillingBenefits actions required by the calculatePrice -> purchase flow. Replaces the built-in Reservation Purchaser, which lacks reservationOrders/purchase/action." + description = "Custom role granting CUDly exactly the Microsoft.Capacity and Microsoft.BillingBenefits actions required by the calculatePrice -> purchase flow. Replaces the built-in Reservation Purchaser, which lacks reservationOrders/write, calculatePrice/action and every Microsoft.BillingBenefits action." permissions { actions = [ @@ -44,7 +45,6 @@ resource "azurerm_role_definition" "cudly_reservation_purchaser" { "Microsoft.Capacity/catalogs/read", "Microsoft.Capacity/reservationOrders/read", "Microsoft.Capacity/reservationOrders/write", - "Microsoft.Capacity/reservationOrders/purchase/action", "Microsoft.Capacity/reservationOrders/reservations/read", "Microsoft.BillingBenefits/register/action", "Microsoft.BillingBenefits/savingsPlanOrderAliases/write",