Making an MSI that doesn't need a UAC/LUA prompt

The goal

I think that most things don't need to require a UAC prompt to install - just install it for that user.  Why not make the MSI so it doesn't prompt and your users get a smoother experience?  (Also, I feel much better installing a program that doesn't require elevation to install - at a minimum I know it's not disabling my anti-malware software.)  Ideally, with that same package you could optionally install per-machine (which requires elevation).  Here's some information on how to make it happen...

Background

I was recently asked to make an MSI for an extremely minimal replacement of the Run As menu item that was removed in Vista (by calling runas.exe).  Although I doubt we would ever ship something like that I decided to make an installation that didn't elevate, or that could elevate and install per-machine.  It was an interesting experience, although I didn't entirely get the behavior I was going for - there are some limitations that make it impossible to do with a MSI in Windows Vista, but still possible in a single download.  (More on that later).

The technical details

All MSIs elevate on Windows Vista by default.  However there is a flag that you can set that will tell MSI that you don't need to elevate.  The 3rd bit of the word count summary property can be set by using the msiinfo tool (comes with the platform SDK).  Since I also used an embedded cab, that means my word count summary property should be set to 10 (8 | 2 == 10).

msiinfo RunAsNewUser.msi -w 10

You need to have the feature include both the per-user and per-machine information if you're trying to put both in one package like I did (not a best practice - more on that later). 

     <Feature Id='RunAsNewUser' Title='Run as New User' Level='1'>
      <ComponentRef Id='pm.runasnewuser.cmd' />
      <ComponentRef Id='pm.runasnewuser.reg.exe' />
      <ComponentRef Id='pm.runasnewuser.reg.msc' />
      <ComponentRef Id='pu.runasnewuser.cmd' />
      <ComponentRef Id='pu.runasnewuser.reg.exe' />
      <ComponentRef Id='pu.runasnewuser.reg.msc' />
    </Feature>

