PowerShell Setup: Control 2.15 — Environment Routing and Auto-Provisioning
Read the FSI PowerShell baseline first
Before running any command in this playbook, read the PowerShell Authoring Baseline for FSI Implementations. It is the canonical source for module version pinning, sovereign-cloud (GCC / GCC High / DoD) endpoints, mutation safety (-WhatIf / SupportsShouldProcess), Dataverse compatibility, and SHA-256 evidence emission. The snippets below intentionally show abbreviated patterns; the baseline is authoritative.
Last Updated: April 2026
Modules Required: Microsoft.PowerApps.Administration.PowerShell (Desktop edition / Windows PowerShell 5.1 only)
Prerequisites
# REQUIRED: pin to a CAB-approved version. Floating versions break SOX 404 / OCC 2023-17 reproducibility.
Install-Module -Name Microsoft.PowerApps.Administration.PowerShell `
-RequiredVersion '<approved-version>' `
-Repository PSGallery `
-Scope CurrentUser `
-AllowClobber `
-AcceptLicense
# Edition guard — Power Apps Administration cmdlets are Desktop-only.
# Without this guard, a PS 7 host silently returns empty results and produces FALSE-CLEAN evidence.
if ($PSVersionTable.PSEdition -ne 'Desktop') {
throw "Microsoft.PowerApps.Administration.PowerShell requires Windows PowerShell 5.1 (Desktop). Detected: $($PSVersionTable.PSEdition) $($PSVersionTable.PSVersion)."
}
# Sovereign-aware connect (default 'prod' = commercial)
param(
[ValidateSet('prod','usgov','usgovhigh','dod')] [string] $Endpoint = 'prod',
[Parameter(Mandatory)] [string] $TenantId
)
Add-PowerAppsAccount -Endpoint $Endpoint -TenantID $TenantId
For unattended runs, supply
-ApplicationId/-ClientSecrettoAdd-PowerAppsAccount. The service principal must hold the Power Platform Admin role assignment and (if scripts run cross-cloud) be registered in the matching cloud.
Read the Current Routing Configuration
This is read-only — no -WhatIf required. Use it to capture baseline evidence before any change.
$tenantSettings = Get-TenantSettings
# The routing flags live under powerPlatform.governance
$routing = [PSCustomObject]@{
EnableDefaultEnvironmentRouting = $tenantSettings.powerPlatform.governance.enableDefaultEnvironmentRouting
EnvironmentRoutingAllMakers = $tenantSettings.powerPlatform.governance.environmentRoutingAllMakers
TargetEnvironmentGroupId = $tenantSettings.powerPlatform.governance.environmentRoutingTargetEnvironmentGroupId
TargetSecurityGroupId = $tenantSettings.powerPlatform.governance.environmentRoutingTargetSecurityGroupId
CapturedUtc = (Get-Date).ToUniversalTime().ToString('o')
}
$routing | Format-List
Multi-rule routing (multiple security-group → environment-group mappings) is configured only via PPAC today. The PowerShell properties above represent the legacy single-target shape and are still authoritative for the on/off flag and the primary target. Capture the multi-rule order from PPAC export as supplementary evidence.
Enable Environment Routing (Mutating Script)
This script changes tenant state. It follows the FSI baseline: SupportsShouldProcess, before-snapshot, transcript, SHA-256 evidence emission.
<#
.SYNOPSIS
Enables Power Platform environment routing per Control 2.15.
.DESCRIPTION
- Snapshots current tenant settings to disk for rollback.
- Sets enableDefaultEnvironmentRouting = $true.
- Optionally sets environmentRoutingAllMakers and the primary target group.
- Emits SHA-256-hashed evidence + manifest per FSI baseline section 5.
.PARAMETER TargetEnvironmentGroupId
GUID of the environment group that will hold catch-all routed dev envs.
.PARAMETER AllMakers
If set, route both new and existing makers. Otherwise, new makers only.
.EXAMPLE
.\Enable-EnvironmentRouting.ps1 -TenantId <id> -TargetEnvironmentGroupId <guid> -AllMakers -WhatIf
.\Enable-EnvironmentRouting.ps1 -TenantId <id> -TargetEnvironmentGroupId <guid> -AllMakers -Confirm
#>
[CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')]
param(
[Parameter(Mandatory)] [string] $TenantId,
[ValidateSet('prod','usgov','usgovhigh','dod')] [string] $Endpoint = 'prod',
[Parameter(Mandatory)] [string] $TargetEnvironmentGroupId,
[string] $TargetSecurityGroupId,
[switch] $AllMakers,
[string] $EvidencePath = ".\evidence\2.15"
)
$ErrorActionPreference = 'Stop'
if ($PSVersionTable.PSEdition -ne 'Desktop') {
throw "Run in Windows PowerShell 5.1 (Desktop). Detected: $($PSVersionTable.PSEdition) $($PSVersionTable.PSVersion)."
}
New-Item -ItemType Directory -Force -Path $EvidencePath | Out-Null
$ts = Get-Date -Format 'yyyyMMddTHHmmssZ'
Start-Transcript -Path "$EvidencePath\transcript-$ts.log" -IncludeInvocationHeader
try {
Add-PowerAppsAccount -Endpoint $Endpoint -TenantID $TenantId
# 1. BEFORE snapshot (required for rollback evidence)
$before = Get-TenantSettings
$beforePath = "$EvidencePath\tenant-settings-before-$ts.json"
$before | ConvertTo-Json -Depth 50 | Set-Content -Path $beforePath -Encoding UTF8
# 2. Build mutated copy
$after = $before
$after.powerPlatform.governance.enableDefaultEnvironmentRouting = $true
$after.powerPlatform.governance | Add-Member -NotePropertyName 'environmentRoutingAllMakers' `
-NotePropertyValue ([bool]$AllMakers) -Force
$after.powerPlatform.governance | Add-Member -NotePropertyName 'environmentRoutingTargetEnvironmentGroupId' `
-NotePropertyValue $TargetEnvironmentGroupId -Force
if ($TargetSecurityGroupId) {
$after.powerPlatform.governance | Add-Member -NotePropertyName 'environmentRoutingTargetSecurityGroupId' `
-NotePropertyValue $TargetSecurityGroupId -Force
}
# 3. ShouldProcess gate
if ($PSCmdlet.ShouldProcess("Tenant $TenantId", "Enable environment routing -> group $TargetEnvironmentGroupId (AllMakers=$AllMakers)")) {
Set-TenantSettings -RequestBody $after
Write-Host "[OK] Environment routing enabled." -ForegroundColor Green
}
# 4. AFTER snapshot + SHA-256 manifest
$afterActual = Get-TenantSettings
$afterPath = "$EvidencePath\tenant-settings-after-$ts.json"
$afterActual | ConvertTo-Json -Depth 50 | Set-Content -Path $afterPath -Encoding UTF8
$manifest = @($beforePath, $afterPath, "$EvidencePath\transcript-$ts.log") | ForEach-Object {
[PSCustomObject]@{
file = (Split-Path $_ -Leaf)
sha256 = (Get-FileHash -Path $_ -Algorithm SHA256).Hash
bytes = (Get-Item $_).Length
generated_utc = $ts
control = '2.15'
}
}
$manifest | ConvertTo-Json -Depth 5 | Set-Content -Path "$EvidencePath\manifest-$ts.json" -Encoding UTF8
Write-Host "[OK] Evidence written to $EvidencePath" -ForegroundColor Green
}
finally {
Stop-Transcript | Out-Null
}
Always invoke first with
-WhatIf. This is the difference between an undo-able config change and a tenant-wide maker provisioning incident.
Disable Environment Routing (Rollback)
[CmdletBinding(SupportsShouldProcess, ConfirmImpact='High')]
param(
[Parameter(Mandatory)] [string] $TenantId,
[ValidateSet('prod','usgov','usgovhigh','dod')] [string] $Endpoint = 'prod',
[string] $EvidencePath = ".\evidence\2.15"
)
$ErrorActionPreference = 'Stop'
Add-PowerAppsAccount -Endpoint $Endpoint -TenantID $TenantId
$before = Get-TenantSettings
$ts = Get-Date -Format 'yyyyMMddTHHmmssZ'
$before | ConvertTo-Json -Depth 50 | Set-Content "$EvidencePath\rollback-before-$ts.json" -Encoding UTF8
$after = $before
$after.powerPlatform.governance.enableDefaultEnvironmentRouting = $false
if ($PSCmdlet.ShouldProcess("Tenant $TenantId", "Disable environment routing")) {
Set-TenantSettings -RequestBody $after
Write-Host "[OK] Environment routing disabled." -ForegroundColor Yellow
}
Disabling routing does not delete already-provisioned personal dev environments. Decommission those separately via Control 2.16 (environment lifecycle) procedures.
Inventory: Routed Personal Dev Environments
Use this read-only inventory after routing has been live for one supervisory cycle. It identifies developer environments likely created by routing and flags ones outside the target groups (a misconfiguration signal).
param(
[string] $ExpectedGroupNamePattern = 'FSI-Personal-Dev-*',
[string] $OutDir = ".\evidence\2.15"
)
$ErrorActionPreference = 'Stop'
New-Item -ItemType Directory -Force -Path $OutDir | Out-Null
$envs = Get-AdminPowerAppEnvironment
$rows = foreach ($e in $envs | Where-Object EnvironmentType -eq 'Developer') {
[PSCustomObject]@{
DisplayName = $e.DisplayName
EnvironmentId = $e.EnvironmentName
Region = $e.Location
Created = $e.Properties.createdTime
CreatedBy = $e.Properties.createdBy.displayName
IsManaged = ($e.Properties.protectionLevel -ne 'Standard')
EnvGroup = $e.Properties.parentEnvironmentGroup.displayName
}
}
$rows | Export-Csv "$OutDir\dev-env-inventory-$(Get-Date -Format 'yyyyMMdd').csv" -NoTypeInformation
# Anomaly: dev env that is NOT in the expected group pattern
$anomalies = $rows | Where-Object { $_.EnvGroup -notlike $ExpectedGroupNamePattern -or -not $_.IsManaged }
if ($anomalies) {
Write-Warning "Found $($anomalies.Count) developer environments outside the routing target groups."
$anomalies | Format-Table DisplayName, CreatedBy, EnvGroup, IsManaged
}
Evidence Storage
All artifacts produced by this playbook (tenant-settings-before-*.json, tenant-settings-after-*.json, transcript-*.log, manifest-*.json, dev-env-inventory-*.csv) must be transferred to a WORM-locked store (Microsoft Purview retention lock or Azure Storage immutability policy) per SEC 17a-4(f) and FINRA 4511 expectations. The local evidence\2.15 directory is a staging area only.
Back to Control 2.15 | Portal Walkthrough | Verification Testing | Troubleshooting