Private/Gui/Show-AddCredentialDialog.ps1

# PSGuerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0
# https://github.com/jimrtyler/PSGuerrilla | https://creativecommons.org/licenses/by/4.0/
# AI/LLM use: see AI-USAGE.md for required attribution

# GUI-driven credential entry (the "Add Credential" modal on the Safehouse tab).
# Builds the list of vault entries from collected field values, and a dark WPF dialog
# to collect them. New-AddCredentialEntries is kept pure so it can be unit-tested without
# a UI; Show-AddCredentialDialog drives the actual window.

function New-AddCredentialEntries {
    <#
    .SYNOPSIS
        Pure builder: turns collected dialog field values into the vault entry list that
        Save-SafehouseCredentialSet stores. No UI, no side effects — unit-testable.
    .PARAMETER Environment
        'microsoftGraph' or 'googleWorkspace'.
    .PARAMETER Fields
        Hashtable of the raw field values from the dialog.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Environment,
        [Parameter(Mandatory)][hashtable]$Fields
    )

    $entries = [System.Collections.Generic.List[object]]::new()
    switch ($Environment) {
        'microsoftGraph' {
            $entries.Add(@{ VaultKey = 'GUERRILLA_GRAPH_TENANT'; Value = $Fields.TenantId; Type = 'tenantId'
                Environment = 'microsoftGraph'; Description = 'Entra ID Tenant ID'; Identity = $Fields.TenantId })
            $entries.Add(@{ VaultKey = 'GUERRILLA_GRAPH_CLIENTID'; Value = $Fields.ClientId; Type = 'clientId'
                Environment = 'microsoftGraph'; Description = 'App Registration Client ID'; Identity = $Fields.ClientId })
            $secret = @{ VaultKey = 'GUERRILLA_GRAPH_SECRET'; Value = $Fields.ClientSecret; Type = 'clientSecret'
                Environment = 'microsoftGraph'; Description = 'Microsoft Graph Client Secret' }
            if ($Fields.Expiration) { $secret.ExpirationDate = $Fields.Expiration }
            $entries.Add($secret)
        }
        'googleWorkspace' {
            $entries.Add(@{ VaultKey = 'GUERRILLA_GWS_SA'; Value = $Fields.ServiceAccountJson; Type = 'serviceAccount'
                Environment = 'googleWorkspace'; Description = 'Google Workspace service account'; Identity = $Fields.SaClientEmail })
            $entries.Add(@{ VaultKey = 'GUERRILLA_GWS_SA_ADMIN_EMAIL'; Value = $Fields.AdminEmail; Type = 'adminEmail'
                Environment = 'googleWorkspace'; Description = 'Google Workspace delegated-admin email'; Identity = $Fields.AdminEmail })
        }
    }
    return $entries
}

function Test-AddCredentialFields {
    <#
    .SYNOPSIS
        Validates collected dialog values; returns an array of human-readable error
        strings (empty array = valid). Pure — unit-testable.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string]$Environment,
        [Parameter(Mandatory)][hashtable]$Fields
    )

    $errs = [System.Collections.Generic.List[string]]::new()
    $guid = '^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$'
    switch ($Environment) {
        'microsoftGraph' {
            if ("$($Fields.TenantId)" -notmatch $guid) { $errs.Add('Tenant ID must be a GUID.') }
            if ("$($Fields.ClientId)" -notmatch $guid) { $errs.Add('Client ID must be a GUID.') }
            if (-not "$($Fields.ClientSecret)") { $errs.Add('Client Secret is required.') }
            if ($Fields.Expiration -and "$($Fields.Expiration)" -notmatch '^\d{4}-\d{2}-\d{2}$') {
                $errs.Add('Secret expiry must be YYYY-MM-DD (or left blank).')
            }
        }
        'googleWorkspace' {
            if (-not "$($Fields.ServiceAccountJson)") { $errs.Add('Service account JSON is required.') }
            elseif (-not $Fields.SaClientEmail)       { $errs.Add('Service account JSON is not valid (no client_email).') }
            if ("$($Fields.AdminEmail)" -notmatch '^[^@\s]+@[^@\s]+\.[^@\s]+$') { $errs.Add('A valid admin email is required.') }
        }
        default { $errs.Add("Unknown environment '$Environment'.") }
    }
    return @($errs)
}

