環境
Windows10 Pro 64bit 22H2
Visual Studio 2017 Community Edition
.NET Framework 4(Windows10にプレインストールされているランタイムで動作するようにするため)
Administratorグループに所属していない一般ユーザでWindowsにログインしており、デバッグの際もそのユーザでログインしている。そのため、管理者権限に昇格する場合はAdministratorグループに入っている別のユーザのユーザー名とパスワードを入力する必要がある点を参考情報として記載します。
背景
元々は自作ソフトを自動更新するためのアップデーターを作成するためにかじった知識だった。
インストーラを使って「Program Files」にインストールすると決まっていれば、管理者権限を昇格すべきかを判定せずに、問答無用で管理者権限を昇格すれば問題ないですが、
仮にインストーラを使った場合でも、UAC対策のためなのか、そのユーザでしか使わないためなのか、「C:\Users\ユーザー名\AppData\Local」という管理者権限が不要のフォルダにインストールする可能性も考えられます。
また、自作ソフトをフォルダに入れてZIP圧縮した状態のまま配布し、使う相手はZIPを解凍するだけで使える状態の場合、管理者権限は不要な場合が多い。
それらの管理者権限が不要なフォルダにあるアプリは、できるだけ管理者権限を要さないままアップデートしたい。
それで、インストールフォルダに対して管理者権限の昇格が最低限必要かどうかを判断したかったわけです。
古いPCのHDDを外部HDDケースに入れて繋ぐ場合など管理者権限があっても権限が足りない場合は、管理者権限があっても書き込めない形にはなりますが、今回の場合は既にインストールされているフォルダ内のアプリの自動更新の実装が最終目的なので、PCが壊れるといった異常事態が無い限りは昇格すれば書き込めるでしょうし、そもそも異常事態が発生した場面では別の対策をするはずなので自動更新処理の出番はないと考えます。
なので、自動更新を実装する場合
インストールフォルダが今ログインしているユーザの権限で書き込めるかどうかを確認し、
昇格せずに書き込めるのであればそのまま自動更新し、
昇格せずに書き込めないのであれば自動更新の前に昇格する
というアプローチをとろうと考えているので、そのための段階の一つとして、
引数で指定したフォルダに対して、
昇格せずに書き込める権限があるかないかを
判定する関数を作ろうと考えました。
コードと解説
流れ
流れ的には
- 現在のユーザの情報を取得
- ディレクトリのセキュリティ情報を取得
- セキュリティ情報をユーザ・グループ別にループ
- 現在のユーザーとセキュリティ情報でチェックしているグループとの所属判定、または、現在のユーザーとセキュリティ情報でチェックしているユーザとの同一判定
- 拒否判定のチェック
- 許可判定のチェック
になります。
資料
・iPentecの「ディレクトリのアクセス権を取得する (C#プログラミング)」の「プログラム例:.NET Framework」
ディレクトリから権限情報を取得して一覧するコードですが、このコードを骨格としました。
しかし、このコードではグループ単位で出力されたものはグループ単位のままなので、グループに属しているかどうかの確認が必要である。
・DOBON.NETの「現在のユーザーが管理者か調べる」の「アプリケーションを管理者として実行しているか調べる」
このコードを改造して、グループに所属しているかどうかを確かめるコードに利用した。
・Get-Aclで取得したファイル・フォルダのアクセス権(FileSystemRights)が、数字表記される場合の変換スクリプト
MicrosoftのドキュメントにFileSystemRightsの値が2097152以上のビット値の説明が無いため、この記事から2097152以上の32ビット値のビット列の情報を得ました。ちなみに私がこのページから参照したビット列は
「(FileSystemRights)0x40000000」:Generic_write、つまり書き込み権限
「(FileSystemRights)0x10000000」:Generic_all、つまりフルコントロール
となります。
ソースコード
trueかfalseで返す形式にしています。
using System;
using System.IO;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Windows.Forms;
private bool WriteJudge(string pass) {
bool ret = false;
try {
//現在のユーザーを表すWindowsIdentityオブジェクトを取得する
WindowsIdentity wi = WindowsIdentity.GetCurrent();
//現在のユーザーのWindowsPrincipalオブジェクトを作成する
WindowsPrincipal wp = new WindowsPrincipal(wi);
//ディレクトリのセキュリティオブジェクト取得
DirectorySecurity security = Directory.GetAccessControl(pass);
//ユーザ・グループ一覧からループ
foreach (FileSystemAccessRule rule in security.GetAccessRules(true, true, typeof(NTAccount))) {
//現在のユーザーがグループに所属しているかどうかをチェック
//(判定の対象がグループでなくても動作した。)
if (!(wp.IsInRole((rule.IdentityReference as NTAccount).Value))) {
//関係のないユーザを飛ばす
continue;
}
//許可判定
if (
(rule.AccessControlType == System.Security.AccessControl.AccessControlType.Allow) &&
(
((rule.FileSystemRights & System.Security.AccessControl.FileSystemRights.Write) == System.Security.AccessControl.FileSystemRights.Write) ||
((rule.FileSystemRights & System.Security.AccessControl.FileSystemRights.FullControl) == System.Security.AccessControl.FileSystemRights.FullControl) ||
((rule.FileSystemRights & (FileSystemRights)0x40000000) == (FileSystemRights)0x40000000) ||
((rule.FileSystemRights & (FileSystemRights)0x10000000) == (FileSystemRights)0x10000000)
)
) {
//許可されている
ret = true;
}
//拒否判定
if (
(rule.AccessControlType == System.Security.AccessControl.AccessControlType.Deny) &&
(
((rule.FileSystemRights & System.Security.AccessControl.FileSystemRights.Write) == System.Security.AccessControl.FileSystemRights.Write) ||
((rule.FileSystemRights & System.Security.AccessControl.FileSystemRights.FullControl) == System.Security.AccessControl.FileSystemRights.FullControl) ||
((rule.FileSystemRights & (FileSystemRights)0x40000000) == (FileSystemRights)0x40000000) ||
((rule.FileSystemRights & (FileSystemRights)0x10000000) == (FileSystemRights)0x10000000)
)
) {
//拒否されている
ret = false;
//ACL上では拒否が許可より優先度が高いので、拒否が出た時点でbreakした。
break;
}
}
} catch (Exception ex) {
MessageBox.Show(ex.ToString(), "エラー", MessageBoxButtons.OK, MessageBoxIcon.Error);
}
return ret;
}
説明
bool ret = false;
戻り値となる変数retは、ACLで許可判定が出た場合にtrueつまり管理者権限不要と見なし、 ACL判定で拒否または判定なしとなった場合にfalseつまり管理者権限を要すると見なしています。
ACL上では拒否も許可もない状態は許可されていない状態、つまり事実上の拒否判定と見なすので、管理者権限が無い状態で書き込めるのは許可判定が出た場合のみと考えられます。
戻り値となる変数retの初期値をfalseにしたのは、拒否も許可もなかった状態を初期値として表す意図があります。
次に、現在ログオンしているWindowsユーザに関する情報を取得します。
//現在のユーザーを表すWindowsIdentityオブジェクトを取得する
WindowsIdentity wi = WindowsIdentity.GetCurrent();
//現在のユーザーのWindowsPrincipalオブジェクトを作成する
WindowsPrincipal wp = new WindowsPrincipal(wi);
チェックしたいフォルダの権限情報を取得
//ディレクトリのセキュリティオブジェクト取得
DirectorySecurity security = Directory.GetAccessControl(pass);
次は、取得した権限情報から、フォルダに対するアクセス権限を取得するため、ユーザーやグループごとにループしています。
//ユーザ・グループ一覧からループ
foreach (FileSystemAccessRule rule in security.GetAccessRules(true, true, typeof(NTAccount))) {
もしかするとNTAccount型である必要はないのかもしれませんが、現時点では調べきれていません。
しかしこの情報からは、現在のログインユーザーがチェック対象のグループに含まれているかどうかまでは分かりませんので、今度はそれを行うための処理を行います。
//現在のユーザーがグループに所属しているかどうかをチェック
//(判定の対象がグループでなくても動作した。)
if (!(wp.IsInRole((rule.IdentityReference as NTAccount).Value))) {
//関係のないユーザを飛ばす
continue;
}
現在ログインしているユーザが「(rule.IdentityReference as NTAccount).Value」のグループまたはユーザに属しているかどうかをチェックしています。
私が動かしてみたところ、「(rule.IdentityReference as NTAccount).Value」がグループではなくユーザになっても判定してくれているので、おそらくユーザの場合は現在のウインドウズのログインユーザと「(rule.IdentityReference as NTAccount).Value」で表されるユーザが同一かどうかを判定していると思われます。
つまり、
・「(rule.IdentityReference as NTAccount).Value」がグループであれば
現在のログインユーザが「(rule.IdentityReference as NTAccount).Value」に所属しているかどうかの判定になり、
・「(rule.IdentityReference as NTAccount).Value」がユーザーであれば
現在のログインユーザと「(rule.IdentityReference as NTAccount).Value」が同一かどうかの判定になると思われます。
もし属していないと判別したのなら、そのユーザまたはグループの情報はチェックする必要はありませんので、continueで飛ばします。
ですから、これより下の行は、全て現在ログイン中のユーザか、ログイン中のユーザが属しているグループに関する権限の条件チェックになります。
//許可判定
if (
(rule.AccessControlType == System.Security.AccessControl.AccessControlType.Allow) &&
(
((rule.FileSystemRights & System.Security.AccessControl.FileSystemRights.Write) == System.Security.AccessControl.FileSystemRights.Write) ||
((rule.FileSystemRights & System.Security.AccessControl.FileSystemRights.FullControl) == System.Security.AccessControl.FileSystemRights.FullControl) ||
((rule.FileSystemRights & (FileSystemRights)0x40000000) == (FileSystemRights)0x40000000) ||
((rule.FileSystemRights & (FileSystemRights)0x10000000) == (FileSystemRights)0x10000000)
)
) {
//許可されている
ret = true;
}
逆に、ACL上では「Allow」つまり許可の権限は拒否の権限よりも優先度が低いので、次以降に拒否判定が出ることを想定して、許可が出た時点ではbreakをしないようにしている。
「(FileSystemRights)0x40000000」:Generic_write、つまり書き込み権限
「(FileSystemRights)0x10000000」:Generic_all、つまりフルコントロール
//拒否判定
if (
(rule.AccessControlType == System.Security.AccessControl.AccessControlType.Deny) &&
(
((rule.FileSystemRights & System.Security.AccessControl.FileSystemRights.Write) == System.Security.AccessControl.FileSystemRights.Write) ||
((rule.FileSystemRights & System.Security.AccessControl.FileSystemRights.FullControl) == System.Security.AccessControl.FileSystemRights.FullControl) ||
((rule.FileSystemRights & (FileSystemRights)0x40000000) == (FileSystemRights)0x40000000) ||
((rule.FileSystemRights & (FileSystemRights)0x10000000) == (FileSystemRights)0x10000000)
)
) {
//拒否されている
ret = false;
//ACL上では拒否が許可より優先度が高いので、拒否が出た時点でbreakした。
break;
}
ACL上では「Deny」つまり拒否の権限は許可の権限よりも優先度が高いので、拒否が出た時点でbreakをして処理自体を終わらせます。
また、breakの前に変数retにfalseを入れているのは、許可判定が出たために変数retにtrueが書き込まれている可能性があるためです。
「(FileSystemRights)0x40000000」:Generic_write、つまり書き込み権限
「(FileSystemRights)0x10000000」:Generic_all、つまりフルコントロール
使用法
bool ret=WriteJudge("C:\Program Files");
引数に管理者権限が必要かどうかを確認したいフォルダのパスを入れ、戻り値がfalseの時は管理者権限が必要、戻り値がtrueの場合は管理者権限不要と判断する関数を作った形になります。
ただ、私のコードまたはアプローチの根本自体に間違いが無いとは言い切れませんし、もっと良いやり方もあるかもしれません。