Devdeer.Azure.psm1

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
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
<#
 .Synopsis
 Removes all firewall rules currently added to the SQL server given.
 .Description
 Removes all firewall rules currently added to the SQL server given.
 .Parameter SubscriptionId
 The unique ID of the Azure subscription where the SQL Azure Server is located.
 .Parameter AzureSqlServerName
 The name of the SQL server
 .Parameter TenantId
 The unique ID of the tenant where the subscription lives in for faster context switch.
 .Example
  Clear-AzdAllSqlFirewallRules -SubscriptionId [Id] -SqlServerName mySQLServerName
  Removes all existing firewall rules from server `mySQLServerName`
#>

Function Clear-AllSqlFirewallRules
{
    param (
        [Parameter(Mandatory=$true)] [string] $SubscriptionId,
        [Parameter(Mandatory=$true)] [string] $AzureSqlServerName,        
        [Parameter(Mandatory=$false)] [string] $TenantId 
    )
    Import-Module Az
    Set-SubscriptionContext -SubscriptionId $SubscriptionId -TenantId $TenantId
    if (!$?) {
        Write-Host "Could not select subscription with id $SubscriptionId" -ForegroundColor Red
        return
    }        
    $server = Get-AzSqlServer | Where-Object -Property ServerName -EQ $AzureSqlServerName
    if (!$server) {
        Write-Host "Could not find SQL Azure Server $AzureSqlServerName in subscription $SubscriptionId" -ForegroundColor Red
        return
    }
    $existintRules = Get-AzSqlServerFirewallRule -ServerName $server.ServerName -ResourceGroupName $server.ResourceGroupName
    $amount = $existintRules.Length
    if ($amount -eq 0) {
        Write-Host "No firewall rules on server $AzureSqlServerName" -ForegroundColor Gray
        return
    }    
    Write-Host "Found $amount firewall rules on server $AzureSqlServerName"
    Write-Host "Removing no-delete-rules from resource group"
    $locks = Remove-NoDeleteLocksForResourceGroup -ResourceGroupName $server.ResourceGroupName
    foreach ($rule in $existintRules) {
        $ruleName = $rule.FirewallRuleName
        if ($ruleName -ne "AllowAllWindowsAzureIps") {
            Remove-AzSqlServerFirewallRule -ServerName $server.ServerName -ResourceGroupName $server.ResourceGroupName -FirewallRuleName $ruleName | Out-Null
            if (!$?) {
                Write-Host "Failed to remove firewall rules." -ForegroundColor Red
            }
            Write-Host "Removed rule $ruleName" -ForegroundColor Cyan
        } else {
            Write-Host "Ignoring default rule $ruleName" -ForegroundColor Gray
        }
    }    
    Write-Host "Removed all firewall rules from server $AzureSqlServerName"  -ForegroundColor Green
    Write-Host "Re-adding no-delete-rules for resource group"
    New-NoDeleteLocksForResourceGroup -ResourceGroupName $server.ResourceGroupName -Locks $locks
}

<#
 .Synopsis
 Syncs a local folder to an Azure storage account recursively.
 .Description
 The app is registered in the given tenant including reply URL and unique ID and gets access
 service principal. Optionally, permissions to external APIs can be added and granted.
 .Parameter TenantId
 The GUID of the AAD where you want to create the app registration in.
 .Parameter SubscriptionId
 The GUID of the Azure Subscription the target storage account is located at.
 .Parameter AccountName
 The unique name of the storage account.
 .Parameter ContainerName
 The name of the storage container where the files should be synced to.
 .Parameter SourceDir
 The path to the local directory or an UNC path where the files are located at.
 .Parameter ExpiryHours
 The expiration time for the SAS token in hours (defaults to 1).
 .Example
 Sync-AzdStorageContainer -TenantId 00000-00000000-000000-000 -SubscriptionId 00000-00000000-000000-000 -AccountName stoddyourname -AccountKey SECRETKEY== -ContainerName uploads -SourceDir C:\Temp\
 Sync local folder C:\temp recursively to storage container
#>

Function Copy-ToStorageContainer
{
    param (        
        [Parameter(Mandatory=$true)] [string] $TenantId,
        [Parameter(Mandatory=$true)] [string] $SubscriptionId,
        [Parameter(Mandatory=$true)] [string] $AccountName,        
        [Parameter(Mandatory=$true)] [string] $ContainerName,                                
        [Parameter(Mandatory=$true)] [string] $SourceDir,
        [int] $ExpiryHours = 1
    )
    Import-Module Az    
    # ensure that we are at the correct subscription
    Set-SubscriptionContext -Tenant $TenantId -Subscription $SubscriptionId
    # get storage context
    $ctx = New-AzStorageContext -StorageAccountName $AccountName -UseConnectedAccount
    # calculate time stamps
    $startTime = Get-Date
    $expiryTime = $startTime.AddHours($ExpiryHours)
    # use context to retrieve SAS token
    $sas = Get-AzStorageContainer -Container $ContainerName -Context $ctx | New-AzStorageContainerSASToken -Permission rwdl -StartTime $startTime -ExpiryTime $expiryTime
    # perform the sync
    azcopy sync $SourceDir "https://$AccountName.blob.core.windows.net/$ContainerName$sas" --recursive=true  
}

