I know this question has been asked in multiple variations, but I'm currently at my wits' and googling prowess' end. I have written a Console application to restart a service on a remote machine.
Other users in our domain also need to be able to use this app for this purpose, even if their credentials would not suffice to restart that service. They do have normal user access (read) rights to that machine though. I therefore am using Impersonation. Unfortunately, the application only works for some users while it fails for others.
All those users belong to the same AD group, all have local admin rights for the computer they are operating, with activated UAC.
Here is the code:
public static extern bool LogonUser(String lpszUsername, String lpszDomain, String lpszPassword,
int dwLogonType, int dwLogonProvider, out SafeTokenHandle phToken);
[DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int DuplicateToken(IntPtr hToken, int impersonationLevel,
ref IntPtr hNewToken);
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public extern static bool CloseHandle(IntPtr handle);
[PermissionSetAttribute(SecurityAction.Demand, Name = "FullTrust")]
static void Main(string[] args)
{
SafeTokenHandle safeTokenHandle;
try
{
const int LOGON32_PROVIDER_DEFAULT = 0;
const int LOGON32_LOGON_INTERACTIVE = 2;
const int LOGON32_LOGON_SERVICE = 5;
// Call LogonUser to obtain a handle to an access token.
bool returnValue = LogonUser(user, domain, password,
LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT,
out safeTokenHandle);
// Use the token handle returned by LogonUser.
using (WindowsIdentity newId = new WindowsIdentity(safeTokenHandle.DangerousGetHandle()))
{
//Duplicate token to set Impersonation Level to Delegate (3)
IntPtr tokenDuplicate = IntPtr.Zero;
DuplicateToken(newId.Token, 3, ref tokenDuplicate);
using (WindowsIdentity newId2 = new WindowsIdentity(tokenDuplicate))
{
using (WindowsImpersonationContext impersonatedUser = newId2.Impersonate())
{
var token = newId2.ImpersonationLevel;
Console.WriteLine("Running as: " + WindowsIdentity.GetCurrent().Name);
Console.WriteLine("Impersonation level: " + token.ToString());
var sc = new ServiceController
{
MachineName = remote,
ServiceName = myService
};
if (sc.Status.Equals(ServiceControllerStatus.Running))
{
sc.Stop();
while (!sc.Status.Equals(ServiceControllerStatus.Stopped))
{
sc.Refresh();
System.Threading.Thread.Sleep(100);
}
}
sc.Refresh();
if (sc.Status.Equals(ServiceControllerStatus.Stopped))
{
sc.Start();
while (!sc.Status.Equals(ServiceControllerStatus.Running))
{
sc.Refresh();
System.Threading.Thread.Sleep(100);
}
}
}
}
}
}
}
public sealed class SafeTokenHandle : SafeHandleZeroOrMinusOneIsInvalid
{
private SafeTokenHandle()
: base(true)
{
}
[DllImport("kernel32.dll")]
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[SuppressUnmanagedCodeSecurity]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseHandle(IntPtr handle);
protected override bool ReleaseHandle()
{
return CloseHandle(handle);
}
}
I have used DuplicateToken because without it, the ImpersonationLevel was "None" so I figured this might be the cause. Alas it didn't change a thing.
The user credentials used in the Impersonation context have local Admin rights on the remote machine and can restart the service. Of the six users using this application, the service gets restarted properly for 4 of them while for 2 others it fails with
Unable to start Service Control Manager on [remote]. You may have insufficient rights...
The console output while running the app is for all users:
Running as: [correct user]
Impersonation level: Delegation
Running the application as Admin does not change anything either. It is a normal standalone Console application, not a WCF service or anything relying on IIS.
All users are running Windows 7 Professional within the domain, directly connected to the network, not via VPN.
Any idea what could be the cause for this? Am I missing something? Could the failing users be missing something? Framework version? Redistributables? Anything?
Got it working, albeit not in the way I really like it (because it doesn't tell me why the other code fails for these two users).
=> using the powers of PowerShell:
using System.Management.Automation;
var command = new PSCommand();
command.AddScript("$pass = convertto-securestring \"thePassword" -asplaintext -force");
command.AddScript("$mycred = new-object -typename System.Management.Automation.PSCredential -argumentlist \"DOMAIN\\user\",$pass");
command.AddScript("$obj = (gwmi -computername \"srv-inet\" -class Win32_Service -credential $mycred | Where-Object { $_.Name -match \"Name of the service\" })");
command.AddScript("$obj.StopService()");
using (var ps = PowerShell.Create())
{
ps.Commands = command;
ps.Invoke();
if (ps.HadErrors)
{
Console.WriteLine(ps.InvocationStateInfo.Reason);
}
while (ps.InvocationStateInfo.State != PSInvocationState.Completed)
System.Threading.Thread.Sleep(10);
//Script completed but service not yet stopped.
//Wait 5 seconds, show countdown to user
for (var i = 10; i > 5; i--)
{
Console.WriteLine(i.ToString());
Thread.Sleep(1000);
}
ps.Commands.Clear();
command = new PSCommand();
command.AddScript("$pass = convertto-securestring \"thePassword\" -asplaintext -force");
command.AddScript("$mycred = new-object -typename System.Management.Automation.PSCredential -argumentlist \"DOMAIN\\user\",$pass");
command.AddScript("$obj = (gwmi -computername \"srv-inet\" -class Win32_Service -credential $mycred | Where-Object { $_.Name -match \"Name of the service\" })");
command.AddScript("$obj.StartService()");
ps.Commands = command;
ps.Invoke();
if (ps.HadErrors)
{
Console.WriteLine(ps.InvocationStateInfo.Reason);
}
while (ps.InvocationStateInfo.State != PSInvocationState.Completed)
System.Threading.Thread.Sleep(10);
//Script completed but service not yet fully started.
//Wait 5 seconds, show countdown to user
for (var i = 5; i > 0; i--)
{
Console.WriteLine(i.ToString());
Thread.Sleep(1000);
}
}
This is not optimized as you can see; I am repeating myself for the service start.
But it works and therefore I am happy.
Still: if anyone knows what could cause the inital code with ServiceController to fail for some users, please do share your knowledge!
Addendum: Even the PowerShell approach did not work at first; I had to download Windows Management Framework 4.0 on one of the failing computers. This seems to strengthen the case that it is indeed some missing component on both machines. Alas I have no idea, which.