My app needs to execute PowerShell scripts to create and manage AD accounts. My current setup uses HangFire to launch background jobs, PowerShell execution being one of them. I have an ImpersonationService setup that Uses RunImpersonated and RunImpersonatedAsync methods of WindowsIdentity.
Impersonation works for API calls, but fails when I run the PowerShell scripts as the scripts are being executed under IIS APPPOOL\APP NAME. My goal is to use a service account which has the required permissions to perform the actions in the scripts.
After a lot of research, I found out that the PowerShell Execution Thread does not inherit the impersonation and have tried multiple ways around it without success.
I understand that I may have to alter my approach if running scripts as an impersonated user from the app is not possible so I'm open to any alternate solutions.
Below is the flow of my current setup.
HangFire queues a background job:
BackgroundJob.Enqueue(() => RunScripts(mac.Username, mac.Id, username));
RunScripts creates an action to be passed through to the ImpersonationService:
public async Task RunScripts(string username, int macId, string responsibleUser)
{
var parameters = new Dictionary<string, object>()
{
{ "MACID", macId }
};
var auditId = await _macAuditTrailService.InitiateScriptProgressTracking(macId, username,
"PowerShell Script Execution", responsibleUser);
var response = new PowerShellRunResponseDto
{
Response = new PSDataCollection<PSObject>()
};
try
{
// Prepare the action to be run under impersonation
Action runPowerShellScript = () =>
{
var runningAs = WindowsIdentity.GetCurrent()?.Name;
parameters.Add("RunningAs", runningAs);
// Directly invoke the script as a synchronous method within the impersonated context
response = _powerShellRunspaceService.RunScript(parameters, auditId, macId).Result; // Ensure RunScript can be called synchronously
};
// Run the action under impersonated context
_impersonationService.RunImpersonated(runPowerShellScript, false);
}
catch (Exception e)
{
// Any exceptions thrown by PowerShell Automation will be caught and logged here
response.Errors = e.Message;
}
// Audit the script execution
await _scriptProgressService.AuditPowerShellScripts(response, username,
action: "PowerShell Script Execution", macId: macId, responsibleUser: responsibleUser,
auditId: auditId);
Console.WriteLine("Script execution completed.");
}
I have tested 3 different methods for Impersonation: RunImpersonated:
public void RunImpersonated(Action action, bool isAderantCall)
{
SafeAccessTokenHandle safeAccessTokenHandle;
bool returnValue = isAderantCall
? LogonUser(username, "Domain", password, 9, 0, out safeAccessTokenHandle)
: LogonUser(username, "Domain", password, 9, 0, out safeAccessTokenHandle);
if (!returnValue)
{
int ret = Marshal.GetLastWin32Error();
safeAccessTokenHandle.Dispose();
throw new System.ComponentModel.Win32Exception(ret);
}
try
{
WindowsIdentity.RunImpersonated(safeAccessTokenHandle, action);
}
finally
{
safeAccessTokenHandle.Dispose();
}
}
RunImpersonatedAsync - Similar setup as above that uses the Async version of RunImpersonated RunImpersonatedThread:
public void RunImpersonatedThread(Action action, bool isAderantCall)
{
Thread newThread = new Thread(() =>
{
SafeAccessTokenHandle safeAccessTokenHandle;
bool returnValue = isAderantCall
? LogonUser(username, "Domain", password, 9, 0, out safeAccessTokenHandle)
: LogonUser(username, "Domain", password, 9, 0, out safeAccessTokenHandle);
if (!returnValue)
{
int ret = Marshal.GetLastWin32Error();
safeAccessTokenHandle.Dispose();
throw new System.ComponentModel.Win32Exception(ret);
}
using (safeAccessTokenHandle)
{
var identity = new WindowsIdentity(safeAccessTokenHandle.DangerousGetHandle());
// Using the synchronous version of RunImpersonated
WindowsIdentity.RunImpersonated(identity.AccessToken, () =>
{
action(); // Execute the passed in action synchronously
});
}
});
newThread.Start();
newThread.Join(); // Optionally wait for the thread to complete
}
This is the RunScript function which creates a Runspace and executes the scripts using PowerShell.Automation:
public async Task<PowerShellRunResponseDto> RunScript(Dictionary<string, object> scriptParameters, int auditId, int macId, string runningAs = "")
{
var dto = new PowerShellRunResponseDto();
InitialSessionState state = InitialSessionState.CreateDefault();
state.ExecutionPolicy = Microsoft.PowerShell.ExecutionPolicy.Unrestricted;
using (Runspace runspace = RunspaceFactory.CreateRunspace(state))
{
runspace.Open();
using (PowerShell ps = PowerShell.Create(runspace))
{
ps.AddScript(
"Write-Output 'Current User:'; Write-Output $([Security.Principal.WindowsIdentity]::GetCurrent().Name)");
ps.Invoke();
var scriptPath = _scriptExecutionSettings.BaseUrl + _scriptExecutionSettings.ScriptName;
// Add the script to the PowerShell instance
ps.AddCommand(scriptPath);
// Append each parameter to the command
foreach (var kvp in scriptParameters)
{
ps.AddParameter(kvp.Key, kvp.Value);
}
ps.Streams.Progress.DataAdded += async (sender, e) =>
{
if (sender is PSDataCollection<ProgressRecord> progressRecords)
{
var progress = progressRecords[e.Index].Activity;
Console.WriteLine("Progress: " + progress);
// Update the database with the progress asynchronously
try
{
await _scriptProgressService.UpdateScriptProgress(auditId, macId, progress,
_ScriptName);
}
catch (Exception ex)
{
throw new Exception(
$"An error occurred while updating powershell script progress: {ex.Message}");
}
}
};
// Execute the script and await the result.
var pipelineObjects = await ps.InvokeAsync().ConfigureAwait(false);
// Check for errors
var errors = "";
if (ps.Streams.Error.Count > 0)
{
foreach (var error in ps.Streams.Error)
{
Console.WriteLine(error.ToString());
errors += error.ToString() + '\n';
}
}
// Print the resulting pipeline objects to the console.
foreach (var item in pipelineObjects)
{
Console.WriteLine(item.BaseObject.ToString());
}
// Assign output to PowerShellRunResponseDto for Auditing logs
dto.Errors = errors;
dto.Data = ps.Streams;
dto.Response = pipelineObjects;
}
runspace.Close();
}
return dto;
}
My setup does what its supposed to do, it provides live progress feedback from the scripts. It just needs to correctly run the impersonation.