<#
 .Synopsis
 Replaces a function call in a JSON file with the contents of a given file.
 .Description
 It is a specialized version of Devdeer.Tools.Invoke-JsonContentReplacement which assumes that the
 JSON proviced in the remote file represents an ARM definition JSON including one JSON-section named
 `definition`.
 .Parameter File
 The path to the file in which the replacement should happen and which has the $FunctionName defined.
 .Parameter FunctionName
 The name of the function inside the $File (e.g. `[FunctionName('filePath')]`)
 .OUTPUTS
 System.String. Import-ArmTemplateJson returns the original content of the file so that the caller can undo replacement later.
 .Example
 Import-AzdArmTemplateJson -File .\parameters.json -FunctionName getJson
 Insert JSON based on the function `getJson` in the `parameters.json` file. You would use
 `[getJson('sample.json')]` to define the replace position and the source file for the JSON.
#>

Function Import-ArmTemplateJson
{
    param (        
        [Parameter(Mandatory = $true)] [string] $File,
        [Parameter(Mandatory = $true)] [string] $FunctionName,
        [bool] $MakeOneLine = $true
    )
    $ParameterFileContent = Get-Content $File
    $fileName = [regex]::Matches($ParameterFileContent, "$FunctionName\('(.*?)'").captures.groups[1].value
    $jsonContent = (Get-Content $fileName -Raw)    
    if ($MakeOneLine) {
        $jsonContent = $jsonContent.Replace("`n","").Replace(" ", "")
    }
    $jsonContent = [regex]::Matches($jsonContent, '{"definition":(.*)}{1}$').captures.groups[1].value
    $jsonContent = $jsonContent.Replace('"', '\"')
    $result = [regex]::Replace($ParameterFileContent, "[\[]$FunctionName\('(.*?)'\)[\]]", $jsonContent)
    Set-Content $File -Value $result
    return $oldContent    
}

<#
 .Synopsis
 Adds specific AAD API permissions for a single API to an existing app.
 .Description
 Adds specific AAD API permissions for a single API to an existing app. If the app does not exist the step just does nothing.
 .Parameter AppObjectId
 The Object ID of the app for which to add the permission.
 .Parameter RequiredPermissionsAppDisplayName
 Display name of a service principal (provider) in AAD which should be added to the app permissions.
 .Parameter RequiredPermissions
 Array of permissions which should be granted on the app.
 .Example
 New-AzdAppPermission -AppObjectId 00000-00000000-000000-000 -RequiredPermissionsAppDisplayName 'Microsoft Graph' -RequiredPermissions 'User.Read'
 Add permission for Graph
#>

Function New-AppPermission
{
    param (
        [Parameter(Mandatory=$true)] [string] $TenantId,
        [Parameter(Mandatory=$true)] [string] $AppObjectId,
        [Parameter(Mandatory=$true)] [string] $RequiredPermissionsAppDisplayName,
        [Parameter(Mandatory=$true)] $RequiredPermissions
    )    
    Import-Module Az
    EnsureTenantSession -TenantId $TenantId            
    $app = Get-ADApplication | Where-Object { $_.ObjectId -eq $AppObjectId }
    if ($app) {
        Add-AppPermission -app $app -permissionPrincipalDisplayName $RequiredPermissionsAppDisplayName -permissions $RequiredPermissions
    }
}

<#
 .Synopsis
 Creates an AAD app registration following DEVDEER's conventions.
 .Description
 The app is registered in the given tenant including reply URL and unique ID and gets access
 service principal. Optionally, permissions to external APIs can be added and granted.
 .Parameter TenantId
  The GUID of the AAD where you want to create the app registration in.
 .Parameter DisplayName
 A string which will appear as a human-readable name in AAD.
 .Parameter ReplyUrl
 An URI that will be used as the reply URL (aka redirect URI).
 .Parameter IdentifierUri
 A URI formatted unique identifier for the app.
 .Parameter AllowImplicitFlow
 If set to $true implicit flow (ID tokens) will be enabled.
 .Parameter RequiredPermissionsAppDisplayName
 Optional display name of a service principal (provider) in AAD which should be added to the app permissions.
 .Parameter RequiredPermissions
 Optional array of permissions which should be granted on the TargetServicePrincipalName.
 .Parameter Homepage
 Optional URI of the providers homepage.
 .Parameter LogoFilePath
 Optional URI to a local file for the logo.
 .Example
 New-AzdAppRegistration -TenantId 00000-00000000-000000-000 -DisplayName 'MyFirstApp' -ReplyUrl 'https://myapp.com/signin-oidc' -IdentifierUri 'ui://myapp.com' -LogoFilePath 'c:\temp\logo.jpg' -Homepage 'https://company.com'
 Create app without permissions
 .Example
 New-AzdAppRegistration -TenantId 00000-00000000-000000-000 -DisplayName 'MyFirstApp' -ReplyUrl 'https://myapp.com/signin-oidc' -IdentifierUri 'ui://myapp.com' -RequiredPermissionsAppDisplayName 'Microsoft Graph' -RequiredPermissions 'User.Read'
 Create app with permissions on Microsoft Graph
