RSS
EditSecurityと戦う

EditSecurityAPIを使うと、以下のよく見かけるアクセス権限の編集ダイアログをプログラムから呼び出して、レジストリやらファイルやらのACLを表示、編集することができます。

このAPI、使おうにも日本語の情報がC++のものですらさっぱりなく、Delphiで呼ぶとなると海外をあたってもほとんど見当たらないので呼ぶだけでも結構苦労します。

なので、これの呼び方を書いておこうと思います。ここではWindowsXPHomeでフォルダのACLを編集するダイアログを簡単に開けるソフトを一から作成しつつ解説していきます。コードを書いているのでかなり長いです。

いちいち読むのがダルい人は一番下にこれを実行した成果物があるのでそれをダウンロードして使ってください。

ちなみに、CodeProjectにあるRunAsExというサンプルプログラムのソースコードを多分に参照しています。VisualC++使いの人でEditSecurityを呼ぼうとしている人はこのアプリケーションを検索して参照したほうが参考になると思います。また、優れたサンプルを提供しているRunAsEx作者の方にこの場を借りてお礼申し上げます。

ISecurityInformationの実装

まず、EditSecurityはISecurityInformationを引数に取るので、ISecurityInformationを実装する必要があります。この複雑怪奇なインターフェイスがこのAPIの呼び出しを困難にしています。

ISecurityInformationの定義や、その他必要になるものについては、JEDIのAPI LibraryのWin32API.zipの中に入っているJwaAclApi.pas, JwaAclUI.pas,JwaWinUser.pas, JwaWinType.pas, JwaWinNt.pasに定義されているので、Win32API.zipをパスの通っている場所に展開し、これを全部usesしてください。

とりあえず、ここで1回コンパイルしてみてください。

[Pascal エラー] JwaWinType.pas(1125): E2086 'PULONG_PTR' 型の宣言が完了していません

が出た場合、該当行の^PULONG_PTRを^ULONG_PTRに書き換えればコンパイルが通ると思います。

では、実装に入ります。クラス名はTSecurityInformationとすることにします。とりあえず、クラス部分は以下のような感じになります。

type
  TSecurityInformation = class(TObject, ISecurityInformation)
    m_cRefs: LongInt;
    m_handle: THANDLE;
    m_grfExtraFlags: Cardinal;
    m_pszObjectName: PWideChar;
    m_pszPageTitle: PWideChar;
  public
    function QueryInterface(const iid: TGUID; out OBj): HRESULT; stdcall;
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
    constructor Create(h: THandle; grfExtraFlags: Cardinal;
      pszObjectName: PWideChar; pszPageTitle: PWideChar = nil);
    function GetObjectInformation(out pObjectInfo: SI_OBJECT_INFO): HRESULT;
      stdcall;
    function GetSecurity(RequestedInformation: SECURITY_INFORMATION;
      out ppSecurityDescriptor: PSECURITY_DESCRIPTOR; fDefault: BOOL): HRESULT;
      stdcall;
    function SetSecurity(SecurityInformation: SECURITY_INFORMATION;
      pSecurityDescriptor: PSECURITY_DESCRIPTOR): HRESULT; stdcall;
    function GetAccessRights(pguidObjectType: LPGUID; dwFlags: DWORD;
      out ppAccess: PSI_ACCESS; out pcAccesses, piDefaultAccess: ULONG):
        HRESULT; stdcall;
    function MapGeneric(pguidObjectType: LPGUID; pAceFlags: PUCHAR;
      pMask: PACCESS_MASK): HRESULT; stdcall;
    function GetInheritTypes(out ppInheritTypes: PSI_INHERIT_TYPE;
      out pcInheritTypes: ULONG): HRESULT; stdcall;
    function PropertySheetPageCallback(hwnd: HWND; uMsg: UINT;
      uPage: SI_PAGE_TYPE): HRESULT; stdcall;
  end;

とりあえず、QueryInterfaceと_AddRef、_Releaseを実装します。これはISecurityInformation固有の処理ではないので説明は不要でしょう。

function TSecurityInformation.QueryInterface(const iid: TGUID;
  out OBj): HRESULT; stdcall;
const
  IID_IUnknown: TGUID = (D1: $00000000; D2: $0000; D3: $0000;
    D4: ($C0, $00, $00, $00, $00, $00, $00, $46));
begin
  if IsEqualGUID(IID_IUnknown, iid) or
    IsEqualGUID(IID_ISecurityInformation, iid) then
  begin
    Result := S_OK;
    GetInterface(iid, OBj);
    _Addref;
  end else begin
    Result := E_NOINTERFACE;
  end;
end;

