Skip to content

Commit

Permalink
tests - setup vault (#9)
Browse files Browse the repository at this point in the history
* download vault

* stealing your resolve

* set env

* add Vault setup script

* add configs

* setup Vault in CI, see if we can run test CLI

* typo

* fix test display

* need a detached process

* update how setup script takes init data

* add CcgVault tests with CLI test

* do test through pester

* fix paths

* reg on bin only

* add tests with docker

* run in CI

* fix credspec

* set test container

* raise timeout

* lessee whas happening

* remove half-baked thing

* harumph

* run-a-ping

* not backgrounded duh

* tryna

* stop looking for ghosts

* test cases!
  • Loading branch information
briantist authored May 17, 2022
1 parent ebf5db6 commit 394987f
Show file tree
Hide file tree
Showing 8 changed files with 343 additions and 1 deletion.
41 changes: 41 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ jobs:
- msi-reinstall
env:
INTEGRATION: .\test\integration
VAULT_ADDR: http://127.0.0.1:8200
VAULT_DEV_ROOT_TOKEN_ID: 323e3e66-e5fe-42ba-a4b9-fad293077754
CcgVaultTestContainer: mcr.microsoft.com/windows/nanoserver:${{ endsWith(matrix.os, '2019') && '1809' || 'ltsc2022' }}
defaults:
run:
shell: pwsh
Expand Down Expand Up @@ -104,6 +107,38 @@ jobs:
modules-to-cache: MSI:3.3.4
shell: pwsh

- name: Download Vault
run: |
$outpath = Join-Path -Path '.vault' -ChildPath 'bin'
$null = New-Item -Path $outpath -ItemType Directory -Force
$w = Invoke-WebRequest -Uri https://www.vaultproject.io/downloads
$dl = $w.Links.href.Where({$_-match'\d+\.\d+\.\d+_windows_amd64.zip$'})[0] -as [uri]
$file = $dl.Segments[-1]
$outfile = Join-Path -Path $outpath -ChildPath $file
Invoke-WebRequest -Uri $dl -OutFile $outfile
Expand-Archive -LiteralPath $outfile -DestinationPath $outpath
$vault = Join-Path -Path $outpath -ChildPath 'vault.exe' -Resolve
& $vault version
echo "VAULT=$vault" >> $env:GITHUB_ENV
- name: Setup Vault
run: |
.vault\setup.ps1 -VaultPath $env:VAULT
- name: Setup Configs
run: |
New-Item -Path C:\ccg -ItemType Directory -Force
Copy-Item -LiteralPath .config/ccgvault.yml -Destination C:\ccg
$env:VAULT_DEV_ROOT_TOKEN_ID | Set-Content -LiteralPath C:\ccg\tokenfile -NoNewline -Encoding utf8NoBOM
# we might need this later but for now this is done within the Pester tests
# Copy-Item -LiteralPath .config/credspec.json -Destination "$env:ProgramData\Docker\credentialspecs" -Force
- name: Set MSI names
if: startsWith(matrix.mode, 'msi')
run: |
Expand Down Expand Up @@ -206,6 +241,7 @@ jobs:
run: CcgVault.exe registry --permission --comclass

- name: Test registry present
if: matrix.mode == 'bin'
run: |
. .\Initialize-Pester.ps1
Invoke-Pester -ExcludeTagFilter Absent -Tag Registry -Output Detailed
Expand Down Expand Up @@ -247,3 +283,8 @@ jobs:
run: |
. .\Initialize-Pester.ps1
Invoke-Pester -ExcludeTagFilter Present -Tag NtService -Output Detailed
- name: Test CcgVault end-to-end
run: |
. .\Initialize-Pester.ps1
Invoke-Pester -Tag CcgVault -Output Detailed
18 changes: 18 additions & 0 deletions test/integration/.config/ccgvault.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
defaults:
vault_addr: http://127.0.0.1:8200
auth:
type: token_from_file
config:
file_path: C:\ccg\tokenfile
sources:
static_kv1:
type: kv1
config:
path: win/gmsa-getter
# mount_point: kv
static_kv2:
type: kv2
config:
path: win/gmsa-getter
mount_point: secret
30 changes: 30 additions & 0 deletions test/integration/.config/credspec.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"CmsPlugins": [
"ActiveDirectory"
],
"DomainJoinConfig": {
"Sid": "S-1-5-21-702590844-1001920913-2680819671",
"MachineAccountName": "ccg-gmsa",
"Guid": "56d9b66c-d746-4f87-bd26-26760cfdca2e",
"DnsTreeName": "contoso.com",
"DnsName": "contoso.com",
"NetBiosName": "CONTOSO"
},
"ActiveDirectoryConfig": {
"GroupManagedServiceAccounts": [
{
"Name": "ccg-gmsa",
"Scope": "contoso.com"
},
{
"Name": "ccg-gmsa",
"Scope": "CONTOSO"
}
],
"HostAccountConfig": {
"PortableCcgVersion": "1",
"PluginGUID": "{01BF101D-BFB6-433F-B416-02885CDC5AD3}",
"PluginInput": "C:\\ccg\\ccgvault.yml|static_kv2"
}
}
}
1 change: 1 addition & 0 deletions test/integration/.vault/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bin/
62 changes: 62 additions & 0 deletions test/integration/.vault/setup.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#Requires -Version 7.0
[CmdletBinding()]
param(
[Parameter()]
[ValidateNotNullOrEmpty()]
[String]
$VaultPath = 'vault.exe',

[Parameter()]
[hashtable]
$Kv1Data = @{
Username = 'ccg-reader-1'
Domain = 'contoso.com'
Password = 'password1'
} ,

[Parameter()]
[hashtable]
$Kv2Data = @{
Username = 'ccg-reader-2'
Domain = 'contoso.com'
Password = 'password2'
}
)