#>

Function New-AppRegistration
{    
    param (        
        [Parameter(Mandatory=$true)] [string] $TenantId,
        [Parameter(Mandatory=$true)] [string] $DisplayName,
        [Parameter(Mandatory=$true)] [string] $ReplyUrl,                                
        [Parameter(Mandatory=$true)] [bool] $AllowImplicitFlow,
        [Parameter(Mandatory=$false)] [bool] $GenerateSecret = $false,
        [Parameter(Mandatory=$false)] [string] $IdentifierUri,
        [Parameter(Mandatory=$false)] [string] $RequiredPermissionsAppDisplayName,
        [Parameter(Mandatory=$false)] $RequiredPermissions,
        [Parameter(Mandatory=$false)] [string] $Homepage,
        [Parameter(Mandatory=$false)] [string] $LogoFilePath
    )
    Import-Module Az
    EnsureTenantSession -TenantId $TenantId       
    $app = New-App -displayName $DisplayName -replyUrls $ReplyUrl -identifierUri $IdentifierUri -homePage $Homepage -allowImplicitFlow $AllowImplicitFlow -logoFilePath $LogoFilePath -generateSecret $GenerateSecret
    Write-Host " "        
    Out-MultiColor     -firstPart "Application " -firstColor Gray -secondPart $DisplayName -secondColor White
    Write-Host "--------------------------------------------------------" -ForegroundColor DarkGray
    Out-MultiColor     -firstPart "Client ID: " -firstColor Gray -secondPart $app.AppId -secondColor Yellow
    Remove-AppExposedScopeIfExists -app $app -scopeName 'user_impersonation'
    Add-AppExposedScope -app $app -scopeName 'full'
    Add-AppPermission -app $app -permissionPrincipalDisplayName $RequiredPermissionsAppDisplayName -permissions $RequiredPermissions
    return $app
}

<#
.Synopsis
Starts an Azure ARM Template deployment with a scope defined in the template.
.Description
This CmdLet will wrap the complete logic and preparation for a deployment in a single command. It uses New-AzResourceGroupDeployment internally.
.Parameter Stage
 The short name of the stage with a capitalized first letter (e.g. 'Test', 'Prod', 'Demo', 'Int')
.Parameter TenantId
The GUID of the Azure Tenant in which the subscription resides.
.Parameter SubscriptionId
The GUID of the subscription to which the deployment should be applied.
.Parameter ProjectName
The name of the project which will be used to build the name of the resource group and the resources. Leave this empty if your template parameter
file contane of the following keys defining the name: project-name, projectName, ProjectName, project or Project.
.Parameter ResourceGroupLocation
The Azure location for the resource group (defaults to 'West Europe').
.Parameter TemplateFile
The path to the template file (if empty the script searches for 'azuredeploy.json' in the current directory).
.Parameter TemplateParameterFile
Optional path to the template parameter file in JSON format.
.Parameter WhatIf
If set to $true a WhatIf-deployment fill be performed.
.Example
New-AzdArmDeployment -Stage Test -TenantId 00000-00000-00000 -SubscriptionId 000000-00000-000000-00000 -WhatIf -TemplateFile c:\temp\azuredeploy.json
Execute an ARM deployment for the Test stage using a deployment file in c:\temp folder
#>

