diff --git a/docs-mslearn/toolkit/changelog.md b/docs-mslearn/toolkit/changelog.md
index d1f201360..3e5b820e4 100644
--- a/docs-mslearn/toolkit/changelog.md
+++ b/docs-mslearn/toolkit/changelog.md
@@ -3,7 +3,7 @@ title: FinOps toolkit changelog
description: Review the latest features and enhancements in the FinOps toolkit, including updates to FinOps hubs, Power BI reports, and more.
author: MSBrett
ms.author: brettwil
-ms.date: 10/28/2026
+ms.date: 10/29/2026
ms.topic: reference
ms.service: finops
ms.subservice: finops-toolkit
@@ -18,14 +18,16 @@ ms.reviewer: brettwil
This article summarizes the features and enhancements in each release of the FinOps toolkit.
-
+### [FinOps hubs](hubs/finops-hubs-overview.md)
+
+- **Fixed**
+ - Fixed Fabric eventhouse deployments leaving the Data Factory identity without access to the Ingestion and Hub databases. `Deploy-FinOpsHub -FabricQueryUri` now grants the admin role automatically, and warns that Fabric ingestion cannot pass the storage firewall in VNet or private modes. Approach informed by [Zahir Hussain Shah](https://github.com/zhshah/Azure-FinOps-Automation-Deployment-Utility) (MIT) ([#2061](https://github.com/microsoft/finops-toolkit/issues/2061)).
diff --git a/docs-mslearn/toolkit/hubs/deploy.md b/docs-mslearn/toolkit/hubs/deploy.md
index 3a7e853ab..fa6600662 100644
--- a/docs-mslearn/toolkit/hubs/deploy.md
+++ b/docs-mslearn/toolkit/hubs/deploy.md
@@ -3,7 +3,7 @@ title: How to create and update FinOps hubs
description: This tutorial helps you create a new or update an existing FinOps hubs instance in Azure or Microsoft Fabric.
author: flanakin
ms.author: micflan
-ms.date: 08/19/2026
+ms.date: 10/29/2026
ms.topic: tutorial
ms.service: finops
ms.subservice: finops-toolkit
@@ -210,7 +210,10 @@ For other parameters, see [Deploy-FinOpsHub](../powershell/hubs/Deploy-FinOpsHub
## Optional: Configure Fabric access
-If you set up Microsoft Fabric as a primary data store, configure access for Data Factory and the Fabric eventhouse.
+If you set up Microsoft Fabric as a primary data store, configure access for Data Factory and the Fabric eventhouse. `Deploy-FinOpsHub -FabricQueryUri` grants the Data Factory access in step 2 for you; follow these steps if you deploy the template another way.
+
+> [!WARNING]
+> Fabric eventhouse ingestion pulls files from the storage account and cannot pass the storage firewall (trusted services bypass doesn't apply). With private or VNet networking, data stays in storage until the firewall allows Fabric access.
1. Get the Data Factory identity:
1. From the Azure portal, open the FinOps hub resource group.
diff --git a/src/powershell/Private/Grant-FinOpsHubFabricAccess.ps1 b/src/powershell/Private/Grant-FinOpsHubFabricAccess.ps1
new file mode 100644
index 000000000..64276279e
--- /dev/null
+++ b/src/powershell/Private/Grant-FinOpsHubFabricAccess.ps1
@@ -0,0 +1,58 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+<#
+ .SYNOPSIS
+ Grants the FinOps hub Data Factory identity admin access to the Fabric eventhouse Ingestion and Hub databases.
+
+ .PARAMETER QueryUri
+ Required. Fabric eventhouse query URI.
+
+ .PARAMETER PrincipalId
+ Required. Object ID of the Data Factory managed identity.
+
+ .PARAMETER TenantId
+ Required. Entra tenant ID.
+
+ .EXAMPLE
+ Grant-FinOpsHubFabricAccess -QueryUri 'https://xxx.kusto.fabric.microsoft.com' -PrincipalId $adfId -TenantId $tenantId
+#>
+function Grant-FinOpsHubFabricAccess
+{
+ [CmdletBinding(SupportsShouldProcess)]
+ param
+ (
+ [Parameter(Mandatory = $true)]
+ [string]
+ $QueryUri,
+
+ [Parameter(Mandatory = $true)]
+ [string]
+ $PrincipalId,
+
+ [Parameter(Mandatory = $true)]
+ [string]
+ $TenantId
+ )
+
+ foreach ($database in 'Ingestion', 'Hub')
+ {
+ if (-not $PSCmdlet.ShouldProcess($database, 'Add Data Factory identity as database admin'))
+ {
+ continue
+ }
+
+ try
+ {
+ $token = (Get-AzAccessToken -ResourceUrl 'https://kusto.kusto.windows.net').Token
+ # Az.Accounts 14+ returns a SecureString; older versions return plain text
+ if ($token -is [securestring]) { $token = [System.Net.NetworkCredential]::new('', $token).Password }
+ $body = @{ db = $database; csl = ".add database $database admins ('aadapp=$PrincipalId;$TenantId')" } | ConvertTo-Json
+ Invoke-RestMethod -Method Post -Uri "$($QueryUri.TrimEnd('/'))/v1/rest/mgmt" -Headers @{ Authorization = "Bearer $token" } -ContentType 'application/json' -Body $body | Out-Null
+ }
+ catch
+ {
+ Write-Warning "Could not grant the Data Factory identity admin on the $database database. Run the .add database command manually (see https://aka.ms/finops/hubs/deploy#optional-configure-fabric-access). $($_.Exception.Message)"
+ }
+ }
+}
diff --git a/src/powershell/Public/Deploy-FinOpsHub.ps1 b/src/powershell/Public/Deploy-FinOpsHub.ps1
index e48f29555..b46b26a86 100644
--- a/src/powershell/Public/Deploy-FinOpsHub.ps1
+++ b/src/powershell/Public/Deploy-FinOpsHub.ps1
@@ -260,6 +260,11 @@ function Deploy-FinOpsHub
# Resolve network mode. -NetworkMode wins; -DisablePublicAccess (deprecated) maps to 'vnet'.
$effectiveNetworkMode = if ($NetworkMode) { $NetworkMode } elseif ($DisablePublicAccess) { 'vnet' } else { 'public' }
+
+ if ($FabricQueryUri -and $effectiveNetworkMode -ne 'public')
+ {
+ Write-Warning 'Fabric eventhouse ingestion pulls files from storage and cannot pass the storage firewall, so data stays in storage until the firewall allows Fabric. See https://github.com/microsoft/finops-toolkit/issues/2061.'
+ }
if (-not $NetworkMode -and $DisablePublicAccess) { Write-Warning "-DisablePublicAccess is deprecated; use -NetworkMode 'vnet' or -NetworkMode 'private'." }
# Accept a "v" prefix (for example, v14 or v14.0); releases are matched without it
@@ -365,7 +370,15 @@ function Deploy-FinOpsHub
if (Test-ShouldProcess $PSCmdlet $ResourceGroupName 'DeployFinOpsHub')
{
Write-Verbose -Message ($LocalizedData.Hub_Deploy_Deploy -f $bicepFile.FullName, $resourceGroupObject.ResourceGroupName)
- return New-AzResourceGroupDeployment @parameterSplat -ResourceGroupName $resourceGroupObject.ResourceGroupName
+ $deployment = New-AzResourceGroupDeployment @parameterSplat -ResourceGroupName $resourceGroupObject.ResourceGroupName
+
+ if ($FabricQueryUri -and $deployment.Outputs -and $deployment.Outputs.dataFactoryName)
+ {
+ $adf = Get-AzDataFactoryV2 -ResourceGroupName $resourceGroupObject.ResourceGroupName -Name $deployment.Outputs.dataFactoryName.Value
+ Grant-FinOpsHubFabricAccess -QueryUri $FabricQueryUri -PrincipalId $adf.Identity.PrincipalId -TenantId $adf.Identity.TenantId -WhatIf:$WhatIfPreference
+ }
+
+ return $deployment
}
}
catch
diff --git a/src/powershell/Tests/Unit/Grant-FinOpsHubFabricAccess.Tests.ps1 b/src/powershell/Tests/Unit/Grant-FinOpsHubFabricAccess.Tests.ps1
new file mode 100644
index 000000000..d68301e6b
--- /dev/null
+++ b/src/powershell/Tests/Unit/Grant-FinOpsHubFabricAccess.Tests.ps1
@@ -0,0 +1,43 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+& "$PSScriptRoot/../Initialize-Tests.ps1"
+
+InModuleScope 'FinOpsToolkit' {
+ Describe 'Grant-FinOpsHubFabricAccess' {
+ BeforeAll {
+ function Get-AzAccessToken { param($ResourceUrl, $AsSecureString) }
+ Mock -CommandName 'Get-AzAccessToken' { [pscustomobject]@{ Token = 'token' } }
+ Mock -CommandName 'Invoke-RestMethod' { }
+ }
+
+ It 'Should grant admin on both databases to the Data Factory identity' {
+ Grant-FinOpsHubFabricAccess -QueryUri 'https://x.kusto.fabric.microsoft.com/' -PrincipalId 'p-id' -TenantId 't-id' -Confirm:$false
+
+ Should -Invoke -CommandName 'Invoke-RestMethod' -Times 2
+ Should -Invoke -CommandName 'Invoke-RestMethod' -Times 1 -ParameterFilter {
+ $Uri -eq 'https://x.kusto.fabric.microsoft.com/v1/rest/mgmt' -and $Body -like "*.add database Ingestion admins ('aadapp=p-id;t-id')*" }
+ Should -Invoke -CommandName 'Invoke-RestMethod' -Times 1 -ParameterFilter { $Body -like "*.add database Hub admins ('aadapp=p-id;t-id')*" }
+ }
+
+ It 'Should send a plain-text bearer token when Get-AzAccessToken returns a SecureString' {
+ Mock -CommandName 'Get-AzAccessToken' { [pscustomobject]@{ Token = (ConvertTo-SecureString 'secret' -AsPlainText -Force) } }
+
+ Grant-FinOpsHubFabricAccess -QueryUri 'https://x/' -PrincipalId 'p' -TenantId 't' -Confirm:$false
+
+ Should -Invoke -CommandName 'Invoke-RestMethod' -Times 2 -ParameterFilter { $Headers.Authorization -eq 'Bearer secret' }
+ }
+
+ It 'Should not call the eventhouse with -WhatIf' {
+ Grant-FinOpsHubFabricAccess -QueryUri 'https://x/' -PrincipalId 'p' -TenantId 't' -WhatIf
+
+ Should -Invoke -CommandName 'Invoke-RestMethod' -Times 0
+ }
+
+ It 'Should warn and not throw when the grant fails' {
+ Mock -CommandName 'Invoke-RestMethod' { throw 'denied' }
+
+ { Grant-FinOpsHubFabricAccess -QueryUri 'https://x/' -PrincipalId 'p' -TenantId 't' -Confirm:$false -WarningVariable w -WarningAction SilentlyContinue } | Should -Not -Throw
+ }
+ }
+}