-
Notifications
You must be signed in to change notification settings - Fork 2
/
New-PAL-MPN-ID-ServicePrincipal_Unsigned.ps1
317 lines (278 loc) · 12.6 KB
/
New-PAL-MPN-ID-ServicePrincipal_Unsigned.ps1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
<#
.SYNOPSIS
Add AAD Application and SPN to Power Platform AAD and configure Power Platform to accept the SPN as tenant admin user.
.DESCRIPTION
This script assists in creating and configuring the ServicePrincipal to be used with
the Power Platform Command Line Interface (CLI) & Build Tools AzureDevOps task library.
Registers an Application object and corresponding ServicePrincipalName (SPN) with the Power Platform AAD instance.
This Application is then added as admin user to the Power Platform tenant itself.
NOTE: This script will prompt *TWICE* with the AAD login dialogs:
1. time: to login as admin to the AAD instance associated with the Power Platform tenant
2. time: to login as tenant admin to the Power Platform tenant itself
.INPUTS
None
.OUTPUTS
Object with Power Platform TenantId, ApplicationId and client secret (in clear text);
use this triple to configure the AzureDevOps ServiceConnection. Aditionaly a PAL Association will be performed
against the MPN_ID provided at runtime
.LINK
https://docs.microsoft.com/en-us/power-apps/developer/data-platform/powerapps-cli
https://marketplace.visualstudio.com/items?itemName=microsoft-IsvExpTools.PowerPlatform-BuildTools
.EXAMPLE
New-PAL-MPN-ID-ServicePrincipal -DryRun -MPN_ID 1234567
.EXAMPLE
New-PAL-MPN-ID-ServicePrincipal -MPN_ID 1234567
.EXAMPLE
New-PAL-MPN-ID-ServicePrincipal -MPN_ID 1234567 -TenantLocation "Europe"
.EXAMPLE
New-PAL-MPN-ID-ServicePrincipal -MPN_ID 1234567 -AdminUrl "https://admin.services.crm4.dynamics.com"
.EXAMPLE
New-PAL-MPN-ID-ServicePrincipal -MPN_ID 1234567 -SecretExpiration (New-TimeSpan -Days 90) # default is 365 days
.NOTES
Title: ©2022 Microsoft - Partner Application Link, Power Platform Service Principal
Credit: Original script adopted from https://pabuildtools.blob.core.windows.net/spn-docs-4133a3fe/New-PAL-MPN-ID-ServicePrincipal.ps1
Author: David Soden
Profile: https://davidsoden.com
Modified: 5/2/2022
#>
[CmdletBinding()]
Param(
# gather permission requests but don't create any AppId nor ServicePrincipal
[switch] $DryRun = $false,
# other possible Azure environments, see: https://docs.microsoft.com/en-us/powershell/module/azuread/connect-azuread?view=azureadps-2.0#parameters
[ValidateSet(
"AzureCloud",
"AzureChinaCloud",
"AzureUSGovernment",
"AzureGermanyCloud"
)]
[string] $AzureEnvironment = "AzureCloud",
[ValidateSet(
"UnitedStates",
"Preview(UnitedStates)",
"Europe",
"EMEA",
"Asia",
"Australia",
"Japan",
"SouthAmerica",
"India",
"Canada",
"UnitedKingdom",
"France"
)]
[string] $TenantLocation = "UnitedStates",
[string] $AdminUrl,
[TimeSpan] $SecretExpiration = (New-TimeSpan -Days 365),
[Parameter(Mandatory=$true)]
[String]$MPN_ID = ""
)
$adminUrls = @{
"UnitedStates" = "https://admin.services.crm.dynamics.com"
"Preview(UnitedStates)" = "https://admin.services.crm9.dynamics.com"
"Europe" = "https://admin.services.crm4.dynamics.com"
"EMEA" = "https://admin.services.crm4.dynamics.com"
"Asia" = "https://admin.services.crm5.dynamics.com"
"Australia" = "https://admin.services.crm6.dynamics.com"
"Japan" = "https://admin.services.crm7.dynamics.com"
"SouthAmerica" = "https://admin.services.crm2.dynamics.com"
"India" = "https://admin.services.crm8.dynamics.com"
"Canada" = "https://admin.services.crm3.dynamics.com"
"UnitedKingdom" = "https://admin.services.crm11.dynamics.com"
"France" = "https://admin.services.crm12.dynamics.com"
}
function ensureModules {
$dependencies = @(
# the more general and modern "Az" a "AzureRM" do not have proper support to manage permissions
@{ Name = "AzureAD"; Version = [Version]"2.0.2.137"; "InstallWith" = "Install-Module -Name AzureAD -AllowClobber -Scope CurrentUser" },
@{ Name = "Microsoft.PowerApps.Administration.PowerShell"; Version = [Version]"2.0.131"; "InstallWith" = "Install-Module -Name Microsoft.PowerApps.Administration.PowerShell -AllowClobber -Scope CurrentUser"},
@{ Name = "Az.ManagementPartner"; Version = [Version]"0.7.2"; "InstallWith" = "Install-Module -Name Az.ManagementPartner -AllowClobber -Scope CurrentUser"}
)
$missingDependencies = $false
$dependencies | ForEach-Object -Process {
$moduleName = $_.Name
$deps = (Get-Module -ListAvailable -Name $moduleName `
| Sort-Object -Descending -Property Version)
if ($deps -eq $null) {
Write-Host @"
ERROR: Required module not installed; install from PowerShell prompt with:
>> $($_.InstallWith) -MinimumVersion $($_.Version)
"@
$missingDependencies = $true
return
}
$dep = $deps[0]
if ($dep.Version -lt $_.Version) {
Write-Host @"
ERROR: Required module installed but does not meet minimal required version:
found: $($dep.Version), required: >= $($_.Version); to fix, please run:
>> Update-Module $($_.Name) -Scope CurrentUser -RequiredVersion $($_.Version)
"@
$missingDependencies = $true
return
}
Import-Module $moduleName -MinimumVersion $_.Version
}
if ($missingDependencies) {
throw "Missing required dependencies!"
}
}
function connectAAD {
Write-Host @"
Connecting to AzureAD: Please log in, using your Power Platform / Power Platform tenant ADMIN credentials:
"@
try {
Connect-AzureAD -AzureEnvironmentName $AzureEnvironment -ErrorAction Stop | Out-Null
}
catch {
throw "Failed to login: $($_.Exception.Message)"
}
return Get-AzureADCurrentSessionInfo
}
function reconnectAAD {
# for tenantID, see DirectoryID here: https://aad.portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/Overview
try {
$session = Get-AzureADCurrentSessionInfo -ErrorAction SilentlyContinue
if ($session.Environment.Name -ne $AzureEnvironment) {
Disconnect-AzureAd
$session = connectAAD
}
}
catch [Microsoft.Open.Azure.AD.CommonLibrary.AadNeedAuthenticationException] {
$session = connectAAD
}
$tenantId = $session.TenantId
Write-Host @"
Connected to AAD tenant: $($session.TenantDomain) ($($tenantId)) in $($session.Environment.Name)
"@
return $tenantId
}
function addRequiredAccess {
param(
[System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.RequiredResourceAccess]] $requestList,
[Microsoft.Open.AzureAD.Model.ServicePrincipal[]] $spns,
[string] $spnDisplayName,
[string] $permissionName
)
Write-Host " - requiredAccess for $spnDisplayName - $permissionName"
$selectedSpns = $spns | Where-Object { $_.DisplayName -eq $spnDisplayName }
# have to build the List<ResourceAccess> item by item since PS doesn't deal well with generic lists (which is the signature for .ResourceAccess)
$selectedSpns | ForEach-Object -process {
$spn = $_
$accessList = New-Object -TypeName 'System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.ResourceAccess]'
( $spn.OAuth2Permissions `
| Where-Object { $_.Value -eq $permissionName } `
| ForEach-Object -process {
$acc = New-Object -TypeName 'Microsoft.Open.AzureAD.Model.ResourceAccess'
$acc.Id = $_.Id
$acc.Type = "Scope"
$accessList.Add($acc)
} )
Write-Verbose "accessList: $accessList"
# TODO: filter out the now-obsoleted SPN for CDS user_impersonation: id = 9f7cb6a3-2591-431e-b80d-385fce1f93aa (PowerApps Runtime), see once granted admin consent in SPN permissions
$req = New-Object -TypeName 'Microsoft.Open.AzureAD.Model.RequiredResourceAccess'
$req.ResourceAppId = $spn.AppId
$req.ResourceAccess = $accessList
$requestList.Add($req)
}
}
function calculateSecretKey {
param (
[int] $length = 32
)
$secret = [System.Byte[]]::new($length)
$rng = New-Object System.Security.Cryptography.RNGCryptoServiceProvider
# restrict to printable alpha-numeric characters
$validCharSet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
function getRandomChar {
param (
[uint32] $min = 0,
[uint32] $max = $validCharSet.length - 1
)
$diff = $max - $min + 1
[Byte[]] $bytes = 1..4
$rng.getbytes($bytes)
$number = [System.BitConverter]::ToUInt32(($bytes), 0)
$index = [char] ($number % $diff + $min)
return $validCharSet[$index]
}
for ($i = 0; $i -lt $length; $i++) {
$secret[$i] = getRandomChar
}
return $secret
}
if ($PSVersionTable.PSEdition -ne "Desktop") {
throw "This script must be run on PowerShell Desktop/Windows; the AzureAD module is not supported for PowershellCore yet!"
}
ensureModules
$ErrorActionPreference = "Stop"
$tenantId = reconnectAAD
$allSPN = Get-AzureADServicePrincipal -All $true
$requiredAccess = New-Object -TypeName 'System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.RequiredResourceAccess]'
addRequiredAccess $requiredAccess $allSPN "Microsoft Graph" "User.Read"
addRequiredAccess $requiredAccess $allSPN "PowerApps-Advisor" "Analysis.All"
addRequiredAccess $requiredAccess $allSPN "Common Data Service" "user_impersonation"
CLS
$appBaseName = $MPN_ID
$spnDisplayName = "PAL-$($appBaseName)-SPN"
Write-Verbose "Creating AAD Application: '$spnDisplayName'..."
$appId = "<dryrun-no-app-created>"
$spnId = "<dryrun-no-spn-created>"
if (!$DryRun) {
# https://docs.microsoft.com/en-us/azure/active-directory/develop/app-objects-and-service-principals
$app = New-AzureADApplication -DisplayName $spnDisplayName -PublicClient $true -ReplyUrls "urn:ietf:wg:oauth:2.0:oob" -RequiredResourceAccess $requiredAccess
$appId = $app.AppId
}
CLS
Write-Host "Created AAD Application: '$spnDisplayName' with appID $appId (objectId: $($app.ObjectId)"
$secretText = [System.Text.Encoding]::UTF8.GetString((calculateSecretKey))
Write-Verbose "Creating Service Principal Name (SPN): '$spnDisplayName'..."
$secretExpires = (get-date).Add($SecretExpiration)
if (!$DryRun) {
# display name of SPN must be same as for the App itself
# https://docs.microsoft.com/en-us/powershell/module/azuread/new-azureadserviceprincipal?view=azureadps-2.0
$spn = New-AzureADServicePrincipal -AccountEnabled $true -AppId $appId -AppRoleAssignmentRequired $true -DisplayName $spnDisplayName -Tags {WindowsAzureActiveDirectoryIntegratedApp}
$spnId = $spn.ObjectId
$spnKey = New-AzureADServicePrincipalPasswordCredential -ObjectId $spn.ObjectId -StartDate (get-date).AddHours(-1) -EndDate $secretExpires -Value $secretText
Set-AzureADServicePrincipal -ObjectId $spn.ObjectId -PasswordCredentials @($spnKey)
}
Write-Host "Created SPN '$spnDisplayName' with objectId: $spnId"
Write-Host @"
Connecting to Power Platform CRM managment API and adding appID to Power Platform tenant:
Please log in, using your Power Platform / Power Platform tenant ADMIN credentials:
"@
if (!$DryRun) {
if ($PSBoundParameters.ContainsKey("AdminUrl")) {
$adminApi = $AdminUrl
} else {
$adminApi = $adminUrls[$TenantLocation]
}
Write-Host "Admin Api is: $adminApi"
Add-PowerAppsAccount -Endpoint "prod"
$mgmtApp = New-PowerAppManagementApp -ApplicationId $appId
Write-Host @"
Added appId $($appId) to D365 tenant ($($tenantId))
"@
}
$result = [PSCustomObject] @{
TenantId = $tenantId;
ApplicationId = $appId;
ClientSecret = $secretText;
Expiration = $secretExpires;
}
Write-Output $result | Format-List
Write-host "Now Registering " $spnDisplayName " with MPN-ID " $MPN_ID
Write-host "Please Wait... "
if (!$DryRun) {
# Management Partner cmdlets for Azure Resource Manager in Windows PowerShell and PowerShell Core.
Start-Sleep -Seconds 15
# Sign in with newly created Service Principal
$SecureStringPwd = $secretText | ConvertTo-SecureString -AsPlainText -Force
$pscredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $appId, $SecureStringPwd
Connect-AzAccount -ServicePrincipal -Credential $pscredential -Tenant $tenantId
# Assign Partner ID -> example 2442485
New-AzManagementPartner -PartnerId $MPN_ID
Disconnect-AzAccount
} else {
Write-Host "If this were not a dry run a PAL Association would have happened for $spnDisplayName " with MPN-ID " $MPN_ID" -ForegroundColor Black -BackgroundColor Yellow
}