LibreDevOpsHelpers.AzureStorage/LibreDevOpsHelpers.AzureStorage.psm1
|
Set-StrictMode -Version Latest # Remembers each storage account's original network configuration so it can be # restored after a temporary access rule is removed. Keyed by account name. $script:LdoStorageStateCache = @{ } # Remembers accounts whose Add was skipped (-SoftFail, account absent), so the paired Remove also # skips instead of "restoring" a locked-down default over network rules the run's own apply just # created. Keyed by account name. $script:LdoStorageDanceSkipped = @{ } # Probes a storage account for the dance. Returns $true when the account exists. When it does # not: with -SoftFail logs a WARN and returns $false (first run, the stack creates the account; # the next run finds it and dances normally); without -SoftFail throws. Failures OTHER than # absence always throw, so auth or network problems never masquerade as a first run. function Test-LdoStorageDanceTarget { [CmdletBinding()] [OutputType([bool])] param( [Parameter(Mandatory)][string]$ResourceGroup, [Parameter(Mandatory)][string]$StorageAccountName, [switch]$SoftFail ) $probe = az storage account show -g $ResourceGroup -n $StorageAccountName -o none 2>&1 if ($LASTEXITCODE -eq 0) { return $true } $text = ($probe | Out-String) if ($text -notmatch '(?i)notfound|could not be found|does not exist|was not found') { throw "az storage account show ($StorageAccountName) failed for a reason other than absence: $text" } if (-not $SoftFail) { throw "Storage account $StorageAccountName was not found in $ResourceGroup. Pass -SoftFail when the stack itself creates the account (first run)." } Write-LdoLog -Level WARN -Message "Storage account $StorageAccountName does not exist, so cannot append the runner IP; skipping (-SoftFail). The next run will find it and dance normally." return $false } function Add-LdoStorageCurrentIpRule { <# .SYNOPSIS Temporarily grants the caller's public IP access to a storage account. .DESCRIPTION Caches the account's current network configuration on first use, enables public network access with a default Deny action (preserving any existing bypass), and adds a network rule for the caller's current public IP. Existing IP and VNet rules are left untouched. Use Remove-LdoStorageCurrentIpRule to revert. Requires the Azure CLI to be signed in. .PARAMETER ResourceGroup Resource group containing the storage account. .PARAMETER StorageAccountName Name of the storage account. .PARAMETER SoftFail Skip (with a warning) instead of failing when the account does not exist yet, for stacks that create the account themselves on the first run. Absence is the only condition softened: any other failure still throws. .EXAMPLE Add-LdoStorageCurrentIpRule -ResourceGroup rg-prod -StorageAccountName saprod .OUTPUTS None #> [CmdletBinding()] [OutputType([void])] param( [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$ResourceGroup, [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$StorageAccountName, [switch]$SoftFail ) if (-not (Test-LdoStorageDanceTarget -ResourceGroup $ResourceGroup -StorageAccountName $StorageAccountName -SoftFail:$SoftFail)) { $script:LdoStorageDanceSkipped[$StorageAccountName] = $true return } $ip = Get-LdoPublicIpAddress Write-LdoLog -Level INFO -Message "Current public IP: $ip" if (-not $script:LdoStorageStateCache.ContainsKey($StorageAccountName)) { $account = az storage account show -g $ResourceGroup -n $StorageAccountName -o json | ConvertFrom-Json Assert-LdoLastExitCode -Operation "az storage account show ($StorageAccountName)" $script:LdoStorageStateCache[$StorageAccountName] = @{ PublicNetworkAccess = $account.publicNetworkAccess DefaultAction = $account.networkRuleSet.defaultAction Bypass = $account.networkRuleSet.bypass } Write-LdoLog -Level DEBUG -Message "Captured original network state for $StorageAccountName." } $cached = $script:LdoStorageStateCache[$StorageAccountName] $update = @('storage', 'account', 'update', '-g', $ResourceGroup, '-n', $StorageAccountName, '--public-network-access', 'Enabled', '--default-action', 'Deny') if ($cached.Bypass) { $update += @('--bypass', $cached.Bypass) } az @update | Out-Null Assert-LdoLastExitCode -Operation "az storage account update ($StorageAccountName)" $existing = az storage account network-rule list -g $ResourceGroup -n $StorageAccountName --query "[?ipAddress=='$ip']" -o tsv if (-not $existing) { az storage account network-rule add -g $ResourceGroup -n $StorageAccountName --ip-address $ip | Out-Null Assert-LdoLastExitCode -Operation "az storage account network-rule add ($StorageAccountName)" Write-LdoLog -Level INFO -Message "Added temporary storage rule for $ip on $StorageAccountName." } else { Write-LdoLog -Level INFO -Message "Storage rule for $ip already present on $StorageAccountName; skipping add." } } function Remove-LdoStorageCurrentIpRule { <# .SYNOPSIS Removes the caller's temporary storage account access rule and restores settings. .DESCRIPTION Removes the network rule for the caller's current public IP and restores the account's network configuration captured by Add-LdoStorageCurrentIpRule. If no cached state exists, the account is left with public network access disabled and a default Deny. Requires the Azure CLI to be signed in. .PARAMETER ResourceGroup Resource group containing the storage account. .PARAMETER StorageAccountName Name of the storage account. .PARAMETER SoftFail Skip (with a warning) instead of failing when the account does not exist, for teardown paths where the stack (account included) may already be destroyed. Absence is the only condition softened: any other failure still throws. .PARAMETER RuleOnly Remove only the runner IP rule and leave the account's network configuration (public network access, default action, bypass) untouched. Use when the run's own Terraform apply changed the account, where restoring the pre-run capture would overwrite what Terraform just wrote; the terraform-azure engine detects this from the plan. .EXAMPLE Remove-LdoStorageCurrentIpRule -ResourceGroup rg-prod -StorageAccountName saprod .OUTPUTS None #> [CmdletBinding()] [OutputType([void])] param( [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$ResourceGroup, [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$StorageAccountName, [switch]$SoftFail, [switch]$RuleOnly ) if ($script:LdoStorageDanceSkipped.ContainsKey($StorageAccountName)) { $script:LdoStorageDanceSkipped.Remove($StorageAccountName) | Out-Null Write-LdoLog -Level INFO -Message "Add skipped earlier because $StorageAccountName did not exist; leaving its network configuration exactly as the run applied it." return } if (-not (Test-LdoStorageDanceTarget -ResourceGroup $ResourceGroup -StorageAccountName $StorageAccountName -SoftFail:$SoftFail)) { return } $ip = Get-LdoPublicIpAddress az storage account network-rule remove -g $ResourceGroup -n $StorageAccountName --ip-address $ip 2>$null | Out-Null Write-LdoLog -Level INFO -Message "Removed temporary storage rule for $ip from $StorageAccountName." if ($RuleOnly) { $script:LdoStorageStateCache.Remove($StorageAccountName) | Out-Null Write-LdoLog -Level INFO -Message "Network configuration on $StorageAccountName left untouched (-RuleOnly)." return } if ($script:LdoStorageStateCache.ContainsKey($StorageAccountName)) { $cached = $script:LdoStorageStateCache[$StorageAccountName] # A null captured value means the property was UNSET at capture time, and unset means the # platform default (public network access Enabled, default action Allow): restore that, # never an invented lockdown over an account that was open by design. $publicAccess = if ($cached.PublicNetworkAccess) { $cached.PublicNetworkAccess } else { 'Enabled' } $defaultAction = if ($cached.DefaultAction) { $cached.DefaultAction } else { 'Allow' } } else { # No captured state at all (a standalone Remove that never had a paired Add): fall back # to the locked-down posture, the safe side for the dance's designed target, a # pre-existing firewalled account such as the remote state store. $publicAccess = 'Disabled' $defaultAction = 'Deny' $cached = @{ Bypass = $null } } $restore = @('storage', 'account', 'update', '-g', $ResourceGroup, '-n', $StorageAccountName, '--public-network-access', $publicAccess, '--default-action', $defaultAction) if ($cached.Bypass) { $restore += @('--bypass', $cached.Bypass) } az @restore | Out-Null Assert-LdoLastExitCode -Operation "az storage account update ($StorageAccountName)" $script:LdoStorageStateCache.Remove($StorageAccountName) | Out-Null Write-LdoLog -Level INFO -Message "Restored network ACLs on $StorageAccountName." } Export-ModuleMember -Function ` Add-LdoStorageCurrentIpRule, ` Remove-LdoStorageCurrentIpRule |