Make Inno Setup installer request privileges elevation only when needed

elevated-privilegesinno-setuppascalscriptprivilegesuac

Inno Setup installer has the PrivilegesRequired directive that can be used to control, if privileges elevation is required, when installer is starting. I want my installer to work even for non-admin users (no problem about installing my app to user folder, instead of the Program Files). So I set the PrivilegesRequired to none (undocumented value). This makes UAC prompt popup for admin users only, so they can install even to the Program Files. No UAC prompt for non-admin users, so even them can install the application (to user folder).

This has some drawbacks though:

  • Some people use distinct admin and non-admin accounts on their machines, working with non-admin account normally. In general, when launching installation using non-admin account, when they get UAC prompt, they enter credentials for the admin account to proceed. But this won't work with my installer, because there's no UAC prompt.
  • (Overly suspicious) people with admin account, who want to install to user folder, cannot launch my installer without (not-needed) admin privileges.

Is there some way to make Inno Setup request privileges elevation only when needed (when user selects installation folder writable by admin account only)?

I assume there's no setting for this in Inno Setup. But possibly, there's a programmatic solution (Inno Setup Pascal scripting) or some kind of plugin/DLL.


Note that Inno Setup 6 has a built-in support for non-administrative install mode.

Best Answer

Inno Setup 6 has a built-in support for non-administrative install mode.

Basically, you can simply set PrivilegesRequiredOverridesAllowed:

[Setup]
PrivilegesRequiredOverridesAllowed=commandline dialog

enter image description here

Additionally, you will likely want to use the auto* variants of the constants. Notably the {autopf} for the DefaultDirName.

[Setup]
DefaultDirName={pf}\My Program

The following is my (now obsolete) solution for Inno Setup 5, based on @TLama's answer.

When the setup is started non-elevated, it will request elevation, with some exceptions:

  • Only on Windows Vista and newer (though it should work on Windows XP too)
  • When upgrading, the setup will check if the current user has a write access to the previous installation location. If the user has the write access, the setup won't request the elevation. So if the user has previously installed the application to user folder, the elevation won't be requested on upgrade.

If the user rejects the elevation on a new install, the installer will automatically fall back to "local application data" folder. I.e. C:\Users\standard\AppData\Local\AppName.

Other improvements:

  • the elevated instance won't ask for language again
  • by using PrivilegesRequired=none, the installer will write uninstall information to HKLM, when elevated, not to HKCU.
#define AppId "myapp"
#define AppName "MyApp"

#define InnoSetupReg \
  "Software\Microsoft\Windows\CurrentVersion\Uninstall\" + AppId + "_is1"
#define InnoSetupAppPathReg "Inno Setup: App Path"

[Setup]
AppId={#AppId}
PrivilegesRequired=none
...

[Code]

function IsWinVista: Boolean;
begin
  Result := (GetWindowsVersion >= $06000000);
end;

function HaveWriteAccessToApp: Boolean;
var
  FileName: string;
begin
  FileName := AddBackslash(WizardDirValue) + 'writetest.tmp';
  Result := SaveStringToFile(FileName, 'test', False);
  if Result then
  begin
    Log(Format(
      'Have write access to the last installation path [%s]', [WizardDirValue]));
    DeleteFile(FileName);
  end
    else
  begin
    Log(Format('Does not have write access to the last installation path [%s]', [
      WizardDirValue]));
  end;
end;

procedure ExitProcess(uExitCode: UINT);
  external 'ExitProcess@kernel32.dll stdcall';
function ShellExecute(hwnd: HWND; lpOperation: string; lpFile: string;
  lpParameters: string; lpDirectory: string; nShowCmd: Integer): THandle;
  external 'ShellExecuteW@shell32.dll stdcall';

function Elevate: Boolean;
var
  I: Integer;
  RetVal: Integer;
  Params: string;
  S: string;
begin
  { Collect current instance parameters }
  for I := 1 to ParamCount do
  begin
    S := ParamStr(I);
    { Unique log file name for the elevated instance }
    if CompareText(Copy(S, 1, 5), '/LOG=') = 0 then
    begin
      S := S + '-elevated';
    end;
    { Do not pass our /SL5 switch }
    if CompareText(Copy(S, 1, 5), '/SL5=') <> 0 then
    begin
      Params := Params + AddQuotes(S) + ' ';
    end;
  end;

  { ... and add selected language }
  Params := Params + '/LANG=' + ActiveLanguage;

  Log(Format('Elevating setup with parameters [%s]', [Params]));
  RetVal :=
    ShellExecute(0, 'runas', ExpandConstant('{srcexe}'), Params, '', SW_SHOW);
  Log(Format('Running elevated setup returned [%d]', [RetVal]));
  Result := (RetVal > 32);
  { if elevated executing of this setup succeeded, then... }
  if Result then
  begin
    Log('Elevation succeeded');
    { exit this non-elevated setup instance }
    ExitProcess(0);
  end
    else
  begin
    Log(Format('Elevation failed [%s]', [SysErrorMessage(RetVal)]));
  end;
end;

procedure InitializeWizard;
var
  S: string;
  Upgrade: Boolean;
begin
  Upgrade :=
    RegQueryStringValue(HKLM, '{#InnoSetupReg}', '{#InnoSetupAppPathReg}', S) or
    RegQueryStringValue(HKCU, '{#InnoSetupReg}', '{#InnoSetupAppPathReg}', S);

  { elevate }

  if not IsWinVista then
  begin
    Log(Format('This version of Windows [%x] does not support elevation', [
      GetWindowsVersion]));
  end
    else
  if IsAdminLoggedOn then
  begin
    Log('Running elevated');
  end
    else
  begin
    Log('Running non-elevated');
    if Upgrade then
    begin
      if not HaveWriteAccessToApp then
      begin
        Elevate;
      end;
    end
      else
    begin
      if not Elevate then
      begin
        WizardForm.DirEdit.Text := ExpandConstant('{localappdata}\{#AppName}');
        Log(Format('Falling back to local application user folder [%s]', [
          WizardForm.DirEdit.Text]));
      end;
    end;
  end;
end;
Related Topic