function TSecurityInformation._AddRef: Integer; stdcall;
begin
  InterlockedIncrement(m_cRefs);
  Result := m_cRefs;
end;

function TSecurityInformation._Release: Integer; stdcall;
begin
  InterlockedDecrement(m_cRefs);
  Result := m_cRefs;
end;

では、次はコンストラクタを実装しようと思います。といっても、やることはフィールドに引数の値を入れるだけです。

ハンドルについては略すとして、grfExtraFlagsは今回は使いません。pszObjectNameはレジストリキーなどの編集対象のパスを、pszPageTitleはEditSecurityダイアログのタブに表示するタイトルをそれぞれ指定しています。

constructor TSecurityInformation.Create(h: THandle;
  grfExtraFlags: Cardinal; pszObjectName: PWideChar;
  pszPageTitle: PWideChar = nil);
begin
  m_cRefs := 0;
  m_handle := h;
  m_grfExtraFlags := grfExtraFlags;
  m_pszObjectName := pszObjectName;
  m_pszPageTitle := pszPageTitle;
end;

いよいよISecurityInformation固有のメソッドを埋めていきます。とりあえず、あまりコードを書かなくてよかったり今回は実装しないMapGeneric、GetInheritTypes、PropertySheetPageCallbackをさくっと済ませてしまいます。

function TSecurityInformation.MapGeneric(pguidObjectType: LPGUID;
  pAceFlags: PUCHAR; pMask: PACCESS_MASK): HRESULT; stdcall;
begin
  Result := S_OK;
end;

function TSecurityInformation.GetInheritTypes(
  out ppInheritTypes: PSI_INHERIT_TYPE;
  out pcInheritTypes: ULONG): HRESULT; stdcall;
begin
  Result := S_OK;
end;

function TSecurityInformation.PropertySheetPageCallback(
  hwnd: HWND; uMsg: UINT;
  uPage: SI_PAGE_TYPE): HRESULT; stdcall;
begin
  Result := S_OK;
end;

GetInheritTypesは、アクセス権限の継承について設定できる項目らしいですが、私には実装できませんでした。

残りの二つには、私も何をするものなのかよくわかっていません。必要そうな人は本家MSDNのISecurityInformationの項目を読んでください。

次はGetObjectInformationを実装します。これが呼び出されたときに何をどう編集するかなどを決定することになります。

function TSecurityInformation.GetObjectInformation(
  out pObjectInfo: SI_OBJECT_INFO):
  HRESULT; stdcall;
begin
  pObjectInfo.dwFlags := SI_EDIT_ALL or SI_RESET or
    SI_ADVANCED or SI_CONTAINER or SI_PAGE_TITLE;
  pObjectInfo.hInstance := MainInstance;
  pObjectInfo.pszServerName := nil;
  pObjectInfo.pszObjectName := m_pszObjectName;
  pObjectInfo.pszPageTitle := m_pszPageTitle;
  Result := S_OK;
end;

フラグについてはとりあえず上記を指定しておいてください。他にもいろいろあるので詳細を知りたい方はMSDNのSI_OBJECT_INFOの項目をご覧ください。

タブ部分のテキストに標準の文字列を使いたい場合はSI_PAGE_TITLEをはずしてください。

後の値はコンストラクタで指定したものをそのまま使っているだけです。

さて、残すはGetAccessRightsとGetSecurity、SetSecurityだけになりました。今回は、EditSecurityで編集可能なアクセス権限を決めるGetAccessRightsを実装しますが、その前に以下の_SI_ACCESSの配列を定義しておいてください。

const
  TFileFullAccess =
    STANDARD_RIGHTS_ALL or FILE_READ_DATA
    or FILE_WRITE_DATA or FILE_APPEND_DATA
    or FILE_READ_EA or FILE_WRITE_EA or FILE_EXECUTE
    or FILE_DELETE_CHILD or FILE_READ_ATTRIBUTES or
    FILE_WRITE_ATTRIBUTES;
  TFileChangeAccess =
    FILE_GENERIC_WRITE or FILE_GENERIC_READ or
     FILE_EXECUTE or DELETE;