Your standard MSI that installs to the system directories doesn't face the (fairly simple) issues involved.  Here's the per-machine version of my components (you need two versions of the components in order to put them in different directories(*). [I build all my MSIs using Wix - it gives me fine-grained control for making really solid MSIs and it tends to match the way I want to think about MSIs.]

       <!-- We need a per-machine version as well, with seperate components -->
      <Directory Id='ProgramFilesFolder' Name='AdTools'>
        <Directory Id='pm.Microsoft' Name='Microsoft'>
          <Directory Id='pm.Runas' Name='RunAs'>
            <Component Id='pm.runasnewuser.cmd' Guid='9D448C8B-AF67-423B-9622-D9720770B61E'>
              <File Id='pm.runasnewuser.cmd' Name='runasnewuser.cmd' DiskId='1' Source='runasnewuser.cmd' KeyPath='yes' />
              <Condition>ALLUSERS</Condition>
            </Component>
            <Component Id='pm.runasnewuser.reg.exe' Guid='98A7DE3E-EF1D-434F-80CB-2F878CD0E9F5'>
              <RegistryKey Id='pm.runasnewuser.reg.exe' Key='SystemFileAssociations\.exe\shell\Run as new user...\command' Root='HKCR' Action='createAndRemoveOnUninstall'>
                <RegistryValue Id='pm.runasnewuserCommand.exe' Type='expandable' Value='"[#pm.runasnewuser.cmd]" "%1" %*' KeyPath='yes'/>
              </RegistryKey>
              <Condition>ALLUSERS</Condition>
            </Component>
            <Component Id='pm.runasnewuser.reg.msc' Guid='20D758CC-2774-4532-BD6D-E7C378761C90'>
              <RegistryKey Id='pm.runasnewuser.reg.msc' Key='SystemFileAssociations\.msc\shell\Run as new user...\command' Root='HKCR' Action='createAndRemoveOnUninstall'>
                <RegistryValue Id='pm.runasnewuserCommand.msc' Type='expandable' Value='"[#pm.runasnewuser.cmd]" "%1" %*' KeyPath='yes'/>
              </RegistryKey>
              <Condition>ALLUSERS</Condition>
            </Component>
          </Directory>
        </Directory>
      </Directory>

Notice that I made these components conditional on ALLUSERS.  We'll make the per-user version conditional on NOT (ALLUSERS) .  This is pretty basic stuff, and is more or less simply specifying what goes where without much specialized stuff.  You can do a per-user setup that looks quite similar by putting it somewhere that isn't off the user's profile.  I decided to put mine in the user's profile which means a little extra baggage.

So we're basically done right?  Well, not really, no.  We've disabled the elevation, but now you need to make your installation not require elevation.  Things like installing to Program Files require admin rights, so we'll need to install to somewhere else.  I'm not really sure where that should be - probably %UserProfile%\ProgramFiles or something like that.  In this example, I installed to LocalAppData.

       <Directory Id='LocalAppDataFolder' Name='AdTools'>
        <Directory Id='pu.Microsoft' Name='Microsoft'>
          <Directory Id='pu.Runas' Name='RunAs'>
            <Component Id='pu.runasnewuser.cmd' Guid='06A57A74-7639-4A96-A1FF-6C434ED50CEF'>
              <File Id='pu.runasnewuser.cmd' Name='runasnewuser.cmd' DiskId='1' Source='runasnewuser.cmd' />
              <RegistryValue Id='pu.runasnewuser.cmd.keypath' Root='HKCU' Key='Software\Microsoft\RunAs\KeyPaths' Type='string' Value='RunAsNewUser.cmd' KeyPath='yes' />
              <RemoveFolder Id='pu.Runas' Directory='pu.Runas' On='uninstall'/>
              <RemoveFolder Id='pu.Microsoft' Directory='pu.Microsoft' On='uninstall'/>
              <Condition>NOT (ALLUSERS)</Condition>
            </Component>
            <Component Id='pu.runasnewuser.reg.exe' Guid='C0DB0776-441A-4AB9-871A-5AF1F326FA0A'>
              <RegistryKey Id='pu.runasnewuser.reg.exe' Key='SystemFileAssociations\.exe\shell\Run as new user...\command' Root='HKCR' Action='createAndRemoveOnUninstall'>
                <RegistryValue Id='pu.runasnewuserCommand.exe' Type='expandable' Value='"[#pu.runasnewuser.cmd]" "%1" %*' />
              </RegistryKey>
              <RegistryValue Id='pu.runasnewuser.reg.keypath' Root='HKCU' Key='Software\Microsoft\RunAs\KeyPaths' Type='string' Value='RunAsNewUser.reg' KeyPath='yes' />
              <Condition>NOT (ALLUSERS)</Condition>
            </Component>
            <Component Id='pu.runasnewuser.reg.msc' Guid='C5B94A3B-7D87-4FC0-AC28-111B86679251'>
              <RegistryKey Id='pu.runasnewuser.reg.msc' Key='SystemFileAssociations\.msc\shell\Run as new user...\command' Root='HKCR' Action='createAndRemoveOnUninstall'>
                <RegistryValue Id='pu.runasnewuserCommand.msc' Type='expandable' Value='"[#pu.runasnewuser.cmd]" "%1" %*' />
              </RegistryKey>
              <RegistryValue Id='pu.runasnewuser.reg.msc.keypath' Root='HKCU' Key='Software\Microsoft\RunAs\KeyPaths' Type='string' Value='RunAsNewUser.reg.msc' KeyPath='yes' />
              <Condition>NOT (ALLUSERS)</Condition>
            </Component>
          </Directory>
        </Directory>
      </Directory>

The extra baggage is the HKCU keypath RegistryValue, and the RemoveFolder entries.  Nothing crazy, but a little extra to be aware of.  These are necessary because of things like roaming profiles.

The experience

 You can install it and it goes through without any elevation.  The installation is per-user by default.  (Yay! Per-user works and has a smokin' experience, and supports all kinds of scenarios.)

If you want to install it per-machine, you need to launch it via this command:

msiexec /i RunAsNewUser.msi ALLUSERS=1

This is obviously not ideal - but there's a hidden hiccup.  I thought that it would prompt for elevation if you specified ALLUSERS=1 - it does not.  You get this error message:

You do not have sufficient privileges to complete this installation for all usrs of the machine.

It's not the end of the world.  If you run the same command from an already elevated process (e.g. an elevated command prompt) the installation goes through just fine (as the error message implies).  It's a bit of a shame, but I've confirmed with Carolyn that you can't build a single package that will elevate sometimes and sometimes not (because the flag is in the MSI summary information it can't be changed in-flight).

The best practice for building a per-user (without elevation) and per-machine app (with elevation)

Because of the limitations noted above, here's what is the best practice for building a per-user and per-machine app:  Have a bootstrapper exe, and two separate MSIs (one per-user package and one per-machine package).  If these are embedded in the bootstrapper exe as resources that are extracted at install, then you can a single download that installs either as per-machine or per-user.

Why?  Well, not many folks are trying to do this at the moment.  Of course, more will as Vista is on more and more computers - ultimately I think this is a huge step forward for keeping your computer free of malware.  I think it's worth saying twice: I feel much better installing a program that doesn't require elevation to install - at a minimum I know it's not disabling my anti-malware software.

Other Resources

https://blogs.msdn.com/windows_installer_team/ - Get it straight from the horse's mouth

https://blogs.msdn.com/astebner/archive/2006/12/13/some-useful-things-i-have-learned-about-windows-installer-and-uac.aspx - Good intro article that doesn't bury you in information like Roberts' (great) blog entries

https://blogs.msdn.com/rflaming/archive/2006/10/01/uac-in-msi-notes-answers-to-questions-in-comments-from-earlier-blog-posts.aspx - Summary of Robert's huge series about MSI and UAC

https://blogs.msdn.com/rflaming/archive/2006/09/30/778690.aspx - Robert gives his take on the question: Should I write my installer as a Standard User install? If yes, how?


* I imagine that you don't really need two versions of the components because I imagine that you could change the directory location using a custom action that set it, but I just found things a lot simpler if I used two different components