diff --git a/PowerShell/JumpCloud Module/Private/Console/Clear-Console.ps1 b/PowerShell/JumpCloud Module/Private/Console/Clear-Console.ps1 new file mode 100644 index 00000000..c9742450 --- /dev/null +++ b/PowerShell/JumpCloud Module/Private/Console/Clear-Console.ps1 @@ -0,0 +1,17 @@ +# Clear a specified number of lines from the console +function Clear-Console { + param( + [Parameter(Mandatory=$true)] + [int]$LinesToClear + ) + $currentLine = $Host.UI.RawUI.CursorPosition.Y + $bufferWidth = $Host.UI.RawUI.BufferSize.Width + $startLine = $currentLine - $LinesToClear + if ($startLine -lt 0) { $startLine = 0 } + + for ($i = $startLine; $i -le $currentLine; $i++) { + [Console]::SetCursorPosition(0, $i) + [Console]::Write(" " * $bufferWidth) + } + [Console]::SetCursorPosition(0, $startLine) +} \ No newline at end of file diff --git a/PowerShell/JumpCloud Module/Private/Console/Confim-Console.ps1 b/PowerShell/JumpCloud Module/Private/Console/Confim-Console.ps1 new file mode 100644 index 00000000..6504d30f --- /dev/null +++ b/PowerShell/JumpCloud Module/Private/Console/Confim-Console.ps1 @@ -0,0 +1,23 @@ +function Confirm-Console { + param( + [Parameter(Mandatory=$False)] + [string]$Message, + [Parameter(Mandatory=$False)] + [scriptblock]$YesAction + ) + $LinesToClear = 1 + if($Message) { + Write-Host $Message -ForegroundColor Yellow + # $LinesToClear += ($Message.Split("`n").Count) + } + $res = Find-Interactive -choices @("No", "Yes"); + if($res -eq "Yes" -and $YesAction) { + Invoke-Command -ScriptBlock $YesAction + $temp = $true + } else { + $temp = $false + } + + Clear-Console -LinesToClear $LinesToClear + return $temp +} \ No newline at end of file diff --git a/PowerShell/JumpCloud Module/Private/Console/Find-Interactive.ps1 b/PowerShell/JumpCloud Module/Private/Console/Find-Interactive.ps1 new file mode 100644 index 00000000..2e74abce --- /dev/null +++ b/PowerShell/JumpCloud Module/Private/Console/Find-Interactive.ps1 @@ -0,0 +1,40 @@ +# Interactive function to display choices and handle user input for selection +function Find-Interactive() { + param ( + [string[]]$choices, + [scriptblock]$Callback + ) + $index = 0 + $running = $true + $trigger = $false + [Console]::CursorVisible = $false + while ($running) { + for ($i = 0; $i -lt $choices.Count; $i++) { + if ($i -eq $index) { + Write-Host " > $($choices[$i])" -ForegroundColor Cyan -BackgroundColor DarkGray + } else { + Write-Host " $($choices[$i])" + } + } + $key = [Console]::ReadKey($true) + switch ($key.Key) { + 'UpArrow' { $index = if ($index -gt 0) { $index - 1 } else { $choices.Count - 1 } } + 'DownArrow' { $index = if ($index -lt $choices.Count - 1) { $index + 1 } else { 0 } } + 'Enter' { $running = $false ; $trigger = $false;} + 'Backspace' { if($Callback) { $running = $false; $trigger = $true; } } + 'Escape' { [Console]::CursorVisible = $true; return $null } + } + [Console]::SetCursorPosition(0, [Console]::CursorTop - ($choices.Count)) + } + [Console]::CursorVisible = $true + # Clear the choices from the console + for ($i = 0; $i -lt $choices.Count; $i++) { + Write-Host (" " * ([Console]::WindowWidth - 1)) + } + [Console]::SetCursorPosition(0, [Console]::CursorTop - ($choices.Count)) + if($trigger -and $Callback) { + $temp = Invoke-Command -ScriptBlock $Callback -ArgumentList $choices[$index] + return $false + } + return $choices[$index] +} \ No newline at end of file diff --git a/PowerShell/JumpCloud Module/Private/Vault/Get-KeyFromVault.ps1 b/PowerShell/JumpCloud Module/Private/Vault/Get-KeyFromVault.ps1 new file mode 100644 index 00000000..a88694e9 --- /dev/null +++ b/PowerShell/JumpCloud Module/Private/Vault/Get-KeyFromVault.ps1 @@ -0,0 +1,22 @@ +# Retrieve a key from the vault based on the provided key name +function Get-KeyFromVault() { + param( + [Parameter(Mandatory=$true)] + [string]$Key + ) + Unlock-Platform + $plat = $env:CONSOLE_PLATFORM + if ($plat -eq "MacOS") { + $serviceKey = security find-generic-password -s $Key -w + if ($LASTEXITCODE -ne 0) { + return $null + } + } elseif ($plat -eq "Windows") { + $serviceKey = [CredManager]::GetCreds($Key) + } else { + throw "Unsupported OS." + } + + Write-Host "Retrieved key from Credential Manager: $key" -ForegroundColor Green + return $serviceKey +} \ No newline at end of file diff --git a/PowerShell/JumpCloud Module/Private/Vault/Get-VaultKeys.ps1 b/PowerShell/JumpCloud Module/Private/Vault/Get-VaultKeys.ps1 new file mode 100644 index 00000000..6ac249a9 --- /dev/null +++ b/PowerShell/JumpCloud Module/Private/Vault/Get-VaultKeys.ps1 @@ -0,0 +1,30 @@ +function Get-VaultKeys() { + param( + [Parameter(Mandatory=$true)] + [string]$sufix + ) + + $plat = $env:CONSOLE_PLATFORM + If($plat -eq "Windows") { + # $username = $env:USERNAME.Trim().ToLower().Replace(" ", "_") + try { + $keys = [CredManager]::GetTargetList().Where({ $_.EndsWith($sufix) }) + } catch { + Write-Host "Error retrieving keys: $_" -ForegroundColor Red + return $null + } + } ElseIf($plat -eq "MacOS") { + $keys = security dump-keychain | ForEach-Object { + if ($_ -match '0x00000007\s+="(.+?)"' -or $_ -match '"svce"<.+?>="(.+?'+$sufix+')"') { + $found = $matches[1] + if ($found.EndsWith($sufix)){return $found} + } + } | Select-Object -Unique + } ElseIf($plat -eq "Linux") { + throw "Unsupported OS." + } Else { + throw "Unsupported OS." + } + + return $keys +} \ No newline at end of file diff --git a/PowerShell/JumpCloud Module/Private/Vault/Remove-FromVault.ps1 b/PowerShell/JumpCloud Module/Private/Vault/Remove-FromVault.ps1 new file mode 100644 index 00000000..33712c20 --- /dev/null +++ b/PowerShell/JumpCloud Module/Private/Vault/Remove-FromVault.ps1 @@ -0,0 +1,30 @@ +# Remove a key from the vault based on the provided key name +function Remove-FromVault() { + param( + [Parameter(Mandatory=$true)] + [string]$Key + ) + + $plat = $env:CONSOLE_PLATFORM + if ($plat -eq "MacOS") { + try { + security delete-generic-password -s $Key 2>$null + if ($LASTEXITCODE -ne 0) { + throw "Failed to delete key from Keychain." + } + } catch { + throw "Error deleting key from Keychain: $_" + } + } elseif ($plat -eq "Windows") { + try { + cmdkey /delete:($Key) 2>$null + if ($LASTEXITCODE -ne 0) { + throw "Failed to delete key from Credential Manager." + } + } catch { + throw "Error deleting key from Credential Manager: $_" + } + } else { + throw "Unsupported OS." + } +} \ No newline at end of file diff --git a/PowerShell/JumpCloud Module/Private/Vault/Request-NewKey.ps1 b/PowerShell/JumpCloud Module/Private/Vault/Request-NewKey.ps1 new file mode 100644 index 00000000..9c70b1b0 --- /dev/null +++ b/PowerShell/JumpCloud Module/Private/Vault/Request-NewKey.ps1 @@ -0,0 +1,13 @@ +function Request-NewKey() { + param( + [Parameter(Mandatory=$false)] + [string]$sufix_ = ".api.jc" + ) + Set-ToVault -Value ( + [System.Net.NetworkCredential]::new("", (Read-Host -Prompt "Type the api key" -AsSecureString)).Password + ) -Key ( + Read-Host -Prompt "Type the name to be saved" + ) -sufix $sufix_ + $LinesToClear = $keys.Count + 2 + Clear-Console -LinesToClear $LinesToClear +} diff --git a/PowerShell/JumpCloud Module/Private/Vault/Set-ToVault.ps1 b/PowerShell/JumpCloud Module/Private/Vault/Set-ToVault.ps1 new file mode 100644 index 00000000..978606ab --- /dev/null +++ b/PowerShell/JumpCloud Module/Private/Vault/Set-ToVault.ps1 @@ -0,0 +1,38 @@ +# Aux functions for interactive behavior and vault management +function Set-ToVault() { + param( + [Parameter(Mandatory=$true)] + [string]$Key, + [Parameter(Mandatory=$false)] + [string]$sufix, + [Parameter(Mandatory=$true)] + [string]$Value + ) + if(!$sufix) { + $sufix = "" + } + $key_ = $Key.ToLower().Replace(" ", "_").Trim() + $plat = $env:CONSOLE_PLATFORM + $value = $Value.Trim() + if ($plat -eq "MacOS") { + try { + # Not necessary to use same approach as MacOs + # MacOs is safe to use security command as above + security add-generic-password -a $env:USER -s ($key_+$sufix) -w $value -T "" 2>$null + if ($LASTEXITCODE -ne 0) { + throw "Failed to add key to Keychain." + } + } catch { + throw "Error adding key to Keychain: $_" + } + } elseif ($plat -eq "Windows") { + try { + Unlock-Platform + [CredManager]::SetCreds(($key_ + $sufix), $env:USERNAME, $value) + } catch { + throw "Error adding key to Credential Manager: $_" + } + } else { + throw "Unsupported OS." + } +} \ No newline at end of file diff --git a/PowerShell/JumpCloud Module/Private/Vault/Unlock-Platform.ps1 b/PowerShell/JumpCloud Module/Private/Vault/Unlock-Platform.ps1 new file mode 100644 index 00000000..94e32a20 --- /dev/null +++ b/PowerShell/JumpCloud Module/Private/Vault/Unlock-Platform.ps1 @@ -0,0 +1,115 @@ +function Unlock-Platform() { + if([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Windows)) { + $plat = "Windows" + $Definition = @" + using System; + using System.ComponentModel; + using System.Runtime.InteropServices; + using System.Collections.Generic; + + public class CredManager { + private const int CRED_TYPE_GENERIC = 1; + private const int CRED_PERSIST_LOCAL_MACHINE = 2; + + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern bool CredRead(string target, int type, int reservedFlag, out IntPtr credentialPtr); + + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern bool CredWrite(ref PCREDENTIAL userCredential, uint flags); + + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern bool CredEnumerate(string filter, int flag, out int count, out IntPtr pCredentials); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern void CredFree(IntPtr pBuffer); + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct PCREDENTIAL { + public int flags; + public int type; + public string targetName; + public string comment; + public long lastWritten; + public int credentialBlobSize; + public IntPtr credentialBlob; + public int persist; + public int attributeCount; + public IntPtr attributes; + public string targetAlias; + public string userName; + } + + public static string[] GetTargetList() { + int count; + IntPtr pCredentials; + List targets = new List(); + + // Filtro null traz todas as credenciais genéricas (type 1) + if (CredEnumerate(null, 0, out count, out pCredentials)) { + for (int i = 0; i < count; i++) { + // Calcula o endereço de cada item no array de ponteiros + IntPtr pCurrent = Marshal.ReadIntPtr(pCredentials, i * IntPtr.Size); + PCREDENTIAL cred = (PCREDENTIAL)Marshal.PtrToStructure(pCurrent, typeof(PCREDENTIAL)); + targets.Add(cred.targetName); + } + CredFree(pCredentials); + } + return targets.ToArray(); + } + + public static string GetCreds(string target) { + IntPtr credPtr; + if (CredRead(target, CRED_TYPE_GENERIC, 0, out credPtr)) { + try { + PCREDENTIAL cred = (PCREDENTIAL)Marshal.PtrToStructure(credPtr, typeof(PCREDENTIAL)); + return Marshal.PtrToStringUni(cred.credentialBlob, cred.credentialBlobSize / 2); + } finally { + CredFree(credPtr); + } + } + return null; + } + + public static void SetCreds(string target, string userName, string secret) { + IntPtr blobPtr = Marshal.StringToCoTaskMemUni(secret); + try { + PCREDENTIAL cred = new PCREDENTIAL(); + cred.flags = 0; + cred.type = CRED_TYPE_GENERIC; + cred.targetName = target; + cred.comment = null; + cred.lastWritten = 0; + cred.credentialBlobSize = (secret.Length + 1) * 2; + cred.credentialBlob = blobPtr; + cred.persist = CRED_PERSIST_LOCAL_MACHINE; + cred.attributeCount = 0; + cred.attributes = IntPtr.Zero; + cred.targetAlias = null; + cred.userName = userName; + + if (!CredWrite(ref cred, 0)) { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + } finally { + Marshal.FreeCoTaskMem(blobPtr); + } + } + } +"@ + if (-not ("CredManager" -as [type])) { Add-Type -TypeDefinition $Definition -Language CSharp } + } elseif([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::OSX)) { + $plat = "MacOS" + # Will be asked all times, sadly too + # try { + # security unlock-keychain + # } catch { + # throw "Error unlocking keychain: $_" + # } + } elseif([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform([System.Runtime.InteropServices.OSPlatform]::Linux)) { + $plat = "Linux" + } else { + $plat = "Unknown" + } + + $env:CONSOLE_PLATFORM = $plat +} \ No newline at end of file diff --git a/PowerShell/JumpCloud Module/Public/Authentication/Connect-JCOnline.ps1 b/PowerShell/JumpCloud Module/Public/Authentication/Connect-JCOnline.ps1 index 62061044..e937e6b3 100755 --- a/PowerShell/JumpCloud Module/Public/Authentication/Connect-JCOnline.ps1 +++ b/PowerShell/JumpCloud Module/Public/Authentication/Connect-JCOnline.ps1 @@ -3,18 +3,33 @@ function Connect-JCOnline () { param ( [Parameter( - ParameterSetName = 'force', - HelpMessage = 'Using the "-Force" parameter the module update check is skipped. The ''-Force'' parameter should be used when using the JumpCloud module in scripts or other automation environments.' + Mandatory = $false, + HelpMessage = 'Using the "-Force" parameter the module update check is skipped.' )] - [Switch]$force + [Switch]$force, + [Parameter( + Mandatory = $false, + HelpMessage = 'Use the -select to select from stored API keys. Or informe an value with the same param' + )] + [Switch]$Select, + # Its the key name, not the key value + [Parameter( + Mandatory = $false, + HelpMessage = 'Vault Key Name.' + )] + [string]$Credential ) dynamicparam { + $BoundParams = $PSCmdlet.MyInvocation.BoundParameters + $RuntimeParameterDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary + $RawCommandLine = [string]$MyInvocation.Line + $Param_JumpCloudApiKey = @{ 'Name' = 'JumpCloudApiKey'; 'Type' = [System.String]; 'Position' = 1; 'ValueFromPipelineByPropertyName' = $true; - 'ValidateNotNullOrEmpty' = $true; + 'ValidateNotNullOrEmpty' = $false; 'HelpMessage' = 'Please enter your JumpCloud API key. This can be found in the JumpCloud admin console within "API Settings" accessible from the drop down icon next to the admin email address in the top right corner of the JumpCloud admin console.'; } $Param_JumpCloudOrgId = @{ @@ -35,11 +50,26 @@ function Connect-JCOnline () { 'ValidateSet' = ('STANDARD', 'STAGING', 'EU'); } # If the $env:JCApiKey is not set then make the JumpCloudApiKey mandatory else set the default value to be the env variable - if ([System.String]::IsNullOrEmpty($env:JCApiKey)) { + # Priority for selecting key is: 1) -JumpCloudApiKey parameter, 2) -Select parameter, 3) $env:JCApiKey + # Reformulated to get less confusing + $containsApiKey = $false + if($RawCommandLine -match 'JumpCloudApiKey') {$containsApiKey = $true} + $emp1 = $BoundParams.ContainsKey('Select') -and (-not $BoundParams.ContainsKey('Credential')) -and (-not $containsApiKey) + $emp2 = [System.String]::IsNullOrEmpty($env:JCApiKey) -and (-not $containsApiKey) -and (-not $BoundParams.ContainsKey('Credential')) + if($emp1 -or $emp2) { + $newKey = KeySelector + } + if($BoundParams.ContainsKey('Credential')) { + $newKey = KeySelector -keyName $BoundParams['Credential'] + } + + if(-not [System.String]::IsNullOrEmpty($newKey)) { $env:JCApiKey = $newKey } + if([System.String]::IsNullOrEmpty($env:JCApiKey)) { $Param_JumpCloudApiKey.Add('Mandatory', $true); } else { $Param_JumpCloudApiKey.Add('Default', $env:JCApiKey); } + # If the $env:JCOrgId is set then set the default value to be the env variable if (-not [System.String]::IsNullOrEmpty($env:JCOrgId)) { $Param_JumpCloudOrgId.Add('Default', $env:JCOrgId); @@ -52,7 +82,6 @@ function Connect-JCOnline () { } # Build output # Build parameter array - $RuntimeParameterDictionary = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary $ParamVarPrefix = 'Param_' Get-Variable -Scope:('Local') | Where-Object { $_.Name -like '*' + $ParamVarPrefix + '*' } | Sort-Object { [int]$_.Value.Position } | ForEach-Object { # Add RuntimeDictionary to each parameter @@ -74,7 +103,8 @@ function Connect-JCOnline () { return $RuntimeParameterDictionary } begin { - # Debug message for parameter call + # Debug message for parameter call] + Write-Debug -Message:('Parameter values:') $PSBoundParameters | Out-DebugParameter | Write-Debug } process { @@ -188,7 +218,7 @@ function Connect-JCOnline () { $downRepo += $site } # Clean up the http request by closing it. - if ($HTTP_Response -eq $null) { + if ($null -eq $HTTP_Response) { } else { $HTTP_Response.Close() } @@ -256,3 +286,59 @@ function Connect-JCOnline () { end { } } + +function KeySelector { + param( + [Parameter(Mandatory=$false)] + [string]$keyName + ) + Unlock-Platform + if(-not [System.String]::IsNullOrEmpty($keyName)) { + return Request-Key -vaultKey $keyName + } + + $sufix_ = ".api.jc" + $keys = Get-VaultKeys -sufix $sufix_ + if(($null -eq $keys) -or ($keys.Count -eq 0)) { + Write-Host "No keys found in vault. Please add a new key." -ForegroundColor Yellow + Request-NewKey -sufix $sufix_ + $keys = Get-VaultKeys -sufix $sufix_ + } + + Write-Host "Select the JumpCloud Api Key. Press [Escape] to type a new key. Press [Backspace] to remove the selected key" -ForegroundColor Green + # Stays in selection loop until user selects a key or abort. + while (@($false, $null) -contains ($vaultKey = Find-Interactive -choices $keys -Callback { + param($param) + return Confirm-Console -Message "Selected key: $param. Are you sure want to remove this key from vault?" -YesAction { Remove-FromVault -Key $param } + })) { + $keys = Get-VaultKeys -sufix $sufix_ + if (($null -eq $vaultKey) -or ($null -eq $keys) -or ($keys.Count -eq 0)) { + if($keys.Count -eq 0) { + Write-Host "No keys found in vault. Please add a new key." -ForegroundColor Yellow + } + Request-NewKey -sufix $sufix_ + } + $keys = Get-VaultKeys -sufix $sufix_ + } + + # $vaultKey is being declared above as the output of Find-Interactive, so it will be available here for + $foundKey = Request-Key -vaultKey $vaultKey + + Clear-Console -LinesToClear 2 + return $foundKey +} + +function Request-Key { + param ( + [Parameter(Mandatory=$false)] + [string]$vaultKey + ) + $foundKey = Get-KeyFromVault -Key $vaultKey + + if("" -eq $foundKey -or $null -eq $foundKey) { + Write-Host "Aborted. Exiting." -ForegroundColor Yellow + throw "No key selected" + } else { + return $foundKey + } +} \ No newline at end of file