win become: refactor and add support for passwordless become (#48082)

* win become: refactor and add support for passwordless become

* make tests more stable

* fix up dep message for Load-CommandUtils

* Add further check for System impersonation token

* re-add support for become with accounts that have no password

* doc fixes and slight code improvements

* fix doc sanity issue
This commit is contained in:
Jordan Borean
2018-12-13 11:15:25 +10:00
committed by Matt Davis
parent b3ac5b637a
commit 190d1ed7f1
13 changed files with 2586 additions and 1105 deletions

View File

@@ -143,57 +143,6 @@
- '"LogonUser failed" not in become_invalid_pass.msg'
- '"Win32ErrorCode 1326)" not in become_invalid_pass.msg'
- name: test become with SYSTEM account
win_whoami:
become: yes
become_method: runas
become_user: SYSTEM
register: whoami_out
- name: verify output
assert:
that:
- whoami_out.account.sid == "S-1-5-18"
- whoami_out.account.account_name == "SYSTEM"
- whoami_out.account.domain_name == "NT AUTHORITY"
- whoami_out.label.account_name == 'System Mandatory Level'
- whoami_out.label.sid == 'S-1-16-16384'
- whoami_out.logon_type == 'System'
- name: test become with NetworkService account
win_whoami:
become: yes
become_method: runas
become_user: NetworkService
register: whoami_out
- name: verify output
assert:
that:
- whoami_out.account.sid == "S-1-5-20"
- whoami_out.account.account_name == "NETWORK SERVICE"
- whoami_out.account.domain_name == "NT AUTHORITY"
- whoami_out.label.account_name == 'System Mandatory Level'
- whoami_out.label.sid == 'S-1-16-16384'
- whoami_out.logon_type == 'Service'
- name: test become with LocalService account
win_whoami:
become: yes
become_method: runas
become_user: LocalService
register: whoami_out
- name: verify output
assert:
that:
- whoami_out.account.sid == "S-1-5-19"
- whoami_out.account.account_name == "LOCAL SERVICE"
- whoami_out.account.domain_name == "NT AUTHORITY"
- whoami_out.label.account_name == 'System Mandatory Level'
- whoami_out.label.sid == 'S-1-16-16384'
- whoami_out.logon_type == 'Service'
- name: test become + async
vars: *become_vars
win_command: whoami
@@ -228,82 +177,6 @@
register: failed_flags_invalid_flag
failed_when: "failed_flags_invalid_flag.msg != \"internal error: failed to parse become_flags 'logon_flags=with_profile,invalid': become_flags logon_flags value 'invalid' is not valid, valid values are: with_profile, netcredentials_only\""
# Server 2008 doesn't work with network and network_cleartext, there isn't really a reason why you would want this anyway
- name: check if we are running on a dinosaur, neanderthal or an OS of the modern age
win_shell: |
$version = [System.Environment]::OSVersion.Version
if ($version -lt [Version]"6.1") {
"dinosaur"
} elseif ($version -lt [Version]"6.2") {
"neanderthal"
} else {
"False"
}
register: os_version
- name: become different types
vars: *become_vars
win_whoami:
become_flags: logon_type={{item.type}}
register: become_logon_type
when: not ((item.type == 'network' or item.type == 'network_cleartext') and os_version.stdout_lines[0] == "dinosaur")
failed_when: become_logon_type.logon_type != item.actual and become_logon_type.sid != user_limited_result.sid
with_items:
- type: interactive
actual: Interactive
- type: batch
actual: Batch
- type: network
actual: Network
- type: network_cleartext
actual: NetworkCleartext
- name: become netcredentials with network user
vars:
ansible_become_user: fakeuser
ansible_become_password: fakepassword
ansible_become_method: runas
ansible_become: True
ansible_become_flags: logon_type=new_credentials logon_flags=netcredentials_only
win_whoami:
register: become_netcredentials
- name: assert become netcredentials with network user
assert:
that:
# new_credentials still come up as the ansible_user so we can't test that
- become_netcredentials.label.account_name == 'High Mandatory Level'
- become_netcredentials.label.sid == 'S-1-16-12288'
- name: become logon_flags bitwise tests when loading the profile
# Error code of 2 means no file found == no profile loaded
win_shell: |
Add-Type -Name "Native" -Namespace "Ansible" -MemberDefinition '[DllImport("Userenv.dll", SetLastError=true)]public static extern bool GetProfileType(out UInt32 pdwFlags);'
$profile_type = $null
$res = [Ansible.Native]::GetProfileType([ref]$profile_type)
if (-not $res) {
$last_err = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
if ($last_err -eq 2) {
return $false
} else {
throw [System.ComponentModel.Win32Exception]$last_err
}
} else {
return $true
}
vars: *admin_become_vars
become_flags: logon_flags={{item.flags}}
register: become_logon_flags
failed_when: become_logon_flags.stdout_lines[0]|bool != item.actual
when: os_version.stdout_lines[0] not in ["dinosaur", "neanderthal"] # usual suspect 2008 doesn't support the no profile flags
with_items:
- flags:
actual: False
- flags: netcredentials_only
actual: False
- flags: with_profile,netcredentials_only
actual: True
- name: echo some non ascii characters
win_command: cmd.exe /c echo über den Fußgängerübergang gehen
vars: *become_vars
@@ -348,7 +221,7 @@
win_user:
name: "{{ become_test_username }}"
state: absent
- name: ensure privileged test user is deleted
win_user:
name: "{{ become_test_admin_username }}"
@@ -360,7 +233,7 @@
args:
executable: cmd.exe
when: become_test_username in profile_dir_out.stdout_lines[0]
- name: ensure privileged test user profile is deleted
# NB: have to work around powershell limitation of long filenames until win_file fixes it
win_shell: rmdir /S /Q {{ admin_profile_dir_out.stdout_lines[0] }}

View File

@@ -28,7 +28,7 @@
- cmdout is not changed
- cmdout.cmd == 'bogus_command1234'
- cmdout.rc == 2
- "'Could not locate the following executable bogus_command1234' in cmdout.msg"
- "\"Could not find file 'bogus_command1234.exe'.\" in cmdout.msg"
- name: execute something with error output
win_command: cmd /c "echo some output & echo some error 1>&2"

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,229 @@
#!powershell
#AnsibleRequires -CSharpUtil Ansible.Basic
#AnsibleRequires -CSharpUtil Ansible.Process
$module = [Ansible.Basic.AnsibleModule]::Create($args, @{})
Function Assert-Equals {
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true)][AllowNull()]$Actual,
[Parameter(Mandatory=$true, Position=0)][AllowNull()]$Expected
)
$matched = $false
if ($Actual -is [System.Collections.ArrayList] -or $Actual -is [Array]) {
$Actual.Count | Assert-Equals -Expected $Expected.Count
for ($i = 0; $i -lt $Actual.Count; $i++) {
$actual_value = $Actual[$i]
$expected_value = $Expected[$i]
Assert-Equals -Actual $actual_value -Expected $expected_value
}
$matched = $true
} else {
$matched = $Actual -ceq $Expected
}
if (-not $matched) {
if ($Actual -is [PSObject]) {
$Actual = $Actual.ToString()
}
$call_stack = (Get-PSCallStack)[1]
$module.Result.test = $test
$module.Result.actual = $Actual
$module.Result.expected = $Expected
$module.Result.line = $call_stack.ScriptLineNumber
$module.Result.method = $call_stack.Position.Text
$module.FailJson("AssertionError: actual != expected")
}
}
$tests = @{
"ParseCommandLine empty string" = {
$expected = @((Get-Process -Id $pid).Path)
$actual = [Ansible.Process.ProcessUtil]::ParseCommandLine("")
Assert-Equals -Actual $actual -Expected $expected
}
"ParseCommandLine single argument" = {
$expected = @("powershell.exe")
$actual = [Ansible.Process.ProcessUtil]::ParseCommandLine("powershell.exe")
Assert-Equals -Actual $actual -Expected $expected
}
"ParseCommandLine multiple arguments" = {
$expected = @("powershell.exe", "-File", "C:\temp\script.ps1")
$actual = [Ansible.Process.ProcessUtil]::ParseCommandLine("powershell.exe -File C:\temp\script.ps1")
Assert-Equals -Actual $actual -Expected $expected
}
"ParseCommandLine comples arguments" = {
$expected = @('abc', 'd', 'ef gh', 'i\j', 'k"l', 'm\n op', 'ADDLOCAL=qr, s', 'tuv\', 'w''x', 'yz')
$actual = [Ansible.Process.ProcessUtil]::ParseCommandLine('abc d "ef gh" i\j k\"l m\\"n op" ADDLOCAL="qr, s" tuv\ w''x yz')
Assert-Equals -Actual $actual -Expected $expected
}
"SearchPath normal" = {
$expected = "$($env:SystemRoot)\System32\WindowsPowerShell\v1.0\powershell.exe"
$actual = [Ansible.Process.ProcessUtil]::SearchPath("powershell.exe")
$actual | Assert-Equals -Expected $expected
}
"SearchPath missing" = {
$failed = $false
try {
[Ansible.Process.ProcessUtil]::SearchPath("fake.exe")
} catch {
$failed = $true
$_.Exception.InnerException.GetType().FullName | Assert-Equals -Expected "System.IO.FileNotFoundException"
$expected = 'Exception calling "SearchPath" with "1" argument(s): "Could not find file ''fake.exe''."'
$_.Exception.Message | Assert-Equals -Expected $expected
}
$failed | Assert-Equals -Expected $true
}
"CreateProcess basic" = {
$actual = [Ansible.Process.ProcessUtil]::CreateProcess("whoami.exe")
$actual.GetType().FullName | Assert-Equals -Expected "Ansible.Process.Result"
$actual.StandardOut | Assert-Equals -Expected "$(&whoami.exe)`r`n"
$actual.StandardError | Assert-Equals -Expected ""
$actual.ExitCode | Assert-Equals -Expected 0
}
"CreateProcess stderr" = {
$actual = [Ansible.Process.ProcessUtil]::CreateProcess("powershell.exe [System.Console]::Error.WriteLine('hi')")
$actual.StandardOut | Assert-Equals -Expected ""
$actual.StandardError | Assert-Equals -Expected "hi`r`n"
$actual.ExitCode | Assert-Equals -Expected 0
}
"CreateProcess exit code" = {
$actual = [Ansible.Process.ProcessUtil]::CreateProcess("powershell.exe exit 10")
$actual.StandardOut | Assert-Equals -Expected ""
$actual.StandardError | Assert-Equals -Expected ""
$actual.ExitCode | Assert-Equals -Expected 10
}
"CreateProcess bad executable" = {
$failed = $false
try {
[Ansible.Process.ProcessUtil]::CreateProcess("fake.exe")
} catch {
$failed = $true
$_.Exception.InnerException.GetType().FullName | Assert-Equals -Expected "Ansible.Process.Win32Exception"
$expected = 'Exception calling "CreateProcess" with "1" argument(s): "CreateProcessW() failed '
$expected += '(The system cannot find the file specified, Win32ErrorCode 2)"'
$_.Exception.Message | Assert-Equals -Expected $expected
}
$failed | Assert-Equals -Expected $true
}
"CreateProcess with unicode" = {
$actual = [Ansible.Process.ProcessUtil]::CreateProcess("cmd.exe /c echo 💩 café")
$actual.StandardOut | Assert-Equals -Expected "💩 café`r`n"
$actual.StandardError | Assert-Equals -Expected ""
$actual.ExitCode | Assert-Equals -Expected 0
$actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, "cmd.exe /c echo 💩 café", $null, $null)
$actual.StandardOut | Assert-Equals -Expected "💩 café`r`n"
$actual.StandardError | Assert-Equals -Expected ""
$actual.ExitCode | Assert-Equals -Expected 0
}
"CreateProcess without working dir" = {
$expected = $pwd.Path + "`r`n"
$actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe $pwd.Path', $null, $null)
$actual.StandardOut | Assert-Equals -Expected $expected
$actual.StandardError | Assert-Equals -Expected ""
$actual.ExitCode | Assert-Equals -Expected 0
}
"CreateProcess with working dir" = {
$expected = "C:\Windows`r`n"
$actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe $pwd.Path', "C:\Windows", $null)
$actual.StandardOut | Assert-Equals -Expected $expected
$actual.StandardError | Assert-Equals -Expected ""
$actual.ExitCode | Assert-Equals -Expected 0
}
"CreateProcess without environment" = {
$expected = "$($env:USERNAME)`r`n"
$actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe $env:TEST; $env:USERNAME', $null, $null)
$actual.StandardOut | Assert-Equals -Expected $expected
$actual.StandardError | Assert-Equals -Expected ""
$actual.ExitCode | Assert-Equals -Expected 0
}
"CreateProcess with environment" = {
$env_vars = @{
TEST = "tesTing"
TEST2 = "Testing 2"
}
$actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'cmd.exe /c set', $null, $env_vars)
("TEST=tesTing" -cin $actual.StandardOut.Split("`r`n")) | Assert-Equals -Expected $true
("TEST2=Testing 2" -cin $actual.StandardOut.Split("`r`n")) | Assert-Equals -Expected $true
("USERNAME=$($env:USERNAME)" -cnotin $actual.StandardOut.Split("`r`n")) | Assert-Equals -Expected $true
$actual.StandardError | Assert-Equals -Expected ""
$actual.ExitCode | Assert-Equals -Expected 0
}
"CreateProcess with string stdin" = {
$expected = "input value`r`n`r`n"
$actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe [System.Console]::In.ReadToEnd()',
$null, $null, "input value")
$actual.StandardOut | Assert-Equals -Expected $expected
$actual.StandardError | Assert-Equals -Expected ""
$actual.ExitCode | Assert-Equals -Expected 0
}
"CreateProcess with string stdin and newline" = {
$expected = "input value`r`n`r`n"
$actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe [System.Console]::In.ReadToEnd()',
$null, $null, "input value`r`n")
$actual.StandardOut | Assert-Equals -Expected $expected
$actual.StandardError | Assert-Equals -Expected ""
$actual.ExitCode | Assert-Equals -Expected 0
}
"CreateProcess with byte stdin" = {
$expected = "input value`r`n"
$actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe [System.Console]::In.ReadToEnd()',
$null, $null, [System.Text.Encoding]::UTF8.GetBytes("input value"))
$actual.StandardOut | Assert-Equals -Expected $expected
$actual.StandardError | Assert-Equals -Expected ""
$actual.ExitCode | Assert-Equals -Expected 0
}
"CreateProcess with byte stdin and newline" = {
$expected = "input value`r`n`r`n"
$actual = [Ansible.Process.ProcessUtil]::CreateProcess($null, 'powershell.exe [System.Console]::In.ReadToEnd()',
$null, $null, [System.Text.Encoding]::UTF8.GetBytes("input value`r`n"))
$actual.StandardOut | Assert-Equals -Expected $expected
$actual.StandardError | Assert-Equals -Expected ""
$actual.ExitCode | Assert-Equals -Expected 0
}
"CreateProcess with lpApplicationName" = {
$expected = "abc`r`n"
$full_path = "$($env:SystemRoot)\System32\WindowsPowerShell\v1.0\powershell.exe"
$actual = [Ansible.Process.ProcessUtil]::CreateProcess($full_path, "Write-Output 'abc'", $null, $null)
$actual.StandardOut | Assert-Equals -Expected $expected
$actual.StandardError | Assert-Equals -Expected ""
$actual.ExitCode | Assert-Equals -Expected 0
$actual = [Ansible.Process.ProcessUtil]::CreateProcess($full_path, "powershell.exe Write-Output 'abc'", $null, $null)
$actual.StandardOut | Assert-Equals -Expected $expected
$actual.StandardError | Assert-Equals -Expected ""
$actual.ExitCode | Assert-Equals -Expected 0
}
}
foreach ($test_impl in $tests.GetEnumerator()) {
$test = $test_impl.Key
&$test_impl.Value
}
$module.Result.data = "success"
$module.ExitJson()

