mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
win_become: get admin token and fix async (#32485)
* win_become: make it easier to become with an admin token * Fixed up pep8 whitespace * fix for Server 2008 * Added support for async and become on newer hosts and fix warnings
This commit is contained in:
parent
9cfd0a58b0
commit
15b492ca57
2 changed files with 387 additions and 113 deletions
|
@ -171,9 +171,12 @@ Set-StrictMode -Version 2
|
|||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$helper_def = @"
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.AccessControl;
|
||||
using System.Security.Principal;
|
||||
|
@ -212,9 +215,9 @@ namespace Ansible
|
|||
public Int16 wShowWindow;
|
||||
public Int16 cbReserved2;
|
||||
public IntPtr lpReserved2;
|
||||
public IntPtr hStdInput;
|
||||
public IntPtr hStdOutput;
|
||||
public IntPtr hStdError;
|
||||
public SafeFileHandle hStdInput;
|
||||
public SafeFileHandle hStdOutput;
|
||||
public SafeFileHandle hStdError;
|
||||
public STARTUPINFO()
|
||||
{
|
||||
cb = Marshal.SizeOf(this);
|
||||
|
@ -254,6 +257,42 @@ namespace Ansible
|
|||
public SID_AND_ATTRIBUTES User;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct IO_COUNTERS
|
||||
{
|
||||
public UInt64 ReadOperationCount;
|
||||
public UInt64 WriteOperationCount;
|
||||
public UInt64 OtherOperationCount;
|
||||
public UInt64 ReadTransferCount;
|
||||
public UInt64 WriteTransferCount;
|
||||
public UInt64 OtherTransferCount;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct JOBOBJECT_BASIC_LIMIT_INFORMATION
|
||||
{
|
||||
public UInt64 PerProcessUserTimeLimit;
|
||||
public UInt64 PerJobUserTimeLimit;
|
||||
public LimitFlags LimitFlags;
|
||||
public UIntPtr MinimumWorkingSetSize;
|
||||
public UIntPtr MaximumWorkingSetSize;
|
||||
public UInt32 ActiveProcessLimit;
|
||||
public UIntPtr Affinity;
|
||||
public UInt32 PriorityClass;
|
||||
public UInt32 SchedulingClass;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
|
||||
{
|
||||
public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
|
||||
public IO_COUNTERS IoInfo;
|
||||
public UIntPtr ProcessMemoryLimit;
|
||||
public UIntPtr JobMemoryLimit;
|
||||
public UIntPtr PeakProcessMemoryUsed;
|
||||
public UIntPtr PeakJobMemoryUsed;
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum StartupInfoFlags : uint
|
||||
{
|
||||
|
@ -263,6 +302,7 @@ namespace Ansible
|
|||
[Flags]
|
||||
public enum CreationFlags : uint
|
||||
{
|
||||
CREATE_BREAKAWAY_FROM_JOB = 0x01000000,
|
||||
CREATE_DEFAULT_ERROR_MODE = 0x04000000,
|
||||
CREATE_NEW_CONSOLE = 0x00000010,
|
||||
CREATE_NEW_PROCESS_GROUP = 0x00000200,
|
||||
|
@ -353,11 +393,35 @@ namespace Ansible
|
|||
TokenImpersonation
|
||||
}
|
||||
|
||||
enum JobObjectInfoType
|
||||
{
|
||||
AssociateCompletionPortInformation = 7,
|
||||
BasicLimitInformation = 2,
|
||||
BasicUIRestrictions = 4,
|
||||
EndOfJobTimeInformation = 6,
|
||||
ExtendedLimitInformation = 9,
|
||||
SecurityLimitInformation = 5,
|
||||
GroupInformation = 11
|
||||
}
|
||||
|
||||
[Flags]
|
||||
enum ThreadAccessRights : uint
|
||||
{
|
||||
SUSPEND_RESUME = 0x0002
|
||||
}
|
||||
|
||||
[Flags]
|
||||
public enum LimitFlags : uint
|
||||
{
|
||||
JOB_OBJECT_LIMIT_BREAKAWAY_OK = 0x00000800,
|
||||
JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000
|
||||
}
|
||||
|
||||
class NativeWaitHandle : WaitHandle
|
||||
{
|
||||
public NativeWaitHandle(IntPtr handle)
|
||||
{
|
||||
this.Handle = handle;
|
||||
this.SafeWaitHandle = new SafeWaitHandle(handle, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -380,6 +444,69 @@ namespace Ansible
|
|||
public uint ExitCode { get; internal set; }
|
||||
}
|
||||
|
||||
public class Job : IDisposable
|
||||
{
|
||||
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern IntPtr CreateJobObject(
|
||||
IntPtr lpJobAttributes,
|
||||
string lpName);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool SetInformationJobObject(
|
||||
IntPtr hJob,
|
||||
JobObjectInfoType JobObjectInfoClass,
|
||||
IntPtr lpJobObjectInfo,
|
||||
UInt32 cbJobObjectInfoLength);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool AssignProcessToJobObject(
|
||||
IntPtr hJob,
|
||||
IntPtr hProcess);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern bool CloseHandle(
|
||||
IntPtr hObject);
|
||||
|
||||
private IntPtr handle;
|
||||
|
||||
public Job()
|
||||
{
|
||||
handle = CreateJobObject(IntPtr.Zero, null);
|
||||
if (handle == IntPtr.Zero)
|
||||
throw new Win32Exception("CreateJobObject() failed");
|
||||
|
||||
JOBOBJECT_BASIC_LIMIT_INFORMATION jobInfo = new JOBOBJECT_BASIC_LIMIT_INFORMATION();
|
||||
jobInfo.LimitFlags = LimitFlags.JOB_OBJECT_LIMIT_BREAKAWAY_OK | LimitFlags.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||
|
||||
JOBOBJECT_EXTENDED_LIMIT_INFORMATION extendedJobInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
|
||||
extendedJobInfo.BasicLimitInformation = jobInfo;
|
||||
|
||||
int length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
|
||||
IntPtr pExtendedJobInfo = Marshal.AllocHGlobal(length);
|
||||
Marshal.StructureToPtr(extendedJobInfo, pExtendedJobInfo, false);
|
||||
|
||||
if (!SetInformationJobObject(handle, JobObjectInfoType.ExtendedLimitInformation, pExtendedJobInfo, (UInt32)length))
|
||||
throw new Win32Exception("SetInformationJobObject() failed");
|
||||
}
|
||||
|
||||
public void AssignProcess(IntPtr processHandle)
|
||||
{
|
||||
if (!AssignProcessToJobObject(handle, processHandle))
|
||||
throw new Win32Exception("AssignProcessToJobObject() failed");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (handle != IntPtr.Zero)
|
||||
{
|
||||
CloseHandle(handle);
|
||||
handle = IntPtr.Zero;
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
|
||||
public class BecomeUtil
|
||||
{
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
|
@ -407,14 +534,14 @@ namespace Ansible
|
|||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern bool CreatePipe(
|
||||
out IntPtr hReadPipe,
|
||||
out IntPtr hWritePipe,
|
||||
out SafeFileHandle hReadPipe,
|
||||
out SafeFileHandle hWritePipe,
|
||||
SECURITY_ATTRIBUTES lpPipeAttributes,
|
||||
uint nSize);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool SetHandleInformation(
|
||||
IntPtr hObject,
|
||||
SafeFileHandle hObject,
|
||||
HandleFlags dwMask,
|
||||
int dwFlags);
|
||||
|
||||
|
@ -431,7 +558,8 @@ namespace Ansible
|
|||
private static extern IntPtr GetProcessWindowStation();
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern IntPtr GetThreadDesktop(int dwThreadId);
|
||||
private static extern IntPtr GetThreadDesktop(
|
||||
int dwThreadId);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern int GetCurrentThreadId();
|
||||
|
@ -480,17 +608,27 @@ namespace Ansible
|
|||
out IntPtr phNewToken);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
public static extern bool ImpersonateLoggedOnUser(
|
||||
private static extern bool ImpersonateLoggedOnUser(
|
||||
IntPtr hToken);
|
||||
|
||||
[DllImport("advapi32.dll", SetLastError = true)]
|
||||
public static extern bool RevertToSelf();
|
||||
private static extern bool RevertToSelf();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern SafeFileHandle OpenThread(
|
||||
ThreadAccessRights dwDesiredAccess,
|
||||
bool bInheritHandle,
|
||||
int dwThreadId);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern int ResumeThread(
|
||||
SafeHandle hThread);
|
||||
|
||||
public static CommandResult RunAsUser(string username, string password, string lpCommandLine, string lpCurrentDirectory, string stdinInput)
|
||||
{
|
||||
SecurityIdentifier account = GetBecomeSid(username);
|
||||
|
||||
CreationFlags startup_flags = CreationFlags.CREATE_UNICODE_ENVIRONMENT;
|
||||
CreationFlags startup_flags = CreationFlags.CREATE_UNICODE_ENVIRONMENT | CreationFlags.CREATE_BREAKAWAY_FROM_JOB | CreationFlags.CREATE_SUSPENDED;
|
||||
|
||||
STARTUPINFOEX si = new STARTUPINFOEX();
|
||||
si.startupInfo.dwFlags = (int)StartupInfoFlags.USESTDHANDLES;
|
||||
|
@ -499,7 +637,7 @@ namespace Ansible
|
|||
pipesec.bInheritHandle = true;
|
||||
|
||||
// Create the stdout, stderr and stdin pipes used in the process and add to the startupInfo
|
||||
IntPtr stdout_read, stdout_write, stderr_read, stderr_write, stdin_read, stdin_write = IntPtr.Zero;
|
||||
SafeFileHandle stdout_read, stdout_write, stderr_read, stderr_write, stdin_read, stdin_write;
|
||||
if (!CreatePipe(out stdout_read, out stdout_write, pipesec, 0))
|
||||
throw new Win32Exception("STDOUT pipe setup failed");
|
||||
if (!SetHandleInformation(stdout_read, HandleFlags.INHERIT, 0))
|
||||
|
@ -521,7 +659,7 @@ namespace Ansible
|
|||
|
||||
// Setup the stdin buffer
|
||||
UTF8Encoding utf8_encoding = new UTF8Encoding(false);
|
||||
FileStream stdin_fs = new FileStream(stdin_write, FileAccess.Write, true, 32768);
|
||||
FileStream stdin_fs = new FileStream(stdin_write, FileAccess.Write, 32768);
|
||||
StreamWriter stdin = new StreamWriter(stdin_fs, utf8_encoding, 32768);
|
||||
|
||||
// Create the environment block if set
|
||||
|
@ -554,26 +692,44 @@ namespace Ansible
|
|||
if (!launch_success)
|
||||
throw new Win32Exception("Failed to start become process");
|
||||
|
||||
// Setup the output buffers and get stdout/stderr
|
||||
FileStream stdout_fs = new FileStream(stdout_read, FileAccess.Read, true, 4096);
|
||||
StreamReader stdout = new StreamReader(stdout_fs, utf8_encoding, true, 4096);
|
||||
CloseHandle(stdout_write);
|
||||
// If 2012/8+ OS, create new job with JOB_OBJECT_LIMIT_BREAKAWAY_OK
|
||||
// so that async can work
|
||||
Job job = null;
|
||||
if (Environment.OSVersion.Version >= new Version("6.2"))
|
||||
{
|
||||
job = new Job();
|
||||
job.AssignProcess(pi.hProcess);
|
||||
}
|
||||
ResumeProcessById(pi.dwProcessId);
|
||||
|
||||
FileStream stderr_fs = new FileStream(stderr_read, FileAccess.Read, true, 4096);
|
||||
CommandResult result = new CommandResult();
|
||||
try
|
||||
{
|
||||
// Setup the output buffers and get stdout/stderr
|
||||
FileStream stdout_fs = new FileStream(stdout_read, FileAccess.Read, 4096);
|
||||
StreamReader stdout = new StreamReader(stdout_fs, utf8_encoding, true, 4096);
|
||||
stdout_write.Close();
|
||||
|
||||
FileStream stderr_fs = new FileStream(stderr_read, FileAccess.Read, 4096);
|
||||
StreamReader stderr = new StreamReader(stderr_fs, utf8_encoding, true, 4096);
|
||||
CloseHandle(stderr_write);
|
||||
stderr_write.Close();
|
||||
|
||||
stdin.WriteLine(stdinInput);
|
||||
stdin.Close();
|
||||
|
||||
string stdout_str, stderr_str = null;
|
||||
GetProcessOutput(stdout, stderr, out stdout_str, out stderr_str);
|
||||
uint rc = GetProcessExitCode(pi.hProcess);
|
||||
UInt32 rc = GetProcessExitCode(pi.hProcess);
|
||||
|
||||
CommandResult result = new CommandResult();
|
||||
result.StandardOut = stdout_str;
|
||||
result.StandardError = stderr_str;
|
||||
result.ExitCode = rc;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (job != null)
|
||||
job.Dispose();
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
@ -604,71 +760,64 @@ namespace Ansible
|
|||
|
||||
GrantAccessToWindowStationAndDesktop(account);
|
||||
string account_sid = account.ToString();
|
||||
bool impersonated = false;
|
||||
IntPtr hSystemTokenDup = IntPtr.Zero;
|
||||
|
||||
if (service_sids.Contains(account_sid))
|
||||
// Try to get SYSTEM token handle so we can impersonate to get full admin token
|
||||
IntPtr hSystemToken = GetSystemUserHandle();
|
||||
if (hSystemToken == IntPtr.Zero && service_sids.Contains(account_sid))
|
||||
{
|
||||
// We are trying to become to a service account
|
||||
IntPtr hToken = GetUserHandle();
|
||||
if (hToken == IntPtr.Zero)
|
||||
throw new Exception("Failed to get token for NT AUTHORITY\\SYSTEM");
|
||||
|
||||
IntPtr hTokenDup = IntPtr.Zero;
|
||||
try
|
||||
// We need the SYSTEM token if we want to become one of those accounts, fail here
|
||||
throw new Win32Exception("Failed to get token for NT AUTHORITY\\SYSTEM");
|
||||
}
|
||||
else if (hSystemToken != IntPtr.Zero)
|
||||
{
|
||||
if (!DuplicateTokenEx(
|
||||
hToken,
|
||||
// We have the token, need to duplicate and impersonate
|
||||
bool dupResult = DuplicateTokenEx(
|
||||
hSystemToken,
|
||||
TokenAccessLevels.MaximumAllowed,
|
||||
IntPtr.Zero,
|
||||
SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation,
|
||||
TOKEN_TYPE.TokenPrimary,
|
||||
out hTokenDup))
|
||||
out hSystemTokenDup);
|
||||
int lastError = Marshal.GetLastWin32Error();
|
||||
CloseHandle(hSystemToken);
|
||||
|
||||
if (!dupResult && service_sids.Contains(account_sid))
|
||||
throw new Win32Exception(lastError, "Failed to duplicate token for NT AUTHORITY\\SYSTEM");
|
||||
else if (dupResult && account_sid != "S-1-5-18")
|
||||
{
|
||||
throw new Win32Exception("Failed to duplicate the SYSTEM account token");
|
||||
if (ImpersonateLoggedOnUser(hSystemTokenDup))
|
||||
impersonated = true;
|
||||
else if (service_sids.Contains(account_sid))
|
||||
throw new Win32Exception("Failed to impersonate as SYSTEM account");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
CloseHandle(hToken);
|
||||
}
|
||||
|
||||
string lpszDomain = "NT AUTHORITY";
|
||||
string lpszUsername = null;
|
||||
LogonType logonType;
|
||||
string domain = null;
|
||||
if (service_sids.Contains(account_sid))
|
||||
{
|
||||
logonType = LogonType.LOGON32_LOGON_SERVICE;
|
||||
domain = "NT AUTHORITY";
|
||||
password = null;
|
||||
switch (account_sid)
|
||||
{
|
||||
case "S-1-5-18":
|
||||
tokens.Add(hTokenDup);
|
||||
tokens.Add(hSystemTokenDup);
|
||||
return tokens;
|
||||
case "S-1-5-19":
|
||||
lpszUsername = "LocalService";
|
||||
username = "LocalService";
|
||||
break;
|
||||
case "S-1-5-20":
|
||||
lpszUsername = "NetworkService";
|
||||
username = "NetworkService";
|
||||
break;
|
||||
}
|
||||
|
||||
if (!ImpersonateLoggedOnUser(hTokenDup))
|
||||
throw new Win32Exception("Failed to impersonate as SYSTEM account");
|
||||
|
||||
IntPtr newToken = IntPtr.Zero;
|
||||
if (!LogonUser(
|
||||
lpszUsername,
|
||||
lpszDomain,
|
||||
null,
|
||||
LogonType.LOGON32_LOGON_SERVICE,
|
||||
LogonProvider.LOGON32_PROVIDER_DEFAULT,
|
||||
out newToken))
|
||||
{
|
||||
throw new Win32Exception("LogonUser failed");
|
||||
}
|
||||
|
||||
RevertToSelf();
|
||||
tokens.Add(newToken);
|
||||
return tokens;
|
||||
}
|
||||
else
|
||||
{
|
||||
// We are trying to become a local or domain account
|
||||
string domain = null;
|
||||
logonType = LogonType.LOGON32_LOGON_INTERACTIVE;
|
||||
if (username.Contains(@"\"))
|
||||
{
|
||||
var user_split = username.Split(Convert.ToChar(@"\"));
|
||||
|
@ -679,31 +828,35 @@ namespace Ansible
|
|||
domain = null;
|
||||
else
|
||||
domain = ".";
|
||||
}
|
||||
|
||||
// Logon and get the token
|
||||
IntPtr hToken = IntPtr.Zero;
|
||||
if (!LogonUser(
|
||||
username,
|
||||
domain,
|
||||
password,
|
||||
LogonType.LOGON32_LOGON_INTERACTIVE,
|
||||
logonType,
|
||||
LogonProvider.LOGON32_PROVIDER_DEFAULT,
|
||||
out hToken))
|
||||
{
|
||||
throw new Win32Exception("LogonUser failed");
|
||||
}
|
||||
|
||||
// Get the elevate token
|
||||
if (!service_sids.Contains(account_sid))
|
||||
{
|
||||
// Try and get the elevated token for local/domain account
|
||||
IntPtr hTokenElevated = GetElevatedToken(hToken);
|
||||
|
||||
tokens.Add(hTokenElevated);
|
||||
}
|
||||
tokens.Add(hToken);
|
||||
|
||||
if (impersonated)
|
||||
RevertToSelf();
|
||||
|
||||
return tokens;
|
||||
}
|
||||
}
|
||||
|
||||
private static IntPtr GetUserHandle()
|
||||
private static IntPtr GetSystemUserHandle()
|
||||
{
|
||||
uint array_byte_size = 1024 * sizeof(uint);
|
||||
IntPtr[] pids = new IntPtr[1024];
|
||||
|
@ -864,6 +1017,44 @@ namespace Ansible
|
|||
security.Persist(safeHandle, AccessControlSections.Access);
|
||||
}
|
||||
|
||||
private static void ResumeThreadById(int threadId)
|
||||
{
|
||||
var threadHandle = OpenThread(ThreadAccessRights.SUSPEND_RESUME, false, threadId);
|
||||
if (threadHandle.IsInvalid)
|
||||
throw new Win32Exception(String.Format("Thread ID {0} is invalid", threadId));
|
||||
|
||||
try
|
||||
{
|
||||
if (ResumeThread(threadHandle) == -1)
|
||||
throw new Win32Exception(String.Format("Thread ID {0} cannot be resumed", threadId));
|
||||
}
|
||||
finally
|
||||
{
|
||||
threadHandle.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ResumeProcessById(int pid)
|
||||
{
|
||||
var proc = Process.GetProcessById(pid);
|
||||
|
||||
// wait for at least one suspended thread in the process (this handles possible slow startup race where
|
||||
// primary thread of created-suspended process has not yet become runnable)
|
||||
var retryCount = 0;
|
||||
while (!proc.Threads.OfType<ProcessThread>().Any(t => t.ThreadState == System.Diagnostics.ThreadState.Wait &&
|
||||
t.WaitReason == ThreadWaitReason.Suspended))
|
||||
{
|
||||
proc.Refresh();
|
||||
Thread.Sleep(50);
|
||||
if (retryCount > 100)
|
||||
throw new InvalidOperationException(String.Format("No threads were suspended in target PID {0} after 5s", pid));
|
||||
}
|
||||
|
||||
foreach (var thread in proc.Threads.OfType<ProcessThread>().Where(t => t.ThreadState == System.Diagnostics.ThreadState.Wait &&
|
||||
t.WaitReason == ThreadWaitReason.Suspended))
|
||||
ResumeThreadById(thread.Id);
|
||||
}
|
||||
|
||||
private class GenericSecurity : NativeObjectSecurity
|
||||
{
|
||||
public GenericSecurity(bool isContainer, ResourceType resType, SafeHandle objectHandle, AccessControlSections sectionsRequested)
|
||||
|
@ -969,8 +1160,7 @@ Function Run($payload) {
|
|||
$username = $payload.become_user
|
||||
$password = $payload.become_password
|
||||
|
||||
# FUTURE: convert to SafeHandle so we can stop ignoring warnings?
|
||||
Add-Type -TypeDefinition $helper_def -Debug:$false -IgnoreWarnings
|
||||
Add-Type -TypeDefinition $helper_def -Debug:$false
|
||||
|
||||
# NB: CreateProcessWithTokenW commandline maxes out at 1024 chars, must bootstrap via filesystem
|
||||
$temp = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName() + ".ps1")
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
- set_fact:
|
||||
become_test_username: ansible_become_test
|
||||
become_test_admin_username: ansible_become_admin
|
||||
gen_pw: password123! + {{ lookup('password', '/dev/null chars=ascii_letters,digits length=8') }}
|
||||
|
||||
- name: create unprivileged user
|
||||
|
@ -9,16 +10,19 @@
|
|||
update_password: always
|
||||
groups: Users
|
||||
|
||||
- name: create a privileged user
|
||||
win_user:
|
||||
name: "{{ become_test_admin_username }}"
|
||||
password: "{{ gen_pw }}"
|
||||
update_password: always
|
||||
groups: Administrators
|
||||
|
||||
- name: execute tests and ensure that test user is deleted regardless of success/failure
|
||||
block:
|
||||
- name: ensure current user is not the become user
|
||||
win_shell: whoami
|
||||
register: whoami_out
|
||||
|
||||
- name: verify output
|
||||
assert:
|
||||
that:
|
||||
- not whoami_out.stdout_lines[0].endswith(become_test_username)
|
||||
failed_when: whoami_out.stdout_lines[0].endswith(become_test_username) or whoami_out.stdout_lines[0].endswith(become_test_admin_username)
|
||||
|
||||
- name: get become user profile dir so we can clean it up later
|
||||
vars: &become_vars
|
||||
|
@ -34,7 +38,21 @@
|
|||
that:
|
||||
- become_test_username in profile_dir_out.stdout_lines[0]
|
||||
|
||||
- name: test become runas via task vars
|
||||
- name: get become admin user profile dir so we can clean it up later
|
||||
vars: &admin_become_vars
|
||||
ansible_become_user: "{{ become_test_admin_username }}"
|
||||
ansible_become_password: "{{ gen_pw }}"
|
||||
ansible_become_method: runas
|
||||
ansible_become: yes
|
||||
win_shell: $env:USERPROFILE
|
||||
register: admin_profile_dir_out
|
||||
|
||||
- name: ensure profile dir contains admin test username
|
||||
assert:
|
||||
that:
|
||||
- become_test_admin_username in admin_profile_dir_out.stdout_lines[0]
|
||||
|
||||
- name: test become runas via task vars (underprivileged user)
|
||||
vars: *become_vars
|
||||
win_shell: whoami
|
||||
register: whoami_out
|
||||
|
@ -44,6 +62,36 @@
|
|||
that:
|
||||
- whoami_out.stdout_lines[0].endswith(become_test_username)
|
||||
|
||||
- name: test become runas to ensure underprivileged user has medium integrity level
|
||||
vars: *become_vars
|
||||
win_shell: whoami /groups
|
||||
register: whoami_out
|
||||
|
||||
- name: verify output
|
||||
assert:
|
||||
that:
|
||||
- '"Mandatory Label\Medium Mandatory Level" in whoami_out.stdout'
|
||||
|
||||
- name: test become runas via task vars (privileged user)
|
||||
vars: *admin_become_vars
|
||||
win_shell: whoami
|
||||
register: whoami_out
|
||||
|
||||
- name: verify output
|
||||
assert:
|
||||
that:
|
||||
- whoami_out.stdout_lines[0].endswith(become_test_admin_username)
|
||||
|
||||
- name: test become runas to ensure privileged user has high integrity level
|
||||
vars: *admin_become_vars
|
||||
win_shell: whoami /groups
|
||||
register: whoami_out
|
||||
|
||||
- name: verify output
|
||||
assert:
|
||||
that:
|
||||
- '"Mandatory Label\High Mandatory Level" in whoami_out.stdout'
|
||||
|
||||
- name: test become runas via task keywords
|
||||
vars:
|
||||
ansible_become_password: "{{ gen_pw }}"
|
||||
|
@ -51,7 +99,6 @@
|
|||
become_method: runas
|
||||
become_user: "{{ become_test_username }}"
|
||||
win_shell: whoami
|
||||
|
||||
register: whoami_out
|
||||
|
||||
- name: verify output
|
||||
|
@ -111,17 +158,54 @@
|
|||
that:
|
||||
- whoami_out.stdout_lines[0] == "nt authority\\local service"
|
||||
|
||||
# Test out Async on Windows Server 2012+
|
||||
- name: get OS version
|
||||
win_shell: if ([System.Environment]::OSVersion.Version -ge [Version]"6.2") { $true } else { $false }
|
||||
register: os_version
|
||||
|
||||
- name: test become + async on older hosts
|
||||
vars: *become_vars
|
||||
win_command: whoami
|
||||
async: 10
|
||||
register: whoami_out
|
||||
ignore_errors: yes
|
||||
|
||||
- name: verify older hosts failed with become + async
|
||||
assert:
|
||||
that:
|
||||
- whoami_out|failed
|
||||
when: os_version.stdout_lines[0] == "False"
|
||||
|
||||
- name: verify newer hosts worked with become + async
|
||||
assert:
|
||||
that:
|
||||
- whoami_out|success
|
||||
when: os_version.stdout_lines[0] == "True"
|
||||
|
||||
# FUTURE: test raw + script become behavior once they're running under the exec wrapper again
|
||||
# FUTURE: add standalone playbook tests to include password prompting and play become keywords
|
||||
|
||||
always:
|
||||
- name: ensure test user is deleted
|
||||
- name: ensure underprivileged test user is deleted
|
||||
win_user:
|
||||
name: "{{ become_test_username }}"
|
||||
state: absent
|
||||
- name: ensure test user profile is deleted
|
||||
|
||||
- name: ensure privileged test user is deleted
|
||||
win_user:
|
||||
name: "{{ become_test_admin_username }}"
|
||||
state: absent
|
||||
|
||||
- name: ensure underprivileged 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 {{ profile_dir_out.stdout_lines[0] }}
|
||||
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] }}
|
||||
args:
|
||||
executable: cmd.exe
|
||||
when: become_test_admin_username in admin_profile_dir_out.stdout_lines[0]
|
||||
|
|
Loading…
Reference in a new issue