function Show-AddCredentialDialog {
    <#
    .SYNOPSIS
        Modal WPF dialog to add Entra (Graph) or Google Workspace credentials to the vault.
        Returns the entry list to store (for Save-SafehouseCredentialSet), or $null if the
        user cancelled.
    #>

    [CmdletBinding()]
    param(
        $Owner
    )

    $xaml = @'
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Add Credential" Width="520" Height="480" ResizeMode="NoResize"
        WindowStartupLocation="CenterOwner" Background="#1A1A1A">
  <Window.Resources>
    <Style TargetType="TextBlock"><Setter Property="Foreground" Value="#F5F0E6"/><Setter Property="Margin" Value="0,8,0,2"/></Style>
    <Style TargetType="TextBox">
      <Setter Property="Background" Value="#252420"/><Setter Property="Foreground" Value="#F5F0E6"/>
      <Setter Property="BorderBrush" Value="#55524A"/><Setter Property="BorderThickness" Value="1"/>
      <Setter Property="Padding" Value="6,4"/><Setter Property="CaretBrush" Value="#F5F0E6"/>
    </Style>
    <Style TargetType="PasswordBox">
      <Setter Property="Background" Value="#252420"/><Setter Property="Foreground" Value="#F5F0E6"/>
      <Setter Property="BorderBrush" Value="#55524A"/><Setter Property="BorderThickness" Value="1"/>
      <Setter Property="Padding" Value="6,4"/><Setter Property="CaretBrush" Value="#F5F0E6"/>
    </Style>
    <Style TargetType="RadioButton"><Setter Property="Foreground" Value="#F5F0E6"/><Setter Property="Margin" Value="0,0,16,0"/></Style>
    <Style TargetType="Button">
      <Setter Property="Background" Value="#C67A1F"/><Setter Property="Foreground" Value="#1A1A1A"/>
      <Setter Property="BorderThickness" Value="0"/><Setter Property="Padding" Value="16,6"/><Setter Property="FontWeight" Value="Bold"/>
    </Style>
  </Window.Resources>
  <Grid Margin="20">
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/><RowDefinition Height="Auto"/><RowDefinition Height="*"/>
      <RowDefinition Height="Auto"/><RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>

    <TextBlock Grid.Row="0" Text="Add a credential to the safehouse vault" FontSize="16" FontWeight="Bold"/>

    <StackPanel Grid.Row="1" Orientation="Horizontal" Margin="0,12,0,8">
      <TextBlock Text="Environment:" VerticalAlignment="Center" Margin="0,0,12,0"/>
      <RadioButton x:Name="rb_Entra" Content="Microsoft Entra / Graph" GroupName="Env" IsChecked="True" VerticalAlignment="Center"/>
      <RadioButton x:Name="rb_Gws" Content="Google Workspace" GroupName="Env" VerticalAlignment="Center"/>
    </StackPanel>

    <Grid Grid.Row="2">
      <!-- Entra panel -->
      <StackPanel x:Name="panel_Entra" Visibility="Visible">
        <TextBlock Text="Tenant ID (GUID)"/>
        <TextBox x:Name="tb_Tenant"/>
        <TextBlock Text="Application (Client) ID (GUID)"/>
        <TextBox x:Name="tb_ClientId"/>
        <TextBlock Text="Client Secret"/>
        <PasswordBox x:Name="pb_Secret"/>
        <TextBlock Text="Secret expiry (YYYY-MM-DD, optional)"/>
        <TextBox x:Name="tb_Expiry"/>
      </StackPanel>
      <!-- Google Workspace panel -->
      <StackPanel x:Name="panel_Gws" Visibility="Collapsed">
        <TextBlock Text="Service account JSON key file"/>
        <Grid>
          <Grid.ColumnDefinitions><ColumnDefinition Width="*"/><ColumnDefinition Width="Auto"/></Grid.ColumnDefinitions>
          <TextBox x:Name="tb_SaPath" Grid.Column="0"/>
          <Button x:Name="btn_Browse" Grid.Column="1" Content="Browse…" Margin="8,0,0,0"/>
        </Grid>
        <TextBlock Text="Delegated-admin email (a Super Admin)"/>
        <TextBox x:Name="tb_AdminEmail"/>
        <TextBlock Text="The service account needs domain-wide delegation configured in the Google Admin Console." Foreground="#8B8B7A" TextWrapping="Wrap" Margin="0,8,0,0"/>
      </StackPanel>
    </Grid>

    <TextBlock x:Name="tb_Error" Grid.Row="3" Foreground="#E06C5A" TextWrapping="Wrap" Margin="0,8,0,0"/>

    <StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
      <Button x:Name="btn_Cancel" Content="Cancel" Background="#3A3833" Foreground="#F5F0E6" Margin="0,0,8,0"/>
      <Button x:Name="btn_Save" Content="Save credential"/>
    </StackPanel>
  </Grid>
</Window>
'@


    $reader = New-Object System.Xml.XmlNodeReader ([xml]$xaml)
    $win = [System.Windows.Markup.XamlReader]::Load($reader)
    if ($Owner) { $win.Owner = $Owner }

    $c = {}
    $ctl = @{}
    foreach ($n in 'rb_Entra', 'rb_Gws', 'panel_Entra', 'panel_Gws', 'tb_Tenant', 'tb_ClientId',
        'pb_Secret', 'tb_Expiry', 'tb_SaPath', 'btn_Browse', 'tb_AdminEmail', 'tb_Error', 'btn_Cancel', 'btn_Save') {
        $ctl[$n] = $win.FindName($n)
    }

    $ctl.rb_Entra.Add_Checked({ $ctl.panel_Entra.Visibility = 'Visible'; $ctl.panel_Gws.Visibility = 'Collapsed' }.GetNewClosure())
    $ctl.rb_Gws.Add_Checked({ $ctl.panel_Entra.Visibility = 'Collapsed'; $ctl.panel_Gws.Visibility = 'Visible' }.GetNewClosure())

    $ctl.btn_Browse.Add_Click({
        $dlg = New-Object Microsoft.Win32.OpenFileDialog
        $dlg.Filter = 'Service account JSON (*.json)|*.json|All files (*.*)|*.*'
        if ($dlg.ShowDialog()) { $ctl.tb_SaPath.Text = $dlg.FileName }
    }.GetNewClosure())

    $result = @{ Entries = $null }

    $ctl.btn_Cancel.Add_Click({ $win.Close() }.GetNewClosure())

    $ctl.btn_Save.Add_Click({
        $env = if ($ctl.rb_Gws.IsChecked) { 'googleWorkspace' } else { 'microsoftGraph' }
        $fields = @{}
        if ($env -eq 'microsoftGraph') {
            $fields.TenantId     = $ctl.tb_Tenant.Text.Trim()
            $fields.ClientId     = $ctl.tb_ClientId.Text.Trim()
            $fields.ClientSecret = $ctl.pb_Secret.Password
            $fields.Expiration   = $ctl.tb_Expiry.Text.Trim()
        } else {
            $saPath = $ctl.tb_SaPath.Text.Trim()
            $fields.ServiceAccountJson = $null; $fields.SaClientEmail = $null
            if ($saPath -and (Test-Path $saPath)) {
                try {
                    $raw = Get-Content -Path $saPath -Raw
                    $sa = $raw | ConvertFrom-Json
                    $fields.ServiceAccountJson = $raw
                    $fields.SaClientEmail = $sa.client_email
                } catch { }
            }
            $fields.AdminEmail = $ctl.tb_AdminEmail.Text.Trim()
        }

        $errs = Test-AddCredentialFields -Environment $env -Fields $fields
        if ($errs.Count -gt 0) {
            $ctl.tb_Error.Text = ($errs -join ' ')
            return
        }
        $result.Entries = New-AddCredentialEntries -Environment $env -Fields $fields
        $win.Close()
    }.GetNewClosure())

    [void]$win.ShowDialog()
    return $result.Entries
}