const
  TEditFileAccess: array[0..5] of _SI_ACCESS = (
    (pguid: @GUID_NULL;
    mask: TFileFullAccess; pszName: 'フルコントロール';
    dwFlags: SI_ACCESS_GENERAL or SI_ACCESS_SPECIFIC),
    (pguid: @GUID_NULL;
    mask: TFileChangeAccess; pszName: '変更';
    dwFlags: SI_ACCESS_GENERAL or SI_ACCESS_SPECIFIC),
    (pguid: @GUID_NULL;
    mask: FILE_GENERIC_READ; pszName: '読み取りと実行';
    dwFlags: SI_ACCESS_GENERAL or SI_ACCESS_SPECIFIC),
    (pguid: @GUID_NULL;
    mask: FILE_LIST_DIRECTORY; pszName: 'フォルダ内容の一覧表示';
    dwFlags: SI_ACCESS_GENERAL or SI_ACCESS_SPECIFIC),
    (pguid: @GUID_NULL;
    mask: FILE_READ_DATA; pszName: '読み込み';
    dwFlags: SI_ACCESS_GENERAL or SI_ACCESS_SPECIFIC),
    (pguid: @GUID_NULL;
    mask: FILE_WRITE_DATA; pszName: '書き込み';
    dwFlags: SI_ACCESS_GENERAL or SI_ACCESS_SPECIFIC));

この配列で定義したものが実際にEditSecurityで編集可能になります。重要なのはmaskとpszNameで、maskは何を編集するかを決定するところです。対象オブジェクトによってこの値は異なります。たとえば、レジストリキーの値の書き込みについて制限したいなら、FILE_WRITE_DATAをKEY_SET_VALUEにすればいけます。

pszNameはEditSecurityの画面上で表示される文字列です。ここで指定された文字がそのまま表示されるので、たとえば「実行」についての編集部分に「削除」とウソを表示することもできてしまうのでご注意ください。

では、以下がGetAccessRightsの実装です。

function TSecurityInformation.GetAccessRights(pguidObjectType: LPGUID;
  dwFlags: DWORD; out ppAccess: PSI_ACCESS;
  out pcAccesses, piDefaultAccess: ULONG): HRESULT; stdcall;
begin
  ppAccess := @TEditFileAccess;
  pcAccesses := length(TEditFileAccess);
  piDefaultAccess := 0;
  Result := S_OK;
end;

実装部分はたいしたことはしていません。配列のアドレスと長さを渡しているだけです。

次はGetSecurityを実装します。これが呼び出されたときに編集するアイテムのセキュリティ記述子を渡しています。

function TSecurityInformation.GetSecurity(
  RequestedInformation: SECURITY_INFORMATION;
  out ppSecurityDescriptor: PSECURITY_DESCRIPTOR;
  fDefault: BOOL): HRESULT; stdcall;
var
  pSD: PSECURITY_DESCRIPTOR;
begin
  GetNamedSecurityInfoW(m_pszObjectName, SE_FILE_OBJECT,
    RequestedInformation, nil, nil, nil, nil, pSD);
  ppSecurityDescriptor := pSD;
  Result := S_OK;
end;

Unicode版のGetNamedSecurityInfoを呼んでいる以外は特にハマるところは無いと思います。m_pszObjectNameがPCharならANSI版を呼ぶようにしてください。

では、最後のSetSecurityを定義しますが、その前にSetSecurity中で使うGetSecurityDescriptorDacl、GetSecurityDescriptorSacl、GetSecurityDescriptorOwner、GetSecurityDescriptorGroup、MapGenericMask、GetSecurityDescriptorControl、GetNamedSecurityInfoがJEDIのユニットをusesしている関係で定義が上書きされてそのままでは呼び出すことができないので、再定義を行います。

ChangeAclApi.pasというユニットを作り、そこに以下のコードを記述します。

unit ChangeAclApi;

interface

uses
  Windows,
  SysUtils,
  JwaAclApi,
  JwaAclUI,
  JwaAccCtrl,
  JwaWinUser,
  JwaWinType,
  JwaWinNt;

//再定義
function GetSecurityDescriptorDacl(
  pSecurityDescriptor: PSecurityDescriptor;
  var lpbDaclPresent: BOOL; var pDacl: PACL;
  var lpbDaclDefaulted: BOOL): BOOL; stdcall;
function GetSecurityDescriptorSacl(
  pSecurityDescriptor: PSecurityDescriptor;
  var lpbSaclPresent: BOOL; var pSacl: PACL;
  var lpbSaclDefaulted: BOOL): BOOL; stdcall;
function GetSecurityDescriptorOwner(
  pSecurityDescriptor: PSecurityDescriptor;
  var pOwner: PSID; var lpbOwnerDefaulted: BOOL): BOOL; stdcall;
function GetSecurityDescriptorGroup(
  pSecurityDescriptor: PSecurityDescriptor;
  var pGroup: PSID; var lpbGroupDefaulted: BOOL): BOOL; stdcall;
procedure MapGenericMask(
  var AccessMask: DWORD; const GenericMapping:
  TGenericMapping); stdcall;
function GetSecurityDescriptorControl(
  pSecurityDescriptor: PSecurityDescriptor;
  var pControl: SECURITY_DESCRIPTOR_CONTROL;
  var lpdwRevision: DWORD): BOOL; stdcall;