Function New-ArmDeployment {
    param (        
        [Parameter(Mandatory = $true)] [string] $Stage,        
        [Parameter(Mandatory = $true)] [string] $TenantId,
        [Parameter(Mandatory = $true)] [string] $SubscriptionId,
        [string] $ProjectName,
        [string] $Location = "West Europe",
        [string] $TemplateFile = '.\azuredeploy.json',
        [string] $TemplateParameterFile,
        [switch] $WhatIf 
    )
    Import-Module Az
    try {
        [Microsoft.Azure.Common.Authentication.AzureSession]::ClientFactory.AddUserAgent("VSAzureTools-$UI$($host.name)".replace(' ', '_'), '3.0.0')
    } 
    catch {
    }    
    Set-StrictMode -Version 3
    # check if deployment file exists
    $exists = Test-Path $TemplateFile -PathType Leaf
    if (!$exists) {
        throw "File $TemplateFile not found." 
    }                
    # check if parameter file exists
    $parametersExists = Test-Path $TemplateParameterFile -PathType Leaf    
    # build deployment name
    $DeploymentName = ((Get-ChildItem $TemplateFile).BaseName + '-' + ((Get-Date).ToUniversalTime()).ToString('MMdd-HHmm'));
    # ensure Azure context
    Write-Output 'Setting Azure context...'
    Set-SubscriptionContext -Subscription $SubscriptionId -Tenant $TenantId
    if (!$?) {
        return
    }
    # build resource group name
    if (!$WhatIf) {
        # no WHATIF
        if ($parametersExists -eq $true) {
            Write-Output "Starting template deployment with template $TemplateFile and parameter file $TemplateParameterFile ..."        
            New-AzDeployment `
                -Name $DeploymentName `
                -Location $Location `
                -TemplateFile $TemplateFile `
                -TemplateParameterFile $TemplateParameterFile `
                -Verbose `
                -ErrorVariable ErrorMessages
        }
        else {
            Write-Output "Starting template deployment with template $TemplateFile ..."    
            New-AzDeployment `
                -Name $DeploymentName `
                -Location $Location `
                -TemplateFile $TemplateFile `
                -Verbose `
                -ErrorVariable ErrorMessages
        }
    } else {
        # WHATIF
        if ($parametersExists -eq $true) {
            Write-Output "Starting WHATIF template deployment with template $TemplateFile and parameter file $TemplateParameterFile ..."    
            New-AzDeployment `
                -Name $DeploymentName `
                -Location $Location `
                -TemplateFile $TemplateFile `
                -TemplateParameterFile $TemplarametersFile `
                -Verbose `
                -ErrorVariable ErrorMessages
                -WhatIf
        }
        else {
            Write-Output "Starting WHATIF template deployment with template $TemplateFile ..."
            New-AzDeployment `
                -Name $DeploymentName `
                -Location $Location `
                -TemplateFile $TemplateFile `
                -Verbose `
                -ErrorVariable ErrorMessages
                -WhatIf
        }    
    }
    if ($ErrorMessages) {
        Write-Output '', 'Template deployment returned the following errors:', @(@($ErrorMessages) | ForEach-Object { $_.Exception.Message.TrimEnd("`r`n") })
        return -1
    }
    return 1
}

<#
.Synopsis
Starts an Azure ARM Template deployment for a single resource group.
.Description
This CmdLet will wrap the complete logic and preparation for a deployment in a single command. It uses New-AzResourceGroupDeployment internally.
.Parameter Stage
The short name of the stage with a capitalized first letter (e.g. 'Test', 'Prod', 'Demo', 'Int')
.Parameter TenantId
The GUID of the Azure Tenant in which the subscription resides.
.Parameter SubscriptionId
The GUID of the subscription to which the deployment should be applied.
.Parameter ResourceGroupName
Optional
.Parameter DeleteOnFailure
Indicates if the resource group should be deleted on any error.
.Parameter DoNotSetResourceGroupLock
Indicates if a no-delete-lock should NOT be applied to the ressource group.
.Parameter ProjectName
The name of the project which will be used to build the name of the resource group and the resources. Leave this empty if your template parameter
file contains one of the following keys defining the name: project-name, projectName, ProjectName, project or Project.
.Parameter ResourceGroupLocation
The Azure location for the resource group (defaults to 'West Europe').
.Parameter TemplateFile
The path to the template file (ARM-JSON or BICEP) (if empty the script searches for 'azuredeploy.(json)' in the current directory).
.Parameter TemplateParameterFile
Optional path to the template parameter file in JSON format (if empty the script searches for the matching file built from template file and stage).
.Parameter WhatIf
If set to $true a WhatIf-deployment fill be performed.
.Example
New-AzdArGroupmDeployment -Stage Test -TenantId 00000-00000-00000 -SubscriptionId 000000-00000-000000-00000 -WhatIf -TemplateFile c:\temp\azuredeploy.json
Execute an ARM deployment for the Test stage using a deployment file in c:\temp folder
#>

