Skip to content

fix(hubs): assign requested storage roles to apps that don't create storage (#2253) - #2262

Open
Roland Krummenacher (RolandKrummenacher) wants to merge 1 commit into
devfrom
RolandKrummenacher/fix-managed-exports-storage-roles
Open

fix(hubs): assign requested storage roles to apps that don't create storage (#2253)#2262
Roland Krummenacher (RolandKrummenacher) wants to merge 1 commit into
devfrom
RolandKrummenacher/fix-managed-exports-storage-roles

Conversation

@RolandKrummenacher

Copy link
Copy Markdown
Collaborator

🛠️ Description

Fixes #2253.

Managed exports never created any Cost Management exports. config_ConfigureExports reached the export creation activity and failed with:

{"error":{"code":"Unauthorized","message":"The user does not have authorization to perform
'Microsoft.Authorization/roleAssignments/write' action on specified storage account, ..."}}

Cost Management creates exports with a system-assigned identity and must grant that identity access to the destination, so the caller needs to write role assignments on the destination storage account. The Managed Exports app requests Role Based Access Control Administrator for the Data Factory identity via the storageRoles parameter of fx/hub-app.bicep, but the assignment was never created.

Root cause

The storage role assignment loop was gated on the Storage feature:

var usesStorage = contains(features, 'Storage')

resource storageRoleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [
  for role in factoryStorageRoles: if (usesDataFactory && usesStorage) { ... }
]

Apps only declare Storage when they create the publisher storage account. Managed Exports uses the account created by Microsoft.CostManagement.Exports, so usesStorage is false and every role it passed was silently dropped at build time. It is the only app that passes storageRoles, so it was the only one affected.

This regressed in the app modularization (#1800): in v12 the same role was coupled directly to enableManagedExports and assigned with no feature condition.

Fix

Roles requested via storageRoles are assigned whenever the app uses Data Factory, since the storage account is shared across all apps from the same publisher. The base data management roles (Storage Account Contributor, Storage Blob Data Contributor, Reader) stay tied to the Storage feature, so nothing changes for any other app.

var factoryStorageRoles = union(storageRoles, !usesStorage ? [] : [ /* base data management roles */ ])

resource storageRoleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [
  for role in factoryStorageRoles: if (usesDataFactory) { ... }
]
Why not declare the Storage feature on Managed Exports (the fix suggested in the issue)?

usesStorage doesn't only gate role assignments — it also deploys the storage account, both private endpoints, the private DNS zone groups, the ADF managed private endpoint, the storage linked service, and the Get/ApproveStoragePrivateEndpointConnections nested deployments. Adding the feature to Managed Exports would duplicate all of that against a storage account another app already owns, and under private routing Microsoft.CostManagement.ManagedExports has no dependsOn sequencing against Microsoft.FinOpsHubs.Analytics, so the two would race on identically named private endpoint resources and nested deployments.

Fixing the gate instead keeps the blast radius to the one missing role assignment and also fixes the class of bug for any future app that requests storage roles.

Docs

Item 1 from the issue: the role table and the enableManagedExports tooltip/description still named User Access Administrator, which hasn't appeared anywhere in the template since v13 replaced it with RBAC Administrator. Corrected in template.md, main.bicep, hub.bicep, and createUiDefinition.json.

Item 2 from the issue (Save Scopes failing when the settings file has exactly one scope) is not addressed here — it is non-blocking, the recovery activity already handles it, and the real fix belongs in the PowerShell that serializes settings.json. Happy to file it separately.

🧪 Testing

  • bicep build src/templates/finops-hub/main.bicep — clean (only pre-existing no-unused-params warnings in Recommendations/app.bicep).
  • Verified against the compiled ARM template that Microsoft.CostManagement.ManagedExports_Register now emits a role assignment for f58310d9-a9f6-439a-9e8d-f62e7b41a168 on the storage account, and that factoryStorageRoles is unchanged for every other app.
  • New src/powershell/Tests/Unit/HubsAppStorageRoles.Tests.ps1 regression suite (6 tests): the assignment loop isn't gated on usesStorage, storageRoles always flows into it, every app requesting storage roles declares DataFactory, and Managed Exports still requests RBAC Administrator. Confirmed the suite fails against the pre-fix condition.
  • Full Tests/Unit (2379 passed, 4 skipped) and Tests/Lint (3587 passed) suites pass.

Not yet validated with a live hub deployment — worth a deploy test with enableManagedExports = true before merge.

📋 Checklist

Reviewer tasks

  • Reviewed the fix approach (gate change vs. declaring the Storage feature)
  • Verified the role assignment appears on a test deployment

Contributor tasks

  • Updated the changelog
  • Updated docs
  • Added tests

🤖 Generated with Claude Code

…torage

Managed exports never created any Cost Management exports. Every export
creation failed with an Unauthorized error because Cost Management needs the
caller to write role assignments on the destination storage account (exports
are created with a system-assigned identity that must be granted access to the
destination).

The Managed Exports app requests the Role Based Access Control Administrator
role via the storageRoles parameter of fx/hub-app.bicep, but the role
assignment loop was gated on the "Storage" feature. Apps only declare that
feature when they create the publisher storage account, and Managed Exports
uses the account created by Microsoft.CostManagement.Exports, so every role it
requested was silently dropped at build time.

Roles requested via storageRoles are now assigned whenever the app uses Data
Factory, since the storage account is shared across all apps from the same
publisher. The base data management roles (Storage Account Contributor,
Storage Blob Data Contributor, Reader) stay tied to the "Storage" feature, so
no other app changes.

Also corrects the user-facing role name, which has referred to the removed User
Access Administrator role since it was replaced by RBAC Administrator in v13.

Fixes #2253

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a FinOps hubs deployment regression where Managed Exports requested a storage RBAC role (RBAC Administrator) but the hub-app module never created the corresponding role assignment, causing Cost Management export creation to fail with Unauthorized. It updates the hub app role-assignment gating so requested storageRoles are applied whenever the app uses Data Factory (even if it doesn’t create the publisher storage account), and aligns user-facing documentation/tooling text to the correct role name.

Changes:

  • Update fx/hub-app.bicep so roles requested via storageRoles are included in the storage role assignment loop whenever usesDataFactory is true, while keeping the base data-management roles tied to the Storage feature.
  • Correct “User Access Administrator” wording to “Role Based Access Control Administrator” across the hub template parameter descriptions and portal UI tooltip.
  • Add a Pester regression test suite to validate the role-assignment gating and the Managed Exports role request.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated no comments.

Show a summary per file
File Description
src/templates/finops-hub/modules/hub.bicep Updates the managed exports parameter description to reference RBAC Administrator (not User Access Administrator).
src/templates/finops-hub/modules/fx/hub-app.bicep Fixes storage role assignment gating so storageRoles flow into assignments when using Data Factory, even without the Storage feature.
src/templates/finops-hub/main.bicep Updates the managed exports parameter description to reference RBAC Administrator.
src/templates/finops-hub/createUiDefinition.json Updates the portal UI tooltip text for managed exports to reference RBAC Administrator and clarify the requirement.
src/powershell/Tests/Unit/HubsAppStorageRoles.Tests.ps1 Adds regression tests to ensure storageRoles aren’t gated on Storage and that Managed Exports requests RBAC Administrator.
docs-mslearn/toolkit/hubs/template.md Updates docs role table + parameter docs to reference RBAC Administrator and refreshes ms.date.
docs-mslearn/toolkit/changelog.md Adds a changelog entry for the fix and refreshes ms.date.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs: Review 👀 PR that is ready to be reviewed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Hubs] Managed exports fail with Unauthorized — RBAC Administrator role is never assigned (regression since v13)

4 participants