[Delphi] 管理者権限のチェック&管理者としてアプリを実行する

# はじめに
[Delphi の Discord](https://gist.github.com/freeonterminate/928ccd3ad64c5762315c069afd72e51e) で管理者権限をチェックする関数を提供しました。
せっかくなので、Qiita にも書いておくことにしました。

# 管理者権限をチェックする
Win32 API の [CheckTokenMembership](https://learn.microsoft.com/ja-jp/windows/win32/api/securitybaseapi/nf-securitybaseapi-checktokenmembership) を使い、Sid(Security ID) がグループ（今回は管理者グループ）に属しているかチェックします。

## Sid の取得方法
昔の記事を見ると [AllocateAndInitializeSid](https://learn.microsoft.com/ja-jp/windows/win32/api/securitybaseapi/nf-securitybaseapi-allocateandinitializesid) API に ```SECURITY_NT_AUTHORITY```, ```SECURITY_BUILTIN_DOMAIN_RID```, ```DOMAIN_ALIAS_RID_ADMINS``` を渡して Sid を貰うという方法が出てくるかも知れません。
この方法だと管理者グループを示す Sid は取れるのですが「昇格した管理者権限」についての Sid は取得できません。
つまり、ユーザーに UAC のダイアログが出て実行を許可された場合に付与される一時的な管理者権限をチェックできません。

これを回避するために [CreateWellKnownSid](https://learn.microsoft.com/ja-jp/windows/win32/api/securitybaseapi/nf-securitybaseapi-createwellknownsid) API を使って Sid を取得します。
この API は [WELL_KNOWN_SID_TYPE 列挙型](https://learn.microsoft.com/ja-jp/windows/win32/api/winnt/ne-winnt-well_known_sid_type) に定義されている良く使われるグループの Sid を返してくれます。
今回はその中から ```WinBuiltinAdministratorsSid``` を使います。これはビルトイン管理者グループを示す列挙子です。

# 実装
## API 定義
上記に紹介した、CheckTokenMembership と CreateWellKnownSid はデフォルトでは定義が存在しないので、まずこれを定義します。
```delphi:API定義
function CheckTokenMembership(
  TokenHandle: THandle;
  SidToCheck: PSID;
  var IsMember: BOOL): BOOL; stdcall;
  external advapi32
  name 'CheckTokenMembership';

function CreateWellKnownSid(
  WellKnownSidType: DWORD;
  DomainSid: PSID;
  ptrSID: PSID;
  var cbSid: DWORD): BOOL; stdcall;
  external advapi32
  name 'CreateWellKnownSid';
```

これらの API は型定義からもわかるように、成功すると True を返します。

## Administrator 判定
次に、この２つの API を組み合わせて管理者グループに属しているかをチェックします。

**2023/10/10 修正**
64bit アプリケーションの場合、正しく動作しなかったため修正しました。

```delphi:IsAdminの実装
function IsAdmin: Boolean;
const
  // API 同様、定数も定義されていないのでここで定義
  WinBuiltinAdministratorsSid = 26;
begin
  Result := False;

  var GroupSid: PSID := nil;
  var GroupSidSize: DWORD := 0;

  // nil を渡して必要なサイズを受取る
  CreateWellKnownSid(WinBuiltinAdministratorsSid, nil, nil, GroupSidSize);
  if GetLastError = ERROR_INSUFFICIENT_BUFFER then
  begin
    // 受取ったサイズ分メモリを確保
    GetMem(GroupSid, GroupSidSize);
    try
      var IsMember: BOOL := False;

      if
        // ビルトイン管理者グループの Sid を取得
        CreateWellKnownSid(
          WinBuiltinAdministratorsSid,
          nil,
          GroupSid,
          GroupSidSize
        ) and
        // プロセスがビルトイン管理者グループのメンバーかチェック
        // 第一引数はアクセストークン、0 を指定すると現在のプロセスのトークンを使う
        CheckTokenMembership(0, GroupSid, IsMember)
      then
        Result := IsMember;
    finally
      FreeMem(GroupSid);
    end;
  end;
end;
```

この関数 IsAdmin を使うと自分が管理者権限を持っているのか、が解ります。

```delphi:例
procedure TForm1.Button1Click(Sender: TObject);
begin
  if IsAdmin then
    管理者権限が必要な何らかの処理
  else
    ShowMessage('管理者権限がありません');
end;
```

# 管理者としてアプリを実行する
IsAdmin はインストーラーを作ったり、特殊な場所にファイルをコピーする時に RunAsAdmin と一緒に使われる事が多いです。
RunAsAdmin は exe ファイルを管理者権限で起動する関数です。

以下のように ShellExecuteEx API に ```lpVerb(動詞) = 'runas'``` を指定すると管理者権限として起動する機能をラッピングしただけです。

```delphi:RunAsAdmin
function RunAsAdmin(
  // アプリケーションのフルパス
  const AExeName: String;
  // アプリケーションに渡すスイッチ
  const ASwitch: String;
  // アプリケーションを表示するかどうか
  const AShowWindow: Boolean = True): Boolean;
const
  SHOW_WINDOW: array [Boolean] of Cardinal = (SW_HIDE, SW_SHOWNORMAL);
  MASK_NO_CONSOLE: array [Boolean] of Cardinal = (SEE_MASK_NO_CONSOLE, 0);
var
  SEI: TShellExecuteInfo;
begin
  ZeroMemory(@SEI, SizeOf(SEI));
  with SEI do
  begin
    cbSize := SizeOf(SEI);
    Wnd := 0;
    fMask := SEE_MASK_FLAG_NO_UI or MASK_NO_CONSOLE[AShowWindow];
    lpVerb := 'runas';
    lpFile := PChar(AExeName);
    lpParameters := PChar(ASwitch);
    nShow := SHOW_WINDOW[AShowWindow];
  end;

  Result := ShellExecuteEx(@SEI);
end;
```

以下は C ドライブのルートにファイルをコピーする例です。

```delphi:例
procedure TForm1.Button1Click(Sender: TObject);
begin
  if IsAdmin then
    // 管理者権限を持っている場合は普通の方法でコピーできる
    TFile.Copy('D:\Temp\Foo.txt', 'C:\Foo.txt');
  else
    // 管理者権限を持っていない場合は cmd.exe を管理者として実行し copy させる
    RunAsAdmin('cmd.exe', '/D /C "copy D:\Temp\Foo.txt C:\"', False);
end;
```


# 最後に
Delphi だと必要な API が定義されていないため、古い方法を使っているコードを見ます。
API は手軽に定義できるので、現在ではこちらの方法を使う方が良いでしょう。