Nerdio Manager is now deployed to your Azure subscription and needs to be initialized.
Launch Azure Cloud Shell and run the command below. Be sure that you're logged into Azure as a
Global Administrator and subscription Owner.
& ([ScriptBlock]::Create((Invoke-RestMethod 'https://nwp-web-app.azurewebsites.net/api/package/7.5.2/script/install/cloudshell' -Method POST -Body '{"SubscriptionId":"5d4206f2-8af2-479a-b7f9-5fb434457d5f","ResourceGroupName":"rg-sam-migration-uksouth-001","WebAppName":"nmw-app-zhf2mzp3jf"}' -ContentType 'application/json').script))
$sourceUri = "https://nwmstorageaccount.blob.core.windows.net/nwm-packages/package.7.5.3.zip?skoid=82e3c668-0e13-4157-9931-dfb99f48d53f&sktid=7c851afb-a179-4bca-861c-bfcea2a046c7&skt=2026-01-22T19%3A22%3A51Z&ske=2026-01-29T19%3A22%3A51Z&sks=b&skv=2025-05-05&sv=2025-05-05&st=2026-01-22T19%3A22%3A51Z&se=2026-01-22T21%3A22%3A51Z&sr=b&sp=r&sig=QLBBb3wAHoXlBFB%2BX2vm0SevFwPA0BJDQyZ1HcQpGqY%3D"
$subscriptionId = "5d4206f2-8af2-479a-b7f9-5fb434457d5f"
$resourceGroupName = "rg-sam-migration-uksouth-001"
$webAppName = "nmw-app-zhf2mzp3jf"
# Required params
# Path to App Service
#$subscriptionId = ''
#$resourceGroupName = ''
#$webAppName = ''
# Azure cloud type (AzureCloud|AzureUSGovernment)
#$azureEnv = 'AzureCloud'
# Link to app package to install
#$sourceUri = ''
# Permissions we add to add app
$defaultDelegatedPermissions = 'User.Read|User.ReadBasic.All|User.Read.All|Group.Read.All|Application.Read.All|Organization.Read.All'
# artifacts mode
$artifactsMode = 'Local'
# One of:
# $appName & $splitIdentityTenantId (optional)
# of
# $appId & $appSecret & servicePrincipalObjectId
# should be specified
# Name of AD application
#$appName = 'nerdio-nmw-app'
# TenantId for split-identity deployment (optional)
#$splitIdentityTenantId = ''
# AppId and AppSecret of existing AD application
#$appId = ''
#$appSecret = ''
#$servicePrincipalObjectId = ''
$ErrorActionPreference = 'Stop'
$ProgressPreference = 'SilentlyContinue'
if (-not $azureEnv) {
$azureEnv = (Get-AzContext).Environment.Name
}
if (-not $appName) {
$appName = 'nerdio-nmw-app'
}
$useExistingApp = $appId -and $appSecret -and $servicePrincipalObjectId
[Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12
$webAppUrl = 'https://' + $webAppName + '.azurewebsites.net/'
$microsoftLogin = 'https://login.microsoftonline.com'
$mgmtUri = "https://management.azure.com"
$scmUriSuffix = ".scm.azurewebsites.net"
$databaseScope = "https://database.windows.net"
if ($azureEnv -eq "AzureUSGovernment") {
$webAppUrl = 'https://' + $webAppName + '.azurewebsites.us/'
$microsoftLogin = 'https://login.microsoftonline.us'
$mgmtUri = "https://management.usgovcloudapi.net"
$scmUriSuffix = ".scm.azurewebsites.us"
$databaseScope = "https://database.usgovcloudapi.net"
}
$mgEnvironment = switch ($azureEnv) {"AzureCloud" {"Global"}; "AzureUSGovernment" {"USGov"}}
$mgScopes = @("User.Read", "Application.ReadWrite.All", "AppRoleAssignment.ReadWrite.All")
$loginUrl = $webAppUrl + 'signin-oidc'
$logoutUrl = $webAppUrl + 'signout-oidc'
$setting_RoleAuthorization = 'RoleAuthorization:Enabled'
$setting_CumulativeRbac = 'Features:CumulativeRbac'
$setting_AzureADTenantId = 'AzureAD:TenantId'
$setting_AzureADClientId = 'AzureAD:ClientId'
$setting_AzureADDefaultGraphScopes = 'AzureAD:DefaultGraphScopes'
$setting_WVDAadTenantId = 'WVD:AadTenantId'
$setting_WVDSubscriptionId = 'WVD:SubscriptionId'
$setting_BillingMode = 'Billing:Mode'
$setting_ArtifactsMode = "Artifacts:Mode"
$setting_LocalAdminDisableAfterProvisioning = "Deployment:FallbackOptions:DefaultLocalAdmin:DisableAccount"
$setting_LocalAdminGeneratePassword = "Deployment:FallbackOptions:DefaultLocalAdmin:RandomPassword"
$keyVault_AzureADClientSecret = 'AzureAD--ClientSecret'
$keyVault_ConnectionString = 'ConnectionStrings--DefaultConnection'
$wvdAdminRoleId = 'd1c2ade8-98f8-45fd-aa4a-6d06b947c66f'
# Installing required modules
$requiredModules = @(
@{ name = "Az.Automation"; maxVersion = "1.9.0"; environments = @("AzureUSGovernment") },
@{ name = "Az.Automation" },
@{ name = "Az.Resources" },
@{ name = "Az.Websites" },
@{ name = "Az.KeyVault" },
@{ name = "Az.Sql" },
@{ name = "Microsoft.Graph.Applications"; maxVersion = "2.25.0" },
@{ name = "Microsoft.Graph.Users"; maxVersion = "2.25.0" },
@{ name = "SqlServer" }
)
foreach ($module in $requiredModules) {
if ((-not $module.environments) -or ($azureEnv -in $module.environments)) {
$available = Get-Module $module.name -ListAvailable
if ($module.maxVersion) {
if (-not ($available | Where-Object { [System.Version]$_.Version -le [System.Version]$module.maxVersion })) {
Write-Host "Installing $($module.name) module ($($module.maxVersion))"
Get-Module $module.name | Remove-Module
Install-Module $module.name -MaximumVersion $module.maxVersion -Scope CurrentUser -Repository PSGallery -AllowClobber -Force
}
}
elseif (-not $available) {
Write-Host "Installing $($module.name) module"
Install-Module $module.name -Scope CurrentUser -Repository PSGallery -AllowClobber -Force
}
}
}
foreach ($module in $requiredModules) {
if (((-not $module.environments) -or ($azureEnv -in $module.environments)) -and (-not (Get-Module $module.name))) {
Import-Module $module.name -MaximumVersion $module.maxVersion -Force
}
}
# Adds the requiredAccesses (expressed as a pipe separated string) to the requiredAccess structure
# The exposed permissions are in the $exposedPermissions collection, and the type of permission (Scope | Role) is
# described in $permissionType
Function AddResourcePermission($requiredAccess, $exposedPermissions, [string]$requiredAccesses, [string]$permissionType)
{
foreach ($permission in $requiredAccesses.Trim().Split("|")) {
foreach ($exposedPermission in $exposedPermissions) {
if ($exposedPermission.Value -eq $permission) {
$resourceAccess = @{
Id = $exposedPermission.Id;
Type = $permissionType; # Scope = Delegated permissions | Role = Application permissions
}
$requiredAccess.ResourceAccess += , $resourceAccess
}
}
}
}
# Example: GetRequiredPermissions "00000003-0000-0000-c000-000000000000" "Graph.Read|User.Read"
Function GetRequiredPermissions([string]$appId, [string]$requiredDelegatedPermissions, [string]$requiredApplicationPermissions, $servicePrincipal)
{
# If we are passed the service principal we use it directly, otherwise we find it from the display name (which might not be unique)
if ($servicePrincipal) {
$sp = $servicePrincipal
}
else {
$sp = Get-MgServicePrincipal -Filter "AppId eq '$appId'"
}
$requiredAccess = @{ ResourceAppId = $sp.AppId; ResourceAccess = @() }
# $sp.Oauth2Permissions | Select Id,AdminConsentDisplayName,Value: To see the list of all the Delegated permissions for the application:
if ($requiredDelegatedPermissions) {
AddResourcePermission $requiredAccess -exposedPermissions $sp.Oauth2PermissionScopes -requiredAccesses $requiredDelegatedPermissions -permissionType "Scope"
}
# $sp.AppRoles | Select Id,AdminConsentDisplayName,Value: To see the list of all the Application permissions for the application
if ($requiredApplicationPermissions) {
AddResourcePermission $requiredAccess -exposedPermissions $sp.AppRoles -requiredAccesses $requiredApplicationPermissions -permissionType "Role"
}
return $requiredAccess
}
function CreateOrUpdateApplication() {
Write-Host 'Get NME application'
$application = Get-MgApplication -Filter ("DisplayName eq '" + $appName + "'")
if (!$application) {
Write-Host 'NME application not found'
Write-Host 'Creating NME application'
# Create application
$application = New-MgApplication `
-DisplayName $appName `
-Web @{
HomePageUrl = $webAppUrl;
LogoutUrl = $logoutUrl;
RedirectUris = @($webAppUrl, $loginUrl);
ImplicitGrantSettings = @{
EnableAccessTokenIssuance = $true
EnableIdTokenIssuance = $true
}
} `
-SignInAudience "AzureADMultipleOrgs"
Write-Host 'NME application created'
} else {
Write-Host 'Updating NME application'
if (-not $application.Web.RedirectUris.Contains($webAppUrl)) {
$application.Web.RedirectUris = $application.Web.RedirectUris + $webAppUrl
}
if (-not $application.Web.RedirectUris.Contains($loginUrl)) {
$application.Web.RedirectUris = $application.Web.RedirectUris + $loginUrl
}
# Update application
Update-MgApplication `
-ApplicationId $application.Id `
-Web @{
HomePageUrl = $webAppUrl;
LogoutUrl = $logoutUrl;
RedirectUris = $application.Web.RedirectUris;
ImplicitGrantSettings = @{
EnableAccessTokenIssuance = $true
EnableIdTokenIssuance = $true
}
} `
-SignInAudience "AzureADMultipleOrgs"
Write-Host 'NME application updated'
}
return $application
}
function SetupRoles($app) {
function BuildAppRole([string] $displayName, [string] $value, [Guid] $id, [string] $description, [string] $allowedMemberType) {
return @{
DisplayName = $displayName;
Description = $description;
Value = $value;
Id = $id;
IsEnabled = $true;
AllowedMemberTypes = @($allowedMemberType);
}
}
function GetNewAppRoles() {
$reviewer = BuildAppRole -displayName "Reviewer" -value "Reviewer" -id "0a1b7425-f55a-44a6-9caa-b9a5cc9448da" -description "View access to all areas of NME; no ability to save or make changes." -allowedMemberType "User"
$helpDesk = BuildAppRole -displayName "Help Desk" -value "HelpDesk" -id "a94e83da-b314-4232-b8c8-94508c5ed533" -description "Complete access to User sessions only." -allowedMemberType "User"
$endUser = BuildAppRole -displayName "End-user" -value "EndUser" -id "e856de81-1e53-486a-8668-7d564866ae39" -description "View & manage own sessions (Message, Log off, Disconnect). Personal desktop users can restart, power off and power their personal desktops." -allowedMemberType "User"
$desktopAdmin = BuildAppRole -displayName "Desktop Admin" -value "DesktopAdmin" -id "ed0cdef0-4267-4470-bfff-5e0b6944f9e4" -description "Complete access to User sessions, ability to view Host Pools, hosts and restart them, but no ability to add/remove or change any settings." -allowedMemberType "User"
$wvdAdmin = BuildAppRole -displayName "WVD Admin" -value "WvdAdmin" -id $wvdAdminRoleId -description "Complete access to all areas of NME." -allowedMemberType "User"
$restClient = BuildAppRole -displayName "Rest client" -value "RestClient" -id "3807160f-e77a-4fcf-959a-df572bcc3767" -description "Rest client" -allowedMemberType "Application"
return @($reviewer, $helpDesk, $endUser, $desktopAdmin, $wvdAdmin, $restClient)
}
Write-Output 'Setup Roles'
$appRoles = GetNewAppRoles
Update-MgApplication -ApplicationId $app.Id -AppRoles $appRoles
Write-Output 'Setup Roles - Completed'
}
function GetAppSetting() {
filter ListToHash
{
begin { $hash = @{} }
process { $hash[$_.Name] = $_.Value }
end { return $hash }
}
$webApp = Get-AzWebApp -ResourceGroupName $resourceGroupName -Name $webAppName
$webApp.SiteConfig.AppSettings | ListToHash
}
function SetAppSetting([object] $settings) {
Set-AzWebApp -ResourceGroupName $resourceGroupName -Name $webAppName -AppSettings $settings | Out-Null
}
function ConfigureApiPermissions($app, $azureEnv) {
Write-Output 'Configure API permissions'
# Microsoft Graph
$graphPermissions = GetRequiredPermissions `
-appId "00000003-0000-0000-c000-000000000000" `
-requiredApplicationPermissions "Group.Read.All|GroupMember.Read.All|User.Read.All|Organization.Read.All" `
-requiredDelegatedPermissions "$defaultDelegatedPermissions|AppRoleAssignment.ReadWrite.All|Application.ReadWrite.All|Mail.Send|offline_access|openid|profile"
$rmPermissions = $null
if ($azureEnv -eq 'AzureCloud') {
# Windows Azure Service Management API, Azure Resource Manager
$rmPermissions = GetRequiredPermissions -appId "797f4846-ba00-4fd7-ba43-dac1f8f63013" -requiredDelegatedPermissions "user_impersonation"
}
if ($azureEnv -eq 'AzureUSGovernment') {
# Azure Government Cloud Management API
$rmPermissions = GetRequiredPermissions -appId "40a69793-8fe6-4db1-9591-dbc5c57b17d8" -requiredDelegatedPermissions "user_impersonation"
}
Update-MgApplication -ApplicationId $app.Id -RequiredResourceAccess @($graphPermissions, $rmPermissions)
Write-Output 'Configure API permissions - Completed'
}
function ConfigureAppSecret($app) {
# Check if app secret exists in key vault
$keyVaultValue = Get-AzKeyVaultSecret -VaultName $keyVaultName -Name $keyVault_AzureADClientSecret
if (!$keyVaultValue) {
# Create app secret
Write-Host 'Creating secret key for NME application'
$appSecret = Add-MgApplicationPassword -ApplicationId $app.Id -PasswordCredential @{ StartDateTime = ([DateTime]::UtcNow); EndDateTime = ([DateTime]::UtcNow.AddYears(10)) }
Write-Host 'Secret key for NME application created'
# Add app secret to key vault
Write-Host 'Saving secret key to key vault'
Set-AzKeyVaultSecret -VaultName $keyVaultName -Name $keyVault_AzureADClientSecret -SecretValue (ConvertTo-SecureString -String $appSecret.SecretText -AsPlainText -Force)
Write-Host 'Secret key saved'
}
}
function Convert-SecureStringToPlainText($secureString) {
$ptr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureString)
try {
return [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($ptr)
} finally {
[System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($ptr)
}
}
function Get-AzureToken([string] $scope) {
$tokenParams = @{}
if ($scope) {
$tokenParams.ResourceUrl = $scope
}
if (-not (Get-Command Get-AzAccessToken).Parameters.AsSecureString) {
return (Get-AzAccessToken @tokenParams).Token
}
return Convert-SecureStringToPlainText (Get-AzAccessToken -AsSecureString @tokenParams).Token
}
function Get-AuthHeader {
return 'Bearer {0}' -f (Get-AzureToken)
}
function InstallPackage([string] $packageUri, [string] $subscriptionId, [string] $tenantId, [string] $resourceGroupName, [string] $webAppName) {
function Get-AuthInfo {
Param(
[Parameter(Mandatory = $true)]
[string]$SubscriptionId,
[Parameter(Mandatory = $true)]
[string]$ResourceGroupName,
[Parameter(Mandatory = $true)]
[string]$Name
)
$apiUri = "$mgmtUri/subscriptions/"+ $SubscriptionId +"/resourceGroups/"+$ResourceGroupName+"/providers/Microsoft.Web/sites/"+$Name+"/publishxml?api-version=2016-08-01"
$result = Invoke-RestMethod -Uri $apiUri -Headers @{Authorization = Get-AuthHeader} -Method POST -ContentType "application/json" -Body @{format = "WebDeploy"}
[xml]$publishSettings = $result.InnerXml
$website = $publishSettings.SelectSingleNode("//publishData/publishProfile[@publishMethod='MSDeploy']")
$username = $webSite.userName
$password = $webSite.userPWD
$base64AuthInfo = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes(("{0}:{1}" -f $username,$password)))
return $base64AuthInfo
}
function Get-ApiUri {
Param(
[Parameter(Mandatory = $true)]
[string]$Name,
[Parameter(Mandatory = $true)]
[string]$Method
)
$apiUri ="https://" + $Name + $scmUriSuffix + "/api/" + $Method
return $apiUri
}
function Stop-WebAppJob {
Param(
[Parameter(Mandatory = $true)]
[string]$Name,
[Parameter(Mandatory = $true)]
[string]$AuthInfo
)
$apiUri = Get-ApiUri -Name $Name -Method "jobs/continuous/provision/stop"
Invoke-RestMethod -Uri $apiUri -Headers @{Authorization=("Basic {0}" -f $AuthInfo)} -Method Post -DisableKeepAlive -ContentType ''
}
function Start-WebAppJob {
Param(
[Parameter(Mandatory = $true)]
[string]$Name,
[Parameter(Mandatory = $true)]
[string]$AuthInfo
)
$apiUri = Get-ApiUri -Name $Name -Method "jobs/continuous/provision/start"
Invoke-RestMethod -Uri $apiUri -Headers @{Authorization=("Basic {0}" -f $AuthInfo)} -Method Post -DisableKeepAlive -ContentType ''
}
function Publish-WebApp {
Param(
[Parameter(Mandatory = $true)]
[string]$ArchivePath,
[Parameter(Mandatory = $true)]
[string]$Name,
[Parameter(Mandatory = $true)]
[string]$AuthInfo
)
$apiUri = Get-ApiUri -Name $Name -Method "publish?type=zip&clean=True"
$timeOutSec = 900
Invoke-RestMethod -Uri $apiUri -Headers @{Authorization=("Basic {0}" -f $AuthInfo)} -Method POST -InFile $ArchivePath -ContentType "multipart/form-data" -TimeoutSec $timeOutSec
}
if ($env:AZUREPS_HOST_ENVIRONMENT) {
$folderName = (New-Guid).ToString()
$packageZipPath = Join-Path -Path $Home -ChildPath ($folderName + ".zip")
$packageDestPath = Join-Path -Path $Home -ChildPath ($folderName)
}
else {
$packageZipPath = Join-Path -Path $env:TEMP -ChildPath ((New-Guid).ToString() + '.zip')
$packageDestPath = Join-Path -Path $env:TEMP -ChildPath (New-Guid)
}
$packageDestVersionPath = Join-Path -Path $packageDestPath -ChildPath 'version.txt'
$packageDestAppPath = Join-Path -Path $packageDestPath -ChildPath 'app.zip'
$packageScriptsPath = Join-Path -Path $packageDestPath -ChildPath 'nwm-scripts.psm1'
Write-Output 'Downloading package'
Invoke-WebRequest -Uri $packageUri -OutFile $packageZipPath
$size = (Get-Item -Path $packageZipPath).Length
Write-Output "Package downloaded: $size bytes"
Expand-Archive -Path $packageZipPath -DestinationPath $packageDestPath
if (Test-Path -Path $packageDestVersionPath)
{
Write-Output "Package info"
Get-Content -Path $packageDestVersionPath | Write-Output
}
Import-Module -Name $packageScriptsPath
NWM-Before-Publish -rg $resourceGroupName -appName $webAppName
Write-Output "Get App Service $webAppName"
$appService = Get-AzWebApp -ResourceGroupName $resourceGroupName -Name $webAppName
$appService.Id
$appService = Set-AzWebApp -ResourceGroupName $resourceGroupName -Name $webAppName -FtpsState Disabled -HttpsOnly $true
$authInfo = Get-AuthInfo -SubscriptionId $subscriptionId -ResourceGroupName $resourceGroupName -Name $webAppName
Write-Output "Stopping provision web job.."
Stop-WebAppJob -AuthInfo $authInfo -Name $webAppName
Write-Output "Stopping web app..."
Stop-AzWebApp -ResourceGroupName $resourceGroupName -Name $webAppName | Out-Null
Write-Output "Publishing..."
Publish-WebApp -ArchivePath $packageDestAppPath -AuthInfo $authInfo -Name $webAppName -Verbose
Write-Output "Starting App Service..."
Start-AzWebApp -ResourceGroupName $resourceGroupName -Name $webAppName | Out-Null
Start-Sleep -s 30
Write-Output "Starting provision web job.."
Start-WebAppJob -AuthInfo $authInfo -Name $webAppName
NWM-After-Publish -rg $resourceGroupName -appName $webAppName
}
function MakeUserOwner($servicePrincipal, $userId) {
$userAssignment = Get-MgServicePrincipalOwner -ServicePrincipalId $servicePrincipal.Id -All | Where-Object { $_.Id -eq $userId }
if (!$userAssignment) {
# Assign user owner role to app
Write-Host 'Assign user owner role to NME app'
New-MgServicePrincipalOwnerByRef `
-ServicePrincipalId $servicePrincipal.Id `
-BodyParameter @{ "@odata.id" = "https://graph.microsoft.com/v1.0/directoryObjects/$userId" }
Write-Host 'Assign user owner role to NME app - done'
}
}
function AssignAppRoleToUser($servicePrincipal, $userId, $roleId) {
$userAssignment = Get-MgUserAppRoleAssignment -UserId $userId -All | Where-Object { $_.ResourceId -eq $servicePrincipal.Id -and $_.AppRoleId -eq $roleId }
if (!$userAssignment) {
# Assign user to NME NFA application role
Write-Host 'Assign user to NME application role'
New-MgUserAppRoleAssignment -UserId $userId -PrincipalId $userId -ResourceId $servicePrincipal.Id -AppRoleId $roleId
Write-Host 'User assigned to the NME application'
}
}
function CreateServicePrincipalIfNotExists($application) {
Write-Host 'Get service principal for NME application'
$servicePrincipal = Get-MgServicePrincipal -Filter ("AppId eq '" + $application.AppId + "'")
if (!$servicePrincipal) {
Write-Host 'Service principal not found'
Write-Host 'Create service principal for the NME application'
$servicePrincipal = New-MgServicePrincipal -AppId $application.AppId -Tags {WindowsAzureActiveDirectoryIntegratedApp}
Write-Host 'Waiting 1 minute for service principal creation'
Start-Sleep -s 60
Write-Host 'Service principal created'
}
return $servicePrincipal
}
function AddArmRole($objectId, $scope, $roleName) {
$role = Get-AzRoleAssignment -ObjectId $objectId -Scope $scope | Where-Object {$_.Scope -eq $scope -and $_.RoleDefinitionName -eq $roleName }
if (!$role) {
New-AzRoleAssignment -ObjectId $objectId -Scope $scope -RoleDefinitionName $roleName
}
}
function GetUser($userId) {
$u = $null
try
{
$u = Get-MgUser -UserId $userId
}
catch
{
}
if (!$u) {
$u = Get-MgUser -Filter "Mail eq '$($userId)'"
}
if (!$u) { throw "Unable find user in Entra ID: $userId" }
return $u
}
function CreateCertificateForScriptedActions([string] $keyVaultName) {
$certName = 'nmw-scripted-action-cert'
$cert = Get-AzKeyVaultCertificate -VaultName $keyVaultName -Name $certName
if ($null -eq $cert) {
Write-Host 'Create new certificate'
Write-Host $certName
$monthsUntilExpired = 120
$certSubjectName = "cn=" + $certName
$Policy = New-AzKeyVaultCertificatePolicy -SecretContentType "application/x-pkcs12" -SubjectName $certSubjectName -IssuerName "Self" -ValidityInMonths $monthsUntilExpired -ReuseKeyOnRenewal
$AddAzureKeyVaultCertificateStatus = Add-AzKeyVaultCertificate -VaultName $keyVaultName -Name $certName -CertificatePolicy $Policy
While ($AddAzureKeyVaultCertificateStatus.Status -eq "inProgress") {
Start-Sleep -s 10
$AddAzureKeyVaultCertificateStatus = Get-AzKeyVaultCertificateOperation -VaultName $keyVaultName -Name $certName
}
if ($AddAzureKeyVaultCertificateStatus.Status -ne "completed") {
throw "Key vault cert creation is not successful and its status is: $status.Status"
}
Write-Host 'Create new certificate - done'
} else {
Write-Host 'Certificate already exists'
}
Write-Host 'Exporting certificate'
$secretRetrieved = Get-AzKeyVaultSecret -VaultName $keyVaultName -Name $certName
$secretInPlainText = Convert-SecureStringToPlainText $secretRetrieved.SecretValue
$pfxBytes = [System.Convert]::FromBase64String($secretInPlainText)
$certCollection = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2Collection
$certCollection.Import($pfxBytes, $null, [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable)
if ($env:AZUREPS_HOST_ENVIRONMENT) {
$pfxPath = Join-Path $Home ($certName + ".pfx")
}
else {
$pfxPath = Join-Path $env:TEMP ($certName + ".pfx")
}
Remove-Item $pfxPath -Force -ErrorAction SilentlyContinue
#Export the .pfx file
$certPassword = [Guid]::NewGuid().ToString().Substring(0, 8) + "!"
$protectedCertificateBytes = $certCollection.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pkcs12, $certPassword)
[System.IO.File]::WriteAllBytes($pfxPath, $protectedCertificateBytes)
return @{ pfxPath = $pfxPath; password = $certPassword; }
}
function CreateScriptedActionCertificateAsset([string] $rg, [string] $automationAccount, [string] $pfxPath, [string] $password) {
$assetName = 'ScriptedActionRunAsCert'
$certPassword = ConvertTo-SecureString $password -AsPlainText -Force
Remove-AzAutomationCertificate -ResourceGroupName $rg -automationAccountName $automationAccount -Name $assetName -ErrorAction SilentlyContinue | Out-Null
New-AzAutomationCertificate -ResourceGroupName $rg -automationAccountName $automationAccount -Path $pfxPath -Name $assetName -Password $certPassword -Exportable | Out-Null
}
function SetAppCertificate([string] $appObjectId, [string] $pfxPath, [string] $password) {
$cer = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList @($pfxPath, $password)
$cerHash = [System.Convert]::ToBase64String($cer.GetCertHash())
$existingKeys = (Get-MgApplication -ApplicationId $appObjectId).KeyCredentials
if ($existingKeys | Where-Object { [System.Convert]::ToBase64String($_.CustomKeyIdentifier) -eq $cerHash }) {
return
}
$newKey = @{
keyId = (New-Guid).ToString();
type = "AsymmetricX509Cert";
usage = "Verify";
key = [System.Text.Encoding]::UTF8.GetBytes([Convert]::ToBase64String($cer.GetRawCertData()));
}
Update-MgApplication -ApplicationId $appObjectId -KeyCredentials ($existingKeys + $newKey)
}
function Set-AzKeyVaultAccessPolicyWorkaround($TenantId, $SubscriptionId, $ResourceGroupName, $KeyVaultName, $ObjectId, $SecretPermissions, $CertificatePermissions) {
$baseUri = (Get-AzContext).Environment.ResourceManagerUrl
$apiUri = $baseUri + "/subscriptions/" + $SubscriptionId + "/resourceGroups/" +
$ResourceGroupName + "/providers/Microsoft.KeyVault/vaults/" + $KeyVaultName + "/accessPolicies/add?api-version=2022-07-01"
$body = @{
properties = @{
accessPolicies = @(
@{
tenantId = $TenantId
objectId = $ObjectId
permissions = @{
secrets = $SecretPermissions
certificates = $CertificatePermissions
}
}
)
}
}
$body = $body | ConvertTo-Json -Depth 10
Invoke-RestMethod -Uri $apiUri -Headers @{Authorization = Get-AuthHeader} -Method PUT -ContentType "application/json" -Body $body | Out-Null
}
function Unlock-KeyVaultNetworkAccess([string]$keyVaultName) {
$keyVault = Get-AzKeyVault -ResourceGroupName $resourceGroupName -VaultName $keyVaultName
if ($keyVault.PublicNetworkAccess -eq "Disabled") {
Write-Host "Configuring Key Vault to allow temporary access"
$keyVault | Update-AzKeyVaultNetworkRuleSet -IPAddressRange $localIp -DefaultAction Deny -Bypass None
$keyVault | Update-AzKeyVault -PublicNetworkAccess Enabled | Out-Null
Write-Host "Key Vault is now configured to allow access from the current network. This will be reverted after the script completes"
return $true
}
return $false
}
function Lock-KeyVaultNetworkAccess([string]$keyVaultName) {
Write-Host "Disabling public network access to Key Vault"
Update-AzKeyVault -ResourceGroupName $resourceGroupName -VaultName $keyVaultName -PublicNetworkAccess Disabled | Out-Null
Update-AzKeyVaultNetworkRuleSet -ResourceGroupName $resourceGroupName -VaultName $keyVaultName -Bypass None | Out-Null
Write-Host "Key Vault public access has been disabled"
}
function Test-IpAddressInRange($ipAddress, $fromAddress, $toAddress) {
$ip = [System.Net.IPAddress]::Parse($ipAddress).GetAddressBytes()
[Array]::Reverse($ip)
$ip = [System.BitConverter]::ToUInt32($ip, 0)
$from = [System.Net.IPAddress]::Parse($fromAddress).GetAddressBytes()
[Array]::Reverse($from)
$from = [System.BitConverter]::ToUInt32($from, 0)
$to = [System.Net.IPAddress]::Parse($toAddress).GetAddressBytes()
[Array]::Reverse($to)
$to = [System.BitConverter]::ToUInt32($to, 0)
return $from -le $ip -and $ip -le $to
}
function ConfigureSqlServer($servicePrincipal) {
$spName = $servicePrincipal.DisplayName
if ($spName.Contains("[") -or $spName.Contains("]") -or $spName.Contains("'")) {
throw "Service Principal name contains invalid characters"
}
Write-Host "Get SQL server"
$sqlServer = Get-AzSqlServer -ResourceGroupName $resourceGroupName -ServerName $sqlServerName
$sqlServer.ServerName
Write-Host "Get database name"
$databaseName = Get-AzSqlDatabase -ResourceGroupName $resourceGroupName -ServerName $sqlServerName | Where-Object DatabaseName -ne master | Select-Object -ExpandProperty DatabaseName -First 1
$databaseName
Write-Host "Make the current user as SQL server administrator"
Set-AzSqlServerActiveDirectoryAdministrator `
-ResourceGroupName $resourceGroupName `
-ServerName $sqlServerName `
-DisplayName $user.DisplayName `
-ObjectId $user.Id
# ----- Enable Public Network Access -----
$hardenSqlServer = $sqlServer.PublicNetworkAccess -eq "Disabled"
if ($hardenSqlServer) {
Write-Host "Temporary allow public network access"
$sqlServer | Set-AzSqlServer -PublicNetworkAccess Enabled | Out-Null
}
try {
# ----- Add current IP to firewall ------
$firewallRules = Get-AzSqlServerFirewallRule `
-ResourceGroupName $resourceGroupName `
-ServerName $sqlServerName
if (-not ($firewallRules | Where-Object { Test-IpAddressInRange $localIp $_.StartIpAddress $_.EndIpAddress })) {
New-AzSqlServerFirewallRule `
-ResourceGroupName $resourceGroupName `
-ServerName $sqlServerName `
-FirewallRuleName $localIp `
-StartIpAddress $localIp `
-EndIpAddress $localIp
}
# ----- Configure roles for service principal ------
Write-Host "Add SQL roles"
$sqlToken = Get-AzureToken $databaseScope
$query = @"
IF NOT EXISTS (SELECT 1 FROM sys.database_principals WHERE name = '$($spName)')
BEGIN
CREATE USER [$($spName)] FROM EXTERNAL PROVIDER;
END
ALTER ROLE db_ddladmin ADD MEMBER [$($spName)];
ALTER ROLE db_datareader ADD MEMBER [$($spName)];
ALTER ROLE db_datawriter ADD MEMBER [$($spName)];
"@
Invoke-Sqlcmd `
-ConnectionString "Data Source=tcp:$($sqlServer.FullyQualifiedDomainName),1433;Initial Catalog=$databaseName;Persist Security Info=False;Multiple Active Result Sets=False;Connect Timeout=30;Encrypt=True;Trust Server Certificate=False" `
-AccessToken $sqlToken `
-Query $query
}
finally {
if ($hardenSqlServer) {
Write-Host "Restore disabled public network access"
$sqlServer | Set-AzSqlServer -PublicNetworkAccess Disabled | Out-Null
}
}
# ----- Update connection string -----
Write-Host "Get app secret"
$secretRetrieved = Get-AzKeyVaultSecret -VaultName $keyVaultName -Name $keyVault_AzureADClientSecret
$secretInPlainText = Convert-SecureStringToPlainText $secretRetrieved.SecretValue
Write-Output "Update database connection string"
$connectionString = "Server=tcp:$($sqlServer.FullyQualifiedDomainName),1433;Initial Catalog=$databaseName;Persist Security Info=False;User ID=$appId;Password=$secretInPlainText;MultipleActiveResultSets=False;Authentication=Active Directory Service Principal;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;"
Set-AzKeyVaultSecret `
-VaultName $keyVaultName `
-Name $keyVault_ConnectionString `
-SecretValue (ConvertTo-SecureString -String $connectionString -AsPlainText -Force) | Out-Null
}
if ($env:AZUREPS_HOST_ENVIRONMENT) {
if ($splitIdentityTenantId) {
throw "Cannot deploy Split Identity configuration in CloudShell. Please run the script locally"
}
}
else {
Clear-AzContext -Scope CurrentUser
Write-Output 'Login to Azure Resource Manager (deployment subscription)'
Connect-AzAccount -Environment $azureEnv
}
Set-AzContext -Subscription $subscriptionId
# cache token
Get-AuthHeader | Out-Null
$deploymentAzContext = Get-AzContext
$subscription = Get-AzSubscription -SubscriptionId $subscriptionId
$tenantId = $subscription.TenantId
if ($env:AZUREPS_HOST_ENVIRONMENT) {
$userId = (az ad signed-in-user show | ConvertFrom-Json).id
}
else {
$userId = $deploymentAzContext.Account.Id
}
$identityTenantId = $tenantId
$wvdTenantId = $tenantId
$wvdSubscriptionId = $subscriptionId
Write-Output "Tenant $tenantId"
Write-Output 'Login to Azure Resource Manager - Completed'
Write-Output 'Get resource group'
$rg = Get-AzResourceGroup -Name $resourceGroupName
$rg.ResourceId
Write-Output 'Get NME App Service'
$appService = Get-AzWebApp -ResourceGroupName $resourceGroupName -Name $webAppName
$appService.Id
Write-Output 'Get KeyVault from app service setting'
$keyVaultName = ($appService.SiteConfig.AppSettings | Where-Object {$_.Name -eq 'Deployment:KeyVaultName'} | Select-Object -First 1)[0].Value
$keyVaultName
Write-Output 'Get Automation account for updates installation from app service setting'
$automationAccountName = ($appService.SiteConfig.AppSettings | Where-Object {$_.Name -eq 'Deployment:AutomationAccountName'}).Value
$automationAccountName
Write-Output 'Get Automation account for scripted actions from app service setting'
$scriptedActionAccountId = ($appService.SiteConfig.AppSettings | Where-Object {$_.Name -eq 'Deployment:ScriptedActionAccount'}).Value
$scriptedActionAccountName = ($scriptedActionAccountId -split '/')[-1]
$scriptedActionAccountName
Write-Output 'Get SQL server from app service setting'
$sqlServerId = ($appService.SiteConfig.AppSettings | Where-Object {$_.Name -eq 'Deployment:SqlServerId'}).Value
if ($sqlServerId) {
$sqlServerName = ($sqlServerId -split '/')[-1]
$sqlServerName
}
else {
Write-Output "Deployment:SqlServerId is not found. Entra ID access will not be configured for SQL server"
}
Write-Output "Get client IP address"
$localIp = (Invoke-RestMethod -Uri "https://ipinfo.io/json").ip
$localIp
if (-not $useExistingApp) {
Write-Output ''
Write-Output 'Login to Microsoft Entra ID (deployment tenant)'
if ($env:AZUREPS_HOST_ENVIRONMENT) {
Connect-MgGraph -Identity -Environment $mgEnvironment -NoWelcome
}
else {
Connect-MgGraph -TenantId $tenantId -Environment $mgEnvironment -Scopes $mgScopes -NoWelcome
}
Write-Output 'Login to Microsoft Entra ID - Completed'
$user = GetUser -userId $userId
# Grant permissions to current user to the key vault
Write-Output "Granting permissions to key vault to current user: $($user.UserPrincipalName)"
$secretPermissions = @("Get", "Set", "List", "Delete")
$certificatePermissions = @("Get", "List", "Delete", "Create")
Set-AzKeyVaultAccessPolicyWorkaround `
-TenantId (Get-AzContext).tenant.id `
-SubscriptionId $subscriptionId `
-ResourceGroupName $resourceGroupName `
-KeyVaultName $keyVaultName `
-ObjectId $user.Id `
-SecretPermissions $secretPermissions `
-CertificatePermissions $certificatePermissions
# Set-AzKeyVaultAccessPolicy -VaultName $keyVaultName -ResourceGroupName $resourceGroupName -UserPrincipalName $user.UserPrincipalName -PermissionsToSecrets Get,Set,List,Delete -PermissionsToCertificates Get,List,Delete,Create
Write-Output 'Granting permissions - Completed'
Write-Output ''
$application = CreateOrUpdateApplication
$appId = $application.AppId
SetupRoles -app $application
ConfigureApiPermissions -app $application -azureEnv $azureEnv
$hardenKeyVault = Unlock-KeyVaultNetworkAccess -keyVaultName $keyVaultName
try {
ConfigureAppSecret -app $application
$deploymentSP = CreateServicePrincipalIfNotExists -application $application
$identitySP = $deploymentSP
MakeUserOwner -servicePrincipal $deploymentSP -userId $user.Id
if (-not $env:AZUREPS_HOST_ENVIRONMENT) {
Write-Host 'Request admin consent'
Start-Process -FilePath "$microsoftLogin/$tenantId/adminconsent?client_id=$($application.AppId)"
}
Write-Output 'Create certificate for scripted actions'
$scriptedActionsCert = CreateCertificateForScriptedActions -keyVaultName $keyVaultName
if ($sqlServerId) {
ConfigureSqlServer $deploymentSP
}
}
finally {
if ($hardenKeyVault) {
Lock-KeyVaultNetworkAccess -keyVaultName $keyVaultName
}
}
Write-Output 'Create certificate asset in automation account'
CreateScriptedActionCertificateAsset -rg $resourceGroupName -automationAccount $scriptedActionAccountName -pfxPath $scriptedActionsCert.pfxPath -password $scriptedActionsCert.password
SetAppCertificate -appObjectId $application.Id -pfxPath $scriptedActionsCert.pfxPath -password $scriptedActionsCert.password
Remove-Item $scriptedActionsCert.pfxPath -Force -ErrorAction SilentlyContinue
if ($splitIdentityTenantId) {
$loginResult2 = $null
try {
Write-Host 'Login to Azure Resource Manager (identity tenant)'
$loginResult2 = Connect-AzAccount -Tenant $splitIdentityTenantId -Environment $azureEnv
} catch {
$error[0].Exception
continue
}
Write-Host 'Select WVD subscription'
$subscription = Get-AzSubscription -TenantId $loginResult2.Context.Tenant.TenantId | Out-GridView -PassThru -Title "Select subscription" -Verbose
Set-AzContext -SubscriptionObject $subscription
Write-Host 'Login to Microsoft Entra ID (identity tenant)'
Connect-MgGraph -TenantId $splitIdentityTenantId -Environment $mgEnvironment -Scopes $mgScopes
$user = GetUser -userId $loginResult2.Context.Account.Id
$identityTenantId = $subscription.TenantId
$wvdTenantId = $subscription.TenantId
$wvdSubscriptionId = $subscription.SubscriptionId
$identitySP = CreateServicePrincipalIfNotExists -application $application
MakeUserOwner -servicePrincipal $identitySP -userId $user.Id
Write-Host 'Request admin consent'
Start-Process -FilePath "$microsoftLogin/$splitIdentityTenantId/adminconsent?client_id=$($application.AppId)"
Write-Host 'Add reader to subscription'
AddArmRole -objectId $identitySP.Id -scope "/subscriptions/$($subscription.SubscriptionId)" -roleName "Reader"
Write-Host 'Add Backup Reader to subscription'
AddArmRole -objectId $identitySP.Id -scope "/subscriptions/$($subscription.SubscriptionId)" -roleName "Backup Reader"
}
Update-MgServicePrincipal -ServicePrincipalId $identitySP.Id -AppRoleAssignmentRequired
AssignAppRoleToUser -servicePrincipal $identitySP -userId $user.Id -roleId $wvdAdminRoleId
$deploymentServicePrincipalId = $deploymentSP.Id
}
else {
$deploymentServicePrincipalId = $servicePrincipalObjectId
#$userAccount = $deploymentAzContext.Account
Write-Output ''
Write-Output 'Login to Microsoft Entra ID (deployment tenant)'
if ($env:AZUREPS_HOST_ENVIRONMENT) {
Connect-MgGraph -Identity -Environment $mgEnvironment -NoWelcome
}
else {
Connect-MgGraph -TenantId $tenantId -Environment $mgEnvironment -Scopes $mgScopes -NoWelcome
}
Write-Output 'Login to Microsoft Entra ID - Completed'
$user = GetUser -userId $userId
# Grant permissions to current user to the key vault
Write-Output "Granting permissions to key vault to current user: $($user.UserPrincipalName)"
$secretPermissions = @("Get", "Set", "List", "Delete")
$certificatePermissions = @("Get", "List", "Delete", "Create")
Set-AzKeyVaultAccessPolicyWorkaround `
-TenantId (Get-AzContext).tenant.id `
-SubscriptionId $subscriptionId `
-ResourceGroupName $resourceGroupName `
-KeyVaultName $keyVaultName `
-ObjectId $user.Id `
-SecretPermissions $secretPermissions `
-CertificatePermissions $certificatePermissions
#Set-AzKeyVaultAccessPolicy -VaultName $keyVaultName -ResourceGroupName $resourceGroupName -UserPrincipalName $userAccount.Id -PermissionsToSecrets Get,Set,List,Delete -PermissionsToCertificates Get,List,Delete,Create
Write-Output 'Granting permissions - Completed'
$hardenKeyVault = Unlock-KeyVaultNetworkAccess -keyVaultName $keyVaultName
$scriptedActionsCert = $null
try {
Write-Output 'Save app secret to KeyVault'
Set-AzKeyVaultSecret -VaultName $keyVaultName -Name $keyVault_AzureADClientSecret -SecretValue (ConvertTo-SecureString -String $appSecret -AsPlainText -Force)
Write-Output 'Create certificate for scripted actions'
$scriptedActionsCert = CreateCertificateForScriptedActions -keyVaultName $keyVaultName
if ($sqlServerId) {
Write-Output 'Get deployment service principal'
$deploymentSP = Get-MgServicePrincipal -ServicePrincipalId $deploymentServicePrincipalId
ConfigureSqlServer $deploymentSP
}
}
finally {
if ($hardenKeyVault) {
Lock-KeyVaultNetworkAccess -keyVaultName $keyVaultName
}
}
Write-Output 'Create certificate asset in automation account'
CreateScriptedActionCertificateAsset -rg $resourceGroupName -automationAccount $scriptedActionAccountName -pfxPath $scriptedActionsCert.pfxPath -password $scriptedActionsCert.password
Remove-Item $scriptedActionsCert.pfxPath -Force -ErrorAction SilentlyContinue
}
Set-AzContext -Context $deploymentAzContext
Write-Output ''
Write-Output "Granting 'secret get' permission to key vault to service principal"
$secretPermissions = @("Get")
$certificatePermissions = @()
Set-AzKeyVaultAccessPolicyWorkaround `
-TenantId (Get-AzContext).tenant.id `
-SubscriptionId $subscriptionId `
-ResourceGroupName $resourceGroupName `
-KeyVaultName $keyVaultName `
-ObjectId $deploymentServicePrincipalId `
-SecretPermissions $secretPermissions `
-CertificatePermissions $certificatePermissions
# Set-AzKeyVaultAccessPolicy -VaultName $keyVaultName -ResourceGroupName $resourceGroupName -ObjectId $deploymentServicePrincipalId -PermissionsToSecrets Get
Write-Host 'Add reader to subscription'
AddArmRole -objectId $deploymentServicePrincipalId -scope "/subscriptions/$subscriptionId" -roleName "Reader"
Write-Host 'Add Contributor to resource group'
AddArmRole -objectId $deploymentServicePrincipalId -scope $rg.ResourceId -roleName "Contributor"
Write-Host 'Add Backup Reader to subscription'
AddArmRole -objectId $deploymentServicePrincipalId -scope "/subscriptions/$subscriptionId" -roleName "Backup Reader"
$appSettings = GetAppSetting
$appSettings[$setting_RoleAuthorization] = 'True'
$appSettings[$setting_CumulativeRbac] = 'True'
$appSettings[$setting_AzureADClientId] = $appId
$appSettings[$setting_AzureADTenantId] = $identityTenantId
$appSettings[$setting_AzureADDefaultGraphScopes] = $defaultDelegatedPermissions
$appSettings[$setting_WVDAadTenantId] = $wvdTenantId
$appSettings[$setting_WVDSubscriptionId] = $wvdSubscriptionId
$appSettings[$setting_BillingMode] = "MAU"
$appSettings[$setting_ArtifactsMode] = $artifactsMode
$appSettings[$setting_LocalAdminDisableAfterProvisioning] = 'True'
$appSettings[$setting_LocalAdminGeneratePassword] = 'True'
SetAppSetting -settings $appSettings
Write-Output 'Install latest package'
InstallPackage -packageUri $sourceUri -subscriptionId $subscriptionId -tenantId $tenantId -resourceGroupName $resourceGroupName -webAppName $webAppName