Devdeer.Azure.psm1
######################################################################################## # # Devdeer.Azure Module # # -------------------------------------------------------------------------------------- # # DEVDEER GmbH # Alexander Schmidt # # -------------------------------------------------------------------------------------- # # Provides helpers for Microsoft Azure. # ######################################################################################## Function EnsureTenantSession { param ( [Parameter(Mandatory=$true)] [string] $tenantId ) try { $currentSession = Get-ADCurrentSessionInfo -ErrorAction SilentlyContinue } catch [Microsoft.Open.Azure.AD.CommonLibrary.AadNeedAuthenticationException] { Write-Output "Login needed" } if (!$currentSession -or $currentSession.TenantId -ne $tenantId) { Connect-AD -TenantId $tenantId } } Function CreateOrRetrieveApp { 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 = CreateSecret -app $app Write-Host "🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥🔥" WriteMultiColor -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 CreateSecret { param ( [Parameter(Mandatory=$true)] $app ) $cred = New-ADApplicationPasswordCredential -ObjectId $app.ObjectId -StartDate ([DateTime]::Now) -EndDate ([DateTime]::Now.AddYears(100)) return $cred.Value } Function AddPermission { 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 WriteMultiColorExtended -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 WriteMultiColor -firstPart " -> " -firstColor Gray -secondPart "added" -secondColor Green } else { WriteMultiColor -firstPart " -> " -firstColor Gray -secondPart "existing" -secondColor Yellow } } } } Function RemoveExposedScopeIfExists { 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 AddExposedScope { 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 WriteMultiColor { 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 WriteMultiColorExtended { 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-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 } Function Add-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 } } } <# .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." } <# .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 = CreateOrRetrieveApp -displayName $DisplayName -replyUrls $ReplyUrl -identifierUri $IdentifierUri -homePage $Homepage -allowImplicitFlow $AllowImplicitFlow -logoFilePath $LogoFilePath -generateSecret $GenerateSecret Write-Host " " WriteMultiColor -firstPart "Application " -firstColor Gray -secondPart $DisplayName -secondColor White Write-Host "--------------------------------------------------------" -ForegroundColor DarkGray WriteMultiColor -firstPart "Client ID: " -firstColor Gray -secondPart $app.AppId -secondColor Yellow RemoveExposedScopeIfExists -app $app -scopeName 'user_impersonation' AddExposedScope -app $app -scopeName 'full' AddPermission -app $app -permissionPrincipalDisplayName $RequiredPermissionsAppDisplayName -permissions $RequiredPermissions return $app } <# .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) { AddPermission -app $app -permissionPrincipalDisplayName $RequiredPermissionsAppDisplayName -permissions $RequiredPermissions } } <# .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 Sync-StorageContainer { 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 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.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 # 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 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." } # 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) $TemplateParameterFile = "$path\$fileName.parameters.json" # 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 # build resource group name if (!$WhatIf) { # no WHATIF if ($parametersExists) { 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) { 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 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 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 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" Add-NoDeleteLocksForResourceGroup -ResourceGroupName $server.ResourceGroupName -Locks $locks } <# .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 Set-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 } } # Define the exports Export-ModuleMember -Function Set-SubscriptionContext Export-ModuleMember -Function New-AppRegistration Export-ModuleMember -Function New-AppPermission Export-ModuleMember -Function Sync-StorageContainer Export-ModuleMember -Function New-ArmDeployment Export-ModuleMember -Function New-ArmGroupDeployment Export-ModuleMember -Function Import-ArmTemplateJson Export-ModuleMember -Function New-SqlFirewallRule Export-ModuleMember -Function Clear-AllSqlFirewallRules Export-ModuleMember -Function Set-ResourceGroupDeleteLock |