if (-not $env:VAULT_ADDR) {
$env:VAULT_ADDR = 'http://127.0.0.1:8200'
}

if (-not $env:VAULT_DEV_ROOT_TOKEN_ID) {
$env:VAULT_DEV_ROOT_TOKEN_ID = '323e3e66-e5fe-42ba-a4b9-fad293077754'
}

if (-not $env:VAULT_TOKEN) {
$env:VAULT_TOKEN = $env:VAULT_DEV_ROOT_TOKEN_ID
}

$env:VAULT_CLIENT_TIMEOUT = 1
& $VaultPath status
$running = $?
$env:VAULT_CLIENT_TIMEOUT = ''

if ($running) {
return
}

$w32p = Get-CimClass -ClassName Win32_Process
$w32startup = Get-CimClass -ClassName Win32_ProcessStartup
$vars = [System.Environment]::GetEnvironmentVariables([System.EnvironmentVariableTarget]::Process).GetEnumerator().ForEach({
"{0}={1}" -f $_.Name, $_.Value
}) -as [string[]]
$vars | Write-Verbose
$startup = New-CimInstance -CiMClass $w32startup -Property @{ EnvironmentVariables = $vars } -ClientOnly
$proc = Invoke-CimMethod -CiMClass $w32p -Name Create -Arguments @{
CommandLine = "$VaultPath server -dev"
ProcessStartupInformation = $startup
}
$proc.ProcessId | Set-Content -LiteralPath $env:TMP\vaultpid -NoNewline -Encoding utf8NoBOM

& $VaultPath secrets enable -path kv -version 1 kv
& $VaultPath kv put kv/win/gmsa-getter $($Kv1Data.GetEnumerator().ForEach({"{0}={1}" -f $_.Name, $_.Value}))
& $VaultPath kv put secret/win/gmsa-getter $($Kv2Data.GetEnumerator().ForEach({"{0}={1}" -f $_.Name, $_.Value}))
2 changes: 1 addition & 1 deletion test/integration/COMPlus.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Describe 'COM+ tests' -Tag ComPlus {
$app.Name | Should -BeExactly CcgVault
}