Function New-ArmGroupDeployment {
    param (        
        [Parameter(Mandatory = $true)] [string] $Stage,        
        [Parameter(Mandatory = $true)] [string] $TenantId,
        [Parameter(Mandatory = $true)] [string] $SubscriptionId,
        [string] $ResourceGroupName,
        [switch] $DoNotSetResourceGroupLock,
        [switch] $DeleteOnFailure,
        [string] $ProjectName,
        [string] $ResourceGroupLocation = "West Europe",
        [string] $TemplateFile = '.\azuredeploy.json',
        [string] $TemplateParameterFile,
        [switch] $WhatIf 
    )
    Import-Module Az
    try {
        [Microsoft.Azure.Common.Authentication.AzureSession]::ClientFactory.AddUserAgent("VSAzureTools-$UI$($host.name)".replace(' ', '_'), '3.0.0')
    } 
    catch {
    }    
    Set-StrictMode -Version 3
    # check if deployment file exists
    $exists = Test-Path $TemplateFile -PathType Leaf
    if (!$exists) {
        throw "File $TemplateFile not found." 
    }
    # get base directory and parameter file name for stage
    $item = Get-Item $TemplateFile 
    $path = $item.DirectoryName
    $fileName = $item.Name.Substring(0, $item.Name.Length - $item.Extension.Length)    
    if ($TemplateParameterFile.Length -eq 0) {
        $TemplateParameterFile = "$path\$fileName.parameters.$Stage.json"
    }
    # check if parameter file exists
    $exists = Test-Path $TemplateParameterFile -PathType Leaf
    if (!$exists) {
        throw "File $TemplateParameterFile not found." 
    }
    # build deployment name
    $DeploymentName = ((Get-ChildItem $TemplateFile).BaseName + '-' + ((Get-Date).ToUniversalTime()).ToString('MMdd-HHmm'));
    # ensure Azure context
    Write-Output 'Setting Azure context...'
    Set-SubscriptionContext -Subscription $SubscriptionId -Tenant $TenantId
    if (!$?) {
        return
    }
    # build resource group name
    Write-Output 'Determing resource group name...'    
    if ($ProjectName.Length -eq 0) {
        # try to read project name from the parameter file
        Write-Host "Trying to read project name from $TemplateParameterFile"
        $json = Get-Content $TemplateParameterFile -Raw  | ConvertFrom-Json            
        $projectNameKeys = @( "project-name", "projectName", "ProjectName", "project", "Project" )
        foreach ($key in $projectNameKeys) {
            $tmp = $null
            try {
                $tmp = $json.parameters.$key.value
            }
            catch {
                Write-Host "Key '$key' not found in parameter file" -ForegroundColor Gray
            }
            if ($tmp) {
                Write-Host "Found entry with key '$key' and value '$tmp'"
                $ProjectName = $tmp
                break
            }
        }
    }
    if ($ProjectName.Length -eq 0) {    
        throw "Project name not found in parameter file."
    }
    if ($ResourceGroupName.Length -eq 0) {
        # build resource group name
        $ResourceGroupName = "rg-$ProjectName-$Stage".ToLowerInvariant()    
    }
    Write-Output "Using resource group name $ResourceGroupName."        
    if (!$WhatIf) {
        # Create the resource group only when it doesn't already exist
        if ($null -eq (Get-AzResourceGroup -Name $ResourceGroupName -Location $ResourceGroupLocation -Verbose -ErrorAction SilentlyContinue)) {
            Write-Output "Creating resource group $ResourceGroupName..."
            New-AzResourceGroup -Name $ResourceGroupName -Location $ResourceGroupLocation -Tag @{ purpose = $Stage } -Verbose -Force -ErrorAction Stop 
            Write-Output "Resource group created."
        
            if (!$DoNotSetResourceGroupLock) {
                # caller wants us to ensure the delete lock
                Write-Output "Set No-Delete-Lock for $ResourceGroupName..."
                New-AzResourceLock -LockName no-delete -LockLevel CanNotDelete -ResourceGroupName $ResourceGroupName -Force -ErrorAction Stop
                Write-Output "No-Delete Lock is set."
            }
        } 
        else {
            Write-Output "Resource group $ResourceGroupName already exists."
        }        
        Write-Output "Starting template deployment with template $TemplateParameterFile ..."
        try {
            New-AzResourceGroupDeployment `
                -Name $DeploymentName `
                -ResourceGroupName $ResourceGroupName `
                -TemplateFile $TemplateFile `
                -TemplateParameterFile $TemplateParameterFile `
                -Force -Verbose `
                -ErrorVariable ErrorMessages
        }
        catch {
            if ($DeleteOnFailure) {                
                Write-Host "Are you sure you want to delete the resource group now? (y/n)" -NoNewline
                $hostInput = Read-Host
                if ($hostInput -eq 'y') {
                    # user wants us to delete the resource group if the deployment failed
                    Write-Host "Removing resource group $ResourceGroupName..."
                    Get-AzResourceLock -ResourceGroupName $ResourceGroupName -AtScope | Remove-AzResourceLock -Force
                    Remove-AzResourceGroup -Name $ResourceGroupName -Force
                    Write-Host "Resource group deleted" -ForegroundColor Green
                }
            }
        }
    }
 else {
        Write-Output "Starting template WHATIF with template $TemplateParameterFile ..."
        New-AzResourceGroupDeployment `
            -Name $DeploymentName `
            -ResourceGroupName $ResourceGroupName `
            -TemplateFile $TemplateFile `
            -TemplateParameterFile $TemplateParameterFile `
            -ErrorVariable ErrorMessages `
            -WhatIf
    }
    if ($ErrorMessages) {
        Write-Output '', 'Template deployment returned the following errors:', @(@($ErrorMessages) | ForEach-Object { $_.Exception.Message.TrimEnd("`r`n") })
        return -1
    }
    return 1
}

<#
 .Synopsis
 Ensures that a no-delete lock is directly on a resource group in Azure and creates one if none is existing.
 .Description
 Removes all firewall rules currently added to the SQL server given.
 .Parameter SubscriptionId
 The unique ID of the Azure subscription where the SQL Azure Server is located.
 .Parameter ResourceGroupName
 The name of the resource group
 .Parameter ResourceGroupLocation
 The Azure location of the resource group.
 .Parameter TenantId
 The unique ID of the tenant where the subscription lives in for faster context switch.
 .Example
  Set-AzdResourceGroupDeleteLock -SubscriptionId [Id] -ResourceGroupName rg-test -ResourceGroupLocation westeurope
  Ensures that a no-delete lock is directly on the resource group.