View File

@@ -7,3 +7,40 @@
assert:
that:
- ansible_basic_test.data == "success"
# Users by default don't have this right, temporarily enable it
- name: ensure the Users group have the SeBatchLogonRight
win_user_right:
name: SeBatchLogonRight
users:
- Users
action: add
register: batch_user_add
- block:
- name: test Ansible.Become.cs
ansible_become_tests:
register: ansible_become_tests
always:
- name: remove SeBatchLogonRight from users if added in test
win_user_right:
name: SeBatchLogonRight
users:
- Users
action: remove
when: batch_user_add is changed
- name: assert test Ansible.Become.cs
assert:
that:
- ansible_become_tests.data == "success"
- name: test Ansible.Process.cs
ansible_process_tests:
register: ansible_process_tests
- name: assert test Ansible.Process.cs
assert:
that:
- ansible_process_tests.data == "success"

View File

@@ -34,7 +34,7 @@ try {
$actual = Run-Command -command "C:\fakepath\$exe_filename arg1"
Fail-Json -obj $result -message "Test $test_name failed`nCommand should have thrown an exception"
} catch {
Assert-Equals -actual $_.Exception.Message -expected "Exception calling `"SearchPath`" with `"1`" argument(s): `"Could not locate the following executable C:\fakepath\$exe_filename`""
Assert-Equals -actual $_.Exception.Message -expected "Exception calling `"SearchPath`" with `"1`" argument(s): `"Could not find file 'C:\fakepath\$exe_filename'.`""
}
$test_name = "exe in current folder"