It 'App identity is <data.ComPlus.Identity>' {
It 'App identity is <Identity>' {
$appInfo.Identity | Should -Be $Identity
}

Expand Down
176 changes: 176 additions & 0 deletions test/integration/CcgVault.Tests.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
#Requires -Module @{ ModuleName = 'Pester'; ModuleVersion = '5.2.0' }
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', 'CredSpecPath')]
param(
[Parameter()]
[ValidateNotNullOrEmpty()]
[String]
$CcgVault = "$env:CcgVaultBin\CcgVault.exe" ,

[Parameter()]
[ValidateNotNullOrEmpty()]
[String]
$Config = 'C:\ccg\ccgvault.yml' ,

[Parameter()]
[ValidateNotNullOrEmpty()]
[String]
$ContainerImage = $env:CcgVaultTestContainer,

[Parameter()]
[ValidateNotNullOrEmpty()]
[String]
$CredSpecPath = "$env:ProgramData\Docker\credentialspecs"
)

BeforeAll {
$data = Import-PowerShellDataFile -LiteralPath $PSScriptRoot\TestData.psd1

$Script:ccgPluginId = [Guid]::Parse($data.ComPlus.CcgPlugin.ID)

$kv1data = $data.CcgVault.Kv1Data
$kv2data = $data.CcgVault.Kv2Data

$Script:expected_data = @{
static_kv1 = $kv1data
static_kv2 = $kv2data
}

# this doesn't work for some reason, haven't figured out why yet.
# if the path is wrong, the tests fail, so it seems like it tries to execute it at least,
# but it does not appear to actually start the Vault server; the tests only work if I run
# it separately first (and even in that case, tests fail when the path is wrong).
# & "$PSScriptRoot\.vault\setup.ps1" -Kv1Data $kv1data -Kv2Data $kv2data -ErrorAction Stop -Verbose
}

Describe 'CcgVault tests' -Tag CcgVault {
Context 'CLI test command w/ source: <source>' -Tag CLI -Foreach @(
@{ source = 'static_kv1' }
@{ source = 'static_kv2' }
) {
BeforeAll {
$Script:expected = $expected_data[$source]
$Script:output = & $CcgVault test --input "${Config}|${source}"
$Script:result = $output |
ForEach-Object -Begin { $Script:r = @{} } -Process {
$key, $value = $_ -split ': '
$r[$key] = $value
} -End { $r }
}

It "Source '<source>' has Username '<expected.Username>'" {
$result.User | Should -BeExactly $expected.Username
}

It "Source '<source>' has Domain '<expected.Domain>'" {
$result.Domain | Should -BeExactly $expected.Domain
}

It "Source '<source>' has Password '<expected.Password>'" {
$result.Pass | Should -BeExactly $expected.Password
}
}

Context 'Docker run test w/ source: <source>' -Tag Docker -Foreach @(
@{ source = 'static_kv1' }
@{ source = 'static_kv2' }
) {
BeforeAll {
$Script:timeout = 60
$Script:time = Get-Date
$Script:logname = $data.CcgVault.LogName

$credspecfile = "credspec_${source}.json"
$credspec = Join-Path -Path $CredSpecPath -ChildPath $credspecfile

$spec = Get-Content -LiteralPath "$PSScriptRoot/.config/credspec.json" -Raw | ConvertFrom-Json -AsHashtable -Depth 10
$spec.ActiveDirectoryConfig.HostAccountConfig.PluginInput = "${Config}|${source}"

ConvertTo-Json -InputObject $spec -Depth 10 | Set-Content -LiteralPath $credspec -Encoding utf8NoBOM -Force

$containerName = "ccgtest_${source}"

& docker run --user "NT AUthority\System" --security-opt "credentialspec=file://${credspecfile}" --name "$containerName" --rm -d "$ContainerImage" cmd /c ping -t localhost

if (-not $?) {
throw "Error runnung docker command."
}

function Wait-WinEvent {
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[ValidateNotNullOrEmpty()]
[String]
$LogName ,

[Parameter()]
[UInt16]
$EventID ,

[Parameter()]
[ScriptBlock]
$Condition ,

[Parameter()]
[DateTime]
$MinimumDateTime ,

[Parameter()]
[UInt16]
$TimeoutSeconds ,

[Parameter()]
[UInt16]
$SleepMilliseconds = 250
)

End {
$delay = $false
$timeoutStart = [DateTime]::Now
do {
if ($delay) {
Start-Sleep -Milliseconds $SleepMilliseconds
}
$ev = Get-WinEvent -LogName $LogName -FilterXPath "*[System[EventID=${EventID}]]" -MaxEvents 1 -ErrorAction SilentlyContinue
$delay = $delay -or $SleepMilliseconds -as [bool]
} until (
($null -eq $MinimumDateTime -or $ev.TimeCreated -gt $MinimumDateTime) -and
($null -eq $Condition -or (ForEach-Object -InputObject $ev -Process $Condition)) -or
(-not $TimeoutSeconds -or ($timedOut = ([DateTime]::Now - $timeoutStart).TotalSeconds -gt $TimeoutSeconds))
)

if (-not $timedOut) {
$ev
}
}
}
}

It "The plugin was instantiated" {
$event1 = Wait-WinEvent -LogName $logname -EventId 1 -Condition {
[guid]::Parse($_.Properties[0].Value) -eq $ccgPluginId
} -MinimumDateTime $time -TimeoutSeconds $timeout

$event1 | Should -Not -BeNullOrEmpty
}

# EventID 8 gets logged on my local machine when used with test creds
# and the contoso.com domain, but it doesn't happen in CI nor in a test
# server; maybe this is because my machine is a desktop OS, maybe it's
# because it's domain-joined, not sure. Will keep it around as a possible
# future test-case.
#
# It "The credential fails without a real domain" -Tag NoDomain {
# $event8 = Wait-WinEvent -LogName $logname -EventId 8 -Condition {
# [guid]::Parse($_.Properties[1].Value) -eq $ccgPluginId
# } -MinimumDateTime $time -TimeoutSeconds $timeout

# $event8 | Should -Not -BeNullOrEmpty
# }

AfterAll {
& docker stop "$containerName"
Remove-Item -LiteralPath $credspec -Force
}
}
}
14 changes: 14 additions & 0 deletions test/integration/TestData.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,18 @@
Registry = @{
Path = 'HKLM:\SYSTEM\CurrentControlSet\Control\CCG\COMClasses'
}

CcgVault = @{
LogName = 'Microsoft-Windows-Containers-CCG/Admin'
Kv1Data = @{
Username = 'ccg-reader-1'
Domain = 'contoso.com'
Password = 'password1'
}
Kv2Data = @{
Username = 'ccg-reader-2'
Domain = 'contoso.com'
Password = 'password2'
}
}
}

0 comments on commit 394987f

Please sign in to comment.