Pulled Apart - Part IX: Windows User Account Control
[
]
Note: This is part of a series, you can find the rest of the parts in the series index.
Windows Vista introduced a feature called User Account Control (UAC), which added the following to Windows (along with a lot of hair-pulling by some users). Visually, it brought a small shield overlay icon
to indicate to the user that when you click that icon or button, you’ll be prompted to confirm your action—and possibly to enter an administrator username and password. This was introduced to stop people from shooting themselves in the foot by making certain actions, which could break Windows, require special privileges (called administrator privileges, though I find this confusing alongside administrator users and groups; I call it root privileges).

I’ve been a fan of this idea since its launch, and as a developer, I’ve kept it turned on—mainly because my customers may not have it turned off. Imagine the scenario where I have it off, and something works, but on a customer’s machine, it fails because UAC is on. A Works on my machine scenario, indeed! 🙁
Pull has a component that directly interacts with UAC—registering protocol handlers. I don’t want the entire Pull application to need root privileges when running; I only want the small part where you can register or unregister a protocol handler to require root privileges.
Multiple Processes
The first issue is that root privileges are assigned to an entire process, not to a thread, method, or subpart. To solve this for Pull, I created a second executable file, ProtocolHandler.exe, which handles the registering and unregistering of protocol handlers via command-line parameters.
This enables Pull to launch this secondary executable with the required root privileges and offload the heavy lifting without Pull itself needing any elevated permissions.

Running with Root Privileges
Launching another process in C# is straightforward thanks to the Process class, which handles execution via the Start() method. The Process class knows which process to run because of the ProcessStartInfo class, configured beforehand and passed to the StartInfo property.
To enable root privileges in the new process, simply set the Verb property of ProcessStartInfo to "runas" (line 6 below). Pull also waits for the process to finish running so the user isn’t left confused by an immediate return with no visible action. This is achieved using the WaitForExit() method on the Process instance (line 12 below).
private static void RunProtocolHandler(string arguments)
{
ProcessStartInfo processStartInfo = new ProcessStartInfo();
processStartInfo.FileName = Path.Combine(Directory.GetCurrentDirectory(), "ProtocolHandler.exe");
processStartInfo.Arguments = arguments;
processStartInfo.Verb = "runas";
using (Process process = new Process())
{
process.StartInfo = processStartInfo;
process.Start();
process.WaitForExit();
}
}
The Shield
The final component was to follow UI guidelines by placing a shield icon on buttons that launch the secondary application, alerting users that root privileges are required. While you could manually place a shield image on such buttons, this is not recommended because:
- What if the logo changes in future Windows versions? (You’d be out of date!) 🙁
- What if the shield isn’t needed because the user already has root privileges?
To handle this, I created a small class called UACShield with two methods:
- IsAdmin(): Returns
trueif the user has root privileges,falseotherwise (using .NET’s built-in check for the Administrator role). - AddShieldToButton(): Takes a button and, if the user isn’t an admin, adds the shield icon by calling the Win32 API’s SendMessage to update the button. A caveat here: the button’s FlatStyle must match the system’s style, or custom UI tweaks may break.
internal class UACShield
{
private class NativeMethods
{
[DllImport("user32")]
public static extern UInt32 SendMessage(IntPtr hWnd, UInt32 msg, UInt32 wParam, UInt32 lParam);
public const int BCM_SETSHIELD = 0x160C; // Elevated button
}
public static bool IsAdmin()
{
using (WindowsIdentity currentIdentity = WindowsIdentity.GetCurrent())
{
return new WindowsPrincipal(currentIdentity).IsInRole(WindowsBuiltInRole.Administrator);
}
}
public static void AddShieldToButton(Button button)
{
if (IsAdmin())
{
// No need for admins
return;
}
button.FlatStyle = FlatStyle.System;
NativeMethods.SendMessage(button.Handle, NativeMethods.BCM_SETSHIELD, 0, 0xFFFFFFFF);
}
}