From 73738980d2abf6d005ecddd00224949420f555bf Mon Sep 17 00:00:00 2001 From: Digvijay Chauhan Date: Wed, 2 Sep 2026 22:37:39 +0200 Subject: [PATCH 1/3] Fix Foundry project provisioning and grant Foundry User RBAC (Fabric hack) The Fabric inventory-planning deploy-lab.ps1 created the Foundry project without a managed identity, ignored REST/provisioning failures (reporting success even on error), and never granted attendees access to Foundry - so Challenge 1 could not be completed and Foundry user RBAC was missing. - Create the project with a system-assigned managed identity and fail clearly on PUT/provisioning errors, waiting until it reaches Succeeded with a principalId. - Grant the Foundry User role at the Foundry account scope to each attendee and to the project managed identity, idempotently with retries. - Require at least one attendee object ID and document the new RBAC behaviour. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../labautomation/README.md | 6 +- .../labautomation/deploy-lab.ps1 | 107 +++++++++++++++--- 2 files changed, 95 insertions(+), 18 deletions(-) diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/labautomation/README.md b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/labautomation/README.md index a744e142d..f0e4c1f6b 100644 --- a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/labautomation/README.md +++ b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/labautomation/README.md @@ -7,11 +7,11 @@ Everything for this hack is provisioned per attendee by [`deploy-lab.ps1`](deplo | Resource | Type | Notes | |----------|------|-------| | Azure AI Foundry account | `Microsoft.CognitiveServices/accounts` | One per attendee (AIServices kind) | -| Foundry project | `…/projects/inventory-hack` | Within the Foundry account | +| Foundry project | `…/projects/inventory-hack` | System-assigned managed identity; provisioning is verified before the lab continues | | gpt-5.4-mini model deployment | GlobalStandard, 100K TPM | Used by all three hack agents | | Fabric **F2 capacity** | `Microsoft.Fabric/capacities` | One per attendee; attendee set as **capacity admin** | -The script sets each attendee as **admin of their own F2 capacity** (resolving their Entra object ID to a UPN via the platform `Get-MhhLabUser` helper), so they can create a workspace, assign it to the capacity, and publish their own Data Agent — no shared state, no cross-attendee contention. +The script grants the **Foundry User** role to each attendee and to the Foundry project's managed identity at the Foundry account scope. It also sets each attendee as **admin of their own F2 capacity** (resolving their Entra object ID to a UPN via the platform `Get-MhhLabUser` helper), so they can create a workspace, assign it to the capacity, and publish their own Data Agent — no shared state, no cross-attendee contention. ## What the attendee does themselves (Challenge 1) @@ -33,7 +33,7 @@ Attendees use these in the Foundry and Fabric portals — no SDK or code require ## Prerequisites for facilitators / the platform -- The deploying identity (the platform **service principal**) needs **Owner** on each attendee resource group (creates the Foundry account, model deployment, and Fabric capacity via ARM) — the platform grants this automatically for `resourcegroup` deployments. +- The deploying identity (the platform **service principal**) needs **Owner** on each attendee resource group so it can create resources and assign the **Foundry User** role to the attendee and project managed identity — the platform grants this automatically for `resourcegroup` deployments. - **`groups: ["M365-E5-Users"]`** in [`lab-defaults.json`](lab-defaults.json) — each attendee's lab Entra user gets a **Microsoft 365 E5** license (includes **Power BI Pro**), which is what lets them sign into the Fabric portal ([app.fabric.microsoft.com](https://app.fabric.microsoft.com)) and create a workspace. Without a Fabric/Power BI license the attendee cannot open the portal. - **The lab tenant must have Fabric enabled** — a Fabric **tenant-admin** setting (*“Users can create Fabric items”* / workspaces). This is **not** settable via `lab-defaults.json`; the tenant administrator for the lab tenant must turn it on, or attendees cannot create a workspace even with a license. - **Fabric F-SKU quota** in a preferred region (F2 = 2 CU; a 512-CU subscription supports ~256 attendees). `swedencentral` and `norwayeast` are good defaults. diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/labautomation/deploy-lab.ps1 b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/labautomation/deploy-lab.ps1 index 622c69190..14cf89984 100644 --- a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/labautomation/deploy-lab.ps1 +++ b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/labautomation/deploy-lab.ps1 @@ -63,7 +63,11 @@ $ErrorActionPreference = "Stop" $candidateRegions = if ($PreferredLocation.Count -gt 0) { $PreferredLocation } else { @("swedencentral", "westeurope", "norwayeast") } $effectiveLocation = $candidateRegions[0] $effectiveRG = $ResourceGroupName -$stableHash = (Get-MhhStableHash $AllowedEntraUserIds -Length 12).ToLower() + +if ($AllowedEntraUserIds.Count -eq 0) { + throw "At least one attendee object ID must be supplied in AllowedEntraUserIds." +} +$stableHash = (Get-MhhStableHash $AllowedEntraUserIds -Length 12).ToLower() # For 'subscription' deployments the platform does NOT pre-create a resource group # ($ResourceGroupName arrives empty). Create one ourselves with a deterministic, @@ -170,28 +174,101 @@ if ($existingAccount) { # --------------------------------------------------------------------------- # Foundry project # --------------------------------------------------------------------------- +$accountScope = "/subscriptions/$SubscriptionId/resourceGroups/$effectiveRG" + + "/providers/Microsoft.CognitiveServices/accounts/$foundryAccountName" $projectUri = "/subscriptions/$SubscriptionId/resourceGroups/$effectiveRG" + "/providers/Microsoft.CognitiveServices/accounts/$foundryAccountName" + "/projects/${foundryProjectName}?api-version=2025-04-01-preview" -$existingProject = Invoke-AzRestMethod -Method GET -Path $projectUri -ErrorAction SilentlyContinue -if ($existingProject.StatusCode -ne 200) { - $projectBody = @{ - location = $effectiveLocation - properties = @{ description = "Agentic Inventory Planning MicroHack" } - } | ConvertTo-Json -Depth 3 +$projectBody = @{ + location = $effectiveLocation + identity = @{ type = "SystemAssigned" } + properties = @{ + description = "Agentic Inventory Planning MicroHack" + displayName = "Inventory Planning MicroHack" + } +} | ConvertTo-Json -Depth 4 - Invoke-AzRestMethod -Method PUT -Path $projectUri -Payload $projectBody | Out-Null +$projectResponse = Invoke-AzRestMethod -Method PUT -Path $projectUri -Payload $projectBody +if ($projectResponse.StatusCode -ge 400) { + throw "Foundry project create or update failed (HTTP $($projectResponse.StatusCode)): $($projectResponse.Content)" +} - $elapsed = 0 - do { Start-Sleep -Seconds 8; $elapsed += 8 - $r = Invoke-AzRestMethod -Method GET -Path $projectUri -ErrorAction SilentlyContinue - } while (($r.StatusCode -ne 200) -and ($elapsed -lt 90)) +$projectResource = $null +$elapsed = 0 +do { + Start-Sleep -Seconds 8 + $elapsed += 8 + $projectResponse = Invoke-AzRestMethod -Method GET -Path $projectUri -ErrorAction SilentlyContinue + if ($projectResponse.StatusCode -eq 200) { + $projectResource = $projectResponse.Content | ConvertFrom-Json + if ($projectResource.properties.provisioningState -eq "Failed") { + throw "Foundry project provisioning failed: $($projectResponse.Content)" + } + } +} while ( + ($projectResponse.StatusCode -ne 200 -or + $projectResource.properties.provisioningState -ne "Succeeded" -or + -not $projectResource.identity.principalId) -and + ($elapsed -lt 180) +) - Write-Host "[OK] Foundry project '$foundryProjectName' created." -} else { - Write-Host "[OK] Foundry project already exists — skipping." +if ($projectResponse.StatusCode -ne 200 -or + $projectResource.properties.provisioningState -ne "Succeeded" -or + -not $projectResource.identity.principalId) { + throw "Foundry project did not reach Succeeded with a managed identity within 180 seconds. Last response: $($projectResponse.Content)" +} +Write-Host "[OK] Foundry project '$foundryProjectName' is ready." + +# Foundry User (formerly Azure AI User) enables project access and agent operations. +$foundryUserRoleId = "53ca6127-db72-4b80-b1b0-d745d6d5456d" + +function Grant-FoundryUserRole { + param( + [Parameter(Mandatory=$true)] + [string]$PrincipalId, + + [Parameter(Mandatory=$true)] + [string]$PrincipalDescription + ) + + $existingAssignment = Get-AzRoleAssignment ` + -ObjectId $PrincipalId ` + -Scope $accountScope ` + -RoleDefinitionId $foundryUserRoleId ` + -ErrorAction SilentlyContinue + if ($existingAssignment) { + Write-Host "[OK] Foundry User already assigned to $PrincipalDescription." + return + } + + $lastError = $null + for ($attempt = 1; $attempt -le 6; $attempt++) { + try { + New-AzRoleAssignment ` + -ObjectId $PrincipalId ` + -RoleDefinitionId $foundryUserRoleId ` + -Scope $accountScope ` + -ErrorAction Stop | Out-Null + Write-Host "[OK] Granted Foundry User to $PrincipalDescription." + return + } catch { + $lastError = $_ + if ($attempt -lt 6) { + Start-Sleep -Seconds 10 + } + } + } + + throw "Could not grant Foundry User to $PrincipalDescription after 6 attempts: $lastError" +} + +foreach ($userId in $AllowedEntraUserIds) { + Grant-FoundryUserRole -PrincipalId $userId -PrincipalDescription "attendee '$userId'" } +Grant-FoundryUserRole ` + -PrincipalId $projectResource.identity.principalId ` + -PrincipalDescription "project managed identity '$($projectResource.identity.principalId)'" # --------------------------------------------------------------------------- # gpt-5.4-mini model deployment (ACCOUNT-scoped, GlobalStandard capacity 100). From 2b54e745453fe8dda46533ad723943a34c826eb5 Mon Sep 17 00:00:00 2001 From: Digvijay Chauhan Date: Wed, 2 Sep 2026 23:30:12 +0200 Subject: [PATCH 2/3] Document cold-capacity schema-sync behavior in Challenge 1 (Fabric hack) On a freshly-provisioned F2 capacity the Lakehouse SQL analytics endpoint can take a couple of minutes to expose newly-written tables. The setup notebook's publish cell may raise 'No tables were selected (schema may still be syncing)' and the data agent portal may show 'Error updating data agent data source selection(s)' if tables are ticked too early. Both resolve once the schema syncs. Add an explanatory note after Run all and two troubleshooting rows so participants understand the behaviour and know to confirm tables via the SQL analytics endpoint, then re-run the publish cell or tick tables and Publish. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../challenges/challenge-01.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/challenges/challenge-01.md b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/challenges/challenge-01.md index fae5ab7a5..f6269e4a6 100644 --- a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/challenges/challenge-01.md +++ b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/challenges/challenge-01.md @@ -175,6 +175,15 @@ The data lives in **Microsoft Fabric** (a governed Lakehouse with inventory, dem ![The notebook with InventoryLakehouse attached as the default Lakehouse and the Run all button highlighted](../images/challenge-00-run-all.png) + > [!IMPORTANT] + > **First run on a fresh capacity — the table-selection step may need a moment (or a manual tick).** The publish cell selects your 7 tables automatically, but on a **cold F2 capacity** the Lakehouse's **SQL analytics endpoint** can take a couple of minutes to expose the newly-written tables. Until that schema sync finishes you may see **either**: + > - the notebook raise **`No tables were selected (schema may still be syncing)`**, or + > - an **`Error updating data agent data source selection(s)`** toast if you tick tables in the portal too early. + > + > This is expected first-run timing, **not** a failure — your 7 tables always write successfully; only the *agent's selection* waits on the sync. To finish it: + > 1. Open **`InventoryLakehouse`** → switch the view (top-right) from **Lakehouse** to **SQL analytics endpoint** → expand **`dbo → Tables`** and confirm all **7 tables** are listed. If they aren't yet, wait ~1–2 min and **Refresh** (opening the endpoint also nudges the sync). + > 2. Then **either** re-run the notebook's publish cell (it's idempotent), **or** open the **`inventory-hack-agent`** data agent → tick **all 7 tables** in the **Explorer** (left pane) → **Publish**. + **d. Copy your two IDs.** When it finishes, the **last cell prints your Workspace ID and Agent ID**. Note both — you paste them into the Fabric Data Agent tool in Challenge 2. @@ -233,6 +242,8 @@ The data lives in **Microsoft Fabric** (a governed Lakehouse with inventory, dem | The setup notebook errors on `%pip install` or the publish cell | Preview SDK drift — re-run the failed cell; if a method name differs, check the [SDK reference](https://learn.microsoft.com/fabric/data-science/fabric-data-agent-sdk). Make sure your F2 capacity is **running**. | | **Fabric Data Agent** isn't in the tool catalogue (previewing ahead) | Your F2 capacity must be running, and the setup notebook must have **published** the agent. | | The agent replies *"I'm unable to access the data source"* (or *"No tables selected yet"*) | Its tables aren't selected. The setup notebook selects all seven automatically, but if it didn't: open the data agent → **Data** tab → tick **all** tables under `InventoryLakehouse → dbo` → **Publish**, then re-test. | +| The publish cell raises **`No tables were selected (schema may still be syncing)`** | Expected on a **cold F2** — the 7 tables wrote fine, but the Lakehouse **SQL analytics endpoint** hasn't exposed them yet. Open `InventoryLakehouse` → **SQL analytics endpoint** → confirm all 7 tables under `dbo`, then **re-run the publish cell** (idempotent) or tick the tables in the data agent's **Explorer** and **Publish**. | +| Ticking tables in the data agent shows **`Error updating data agent data source selection(s)`** | Same root cause — the SQL endpoint schema isn't fully synced. Wait until all 7 tables show under the Lakehouse's **SQL analytics endpoint**, **refresh** the data agent page (F5), then re-tick and **Publish**. If it persists, remove and re-add the `InventoryLakehouse` data source (**+ Add data source → OneLake catalog**), tick the 7 tables, and **Publish**. | ![The data agent's Data tab showing InventoryLakehouse tables unselected and the agent replying that it cannot access the data source](../images/challenge-00-no-tables.png) From 963b827aa6cbaee7d5b6e6e1366ece54e60dfc44 Mon Sep 17 00:00:00 2001 From: Digvijay Chauhan Date: Wed, 2 Sep 2026 23:42:16 +0200 Subject: [PATCH 3/3] Emit clickable Foundry project and Fabric portal URLs to the dashboard (Fabric hack) The deploy previously returned only the Foundry SDK endpoint and the Fabric capacity name, so the MicroHack platform dashboard had no clickable link to the attendee's Foundry project or the Fabric portal - attendees had to navigate manually. - Add FoundryProjectUrl: a deep-link straight to the attendee's inventory-hack project in ai.azure.com, built deterministically from the subscription (base64url token), resource group, account, project, and tenant. - Add FabricPortalUrl: a link to app.fabric.microsoft.com to create the workspace on FabricCapacityName (the workspace URL cannot exist until the attendee creates it in Challenge 1). - Document both new credentials in labautomation/README.md. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../labautomation/README.md | 4 ++- .../labautomation/deploy-lab.ps1 | 25 ++++++++++++++++++- 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/labautomation/README.md b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/labautomation/README.md index f0e4c1f6b..1451c764f 100644 --- a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/labautomation/README.md +++ b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/labautomation/README.md @@ -24,9 +24,11 @@ The script grants the **Foundry User** role to each attendee and to the Foundry | Key | Value | |-----|-------| -| `FoundryProjectEndpoint` | `https://.services.ai.azure.com/api/projects/inventory-hack` | +| `FoundryProjectUrl` | Clickable deep-link to the attendee's `inventory-hack` project in the Foundry portal (`ai.azure.com`) — where they build the agents in Challenges 2-5 | +| `FoundryProjectEndpoint` | `https://.services.ai.azure.com/api/projects/inventory-hack` (SDK/API endpoint; optional — the hack is done in the portal) | | `ModelDeploymentName` | `gpt-5.4-mini` | | `FabricCapacityName` | The attendee's own F2 capacity (assign your workspace to it in Challenge 1) | +| `FabricPortalUrl` | Link to the Fabric portal (`app.fabric.microsoft.com`) to create the workspace on `FabricCapacityName` — the workspace URL itself only exists once the attendee creates it in Challenge 1 | | `ResourceGroupName` | The attendee's resource group | Attendees use these in the Foundry and Fabric portals — no SDK or code required. diff --git a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/labautomation/deploy-lab.ps1 b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/labautomation/deploy-lab.ps1 index 14cf89984..71b39bcbf 100644 --- a/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/labautomation/deploy-lab.ps1 +++ b/03-Azure/01-04-AI/02_Inventory_Planning_Fabric/labautomation/deploy-lab.ps1 @@ -385,14 +385,31 @@ if ($adminUpns.Count -eq 0) { # --------------------------------------------------------------------------- # Return credentials to the attendee dashboard # --------------------------------------------------------------------------- +$tenantId = (Get-AzContext).Tenant.Id $projectEndpoint = "https://$foundryAccountName.services.ai.azure.com/api/projects/$foundryProjectName" +# Clickable Foundry portal deep-link to THIS project. The ai.azure.com portal encodes the +# subscription GUID as a base64url token (in string/hex byte order — NOT Guid.ToByteArray()), +# followed by ,,,. Giving attendees this link means they don't have to +# hunt for their project in the portal. +$subHex = $SubscriptionId -replace '-', '' +$subBytes = for ($i = 0; $i -lt $subHex.Length; $i += 2) { [Convert]::ToByte($subHex.Substring($i, 2), 16) } +$subToken = [Convert]::ToBase64String([byte[]]$subBytes).TrimEnd('=').Replace('+', '-').Replace('/', '_') +$projectPortalUrl = "https://ai.azure.com/nextgen/r/$subToken,$effectiveRG,,$foundryAccountName,$foundryProjectName/home?tid=$tenantId" +$fabricPortalUrl = "https://app.fabric.microsoft.com/home?tid=$tenantId" + Write-Host "[OK] Lab provisioning complete." +@{ HackboxCredential = @{ + name = "FoundryProjectUrl" + value = $projectPortalUrl + note = "Open your Foundry project directly (sign in with your lab user). This is where you build the agents in Challenges 2-5." +} } + @{ HackboxCredential = @{ name = "FoundryProjectEndpoint" value = $projectEndpoint - note = "Open ai.azure.com → select this project OR use as SDK endpoint" + note = "SDK / API endpoint for your project (optional - the hack is done in the portal via FoundryProjectUrl)" } } @{ HackboxCredential = @{ @@ -407,6 +424,12 @@ Write-Host "[OK] Lab provisioning complete." note = "Your own Fabric F2 capacity. In Challenge 1 you create a workspace and assign it to this capacity, then Run All the setup notebook to publish your Data Agent." } } +@{ HackboxCredential = @{ + name = "FabricPortalUrl" + value = $fabricPortalUrl + note = "Open the Fabric portal (sign in with your lab user) to create your workspace on your FabricCapacityName in Challenge 1. Your workspace URL doesn't exist until you create it there." +} } + @{ HackboxCredential = @{ name = "ResourceGroupName" value = $effectiveRG