#>

Function New-ResourceGroupDeleteLock 
{
    param (
        [Parameter(Mandatory=$true)] [string] $SubscriptionId,
        [Parameter(Mandatory=$true)] [string] $ResourceGroupName,        
        [Parameter(Mandatory=$true)] [string] $ResourceGroupLocation,
        [Parameter(Mandatory=$false)] [string] $TenantId
    )
    if ($null -ne (Get-AzResourceGroup -Name $ResourceGroupName -Location $ResourceGroupLocation -Verbose -ErrorAction SilentlyContinue)) {                        
        if ($null -eq (Get-AzResourceLock -AtScope -ResourceGroupName $ResourceGroupName | Where-Object { $_.Properties.level -eq "CanNotDelete" })) {
            Write-Host "Set No-Delete-Lock for $ResourceGroupName..." -ForegroundColor Gray
            New-AzResourceLock -LockName nodelete -LockLevel CanNotDelete -ResourceGroupName $ResourceGroupName -Force -ErrorAction Stop | Out-Null
            Write-Host "Delete Lock is set." -ForegroundColor Green
        } else {
            Write-Host "Found exisiting no-delete-lock on $ResourceGroupName." -ForegroundColor Gray
        }
    } else {
        Write-Host "Resource group $ResourceGroupName not found." -ForegroundColor Red        
    }
}

<#
 .Synopsis
 Adds a firewall rule to an Azure SQL Server for a single IP.
 .Description
 Adds a firewall rule to an Azure SQL Server for a single IP.
 .Parameter SubscriptionId
 The unique ID of the Azure subscription where the SQL Azure Server is located.
 .Parameter AzureSqlServerName
 The name of the SQL server
 .Parameter TenantId
 The unique ID of the tenant where the subscription lives in for faster context switch.
 .Parameter IpAddress
 The IP address for which to set the rule. If omitted the current machine public IP will be determined and used.
 .Example
 New-AzdSqlFirewallRule -SubscriptionId [ID] -SqlServerName mySQLServerName
 Add firewall rule for current IP
#>

Function New-SqlFirewallRule
{
    param (
        [Parameter(Mandatory=$true)] [string] $SubscriptionId,
        [Parameter(Mandatory=$true)] [string] $AzureSqlServerName,        
        [Parameter(Mandatory=$false)] [string] $TenantId,
        [Parameter(Mandatory=$false)] [string] $IpAddress
    )
    Import-Module Az
    Set-SubscriptionContext -SubscriptionId $SubscriptionId -TenantId $TenantId
    if (!$?) {
        Write-Host "Could not select subscription with id $SubscriptionId" -ForegroundColor Red
        return
    }
    if (!$IpAddress) {
        $IpAddress = (Invoke-WebRequest -uri "https://ifconfig.me/ip").Content    
    }    
    Write-Host "Current IP address is $IpAddress"
    $server = Get-AzSqlServer | Where-Object -Property ServerName -EQ $AzureSqlServerName
    if (!$server) {
        Write-Host "Could not find SQL Azure Server $AzureSqlServerName in subscription $SubscriptionId" -ForegroundColor Red
        return
    }
    $existintRule = Get-AzSqlServerFirewallRule -ServerName $server.ServerName -ResourceGroupName $server.ResourceGroupName | Where-Object -Property StartIpAddress -EQ $IpAddress
    if ($existintRule) {
        $ruleName = $existintRule.FirewallRuleName
        Write-Host "Firewall rule for your IP $IpAddress already exists in server $AzureSqlServerName : $ruleName" -ForegroundColor Red
        return
    }
    $ruleName = "ClientIpAddress_" + (Get-Date).ToString("yyyy_MM_dd_HH_mm_ss")
    New-AzSqlServerFirewallRule -ServerName $server.ServerName -ResourceGroupName $server.ResourceGroupName -FirewallRuleName $ruleName -StartIpAddress $IpAddress -EndIpAddress $IpAddress
    if (!$?) {
        Write-Host "Failed to add SQL Server firewall rule" -ForegroundColor Red
    }
    Write-Host "Firewall rule with name $ruleName successfully created on Azure SQL Server $AzureSqlServerName for IP $IpAddress"  -ForegroundColor Green
}

<#
 .Synopsis
 Ensures that a given subscription id is the currently selected context in Az.
 .Description
 Optimizes the way the context switcj is happening by only performing the costly operation if the
 current context differs from the one which is currently in the context.
 .Parameter SubscriptionId
 The unique ID of the Azure subscription where the SQL Azure Server is located.
 .Parameter TenantId
 The unique ID of the tenant where the subscription lives in for faster context switch.
 .Example
 Set-AzdSubscriptionContext -SubscriptionId 12345 -TenantId 56789
 Ensures that the context is on subscription with ID 12345 with tenant 56789
 .Example
 Set-AzdSubscriptionContext -SubscriptionId 12345
 Ensures that the context is on subscription with ID 12345 without defining the tenant ID.
