Skip to content

[Hubs] Managed exports fail with Unauthorized β€” RBAC Administrator role is never assigned (regression since v13)Β #2253

Description

πŸ› Problem

Managed exports never create any Cost Management exports. The config_ConfigureExports pipeline runs, reaches the export creation activity, and fails with:

{"error":{"code":"Unauthorized","message":"The user does not have authorization to perform
'Microsoft.Authorization/roleAssignments/write' action on specified storage account, please use a
storage account with sufficient permissions. If the permissions have changed recently then retry
after some time."}}

Cost Management needs the caller to be able to write role assignments on the destination storage account, because the export is created with a system-assigned identity ("identity": { "type": "systemAssigned" }) for which it must grant access to the destination.

The template is aware of this and passes the Role Based Access Control Administrator role for the Data Factory identity β€” but the assignment is never actually created, so the Data Factory identity cannot create exports.

πŸ” Root cause

The Managed Exports app registers itself with only the DataFactory feature while passing storageRoles:

// src/templates/finops-hub/modules/Microsoft.CostManagement/ManagedExports/app.bicep#L69-L82
module appRegistration '../../fx/hub-app.bicep' = {
  name: 'Microsoft.CostManagement.ManagedExports_Register'
  params: {
    features: [
      'DataFactory'          // <-- 'Storage' is not declared
    ]
    storageRoles: [
      // RBAC Administrator -- used to create Cost Management exports (which require access to grant access)
      'f58310d9-a9f6-439a-9e8d-f62e7b41a168'
    ]
  }
}

hub-app.bicep gates all storage role assignments on that feature:

// src/templates/finops-hub/modules/fx/hub-app.bicep#L40
var usesStorage = contains(features, 'Storage')

// src/templates/finops-hub/modules/fx/hub-app.bicep#L287-L298
resource storageRoleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [
  for role in factoryStorageRoles: if (usesDataFactory && usesStorage) {
    ...
  }
]

Because usesStorage is false for this app, the entire loop is skipped and the RBAC Administrator role it passes is silently dropped. Managed Exports is the only app in the template that passes storageRoles at all β€” and the only one of those that does not declare the Storage feature.

This is visible in the shipped template as well: the role GUID appears exactly once in azuredeploy.json, as the parameter value handed to Microsoft.CostManagement.ManagedExports_Register, immediately next to "features": ["DataFactory"].

πŸ“‰ Regression

This worked in v12, where the same role was coupled directly to the managed exports option and assigned without a feature condition:

// v12 -- src/templates/finops-hub/modules/dataFactory.bicep
var storageRbacRoles = union(
  [ /* Storage Account Contributor, Storage Blob Data Contributor, Reader */ ],
  // Only use User Access Administrator if managed exports are enabled for least privileged access
  !enableManagedExports ? [] : [ '18d7d88d-d35e-4fb5-a5c3-7773c20a72d9' ]
)

resource factoryIdentityStorageRoleAssignments 'Microsoft.Authorization/roleAssignments@2022-04-01' = [
  for role in storageRbacRoles: { ... }   // no feature condition
]

The app modularization (#1800) moved roles into a per-app storageRoles parameter and introduced the feature gate; the coupling to enableManagedExports was lost in the process. Present in v13, v14 and current dev.

πŸ‘£ Repro steps

  1. Deploy a FinOps hub with enableManagedExports = true and at least one subscription scope in scopesToMonitor.
  2. Grant the hub managed identity Cost Management Contributor on that scope, as documented.
  3. Write to config/settings.json in the hub storage account (any write) so the config_SettingsUpdated trigger fires.
  4. config_ConfigureExports runs and fails at the ... open month focus export activity with the Unauthorized error above (3 attempts).
  5. No export is created in Cost Management, so nothing lands in the msexports container and no data is ingested.

Checking the Data Factory identity afterwards shows four role assignments β€” Data Factory Contributor on the factory, plus Storage Account Contributor, Storage Blob Data Contributor and Reader on the hub storage account. RBAC Administrator is not among them.

πŸ€” Expected

When enableManagedExports is enabled, the deployment assigns the RBAC Administrator role to the Data Factory identity on the hub storage account, so config_ConfigureExports can create Cost Management exports. This matches the documented behavior in Hub template, which lists the role under the assignments made "as part of the deployment", noting only that it is "not applied when enableManagedExports is disabled".

πŸ”§ Environment

  • FinOps hub version: current dev (15.0-dev); also present in v13 and v14. Last known good: v12.
  • Billing account type: reproduced on a subscription scope, which is documented as supported for managed exports (MCA billing accounts/profiles are explicitly not supported).
  • Cost Management export: FocusCost

Reproduced on two independent deployments with different shapes (with and without Data Explorer, public networking), with the same result.

πŸ’‘ Suggested fix

Declare the Storage feature on the Managed Exports app registration so the role assignment loop runs:

features: [
  'DataFactory'
  'Storage'
]

ℹ️ Additional context

Two related observations found while investigating, both minor β€” happy to split them into their own issues if preferred:

  1. The role table in the hub template docs still lists User Access Administrator. That GUID no longer appears anywhere in the shipped template since it was replaced by RBAC Administrator, so the documented role cannot be found by anyone looking for it.

  2. In the same pipeline run, the Save Scopes activity of config_ConfigureExports fails when the settings file contains exactly one scope:

    The variable 'scopesArray' of type 'Array' cannot be initialized or updated with value of type 'Object'.
    

    The settings file serializes a single scope as an object rather than an array, because sorting a one-element collection in PowerShell yields a scalar. This is non-blocking β€” the following Save Scopes as Array activity recovers and the scope reaches export creation β€” but it shows up as a failed activity on every run with a single scope and makes troubleshooting harder.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions