Repository navigation
Add optional row-level security guide for FinOps hubs - #2374
Keerthi Math (keerthimath) wants to merge 2 commits into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Raw-table protection and security validation guidance need corrections to avoid ingestion failures and unintended data exposure.
5 open issues
Restricted raw views can break update-policy ingestion · New Verify raw tables are empty before granting access · New Detect missing RLS policies on all final tables · New Inspect all update-policy dependencies for RLS blockers · New Attach disabled RLS policy before force-option safety checks · New
What changed in this PR
Adds optional subscription-scoped row-level security guidance for FinOps hubs on Azure Data Explorer, addressing #2372 without changing hub templates.
Changes:
- Documents prerequisites, limitations, setup, verification, and rollback.
- Adds the guide to the navigation and changelog.
| File | Description |
|---|---|
docs-mslearn/toolkit/hubs/configure-row-level-security.md |
Adds the optional security configuration guide. |
docs-mslearn/toolkit/changelog.md |
Announces the new guidance. |
docs-mslearn/TOC.yml |
Adds navigation to the guide. |
Review effort: Balanced
| Review these limitations before you start: | ||
|
|
||
| - **Azure Data Explorer only**: This article covers hubs that use Azure Data Explorer. Microsoft Fabric eventhouses are out of scope. | ||
| - **Raw tables**: Row-level security can't be enabled on the raw tables in the **Ingestion** database, like `Costs_raw`, because they feed update policies. They're only safe when the hub's raw retention (`dataExplorerRawRetentionInDays`) is `0`, the default, because they then keep no data. With raw retention above 0, team members could read the retained raw rows, so you also need the [restricted view access policy](/kusto/management/restricted-view-access-policy) on the raw tables. |
There was a problem hiding this comment.
Agreed, thanks. Fixed in d11232a: the guide now requires raw retention 0 and treats hubs that retain raw data as out of scope. Limitations states that restricted view access isn't an alternative, with a link to the update policy query limitations, and Step 1 matches that requirement.
|
|
||
| ### Step 1: Check the raw retention and blockers | ||
|
|
||
| Confirm that the raw tables don't keep data. Run this in the **Ingestion** database; every raw table should show a `SoftDeletePeriod` of `00:00:00`: |
There was a problem hiding this comment.
Good catch. Fixed in d11232a: Step 1 now also requires every raw table to have a TotalRowCount of 0 (.show tables details), and explains that rows kept under an earlier retention setting are deleted asynchronously. Verified on a v14 hub: all seven raw tables return 0.
|
|
||
| ```kusto | ||
| .show table * policy row_level_security | ||
| | where Policy != 'null' |
There was a problem hiding this comment.
Agreed. You were also right that the policy listing omits tables that have no policy, so filtering it can't catch a missing one. Fixed in d11232a: the check now uses .show tables details and the RowLevelSecurityPolicy column, which covers every final table and reports missing or disabled policies; it says to run it in Ingestion. Verified: it lists all ten final tables, with the two Prices tables shown as missing.
| Row-level security can't be enabled on a table in some cases. For each table, run these commands in the **Ingestion** database and resolve anything they report: | ||
|
|
||
| ```kusto | ||
| .show table Costs_final_v1_2 policy update |
There was a problem hiding this comment.
Fixed in d11232a: the blocker check now looks for update policies that read the final tables (.show tables details | where UpdatePolicy contains '_final_v'), notes that the hub's own policies read the raw tables, and asks readers to also check the functions in any update policies they added. On a v14 hub it returns no rows, out of 11 tables with update policies.
|
|
||
| ### Step 3: Test before you enable | ||
|
|
||
| Creating the function doesn't change anything yet. Before you enable the policy, check that you keep seeing all rows when the function is applied to you. Both counts must match: |
There was a problem hiding this comment.
Correct, thanks. I reproduced it on a scratch table: with no policy attached, the forced count matched the plain count, and once the function was attached as a disabled policy, the forced count applied it. Fixed in d11232a: Step 3 attaches the function as a disabled policy before comparing the counts. A disabled policy affects no one, and Step 4 then enables it.
|
@microsoft-github-policy-service agree |
set query_force_row_level_security only applies a policy that is attached to the table, so on a table's first rollout the pre-enable comparison always matched and proved nothing. apply now attaches the function as a disabled policy first when no policy is enabled, keeps an enabled policy enabled, and aborts if an enabled policy uses another query. Found in review of microsoft/finops-toolkit#2374.
- Require raw retention 0 and empty raw tables (TotalRowCount check); restricted view access isn't an alternative because the hub's update policies read the raw tables. - Blocker check now looks for update policies that read the final tables (.show tables details). - Attach the function as a disabled policy before the forced-RLS comparison, so the test applies it. - Upgrade check lists every final table, including tables without a policy, and runs in Ingestion.


🛠️ Description
Adds an optional, opt-in docs page for scoping FinOps hub data by subscription with Azure Data Explorer row-level security: Configure row-level security for FinOps hubs (
docs-mslearn/toolkit/hubs/configure-row-level-security.md), with a TOC entry and a changelog line. Nothing in the hub template changes.Following the review notes on #2372:
SubAccountIdare admin-only, RLS is bound to table names and must be re-checked after every hub upgrade, shared identities, and RLS filters rows but not metadata.The PowerShell command will be a separate follow-up PR.
Fixes #2372
📷 Screenshots
N/A (docs only).
📋 Checklist
🔬 How did you test this change?
Lint: markdownlint with
docs-mslearn/.markdownlint.json, 0 errors. Manual: the procedure on a FinOps hub v14.0 (Azure Data Explorer).Tested the procedure end to end with a temporary team and a member-only test user (removed afterwards); outcomes from the cluster's
.show querieslog:HubHubonlyCosts() | summarize ... by SubAccountIdSEM0056 ... Database='Ingestion': Access deniedHubandIngestionHubandIngestiondatabase('Ingestion').Costs_raw | count0(raw retention 0)HubandIngestionCosts() | summarize ... by SubAccountIdAdmin row counts matched with and without
set query_force_row_level_securitybefore and after each change. The Data Explorer dashboard and an AI agent also showed only the team's data for a restricted member. Ingestion kept working with RLS enabled on the final tables. With only Viewer, a member can still list the database's principals, which is why the Limitations section says RLS filters rows, not metadata.📦 Deploy to test?
Docs only; nothing to deploy.
🙋♀️ Do any of the following that apply?
📑 Did you update
docs/changelog.md?📖 Did you update documentation?