From af0068a041bb64a42477735f1a6ac9ca27aa68b2 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Tue, 26 May 2026 03:03:39 +0200 Subject: [PATCH 1/2] fix(azure-arm): add custom role with purchase and savings plan actions The built-in Reservation Purchaser role (f7b75c60) is missing Microsoft.Capacity/calculateprice/action, reservationorders/write, and the Microsoft.BillingBenefits actions needed for savings plans, which caused 403 on live purchases (issue #731). Add a custom role definition "CUDly Reservation and Savings Plan Purchaser" with the exact actions observed in the SDK call sites: Microsoft.Capacity: calculateprice/action, reservationorders/write, reservationorders/read, reservationorders/reservations/read, register/action, catalogs/read Microsoft.BillingBenefits: savingsPlanOrderAliases/write, savingsPlanOrders/read, savingsPlanOrders/savingsPlans/read, savingsPlanOrders/action The built-in Reservation Purchaser and Reader assignments are kept unchanged. The custom role uses a deterministic GUID derived from the subscription ID so re-deployments are idempotent. Validated: az deployment sub validate exits cleanly. Document re-deployment requirement in known-issues.md. --- arm/CUDly-CrossSubscription/template.json | 64 +++++++++++++++++++++-- known-issues.md | 41 ++++++++++++--- 2 files changed, 95 insertions(+), 10 deletions(-) diff --git a/arm/CUDly-CrossSubscription/template.json b/arm/CUDly-CrossSubscription/template.json index 7607366c1..3ea48fbe9 100644 --- a/arm/CUDly-CrossSubscription/template.json +++ b/arm/CUDly-CrossSubscription/template.json @@ -3,7 +3,7 @@ "contentVersion": "1.0.0.0", "metadata": { - "description": "CUDly Cross-Subscription Role Assignments — deploy this in every target Azure subscription that CUDly should manage. It grants the CUDly service principal the permissions needed to query reservation recommendations and purchase Azure Reservations." + "description": "CUDly Cross-Subscription Role Assignments — deploy this in every target Azure subscription that CUDly should manage. It grants the CUDly service principal the permissions needed to query reservation recommendations and purchase Azure Reservations and Savings Plans." }, "parameters": { @@ -27,10 +27,58 @@ "reader": "/providers/Microsoft.Authorization/roleDefinitions/acdd72a7-3385-48ef-bd42-f606fba81ae7", "costManagementReader": "/providers/Microsoft.Authorization/roleDefinitions/72fafb9e-0641-4937-9268-a91bfd8191a3", "reservationPurchaser": "/providers/Microsoft.Authorization/roleDefinitions/f7b75c60-3036-4b75-91c3-6b41c27c1689" - } + }, + "customRoleName": "[guid(subscription().subscriptionId, 'cudly-reservation-purchaser')]", + "customRoleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', guid(subscription().subscriptionId, 'cudly-reservation-purchaser'))]" }, "resources": [ + { + "type": "Microsoft.Authorization/roleDefinitions", + "apiVersion": "2022-04-01", + "name": "[variables('customRoleName')]", + "properties": { + "roleName": "CUDly Reservation and Savings Plan Purchaser", + "description": "Custom role granting CUDly exactly the Microsoft.Capacity and Microsoft.BillingBenefits actions it calls at runtime. Complements the built-in Reservation Purchaser assignment (which covers catalog reads and recommendations) by adding the purchase and calculatePrice actions that the built-in role omits.", + "type": "CustomRole", + "permissions": [ + { + "actions": [ + "Microsoft.Capacity/calculateprice/action", + "Microsoft.Capacity/reservationorders/write", + "Microsoft.Capacity/reservationorders/read", + "Microsoft.Capacity/reservationorders/reservations/read", + "Microsoft.Capacity/register/action", + "Microsoft.Capacity/catalogs/read", + "Microsoft.BillingBenefits/savingsPlanOrderAliases/write", + "Microsoft.BillingBenefits/savingsPlanOrders/read", + "Microsoft.BillingBenefits/savingsPlanOrders/savingsPlans/read", + "Microsoft.BillingBenefits/savingsPlanOrders/action" + ], + "notActions": [] + } + ], + "assignableScopes": [ + "[subscriptionResourceId('Microsoft.Resources/subscriptions', subscription().subscriptionId)]" + ] + } + }, + + { + "type": "Microsoft.Authorization/roleAssignments", + "apiVersion": "2022-04-01", + "name": "[guid(parameters('servicePrincipalObjectId'), 'cudlyCustomRole', subscription().subscriptionId)]", + "dependsOn": [ + "[variables('customRoleDefinitionId')]" + ], + "properties": { + "roleDefinitionId": "[variables('customRoleDefinitionId')]", + "principalId": "[parameters('servicePrincipalObjectId')]", + "principalType": "ServicePrincipal", + "description": "CUDly — purchase reservations and savings plans via custom role that enumerates every required action explicitly" + } + }, + { "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", @@ -42,6 +90,7 @@ "description": "CUDly — enumerate subscription resources and locations" } }, + { "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", @@ -53,6 +102,7 @@ "description": "CUDly — read cost and reservation utilisation data" } }, + { "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", @@ -61,9 +111,10 @@ "roleDefinitionId": "[variables('roles').reservationPurchaser]", "principalId": "[parameters('servicePrincipalObjectId')]", "principalType": "ServicePrincipal", - "description": "CUDly — purchase Azure Reservations in this subscription" + "description": "CUDly — reservation catalog reads and recommendations via built-in Reservation Purchaser role (kept in addition to the custom role)" } }, + { "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", @@ -92,6 +143,13 @@ "metadata": { "description": "Azure AD tenant ID to provide when registering this account in CUDly (azure_tenant_id field)." } + }, + "customRoleDefinitionId": { + "type": "string", + "value": "[variables('customRoleDefinitionId')]", + "metadata": { + "description": "Resource ID of the CUDly custom role definition created in this subscription." + } } } } diff --git a/known-issues.md b/known-issues.md index 8d2575807..4b7829b8d 100644 --- a/known-issues.md +++ b/known-issues.md @@ -1,8 +1,33 @@ # Known Issues -The seven limitations previously listed here have been resolved or -scoped-down with explicit follow-ups. This file tracks what's still -outstanding so future work has a clear starting point. +This file tracks outstanding limitations and things that require operator +action. Resolved items are moved to the Resolved section at the bottom. + +## Outstanding + +### Azure ARM template re-deployment required for purchase support (issue #731) + +The built-in "Reservation Purchaser" role (f7b75c60-3036-4b75-91c3-6b41c27c1689) +does not include `Microsoft.Capacity/calculateprice/action`, +`Microsoft.Capacity/reservationorders/write`, or +`Microsoft.BillingBenefits/savingsPlanOrderAliases/write`. Without these, +the live purchase API returns 403. + +`arm/CUDly-CrossSubscription/template.json` has been updated (fix/731-arm-roles) +to add a custom role "CUDly Reservation and Savings Plan Purchaser" that enumerates +all required actions explicitly. Existing tenants who applied the ARM template before +this fix MUST re-deploy it: + +```bash +az deployment sub create \ + --location eastus \ + --template-file arm/CUDly-CrossSubscription/template.json \ + --parameters servicePrincipalObjectId= \ + --name CUDly-CrossSubscription +``` + +Until re-deployed, `PurchaseCommitment` and `ValidateOffering` for savings plans +will continue to return 403. ## Resolved @@ -184,10 +209,12 @@ outstanding so future work has a clear starting point. - **GCP account `serene-bazaar-666` deploy SA missing `compute.regions.list`**: Visible in production Lambda logs (`2026-04-21T16:28:22Z` and onward): - [ERROR] GCP account GCP serene-bazaar-666 (serene-bazaar-666): - get recommendations: failed to get regions: failed to list regions: - googleapi: Error 403: Required 'compute.regions.list' permission - for 'projects/serene-bazaar-666' + ```text + [ERROR] GCP account GCP serene-bazaar-666 (serene-bazaar-666): + get recommendations: failed to get regions: failed to list regions: + googleapi: Error 403: Required 'compute.regions.list' permission + for 'projects/serene-bazaar-666' + ``` The deploy service account that CUDly impersonates for that project doesn't have `roles/compute.viewer` (or a custom role that includes From cca72f7a876b1d2242a248d49a63cb1cf85fcb83 Mon Sep 17 00:00:00 2001 From: Cristian Magherusan-Stanciu Date: Tue, 26 May 2026 03:14:19 +0200 Subject: [PATCH 2/2] fix(azure-arm): use subscription().id in assignableScopes; add --no-prompt to known-issues example - assignableScopes was using subscriptionResourceId('Microsoft.Resources/subscriptions', subscription().subscriptionId) which expands to the provider-scoped path /subscriptions/{id}/providers/Microsoft.Resources/subscriptions/{id} -- not the canonical subscription scope /subscriptions/{id} that ARM RBAC expects. Switch to subscription().id which returns exactly the right value. ARM validate returns error:null after the fix. - known-issues.md redeploy command now includes --no-prompt, matching setup.sh line 122 and preventing interactive hangs in automated/scripted re-deployments. Addresses CR findings on PR #732 (review 4359641918). --- arm/CUDly-CrossSubscription/template.json | 2 +- known-issues.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/arm/CUDly-CrossSubscription/template.json b/arm/CUDly-CrossSubscription/template.json index 3ea48fbe9..44f2f0d91 100644 --- a/arm/CUDly-CrossSubscription/template.json +++ b/arm/CUDly-CrossSubscription/template.json @@ -59,7 +59,7 @@ } ], "assignableScopes": [ - "[subscriptionResourceId('Microsoft.Resources/subscriptions', subscription().subscriptionId)]" + "[subscription().id]" ] } }, diff --git a/known-issues.md b/known-issues.md index 4b7829b8d..df36aac8d 100644 --- a/known-issues.md +++ b/known-issues.md @@ -23,7 +23,8 @@ az deployment sub create \ --location eastus \ --template-file arm/CUDly-CrossSubscription/template.json \ --parameters servicePrincipalObjectId= \ - --name CUDly-CrossSubscription + --name CUDly-CrossSubscription \ + --no-prompt ``` Until re-deployed, `PurchaseCommitment` and `ValidateOffering` for savings plans