Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions docs-mslearn/toolkit/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -18,14 +18,16 @@ ms.reviewer: brettwil

This article summarizes the features and enhancements in each release of the FinOps toolkit.

<!--
<br>

## Unreleased

The following section lists features and enhancements that are currently in development.

-->
### [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)).

<br><a name="latest"></a>

Expand Down
7 changes: 5 additions & 2 deletions docs-mslearn/toolkit/hubs/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
58 changes: 58 additions & 0 deletions src/powershell/Private/Grant-FinOpsHubFabricAccess.ps1
Original file line number Diff line number Diff line change
@@ -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)"
}
}
}
15 changes: 14 additions & 1 deletion src/powershell/Public/Deploy-FinOpsHub.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions src/powershell/Tests/Unit/Grant-FinOpsHubFabricAccess.Tests.ps1
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Loading