Modules/Private/30-Credentials.ps1
|
function Test-RangerKeyVaultUri { param( [AllowNull()] $Value ) $Value -is [string] -and $Value.StartsWith('keyvault://') } function ConvertFrom-RangerKeyVaultUri { param( [Parameter(Mandatory = $true)] [string]$Uri ) if (-not (Test-RangerKeyVaultUri -Value $Uri)) { throw "Invalid Key Vault URI: $Uri" } $parts = $Uri.Substring(11).Split('/') if ($parts.Count -lt 2) { throw "Invalid Key Vault URI format: $Uri" } [ordered]@{ VaultName = $parts[0] SecretName = $parts[1] Version = if ($parts.Count -gt 2) { $parts[2] } else { $null } } } function ConvertTo-RangerSecureString { param( [AllowNull()] [string]$Value ) if ($null -eq $Value) { return $null } $secureValue = [System.Security.SecureString]::new() foreach ($character in $Value.ToCharArray()) { $secureValue.AppendChar($character) } $secureValue.MakeReadOnly() return $secureValue } function Get-RangerSecretFromUri { param( [Parameter(Mandatory = $true)] [string]$Uri, [switch]$AsPlainText ) $parsed = ConvertFrom-RangerKeyVaultUri -Uri $Uri $providerFailures = [System.Collections.Generic.List[string]]::new() if (Test-RangerCommandAvailable -Name 'Get-AzKeyVaultSecret') { try { $secretParams = @{ VaultName = $parsed.VaultName Name = $parsed.SecretName ErrorAction = 'Stop' } if ($parsed.Version) { $secretParams.Version = $parsed.Version } $secret = Get-AzKeyVaultSecret @secretParams if ($AsPlainText) { return ConvertTo-RangerPlainText -Value $secret.SecretValue } return $secret.SecretValue } catch { [void]$providerFailures.Add("Az.KeyVault failed: $($_.Exception.Message)") } } if (Test-RangerCommandAvailable -Name 'az') { try { $arguments = @('keyvault', 'secret', 'show', '--vault-name', $parsed.VaultName, '--name', $parsed.SecretName, '--query', 'value', '-o', 'tsv') if ($parsed.Version) { $arguments += @('--version', $parsed.Version) } $value = & az @arguments if ($LASTEXITCODE -ne 0) { throw "Azure CLI exited with code $LASTEXITCODE." } if ($AsPlainText) { return $value } return (ConvertTo-RangerSecureString -Value $value) } catch { [void]$providerFailures.Add("Azure CLI failed: $($_.Exception.Message)") } } if ($providerFailures.Count -gt 0) { $joined = $providerFailures -join ' ' $dnsPatterns = @('getaddrinfo failed', 'No such host is known', 'could not be resolved', 'name or service not known') $isDnsFailure = $false foreach ($pattern in $dnsPatterns) { if ($joined -match [regex]::Escape($pattern)) { $isDnsFailure = $true; break } } if ($isDnsFailure) { $kvHost = "$($parsed.VaultName).vault.azure.net" $message = @( "Key Vault hostname '$kvHost' could not be resolved.", "Likely causes: (1) VPN or private endpoint network not connected, (2) Key Vault name '$($parsed.VaultName)' is incorrect in your config, (3) DNS resolver cannot reach the private zone.", "Action: verify the Key Vault name, confirm you are on the required network, or enable 'behavior.promptForMissingCredentials' in your config to be prompted interactively when KV is unreachable.", "Provider detail: $joined" ) -join ' ' $ex = [System.Management.Automation.RuntimeException]::new($message) $ex.Data['RangerKeyVaultDnsFailure'] = $true $ex.Data['RangerKeyVaultHost'] = $kvHost $ex.Data['RangerKeyVaultUri'] = $Uri throw $ex } throw "Could not resolve Key Vault secret '$Uri'. $joined" } throw 'Neither Az.KeyVault nor the Azure CLI is available for Key Vault secret resolution.' } function Resolve-RangerPasswordValue { param( $CredentialBlock ) if (-not $CredentialBlock) { return $null } if ($CredentialBlock.passwordSecureString -is [securestring]) { return $CredentialBlock.passwordSecureString } if ($CredentialBlock.password) { return (ConvertTo-RangerSecureString -Value ([string]$CredentialBlock.password)) } if ($CredentialBlock.passwordRef) { return Get-RangerSecretFromUri -Uri $CredentialBlock.passwordRef } return $null } function Get-RangerCredentialPromptText { # Issue #302 — per-credential-kind prompt text so operators know what # account type, format, and target to supply. param( [Parameter(Mandatory = $true)][string]$Name, [string]$TargetHint ) $targetSuffix = if (-not [string]::IsNullOrWhiteSpace($TargetHint)) { " for $TargetHint" } else { '' } switch ($Name) { 'cluster' { return [ordered]@{ Title = "Cluster node credential (WinRM)$targetSuffix" Message = "Enter a Windows domain account with local admin rights on the cluster nodes.`nFormat: DOMAIN\username or username@domain.com" } } 'domain' { return [ordered]@{ Title = "Active Directory read credential$targetSuffix" Message = "Enter a domain account with read access to AD.`nFormat: DOMAIN\username or username@domain.com.`nLeave blank to reuse the cluster credential." } } 'bmc' { return [ordered]@{ Title = "BMC / iDRAC credential$targetSuffix" Message = "Enter the baseboard management controller (iDRAC / iLO / XClarity) login.`nFormat: local username (e.g. 'root' or 'admin'), no domain prefix." } } 'switch' { return [ordered]@{ Title = "Network switch credential$targetSuffix" Message = "Enter the ToR switch management login.`nFormat: local username as configured on the switch (e.g. 'admin')." } } 'firewall' { return [ordered]@{ Title = "Firewall credential$targetSuffix" Message = "Enter the firewall management login.`nFormat: local username as configured on the appliance." } } default { return [ordered]@{ Title = "$Name credential$targetSuffix" Message = "Enter the $Name credential for Azure Local Ranger." } } } } function Resolve-RangerCredentialDefinition { param( [string]$Name, $CredentialBlock, [PSCredential]$OverrideCredential, [bool]$AllowPrompt = $true, [string]$TargetHint ) if ($OverrideCredential) { return $OverrideCredential } if ($CredentialBlock -is [PSCredential]) { return $CredentialBlock } if ($CredentialBlock -and $CredentialBlock.username) { try { $password = Resolve-RangerPasswordValue -CredentialBlock $CredentialBlock if ($password) { return [PSCredential]::new([string]$CredentialBlock.username, $password) } } catch { $isDnsFailure = $false if ($_.Exception.Data -and $_.Exception.Data.Contains('RangerKeyVaultDnsFailure')) { $isDnsFailure = [bool]$_.Exception.Data['RangerKeyVaultDnsFailure'] } if ($isDnsFailure -and $AllowPrompt) { Write-Warning $_.Exception.Message Write-Warning "Falling back to interactive prompt for '$Name' credential because Key Vault is unreachable." } else { throw } } } if ($AllowPrompt -and $Name -ne 'azure') { try { $promptText = Get-RangerCredentialPromptText -Name $Name -TargetHint $TargetHint return Get-Credential -Message $promptText.Message -Title $promptText.Title } catch { try { return Get-Credential -Message "Enter the $Name credential for Azure Local Ranger" } catch { return $null } } } return $null } function Resolve-RangerAzureCredentialSettings { param( [Parameter(Mandatory = $true)] [System.Collections.IDictionary]$Config, [switch]$SkipSecretResolution ) $settings = if ($Config.credentials.azure) { ConvertTo-RangerHashtable -InputObject $Config.credentials.azure } else { [ordered]@{} } if (-not $settings.method) { $settings.method = 'existing-context' } if (-not $settings.Contains('tenantId') -and -not [string]::IsNullOrWhiteSpace($Config.targets.azure.tenantId)) { $settings.tenantId = $Config.targets.azure.tenantId } if (-not $settings.Contains('subscriptionId') -and -not [string]::IsNullOrWhiteSpace($Config.targets.azure.subscriptionId)) { $settings.subscriptionId = $Config.targets.azure.subscriptionId } if (-not $settings.Contains('useAzureCliFallback')) { $settings.useAzureCliFallback = $true } if (-not $SkipSecretResolution) { if ($settings.clientSecretRef) { $settings.clientSecretSecureString = Get-RangerSecretFromUri -Uri $settings.clientSecretRef } elseif ($settings.clientSecret) { $settings.clientSecretSecureString = ConvertTo-RangerSecureString -Value ([string]$settings.clientSecret) } } return $settings } function Resolve-RangerCredentialMap { param( [Parameter(Mandatory = $true)] [System.Collections.IDictionary]$Config, [hashtable]$Overrides ) $allowPrompt = [bool]$Config.behavior.promptForMissingCredentials $overrides = if ($Overrides) { $Overrides } else { @{} } # v2.6.3 (#295): only prompt for BMC / switch / firewall credentials when # the relevant collector is in scope AND a target is configured. Previous # behavior always ran the prompt chain for every credential name, which # surfaced Get-Credential dialogs for BMC / device creds on runs where no # BMC / network device work was going to happen anyway. $selectedCollectors = Resolve-RangerSelectedCollectors -Config $Config $bmcInScope = ($selectedCollectors | Where-Object { 'bmc' -in @($_.RequiredTargets) }).Count -gt 0 -and ` (@($Config.targets.bmc.endpoints).Count -gt 0) $switchInScope = @($Config.targets.switches).Count -gt 0 $firewallInScope = @($Config.targets.firewalls).Count -gt 0 # If the caller supplied an explicit credential override, honor it even # when the target list is empty — the operator asked for this credential # by name, presumably because they're about to populate the target list # interactively or they want to validate the credential itself. if ($overrides.bmc) { $bmcInScope = $true } if ($overrides.switch) { $switchInScope = $true } if ($overrides.firewall) { $firewallInScope = $true } $clusterTargetHint = if (-not [string]::IsNullOrWhiteSpace($Config.targets.cluster.fqdn)) { [string]$Config.targets.cluster.fqdn } elseif (-not [string]::IsNullOrWhiteSpace($Config.environment.clusterName)) { [string]$Config.environment.clusterName } else { '' } $clusterCred = Resolve-RangerCredentialDefinition -Name 'cluster' -CredentialBlock $Config.credentials.cluster -OverrideCredential $overrides.cluster -AllowPrompt $allowPrompt -TargetHint $clusterTargetHint # Issue #304: when credentials.domain has no username and no passwordRef # configured, reuse the cluster credential automatically. The config # template has documented this reuse intent since v1.0 but the code path # never honored it, so operators got prompted twice for the same account. $domainBlock = $Config.credentials.domain $domainHasUsername = $domainBlock -and -not [string]::IsNullOrWhiteSpace([string]$domainBlock.username) $domainHasPasswordRef = $domainBlock -and -not [string]::IsNullOrWhiteSpace([string]$domainBlock.passwordRef) $domainHasPassword = $domainBlock -and -not [string]::IsNullOrWhiteSpace([string]$domainBlock.password) $domainIsConfigured = $domainHasUsername -or $domainHasPasswordRef -or $domainHasPassword if ($overrides.domain) { $domainCred = $overrides.domain } elseif ($domainIsConfigured) { $domainCred = Resolve-RangerCredentialDefinition -Name 'domain' -CredentialBlock $domainBlock -OverrideCredential $null -AllowPrompt $allowPrompt -TargetHint $clusterTargetHint } elseif ($clusterCred) { Write-RangerLog -Level info -Message "Resolve-RangerCredentialMap: reusing cluster credential '$($clusterCred.UserName)' for domain queries (credentials.domain is unconfigured)." $domainCred = $clusterCred } else { $domainCred = Resolve-RangerCredentialDefinition -Name 'domain' -CredentialBlock $domainBlock -OverrideCredential $null -AllowPrompt $allowPrompt -TargetHint $clusterTargetHint } [ordered]@{ azure = Resolve-RangerAzureCredentialSettings -Config $Config cluster = $clusterCred domain = $domainCred bmc = if ($bmcInScope) { Resolve-RangerCredentialDefinition -Name 'bmc' -CredentialBlock $Config.credentials.bmc -OverrideCredential $overrides.bmc -AllowPrompt $allowPrompt -TargetHint $clusterTargetHint } else { $null } firewall = if ($firewallInScope) { Resolve-RangerCredentialDefinition -Name 'firewall' -CredentialBlock $Config.credentials.firewall -OverrideCredential $overrides.firewall -AllowPrompt $allowPrompt -TargetHint $clusterTargetHint } else { $null } switch = if ($switchInScope) { Resolve-RangerCredentialDefinition -Name 'switch' -CredentialBlock $Config.credentials.switch -OverrideCredential $overrides.switch -AllowPrompt $allowPrompt -TargetHint $clusterTargetHint } else { $null } } } |