Sensitivity labels on a Post-It note aren't governance. This deep-dive lesson teaches you how to architect a complete information protection system in Power BI using Microsoft Purview — automating label inheritance across your semantic layer, enforcing DLP policies at data egress, and building audit-ready compliance reporting pipelines that satisfy real regulatory requirements.

Picture this: your enterprise data team has spent six months building a pristine semantic layer in Power BI — certified datasets, carefully modeled star schemas, row-level security locked down tight. Then your compliance officer walks in with a regulatory audit finding. A mid-level analyst exported a report containing salary data to an unencrypted Excel file, emailed it to a vendor, and nobody caught it until the vendor's inbox was compromised. The data was never marked as sensitive. No policy blocked the export. No alert fired. The semantic layer was technically secure, but the information protection layer simply didn't exist.
This is the gap that Microsoft Purview's information protection integration with Power BI is designed to close. It's not just about putting labels on datasets — it's about creating a system where sensitivity propagates automatically from your most trusted data sources down through dataflows, semantic models, and reports, and where policy enforcement happens at the point of export, not as an afterthought in the SOC. By the end of this lesson, you'll understand not just how to configure this system, but why each architectural decision matters, where the sharp edges are, and how to build compliance reporting that actually satisfies an auditor rather than just looking good in a dashboard.
What you'll learn:
Before diving in, you should be comfortable with:
Before you configure a single label, you need a clear mental model of what's actually happening under the hood. Microsoft Purview sensitivity labels aren't just metadata tags — they're policy containers. When a label is applied to a Power BI dataset, three things happen simultaneously:
The key architectural insight is that Power BI is a protected container, not a protected document format. A .pbix file on disk is not natively protected by sensitivity labels the way a Word document is — the protection is enforced by the Power BI service at the point of data egress. This distinction matters enormously for your threat model.
Key insight
Sensitivity labels in Power BI protect the export path, not the semantic model itself. A user with direct XMLA endpoint access or a compromised service principal can bypass label-based export controls entirely. Information protection should always be layered with object-level security and row-level security at the model level.
Microsoft Purview supports a concept called label inheritance, which works in a cascading fashion across the Power BI artifact hierarchy:
Data Source (SQL Server / Synapse / ADLS)
│
▼
Power BI Dataflow (Gen1 / Gen2)
│
▼
Power BI Semantic Model (Dataset)
│
▼
Power BI Report / Dashboard / App
│
▼
Exported File (Excel / PDF / CSV / PowerPoint)
Inheritance flows downward, and it's governed by a simple rule: the highest-priority label wins, and it propagates to downstream artifacts. If your Synapse source table has a "Highly Confidential" label and your dataflow has "General," the dataset connecting to that dataflow will automatically be promoted to "Highly Confidential" — but only if tenant settings and Purview policies are correctly configured to enable this behavior.
Warning
Label inheritance from data sources is not retroactive by default. It fires when a dataset is refreshed. If you have 400 certified datasets and you add a "Confidential" label to a source table today, those datasets won't inherit it until their next scheduled refresh. Plan your rollout timeline accordingly and schedule a forced refresh sweep after major labeling initiatives.
The full information protection stack requires careful license alignment:
| Feature | Minimum License |
|---|---|
| Apply sensitivity labels in Power BI service | Power BI Pro + M365 E3 |
| Mandatory label enforcement (users must label) | M365 E5 Compliance |
| DLP policies for Power BI | M365 E5 Compliance |
| Auto-labeling from data source inheritance | Power BI Premium (P SKU or PPU) + M365 E5 |
| Purview Scanner API for compliance reporting | Power BI Premium |
| Label encryption on exported files | Azure RMS (included in E3+) |
The Premium or Premium Per User requirement for automatic inheritance is the one that catches teams off guard. If you're on Pro licensing only, label inheritance from sources won't fire automatically — users must apply labels manually.
The biggest governance mistake organizations make is deploying sensitivity labels without first designing a coherent taxonomy. You do not want to create 47 labels over 18 months and then discover that half your users apply "Internal" when they mean "Confidential - HR" because the names were ambiguous.
For enterprise Power BI deployments, a four-tier taxonomy maps well to both regulatory frameworks (GDPR, HIPAA, SOX) and practical data classification needs:
Public
└── General (Internal Use)
├── Confidential
│ ├── Confidential - Finance
│ ├── Confidential - HR
│ └── Confidential - Customer PII
└── Highly Confidential
├── Highly Confidential - Trade Secret
└── Highly Confidential - Regulated (PHI/PCI)
Each label should encode clear answers to three questions: Who can see this? Where can it go? What happens when it's exported?
Configure labels in the Microsoft Purview compliance portal (compliance.microsoft.com). Navigate to Information Protection > Labels, create your hierarchy, and for each label, configure:
Tip
Resist the urge to encrypt everything. Applying RMS encryption to a "General" label breaks many legitimate workflows — Power Automate flows, email attachments, SharePoint indexing. Reserve encryption for "Highly Confidential" and evaluate "Confidential" case-by-case. The goal is proportional protection, not maximum friction.
Once your label taxonomy exists in Purview, you need to enable it for Power BI through two mechanisms:
Step 1: Publish the label policy to Power BI
In the Purview compliance portal, go to Information Protection > Label policies and either edit your existing policy or create one targeting the security groups that include your Power BI users. In the policy settings, confirm that Power BI is selected as a workload. This is the step most admins miss — labels exist, but they haven't been published to the Power BI workload.
Step 2: Configure Power BI admin tenant settings
In the Power BI admin portal (app.powerbi.com/admin-portal), navigate to Tenant settings > Information protection:
For a deeper look at these tenant settings in context, see Implementing Power BI Tenant Settings and Sensitivity Labels for Enterprise Data Protection and Compliance.
Manual label application is governance theater. In a mature enterprise with hundreds of datasets spanning multiple workspaces, human-driven labeling creates coverage gaps within weeks of any initiative. The sustainable approach is automation: programmatic labeling at dataset creation, inheritance from upstream sources, and scheduled audits that flag unlabeled assets.
The setLabels endpoint in the Power BI Admin API allows you to apply sensitivity labels to datasets, reports, dashboards, and dataflows programmatically. Here's a production-ready Python implementation that applies labels based on a classification mapping:
import requests
import json
from msal import ConfidentialClientApplication
# Authentication setup - use a service principal with Power BI Admin rights
TENANT_ID = "your-tenant-id"
CLIENT_ID = "your-service-principal-client-id"
CLIENT_SECRET = "your-client-secret"
POWER_BI_SCOPE = ["https://analysis.windows.net/powerbi/api/.default"]
# Your Purview label GUIDs - retrieve these from the Purview compliance portal
LABEL_MAP = {
"finance": "a1b2c3d4-0001-0001-0001-000000000001",
"hr": "a1b2c3d4-0002-0002-0002-000000000002",
"customer_pii": "a1b2c3d4-0003-0003-0003-000000000003",
"general": "a1b2c3d4-0004-0004-0004-000000000004",
"highly_confidential": "a1b2c3d4-0005-0005-0005-000000000005",
}
def get_access_token():
app = ConfidentialClientApplication(
CLIENT_ID,
authority=f"https://login.microsoftonline.com/{TENANT_ID}",
client_credential=CLIENT_SECRET,
)
result = app.acquire_token_for_client(scopes=POWER_BI_SCOPE)
if "access_token" not in result:
raise ValueError(f"Token acquisition failed: {result.get('error_description')}")
return result["access_token"]
def get_all_datasets(token: str) -> list:
"""Retrieve all datasets across the tenant using the admin endpoint."""
url = "https://api.powerbi.com/v1.0/myorg/admin/datasets"
headers = {"Authorization": f"Bearer {token}"}
datasets = []
# The admin endpoint returns up to 5000 items; paginate if needed
params = {"$top": 5000, "$skip": 0}
while True:
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
data = response.json()
batch = data.get("value", [])
datasets.extend(batch)
if len(batch) < 5000:
break
params["$skip"] += 5000
return datasets
def classify_dataset(dataset: dict) -> str:
"""
Determine the appropriate label based on dataset name, workspace,
or other metadata. In production, this would query your data catalog
or classification database.
"""
name_lower = dataset.get("name", "").lower()
workspace_id = dataset.get("workspaceId", "")
# Example classification rules - replace with your actual logic
HR_WORKSPACES = {"ws-001", "ws-002"} # Replace with actual workspace GUIDs
if workspace_id in HR_WORKSPACES:
return "hr"
elif any(term in name_lower for term in ["salary", "compensation", "payroll"]):
return "hr"
elif any(term in name_lower for term in ["customer", "pii", "email", "phone"]):
return "customer_pii"
elif any(term in name_lower for term in ["revenue", "p&l", "financial", "budget"]):
return "finance"
elif dataset.get("sensitivityLabel") is not None:
return None # Already labeled - don't overwrite
else:
return "general"
def apply_labels_batch(token: str, label_requests: list) -> dict:
"""
Apply labels in batches of 25 (API limit per call).
label_requests: list of {"artifactId": "...", "labelId": "..."}
"""
url = "https://api.powerbi.com/v1.0/myorg/admin/informationProtectionSetLabels"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
}
results = {"succeeded": [], "failed": []}
BATCH_SIZE = 25
for i in range(0, len(label_requests), BATCH_SIZE):
batch = label_requests[i:i + BATCH_SIZE]
payload = {
"updateDetails": [
{
"artifactId": req["artifactId"],
"artifactType": req.get("artifactType", "Dataset"),
"sensitivityLabelId": req["labelId"],
"datasetId": req["artifactId"], # Required for datasets
}
for req in batch
]
}
response = requests.post(url, headers=headers, json=payload)
if response.status_code == 200:
response_data = response.json()
results["succeeded"].extend(
response_data.get("succeededArtifacts", [])
)
results["failed"].extend(
response_data.get("failedArtifacts", [])
)
else:
# Log the failure but don't abort the entire batch run
print(f"Batch {i//BATCH_SIZE + 1} failed: {response.status_code} - {response.text}")
results["failed"].extend([req["artifactId"] for req in batch])
return results
def run_labeling_sweep():
token = get_access_token()
datasets = get_all_datasets(token)
label_requests = []
skipped_count = 0
for dataset in datasets:
classification = classify_dataset(dataset)
if classification is None:
skipped_count += 1
continue
label_requests.append({
"artifactId": dataset["id"],
"artifactType": "Dataset",
"labelId": LABEL_MAP[classification],
})
print(f"Processing {len(label_requests)} datasets, skipping {skipped_count} already labeled")
results = apply_labels_batch(token, label_requests)
print(f"Succeeded: {len(results['succeeded'])}")
print(f"Failed: {len(results['failed'])}")
return results
if __name__ == "__main__":
run_labeling_sweep()
Schedule this script as an Azure Function running on a timer trigger — weekly at minimum, daily for high-compliance environments. The classify_dataset function is where your business logic lives; in a mature implementation, it would query a master data catalog, check the dataset's upstream lineage, or call a Microsoft Graph API to inspect the workspace's classification metadata.
Warning
The informationProtectionSetLabels admin API endpoint requires that the calling service principal has the Power BI tenant setting "Service principals can use Fabric APIs" enabled, and the service principal must be a member of the security group whitelisted for that setting. Additionally, the service principal needs the Dataset.ReadWrite.All and Content.Create permissions in the Power BI service. Missing either of these is the most common reason bulk labeling scripts fail silently.
If your organization uses Microsoft Purview Data Map (the catalog, not just the compliance tools), you can establish a tighter inheritance loop. Data assets in Purview Data Map — including Azure SQL tables, ADLS Gen2 paths, and Synapse Analytics tables — can carry sensitivity classifications. When Power BI refreshes a dataset connected to these sources, Purview propagates the highest-priority label from the source asset to the dataset.
The integration works through the Purview connector in Power BI, which is enabled through the tenant settings described earlier. Under the hood, Power BI registers each refresh activity with the Purview lineage API, which allows Purview to trace ColumnMapping relationships from source columns to dataset tables and apply the maximum-sensitivity label found in that lineage chain.
To verify this is working, after enabling the tenant setting and completing a dataset refresh, navigate to the dataset in the Power BI workspace, select Settings, and check the Sensitivity label field. It should show the inherited label and — critically — show the label as "auto-applied" rather than "manually applied," which affects how DLP policies can treat it.
This connects naturally to the broader lineage story described in Implementing Power BI Workspace-Level Lineage and Impact Analysis to Manage Dataset Dependencies Across the Enterprise — lineage isn't just an operational tool, it's a compliance mechanism.
Data Loss Prevention policies in Microsoft Purview are the enforcement layer. Where sensitivity labels classify data, DLP policies constrain what users can do with it. Power BI DLP support arrived in 2022 and has matured significantly, but it still has meaningful differences from Office DLP that you need to understand before you design your rule set.
What DLP policies can do in Power BI:
What Power BI DLP currently cannot do:
In the Microsoft Purview compliance portal, navigate to Data loss prevention > Policies > Create policy. Walk through the wizard:
Step 1: Template selection
Start with a custom policy template rather than a prebuilt regulatory template. The prebuilt templates (HIPAA, PCI-DSS, etc.) are designed for document workflows and their condition logic doesn't map cleanly to Power BI dataset metadata.
Step 2: Locations
Select Power BI workspaces as the location. You can scope the policy to specific workspaces by name or apply it tenant-wide. For your initial rollout, target your production and certified workspaces first — these are the datasets most likely to contain regulated data and most likely to be shared externally.
Step 3: Define policy rules
Here's where the real design work happens. A well-structured Power BI DLP policy for a financial services firm might look like:
Rule 1: Block External Sharing of Highly Confidential Data
Rule 2: Restrict Confidential Financial Data Sharing
Rule 3: Detect Unlabeled Datasets with Sensitive Content
Rule 3 is particularly powerful: it catches the "we forgot to label this" scenario by scanning the actual data content, not just the label metadata.
Note
Content scanning (inspecting actual data values in datasets) is performed by the Microsoft Purview Scanner, which needs to be configured with access to your Power BI tenant. The scanner runs asynchronously after dataset refresh, so there's a window between refresh completion and when content-based DLP rules are evaluated. Don't design workflows that depend on instantaneous content scanning.
The single biggest adoption failure mode for DLP is the "everything is blocked" experience. When users encounter policy tips that block legitimate work without explanation, they file support tickets, escalate to their managers, and generate political pressure to disable the policies entirely.
Design your policy tips as teaching moments, not just walls. Configure custom notification messages that:
Example policy tip message for Rule 2:
"This dataset contains financial data classified as Confidential. Sharing outside your department requires manager approval. To request an exception, submit a ticket at [your-intranet-url]. Questions? Contact dataprivacy@yourcompany.com."
Configure overrides with business justification for "Confidential" labels but not for "Highly Confidential." The asymmetry is intentional: "Confidential" data sometimes legitimately needs to be shared more broadly, but "Highly Confidential" data should have no self-service override path.
The standard documentation covers straightforward Import-mode datasets. The edge cases are where real implementations encounter friction.
When a dataset uses a composite model that connects to multiple sources with different sensitivity levels, Power BI applies the highest-sensitivity label from any connected source. This is the correct security behavior, but it creates an operational challenge: a "General" aggregation over a "Highly Confidential" fact table will inherit "Highly Confidential" — even if no actually sensitive columns are exposed in the model.
The practical implication: if your aggregation layer is pulling pre-aggregated, non-sensitive data from a warehouse that also contains PII in other tables, the dataset-level label will reflect the source system's classification, not the actual content of the model. This is a known limitation of the current architecture.
Your mitigation options are:
Architectural separation: Route non-sensitive aggregate sources through a separate data tier with an appropriate lower-sensitivity classification. This is the cleanest solution and connects to the Power BI and Azure Synapse enterprise analytics architecture pattern where you maintain separate Gold-layer tables for public vs. restricted data.
Manual label override with documentation: Allow workspace admins to manually downgrade the label with documented justification. This requires enabling the "Allow workspace admins to override" tenant setting, which creates audit risk, so mandate that every override has a corresponding ticket number in the audit log.
Object-level security as a compensating control: If the inherited label is higher than the actual data sensitivity justifies, ensure that the actually sensitive columns from the source are excluded via object-level security, then document this compensation in your compliance evidence.
DirectQuery datasets present a specific timing problem. Since a DirectQuery dataset doesn't import data — it passes queries to the source at render time — the "refresh" trigger for label inheritance doesn't apply the same way. The label inheritance for DirectQuery is evaluated when the dataset connection metadata is updated, not on a scheduled refresh cycle.
Key insight
For DirectQuery datasets connected to Azure Synapse or SQL Server with Purview-classified sources, label inheritance fires when the dataset is published or updated, not on a schedule. If your source classification changes (e.g., a table gets reclassified from "General" to "Confidential"), the downstream DirectQuery dataset won't inherit the new label until someone touches the dataset — republishes it, changes a setting, or you trigger an update via API. Build a monitoring step into your source reclassification workflow.
When you use Power BI deployment pipelines to promote content from Development through Test to Production, sensitivity labels can create friction that trips up CI/CD workflows if you haven't planned for them.
The label behavior in deployment pipelines follows these rules:
InformationProtection.ReadWrite.All in Microsoft Graph).The most common pipeline failure scenario: a service principal promotes a dataset that has an inherited "Highly Confidential" label to a Production workspace where DLP policies block sharing. The promotion itself succeeds, but the dataset immediately becomes non-shareable, breaking downstream reports that were working in Test. The fix is to ensure your DLP policy scoping explicitly excludes your deployment service principal's operations from blocking actions — use the Exceptions section in the DLP policy to whitelist the deployment service principal's activities.
For automated CI/CD pipelines using Azure DevOps, see Implementing Power BI Deployment Pipelines with Git Integration and Azure DevOps for Automated CI/CD of Enterprise Reports for the deployment architecture context.
Sensitivity labels and DLP policies are worthless without visibility into whether they're working. Compliance reporting for Power BI information protection requires pulling from three distinct data sources and synthesizing them into actionable intelligence.
Source 1: Power BI Activity Log (via REST API)
The Power BI activity log captures every label application, label change, export event, and DLP policy match. Extract it daily via the admin API:
import requests
from datetime import datetime, timedelta, timezone
def get_activity_log(token: str, date: str) -> list:
"""
Retrieve Power BI activity events for a specific date.
date format: "2024-01-15"
"""
# Activity log uses UTC times
start_time = f"{date}T00:00:00.000Z"
end_time = f"{date}T23:59:59.999Z"
url = "https://api.powerbi.com/v1.0/myorg/admin/activityevents"
headers = {"Authorization": f"Bearer {token}"}
all_events = []
params = {
"startDateTime": f"'{start_time}'",
"endDateTime": f"'{end_time}'",
}
# Filter for label-relevant event types
label_event_types = [
"SensitivityLabelApplied",
"SensitivityLabelChanged",
"SensitivityLabelRemoved",
"ExportReport",
"ExportDataflow",
"DownloadReport",
"ShareReport",
"PublishedToWebReport",
]
filter_clause = " or ".join(
[f"Activity eq '{e}'" for e in label_event_types]
)
params["$filter"] = filter_clause
while True:
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
data = response.json()
events = data.get("activityEventEntities", [])
all_events.extend(events)
# Handle pagination via continuationUri
continuation = data.get("continuationUri")
continuation_token = data.get("continuationToken")
if not continuation_token:
break
# Use continuation token for next page
params = {"continuationToken": f"'{continuation_token}'"}
return all_events
Source 2: Microsoft Purview Activity Explorer (via Graph API)
DLP policy matches are recorded in the Microsoft Purview Activity Explorer. These events are queryable via the Office 365 Management Activity API:
def get_purview_dlp_events(token: str, days_back: int = 7) -> list:
"""
Retrieve DLP policy match events from Office 365 Management Activity API.
Requires separate authentication with scope:
https://manage.office.com/.default
"""
# Note: This requires a DIFFERENT token scope than Power BI API
# Use manage.office.com scope, not analysis.windows.net
tenant_id = "your-tenant-id"
base_url = f"https://manage.office.com/api/v1.0/{tenant_id}/activity/feed"
headers = {"Authorization": f"Bearer {token}"}
# First, start or resume a subscription for DLP content
# Content type for DLP events
content_type = "Audit.General"
# Get available content (DLP events are in Audit.General and DLP.All)
start_time = (
datetime.now(timezone.utc) - timedelta(days=days_back)
).strftime("%Y-%m-%dT%H:%M:%SZ")
end_time = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
content_url = (
f"{base_url}/subscriptions/content"
f"?contentType=DLP.All"
f"&startTime={start_time}&endTime={end_time}"
)
response = requests.get(content_url, headers=headers)
response.raise_for_status()
content_blobs = response.json()
dlp_events = []
# Retrieve each content blob
for blob in content_blobs:
blob_url = blob.get("contentUri")
blob_response = requests.get(blob_url, headers=headers)
blob_response.raise_for_status()
events = blob_response.json()
# Filter for Power BI DLP events
pbi_dlp_events = [
e for e in events
if e.get("Workload") == "PowerBI"
and e.get("Operation", "").startswith("DLP")
]
dlp_events.extend(pbi_dlp_events)
return dlp_events
Source 3: Scanner API for Label Coverage Audit
The Purview Scanner API (or the Power BI Admin API's metadata scanning endpoints) lets you snapshot the current label state across all artifacts:
def get_label_coverage_snapshot(token: str) -> dict:
"""
Get current sensitivity label state for all Power BI artifacts.
Returns coverage statistics by artifact type.
"""
headers = {"Authorization": f"Bearer {token}"}
# Trigger metadata scan
scan_url = "https://api.powerbi.com/v1.0/myorg/admin/workspaces/modified"
# Get all workspaces first
workspaces_url = "https://api.powerbi.com/v1.0/myorg/admin/groups"
ws_response = requests.get(
workspaces_url,
headers=headers,
params={"$top": 5000, "$filter": "type eq 'Workspace'"}
)
ws_response.raise_for_status()
workspaces = ws_response.json().get("value", [])
coverage = {
"datasets": {"labeled": 0, "unlabeled": 0, "by_label": {}},
"reports": {"labeled": 0, "unlabeled": 0},
"dashboards": {"labeled": 0, "unlabeled": 0},
"dataflows": {"labeled": 0, "unlabeled": 0},
}
# For each workspace, get artifacts and their labels
for workspace in workspaces:
ws_id = workspace["id"]
# Get datasets in this workspace
ds_url = f"https://api.powerbi.com/v1.0/myorg/admin/groups/{ws_id}/datasets"
ds_response = requests.get(ds_url, headers=headers)
if ds_response.status_code == 200:
datasets = ds_response.json().get("value", [])
for dataset in datasets:
label = dataset.get("sensitivityLabel")
if label:
coverage["datasets"]["labeled"] += 1
label_name = label.get("labelId", "unknown")
coverage["datasets"]["by_label"][label_name] = (
coverage["datasets"]["by_label"].get(label_name, 0) + 1
)
else:
coverage["datasets"]["unlabeled"] += 1
# Calculate coverage percentage
for artifact_type in coverage:
total = (
coverage[artifact_type]["labeled"] +
coverage[artifact_type]["unlabeled"]
)
coverage[artifact_type]["coverage_pct"] = (
round(coverage[artifact_type]["labeled"] / total * 100, 1)
if total > 0 else 0
)
return coverage
The final step is turning this audit data into a living compliance report. Load the three data sources above into a dedicated "Compliance Reporting" dataset — this is itself an excellent candidate for a Power BI dataflow to centralize the extraction logic.
Your compliance report should contain at minimum:
Page 1: Label Coverage Dashboard
Page 2: DLP Policy Activity
Page 3: Label Change Audit Trail
Page 4: Export Risk Analysis
Tip
Certify this compliance reporting dataset using the endorsement workflow (see Implementing Power BI Dataset Certification and Endorsement Workflows) and apply a "Highly Confidential" label to the dataset itself — it contains details about your security posture, which is sensitive in its own right.
In this exercise, you'll build a complete information protection sweep for a fictional pharmaceutical company, "NovaTech Pharma," which has 80 Power BI datasets across Finance, Clinical Research, and HR domains, with a mix of labeled and unlabeled assets.
Scenario setup:
NovaTech has three sensitivity-relevant dataset categories:
Exercise steps:
Step 1: Audit current state
Run the get_label_coverage_snapshot() function against the NovaTech tenant (use your lab tenant). Note the current coverage percentage and which datasets are unlabeled.
Step 2: Build the classification function
Extend the classify_dataset() function to detect NovaTech's naming conventions:
Step 3: Run the labeling sweep
Execute run_labeling_sweep() and verify in the Power BI service that labels have been applied correctly. Check three datasets manually in the UI to confirm.
Step 4: Configure a DLP rule
In the Purview compliance portal, create a DLP policy that:
Step 5: Test the DLP rule
In the Power BI service, navigate to a dataset you labeled "Highly Confidential - Regulated" and attempt to share it externally. Verify that the policy tip fires and the share is blocked (allow up to 15 minutes for policy propagation).
Step 6: Build the compliance report
Using the activity log extraction function, pull 7 days of label-related events and load them into a Power BI Desktop model. Create a simple report page showing label changes over time and any DLP matches from your test in Step 5.
Cause: The label policy hasn't been published to the Power BI workload, or the policy publication hasn't propagated yet (can take up to 24 hours).
Fix: In the Purview compliance portal, open the label policy, go to Locations, confirm Power BI is checked. If it was just enabled, wait the propagation period. You can force-check by having a user log out and back in to the Power BI service.
Cause 1: The dataset is not on Premium capacity. Auto-inheritance requires Premium or PPU.
Cause 2: The source data system isn't registered in Purview Data Map with classifications applied. Inheritance only works from Purview-classified sources, not arbitrary data sources.
Cause 3: The "Apply sensitivity labels from data sources" tenant setting is off or scoped to a security group that doesn't include the dataset owner.
Cause: The "restrict sharing" action in the DLP policy is too broad — it's blocking sharing with internal users who should have access.
Fix: In the DLP rule action configuration, use "Restrict sharing with people outside the organization only" rather than blanket "Restrict access." Alternatively, create a security group for approved sharing recipients and add them as an exception in the DLP policy conditions.
Cause 1: The service principal doesn't have the InformationProtection.ReadWrite.All Graph permission with admin consent.
Cause 2: The service principal is not in the security group whitelisted for the "Service principals can use Fabric APIs" tenant setting.
Cause 3: The labeling endpoint requires the caller to have an Azure AD Premium license associated — service principals authenticating with client credentials sometimes hit this. Switch to using a managed identity on an Azure Function if you encounter persistent 403s.
This is a genuine architectural gap. Users with XMLA endpoint access (requires Premium) can query the tabular model directly and export results without triggering Power BI's export-path DLP evaluation.
Mitigation layers:
The XMLA gap is a strong argument for defense-in-depth — DLP is one layer, not the only layer.
Cause: The promoted dataset inherited a higher-sensitivity label in the source stage, and the target stage's DLP policy blocks sharing for that label.
Fix: Add the deployment service principal to the DLP policy exceptions list. Navigate to the policy in Purview, select Advanced settings > Exceptions for service accounts, and add your deployment service principal's object ID.
You've now built a complete understanding of how Microsoft Purview's information protection capabilities integrate with the Power BI semantic layer — not just the configuration steps, but the architectural reasoning behind each decision.
The core principles to carry forward:
Where to go next:
Since you've established how information leaves the semantic layer safely, the logical next focus is controlling what data enters and how it's modeled. Consider diving into Implementing Power BI Dataset Sharing and Cross-Workspace Live Connections to Build a Reusable Enterprise Semantic Layer to understand how certified, labeled datasets can be safely shared across workspace boundaries without creating labeling inconsistencies.
If your compliance requirements extend to encryption at rest for Premium datasets — particularly relevant for PHI or PCI data — explore Implementing Power BI Bring Your Own Key (BYOK) Encryption, which complements information protection by ensuring that even Microsoft cannot access your encrypted dataset storage.
Finally, to make your compliance reporting operationally robust rather than a one-time effort, the automation patterns in Power BI REST API: Automate Administration and Deployments will help you schedule and maintain the audit pipelines you built in this lesson.