#>

Function Set-SubscriptionContext
{
    param (
        [Parameter(Mandatory=$true)] [string] $SubscriptionId,
        [Parameter(Mandatory=$false)] [string] $TenantId
    )
    Import-Module Az
    $current = Get-AzContext 
    if (!$?) {
        Write-Host "Could not retrieve current Azure context. Maybe perform Login-AzAccount first." -ForegroundColor Red
        return
    }
    if ($current.Subscription.Id -eq $SubscriptionId) {
        Write-Host "Subscription already set to $SubscriptionId."
        return
    }
    if ($TenantId) {
        Set-AzContext -Subscription $SubscriptionId -Tenant $TenantId
    } else     {
        Set-AzContext -Subscription $SubscriptionId
    }
    if (!$?) {
        Write-Host "Could not set current subscription to $SubscriptionId." -ForegroundColor Red
        return
    }
    Write-Host "Current subscription is $SubscriptionId now."
}

Function Add-AppExposedScope
{
    param (
        [Parameter(Mandatory=$true)] $app,
        [Parameter(Mandatory=$true)] [string] $scopeName
    )    
    $permissions = $app.Oauth2Permissions 
    $perm = ($permissions | Where-Object { $_.Value -eq $scopeName })
    if (!$perm) {
        $perm = New-Object -TypeName "Microsoft.Open.AzureAD.Model.OAuth2Permission"
        $perm.Type = 'User'
        $perm.UserConsentDisplayName = 'Grant access to ' + $app.DisplayName
        $perm.UserConsentDescription = 'Do you really want to give ' + $app.DisplayName + ' access?'
        $perm.AdminConsentDisplayName = 'Grant access to ' + $app.DisplayName
        $perm.AdminConsentDescription = 'Do you really want to give ' + $app.DisplayName + ' access?'
        $perm.Value = $scopeName
        $perm.Id = [guid]::NewGuid() 
        $permissions.Add($perm)
        Set-ADApplication -ObjectId $app.ObjectId -Oauth2Permissions $permissions
        Write-Output "Scope '$scopeName' defined"
    }
}

Function Add-AppPermission
{
    param (
        [Parameter(Mandatory=$true)] $app,
        [Parameter(Mandatory=$true)] [string] $permissionPrincipalDisplayName,
        [Parameter(Mandatory=$true)] $permissions
    )    
    $requiredResources = $app.RequiredResourceAccess    
    $remotePrincipal = Get-ADServicePrincipal -Filter "DisplayName eq '$permissionPrincipalDisplayName'"    
    foreach($permission in $permissions) {
        $requiredPermission = $remotePrincipal.Oauth2Permissions | Where-Object { $_.Value -eq $permission } 
        if ($requiredPermission) {
            $newAccess = New-Object -TypeName "Microsoft.Open.AzureAD.Model.ResourceAccess"
            $newAccess.Id = $requiredPermission.Id
            $newAccess.Type = 'Scope' 
            $newAccessRequired = New-Object -TypeName "Microsoft.Open.AzureAD.Model.RequiredResourceAccess"
            $newAccessRequired.ResourceAppId = $remotePrincipal.AppId    
            $newAccessRequired.ResourceAccess = $newAccess
            Out-MultiColorExtended -firstPart "Permissions for API " -firstColor Gray -secondPart $permissionPrincipalDisplayName -secondColor Yellow -thirdPart " with " -thirdColor Gray -fourthPart $permission -fourthColor Yellow -NoNewline
            if (!($requiredResources | Where-Object { $_.ResourceAppId -eq $remotePrincipal.AppId  -and $_.ResourceAccess.Id -eq $requiredPermission.Id })) {
                $requiredResources.Add($newAccessRequired)            
                Set-ADApplication -ObjectId $app.ObjectId -RequiredResourceAccess $requiredResources                
                Out-MultiColor -firstPart " -> " -firstColor Gray -secondPart "added" -secondColor Green
            } else {
                Out-MultiColor -firstPart " -> " -firstColor Gray -secondPart "existing" -secondColor Yellow
            }
        }
    }
}

