Skip to content

Commit 2a4c428

Browse files
committed
feat: add ROPC authentication support in Get-PBIWorkspaceUsageReport.ps1 for unattended service accounts
1 parent 0637071 commit 2a4c428

2 files changed

Lines changed: 449 additions & 11 deletions

File tree

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
# PBI Workspace Usage → Dataverse Automation Architecture
2+
3+
## Overview
4+
5+
This document describes the automated pipeline that runs `Get-PBIWorkspaceUsageReport.ps1` on a schedule via an Azure Function, stages the output in Blob Storage, and uses a Power Automate flow to validate and load the data into Dataverse.
6+
7+
---
8+
9+
## High-Level Architecture
10+
11+
```mermaid
12+
flowchart TD
13+
KV[(Azure Key Vault\nROPC Password\nTenant ID / Client ID)]
14+
AF[Azure Function\nTimer Trigger\nPowerShell]
15+
BLOB[(Blob Storage\nreports/pending/\nreports/failed/)]
16+
PA[Power Automate\nBlob Created Trigger]
17+
DV[(Dataverse\nPBI Usage Table)]
18+
ALERT[Email / Teams Alert\nMax retries exceeded]
19+
20+
KV -->|secrets at runtime| AF
21+
AF -->|writes report JSON| BLOB
22+
BLOB -->|new file event| PA
23+
PA -->|valid data| DV
24+
PA -->|invalid / parse error| BLOB
25+
BLOB -->|retry count exceeded| ALERT
26+
PA -->|retry — HTTP POST| AF
27+
```
28+
29+
---
30+
31+
## Azure Function — Internal Flow
32+
33+
```mermaid
34+
flowchart TD
35+
START([Timer Trigger\nor HTTP POST])
36+
PARAMS{Retry count\nin request body?}
37+
RETRY_CHK{RetryCount\n≥ 3?}
38+
FAIL_OUT[Write report to\nreports/failed/\nwith error metadata]
39+
DEAD([Exit — Dead Letter\nAlert upstream])
40+
KV_FETCH[Fetch secrets\nfrom Key Vault\nvia Managed Identity]
41+
ROPC[ROPC Token Request\nPBI Admin API]
42+
TOKEN_OK{Token\nacquired?}
43+
TOKEN_ERR[Write error blob\nto reports/failed/]
44+
PBI_GROUPS[GET /admin/groups\nall workspaces + reports]
45+
PBI_ACTIVITY[GET /admin/activityEvents\nViewReport events\nfor ActivityDays]
46+
CORRELATE[Correlate reports\nwith activity data\nbuild usage objects]
47+
SERIALIZE[Serialize to JSON\nwith metadata block:\n- generatedAt\n- retryCount\n- tenantId]
48+
WRITE_BLOB[Write to\nreports/pending/\nyyyyMMdd_HHmmss.json]
49+
DONE([Exit — Success])
50+
51+
START --> PARAMS
52+
PARAMS -->|yes| RETRY_CHK
53+
PARAMS -->|no, retryCount = 0| KV_FETCH
54+
RETRY_CHK -->|yes| FAIL_OUT
55+
RETRY_CHK -->|no| KV_FETCH
56+
FAIL_OUT --> DEAD
57+
KV_FETCH --> ROPC
58+
ROPC --> TOKEN_OK
59+
TOKEN_OK -->|no| TOKEN_ERR
60+
TOKEN_ERR --> DEAD
61+
TOKEN_OK -->|yes| PBI_GROUPS
62+
PBI_GROUPS --> PBI_ACTIVITY
63+
PBI_ACTIVITY --> CORRELATE
64+
CORRELATE --> SERIALIZE
65+
SERIALIZE --> WRITE_BLOB
66+
WRITE_BLOB --> DONE
67+
```
68+
69+
---
70+
71+
## Power Automate Flow — Internal Logic
72+
73+
```mermaid
74+
flowchart TD
75+
TRIGGER([When a blob is created\nreports/pending/ container])
76+
GET_BLOB[Get blob content]
77+
PARSE[Parse JSON\ncheck schema:\n- generatedAt present\n- workspaces array not empty\n- retryCount field]
78+
VALID{Schema\nvalid?}
79+
RETRY_CHK{retryCount\n≥ 3?}
80+
ALERT[Send Teams / Email alert\nBlob name + error reason\nManual review required]
81+
MOVE_FAILED[Move blob to\nreports/failed/]
82+
LOOP[For each workspace row\nUpsert into Dataverse\nPBI Usage table\nmatch on ReportId + Date]
83+
DV_ERR{Dataverse\nerror?}
84+
MOVE_DONE[Move blob to\nreports/processed/]
85+
INCREMENT[RetryCount + 1\nRebuild request body]
86+
HTTP[HTTP POST\nAzure Function URL\nbody: retryCount]
87+
DONE([End])
88+
89+
TRIGGER --> GET_BLOB
90+
GET_BLOB --> PARSE
91+
PARSE --> VALID
92+
VALID -->|yes| LOOP
93+
VALID -->|no| RETRY_CHK
94+
RETRY_CHK -->|yes| ALERT
95+
RETRY_CHK -->|no| INCREMENT
96+
ALERT --> MOVE_FAILED
97+
MOVE_FAILED --> DONE
98+
INCREMENT --> HTTP
99+
HTTP --> DONE
100+
LOOP --> DV_ERR
101+
DV_ERR -->|yes| RETRY_CHK
102+
DV_ERR -->|no| MOVE_DONE
103+
MOVE_DONE --> DONE
104+
```
105+
106+
---
107+
108+
## Deployment Guide
109+
110+
### 1. Prerequisites
111+
112+
| Resource | Notes |
113+
|---|---|
114+
| Azure Subscription | Contributor access required |
115+
| Azure Function App | PowerShell 7.4 runtime, Windows or Linux |
116+
| Azure Key Vault | For ROPC credentials |
117+
| Azure Storage Account | Blob Storage (LRS sufficient) |
118+
| Power Automate | Premium license (Azure Blob connector is premium) |
119+
| Dataverse environment | Table pre-created — see schema below |
120+
| Entra App Registration | Existing SP from `Get-PBIWorkspaceUsageReport.ps1` |
121+
122+
---
123+
124+
### 2. Blob Storage Setup
125+
126+
Create three containers in the storage account:
127+
128+
| Container | Purpose |
129+
|---|---|
130+
| `reports/pending` | Function writes here; PA trigger watches here |
131+
| `reports/processed` | PA moves valid blobs here after Dataverse load |
132+
| `reports/failed` | Dead-letter — blobs that exceeded retry limit |
133+
134+
Set a **Lifecycle Management policy** on `reports/processed` to delete blobs older than 90 days.
135+
136+
---
137+
138+
### 3. Azure Key Vault — Secrets
139+
140+
Store the following secrets:
141+
142+
| Secret Name | Value |
143+
|---|---|
144+
| `pbi-tenant-id` | Entra Tenant ID |
145+
| `pbi-client-id` | App Registration Client ID |
146+
| `pbi-svc-username` | Service account UPN |
147+
| `pbi-svc-password` | Service account password |
148+
149+
---
150+
151+
### 4. Azure Function App Setup
152+
153+
#### 4a. Enable System-Assigned Managed Identity
154+
155+
**Function App → Identity → System assigned → Status: On → Save**
156+
157+
#### 4b. Grant Managed Identity access to Key Vault
158+
159+
In Key Vault → **Access policies** (or RBAC if using Azure RBAC model):
160+
161+
- Grant the Function App's identity the **Key Vault Secrets User** role
162+
163+
#### 4c. Grant Managed Identity access to Blob Storage
164+
165+
In the Storage Account → **Access Control (IAM)**:
166+
167+
- Assign **Storage Blob Data Contributor** to the Function App's identity
168+
169+
#### 4d. Function App Settings
170+
171+
Add the following Application Settings (not secrets — these are non-sensitive references):
172+
173+
| Setting | Value |
174+
|---|---|
175+
| `KEY_VAULT_NAME` | Your Key Vault name |
176+
| `BLOB_ACCOUNT_NAME` | Your Storage Account name |
177+
| `BLOB_CONTAINER_PENDING` | `reports/pending` |
178+
| `ACTIVITY_DAYS` | `30` (or `90` for Fabric/Premium) |
179+
180+
#### 4e. Deploy the Function
181+
182+
The Function wraps `Get-PBIWorkspaceUsageReport.ps1` with the following entry point pattern:
183+
184+
```powershell
185+
# run.ps1 (timer trigger)
186+
using namespace System.Net
187+
188+
param($Timer, $TriggerMetadata)
189+
190+
# Read retry count from binding metadata (HTTP trigger passes this)
191+
$retryCount = $TriggerMetadata.RetryCount ?? 0
192+
193+
# Fetch secrets from Key Vault via Managed Identity
194+
$kvUri = "https://$env:KEY_VAULT_NAME.vault.azure.net"
195+
$tenantId = (Invoke-RestMethod "$kvUri/secrets/pbi-tenant-id?api-version=7.4" -Headers (Get-MIAuthHeader)).value
196+
$clientId = (Invoke-RestMethod "$kvUri/secrets/pbi-client-id?api-version=7.4" -Headers (Get-MIAuthHeader)).value
197+
$username = (Invoke-RestMethod "$kvUri/secrets/pbi-svc-username?api-version=7.4" -Headers (Get-MIAuthHeader)).value
198+
$password = (Invoke-RestMethod "$kvUri/secrets/pbi-svc-password?api-version=7.4" -Headers (Get-MIAuthHeader)).value | ConvertTo-SecureString -AsPlainText -Force
199+
200+
# Run the report script — output to temp path
201+
$outPath = [System.IO.Path]::GetTempPath()
202+
.\Get-PBIWorkspaceUsageReport.ps1 `
203+
-TenantId $tenantId `
204+
-ClientId $clientId `
205+
-Username $username `
206+
-Password $password `
207+
-OutputPath $outPath `
208+
-OutputFormat json `
209+
-ActivityDays $env:ACTIVITY_DAYS
210+
211+
# Upload result blob with retry metadata injected
212+
# ... (upload logic using Az.Storage or REST with MI token)
213+
```
214+
215+
> The full Function implementation is out of scope for this doc. The pattern above shows the secret retrieval and script invocation approach.
216+
217+
#### 4f. Timer Schedule
218+
219+
Set the CRON expression in `function.json`:
220+
221+
```json
222+
{
223+
"schedule": "0 0 6 * * 1"
224+
}
225+
```
226+
227+
This runs **every Monday at 06:00 UTC**. Adjust to suit your reporting cadence.
228+
229+
---
230+
231+
### 5. Dataverse Table Schema
232+
233+
Create a custom table `pbi_workspaceusage` with the following columns:
234+
235+
| Display Name | Schema Name | Type | Notes |
236+
|---|---|---|---|
237+
| Report ID | `pbi_reportid` | Text | Unique identifier from PBI |
238+
| Report Name | `pbi_reportname` | Text | |
239+
| Workspace ID | `pbi_workspaceid` | Text | |
240+
| Workspace Name | `pbi_workspacename` | Text | |
241+
| View Count | `pbi_viewcount` | Whole Number | |
242+
| Unique Users | `pbi_uniqueusers` | Whole Number | |
243+
| Report Date | `pbi_reportdate` | Date Only | Date of the report run |
244+
| Generated At | `pbi_generatedat` | Date and Time | Timestamp from JSON metadata |
245+
| Is Personal Workspace | `pbi_ispersonal` | Yes/No | |
246+
247+
Use **Report ID + Report Date** as the alternate key for upsert deduplication.
248+
249+
---
250+
251+
### 6. Power Automate Flow Setup
252+
253+
1. **Create a new Automated Cloud Flow**
254+
2. **Trigger:** `Azure Blob Storage — When a blob is added or modified`
255+
- Storage account: your account
256+
- Container: `reports/pending`
257+
258+
3. **Actions (in order):**
259+
- `Get blob content` — get the new file
260+
- `Parse JSON` — use the report schema
261+
- `Condition` — check schema validity (`generatedAt` exists, `workspaces` length > 0)
262+
- **Yes branch:** `Apply to each` over workspace rows → `Add a new row` (Dataverse, upsert on alternate key) → Move blob to `reports/processed`
263+
- **No branch:** Check `retryCount` field
264+
- **retryCount < 3:** Increment, `HTTP POST` to Function HTTP trigger URL with `{ "RetryCount": N }`
265+
- **retryCount ≥ 3:** Move blob to `reports/failed`, send Teams/Email alert
266+
267+
4. **Store the Function HTTP trigger URL** in a PA environment variable — do not hardcode it in the flow.
268+
269+
---
270+
271+
### 7. Security Notes
272+
273+
- The Power Automate HTTP action to re-trigger the Function must use the **Function Key** (not the master key). Store it in a PA environment variable.
274+
- The service account used for ROPC must be excluded from any Conditional Access policies that enforce MFA or device compliance.
275+
- Enable **Soft Delete** on the Storage Account to recover accidentally deleted blobs.
276+
- Enable **Diagnostic Logging** on the Function App → Log Analytics workspace for alerting on failures.
277+
278+
---
279+
280+
### 8. Retry Flow Summary
281+
282+
```mermaid
283+
sequenceDiagram
284+
participant F as Azure Function
285+
participant B as Blob Storage
286+
participant PA as Power Automate
287+
participant DV as Dataverse
288+
289+
F->>B: Write report (retryCount=0)
290+
B-->>PA: Blob created trigger
291+
PA->>PA: Validate schema — FAIL
292+
PA->>F: HTTP POST retryCount=1
293+
F->>B: Write report (retryCount=1)
294+
B-->>PA: Blob created trigger
295+
PA->>PA: Validate schema — PASS
296+
PA->>DV: Upsert rows
297+
DV-->>PA: Success
298+
PA->>B: Move to reports/processed
299+
```
300+
301+
---
302+
303+
*Author: Managed Solution — Will Ford*
304+
*Last Updated: 2026-03-26*

0 commit comments

Comments
 (0)