function GetNamedSecurityInfo(pObjectName: LPSTR;
  ObjectType: SE_OBJECT_TYPE;
  SecurityInfo: SECURITY_INFORMATION;
  ppsidOwner, ppsidGroup: PPSID;
  ppDacl, ppSacl: PPACL;
  var ppSecurityDescriptor: PSECURITY_DESCRIPTOR): DWORD; stdcall;

implementation

function GetSecurityDescriptorDacl; external advapi32 name
'GetSecurityDescriptorDacl';
function GetSecurityDescriptorGroup; external advapi32 name
'GetSecurityDescriptorGroup';
function GetSecurityDescriptorOwner; external advapi32 name
'GetSecurityDescriptorOwner';
function GetSecurityDescriptorSacl; external advapi32 name
'GetSecurityDescriptorSacl';
procedure MapGenericMask; external advapi32 name 'MapGenericMask';
function GetSecurityDescriptorControl; external advapi32 name
'GetSecurityDescriptorControl';
function GetNamedSecurityInfo; external advapi32 name 'GetNamedSecurityInfoA';

end.

これでエラーが出ることなく呼び出しが行えます。

では、SetSecurityを実装します。SetSecurityは実際にACLエディタでセットしたACLを書き出す処理を行う部分です。

function TSecurityInformation.SetSecurity(
  SecurityInformation: SECURITY_INFORMATION;
  pSecurityDescriptor: PSECURITY_DESCRIPTOR): HRESULT; stdcall;
var
  pOwner: pSid;
  pGroup: pSid;
  pdacl: PACL;
  psacl: PACL;
  bDefaulted: LongBool;
  bPresent: LongBool;
  ulRevision: ULONG;
  sdCtrl: SECURITY_DESCRIPTOR_CONTROL;
begin
  pOwner := nil;
  GetSecurityDescriptorOwner(pSecurityDescriptor, pOwner,
    bDefaulted);

  pGroup := nil;
  GetSecurityDescriptorGroup(pSecurityDescriptor, pGroup,
    bDefaulted);

  pdacl := nil;
  GetSecurityDescriptorDacl(pSecurityDescriptor, bPresent,
    pdacl, bDefaulted);

  psacl := nil;
  GetSecurityDescriptorSacl(pSecurityDescriptor, bPresent,
    psacl, bDefaulted);

  sdCtrl := 0;
  GetSecurityDescriptorControl(pSecurityDescriptor, sdCtrl, ulRevision);
  if ((sdCtrl and SE_DACL_PROTECTED) <> SE_DACL_PROTECTED) then
    SecurityInformation := SecurityInformation or
      UNPROTECTED_DACL_SECURITY_INFORMATION
  else
    SecurityInformation := SecurityInformation or
      PROTECTED_DACL_SECURITY_INFORMATION;

  if ((sdCtrl and SE_SACL_PROTECTED) <> SE_SACL_PROTECTED) then
    SecurityInformation := SecurityInformation or
      UNPROTECTED_SACL_SECURITY_INFORMATION
  else
    SecurityInformation := SecurityInformation or
      PROTECTED_SACL_SECURITY_INFORMATION;

  SetNamedSecurityInfoW(m_pszObjectName, SE_FILE_OBJECT,
    SecurityInformation,
    pOwner, pGroup, pdacl, psacl);

  Result := S_OK;
end;

これが何をしているのかについては、正直よくわかりません。とりあえず注意する点はGetSecurityと同じく、ユニコード版のSetNamedSecurityInfoを呼んでいるという点くらいでしょうか。

EditSecurityを使う

さて、ようやくISecurityInformationの実装が終わりました。これでようやくEditSecurityが呼び出せます。

これの呼び出し自体は極めて簡単で

var
  acl: TSecurityInformation;
begin
  acl := TSecurityInformation.Create(0, 0,
    PWideChar('ディレクトリ名'),
    PWideChar('タブ名'));
  try
    EditSecurity(0, acl);
  finally
    acl.Free;
  end;
end;

とするだけです。後はEditSecurityが処理してくれます。

とりあえず実装は終わりましたが、実際にこれを動かしてみると、アクセスを拒否すればちゃんと拒否する動作は動いているんですが、Windowsの標準とは微妙に動作が違います。

このあたりについては私にはもうわかりません。とりあえず、表示させることはできるようになったので、後は各自でがんばって調べてみてください。

では、一番下に成果物の実行ファイルと、ソースコードを置いておきます。興味のある人は研究してみてください。

使い方としては、コマンドラインパラメータで受け取ったフォルダについてACLエディタを開くようになっています。ファイルについては無視します。

ダウンロード

 ■EXEとソース