Function Create-App
{
    param (
        [Parameter(Mandatory=$true)] $displayName,
        [Parameter(Mandatory=$true)] $replyUrls,
        [Parameter(Mandatory=$true)] [bool] $allowImplicitFlow,
        [Parameter(Mandatory=$false)] [string] $identifierUri,
        [Parameter(Mandatory=$false)] [string] $homePage,        
        [Parameter(Mandatory=$false)] [string] $logoFilePath,
        [Parameter(Mandatory=$false)] [bool] $generateSecret

    )    
    if (!(Get-ADApplication -SearchString $displayName)) {   
        if ([string]::IsNullOrEmpty($identifierUri)) {
            # the user didn't set an identifier URI so it is built from the scheme api://{AppId}
            $app = New-ADApplication -DisplayName $displayName -ReplyUrls $replyUrls -Homepage $homePage -Oauth2AllowImplicitFlow $allowImplicitFlow
            $identifierUri = "api://" + $app.AppId
            Write-Host $identifierUri -ForegroundColor Cyan
            Set-ADApplication -ObjectId $app.ObjectId -IdentifierUris $identifierUri
        } else {
            $app = New-ADApplication -DisplayName $displayName -ReplyUrls $replyUrls -IdentifierUris $identifierUri -Homepage $homePage -Oauth2AllowImplicitFlow $allowImplicitFlow
        }
        New-ADServicePrincipal -AppId $app.AppId | Out-Null     
        if ($app) {
            if ($GenerateSecret) {
                $secret = New-AppSecret -app $app
                Write-Host "🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥"                 
                Out-MultiColor     -firstPart "Client secret: " -firstColor Gray -secondPart $secret -secondColor Green                
                Write-Host "🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥"
            }        
            if (!([string]::IsNullOrEmpty($logoFilePath))) {
                Set-ADApplicationLogo -ObjectId $app.ObjectId -FilePath $logoFilePath | Out-Null        
            }
        }
    } else {
        $app = Get-ADApplication -SearchString $displayName        
    }    
    return $app
}

Function New-AppSecret
{
    param (
        [Parameter(Mandatory=$true)] $app        
    )
    $cred = New-ADApplicationPasswordCredential -ObjectId $app.ObjectId -StartDate ([DateTime]::Now) -EndDate ([DateTime]::Now.AddYears(100))    
    return $cred.Value
}

Function New-NoDeleteLocksForResourceGroup
{
    param (
        [Parameter(Mandatory=$true)] [string] $ResourceGroupName,
        [Parameter(Mandatory=$true)] [object[]] $Locks
    )    
    foreach ($lock in $Locks) {    
        if ($lock.Properties.level -eq "CanNotDelete") {
            New-AzResourceLock -LockName $lock.Name -LockLevel CanNotDelete -ResourceGroupName $ResourceGroupName -Force | Out-Null
        }
    }    
}

Function Out-MultiColor
{
    param (
        [string] $firstPart,
        [ConsoleColor] $firstColor,
        [string] $secondPart,
        [ConsoleColor] $secondColor,
        [switch] $NoNewline
    )
    Write-Host $firstPart -NoNewLine -ForegroundColor $firstColor
    if ($NoNewline) {
        Write-Host $secondPart -NoNewline -ForegroundColor $secondColor
    } else {
        Write-Host $secondPart -ForegroundColor $secondColor
    }
}

Function Out-MultiColorExtended
{
    param (
        [string] $firstPart,
        [ConsoleColor] $firstColor,
        [string] $secondPart,
        [ConsoleColor] $secondColor,
        [string] $thirdPart,
        [ConsoleColor] $thirdColor,
        [string] $fourthPart,
        [ConsoleColor] $fourthColor,
        [switch] $NoNewline
    )
    Write-Host $firstPart -NoNewLine -ForegroundColor $firstColor
    Write-Host $secondPart -NoNewline -ForegroundColor $secondColor
    Write-Host $thirdPart -NoNewLine -ForegroundColor $thirdColor
    if ($NoNewline) {
        Write-Host $fourthPart -NoNewline -ForegroundColor $fourthColor
    } else {
        Write-Host $fourthPart -ForegroundColor $fourthColor
    }
}

Function Remove-AppExposedScopeIfExists
{
    param (
        [Parameter(Mandatory=$true)] $app,
        [Parameter(Mandatory=$true)] [string] $scopeName
    )
    $permissions = $app.Oauth2Permissions    
    $perm = ($permissions | Where-Object { $_.Value -eq $scopeName })
    if ($perm) {
        $perm.IsEnabled = $false
        Set-ADApplication -ObjectId $app.ObjectId -Oauth2Permissions $permissions
        $permissions.Remove($perm);
        Set-ADApplication -ObjectId $app.ObjectId -Oauth2Permissions $permissions
    }
}


Function Remove-NoDeleteLocksForResourceGroup
{
    param (
        [Parameter(Mandatory=$true)] [string] $ResourceGroupName
    )
    $locks = Get-AzResourceLock -ResourceGroupName $ResourceGroupName 
    foreach ($lock in $locks) {    
        if ($lock.Properties.level -eq "CanNotDelete") {
            Remove-AzResourceLock -LockName $lock.Name -ResourceGroupName $ResourceGroupName -Force | Out-Null
        }
    }
    return $locks
}

Export-ModuleMember -Function Clear-AllSqlFirewallRules
Export-ModuleMember -Function Copy-ToStorageContainer
Export-ModuleMember -Function Import-ArmTemplateJson
Export-ModuleMember -Function New-AppPermission
Export-ModuleMember -Function New-AppRegistration
Export-ModuleMember -Function New-ArmDeployment
Export-ModuleMember -Function New-ArmGroupDeployment
Export-ModuleMember -Function New-ResourceGroupDeleteLock
Export-ModuleMember -Function New-SqlFirewallRule
Export-ModuleMember -Function Set-SubscriptionContext