◆Accessの使用記録 〜 誰が使ったの?

[ ホーム ] [ 上へ ]

久しぶりのAccessのネタのわりには、ほんとたいしたことはなく使えそうなのかも よくわかりませんがとりあえず書きます。内容は表題のとおりですが、たいていのDBMSは 過去のログインは記録できるようになっていると思いますが、Accessの場合はサービスとして 監視しているわけではなく、ファイルベースなのでそういう機能はなかったはずです (もし、あったら教えてください)。 で、だれがそのデータベースを使っていたのかを 記録したいときは、mdb内にテーブルを作ってユーザ名や時間を記録するパターンが定番かなと 思います。ほかにメジャーな方法があったらこれも教えてください。これはこれでりっぱな 方法ですが、場合によっては記録されたレコードが勝手に書き換えることができてしまいます。 これから書く方法も消去できますが簡単にはいきません。

・イベントログの使用

スタート→ファイル名を指定して実行(R)から
eventvwr → OK
を実行すると、以下のようなイベントビューワが起動します(Windows 2000/XP)。 特定のmdbを起動した時に、このイベントログ にユーザや時刻を記録していこうというのが今回のお題です。

ここに記録を残すにはWin32APIを使いますが、中心の関数は

HANDLE RegisterEventSource(
  LPCTSTR lpUNCServerName, // 操作を実行するサーバーの名前
  LPCTSTR lpSourceName     // 登録済みハンドルを取得するイベントソースの名前
);

BOOL ReportEvent(
HANDLE hEventLog,      // イベントログのハンドル
  WORD wType,          // ログに書き込むイベントの種類
  WORD wCategory,      // イベントの分類
  DWORD dwEventID,     // イベント識別子
  PSID lpUserSid,      // ユーザーセキュリティ識別子(省略可能)
  WORD wNumStrings,    // メッセージにマージする文字列の数
  DWORD dwDataSize,    // バイナリデータのサイズ(バイト数)
  LPCTSTR *lpStrings,  // メッセージにマージする文字列の配列
  LPVOID lpRawData     // バイナリデータのアドレス
);

です。RegisterEventSourceで取得した登録済みイベントログのハンドルを ReportEventの第一引数に指定して、イベントログに書き込むという具合です。イベントログに 何が書き込めるかというと、イベントビューワの1つのレコードを ダブルクリックするとわかりますが、

  1. 日付・時刻
  2. イベントソース (ReportEventのhEventLog引数)
  3. イベントの種類 (ReportEventのwType引数)
  4. イベントの分類 (ReportEventのwCategory引数)
  5. イベントID (ReportEventのdwEventID引数)
  6. ユーザ名(ログイン名) (ReportEventのlpUserSid引数)
  7. イベントIDに対する説明(パラメータによってマージする文字列も指定可能)
  8. 任意のデータ (ReportEventのlpRawData引数)

です。イベントログに関するテーマで別途書くつもりなので細かいことは省略しますが、 レジストリに登録したイベントソースと呼ばれるイベントを記録する アプリケーション情報が書かれたエントリを元に上記の情報をReportEventの発行によって 書き込んでいきます。レジストリのイベントソースの情報には、サポートするイベントの種類、 イベントIDに対する説明やイベントの分類名を記録したDLLファイルへのパス などが書き込まれています。
ということは、このDLLを作ってイベントソースを登録しないといけませんがVBAには その機能がありません。無料のExpress版でもいいですがVisual StudioでDLLを作る必要がありますが、 今回はDLLも新たなイベントソースも作りません。作成しなかったらイベントログを 記録できないでしょ?と思われるかもしれませんが、これができるんです。ただし、 イベントソースの未登録とDLLの未作成の場合、制限が加わります。
イベントソースがないとアプリケーションに対応したイベントログは作成されず、既に存在する "アプリケーションログファイル"が使われます。これはイベントログビューワで 見ている分にはそれほど変化ないですが、これ以外にもイベントの分類を表す値に対する分類名、 イベントIDに対応するイベントの解説が書き込まれているDLLが無いので、この項目は使えません。 かろうじてイベントIDに対応する説明にマージする文字列がReportEventのlpStrings引数で 指定できますがこれでは不完全です。MSもイベントソースを作成するように推奨していますが、 なしでも最低限のことはできますので省略します。

・実装

イベントログを記録するAPI はC言語の宣言なのでVBAで使えるようにDeclareステートメントを使って定義したりして、 イベントログに記録できるような関数を作成します。なお、実装環境は Windows 2000/XP + Access2000です。
MS-Access起動してイベントを記録しようとするmdbを開いた後、Alt + F11でVBエディタを 起動してください。ツールバーの挿入(I)→標準モジュール(M)で新しいモジュールを作成して 以下のコードを記述してください。

Option Compare Database 
Option Explicit 

'現在のログオンユーザ名の取得
Private Declare Function GetUserName Lib "advapi32.dll" Alias "GetUserNameA" _ 
                                    (ByVal lpBuffer As String, _ 
                                     nSize As Long) As Long 

'システム名/アカウント名からセキュリティID(SID)を取得
Private Declare Function LookupAccountName Lib "advapi32.dll" Alias "LookupAccountNameA" _
                                    (ByVal lpSystemName As Long, _ 
                                     ByVal lpAccountName As String, _ 
                                     Sid As Byte, _ 
                                     cbSid As Long, _ 
                                     ByVal ReferencedDomainName As String, _ 
                                     cbReferencedDomainName As Long, _ 
                                     peUse As SID_NAME_USE) As Long 

'登録済みイベントログハンドル取得
Private Declare Function RegisterEventSource Lib "advapi32.dll" _
                                                   Alias "RegisterEventSourceA" _ 
                                    (ByVal lpUNCServerName As Long, _ 
                                     ByVal lpSourceName As String) As Long 

'イベントログへの報告
Private Declare Function ReportEvent Lib "advapi32.dll" Alias "ReportEventA" _ 
                                    (ByVal hEventLog As Long, _ 
                                     ByVal wType As Long, _ 
                                     ByVal wCategory As Long, _ 
                                     ByVal dwEventID As Long, _ 
                                     lpUserSid As Byte, _ 
                                     ByVal wNumStrings As Long, _ 
                                     ByVal dwDataSize As Long, _ 
                                     lpString As String, _ 
                                     lpRawData As Long) As Long 

'アカウントのタイプ
Private Enum SID_NAME_USE 
   SidTypeUser = 1 
   SidTypeGroup
   SidTypeDomain
   SidTypeAlias
   SidTypeWellKnownGroup
   SidTypeDeletedAccount
   SidTypeInvalid
   SidTypeUnknown
   SidTypeComputer
End Enum 

'イベントの種類
Private Const EVENTLOG_SUCCESS          As Long = &H0 
Private Const EVENTLOG_ERROR_TYPE       As Long = &H1 
Private Const EVENTLOG_WARNING_TYPE     As Long = &H2 
Private Const EVENTLOG_INFORMATION_TYPE As Long = &H4 
Private Const EVENTLOG_AUDIT_SUCCESS    As Long = &H8 
Private Const EVENTLOG_AUDIT_FAILURE    As Long = &H10 

'現在のログオンユーザのセキュリティID(SID)を取得
'引数 argcSid 現在のログオンユーザのSIDを受取る 
'戻値 SID取得成功時は argcSidにセキュリティIDが格納されTRUEを返却 
'仕様 argcSidのサイズ(可変長)はこの関数内で計算後、領域確保される
Private Function FnGetCurrentUserSid(argcSid() As Byte) As Boolean 
    Dim vsUser          As String * 256  'ログオンユーザ名
    Dim vlUserSize      As Long          'vsUserの文字数
    Dim vsDomain        As String * 256  'ドメイン名
    Dim vlDomainSize    As Long          'vsDomainの文字数
    Dim vlSidSize       As Long          'セキュリティID(SID)のサイズ(可変長)
    Dim veSnu           As SID_NAME_USE  'SIDの種類

    vlUserSize = Len(vsUser) 

    '現在のログオンユーザ名の取得
    If GetUserName(vsUser, vlUserSize) = 0 Then Exit Function 

    vlDomainSize = Len(vsDomain) 
    vlSidSize = 0 

    'セキュリティID(SID)のサイズ取得
    LookupAccountName lpSystemName:=0, _ 
                      lpAccountName:=vsUser, _ 
                      Sid:=0, _ 
                      cbSid:=vlSidSize, _ 
                      ReferencedDomainName:=vsDomain, _ 
                      cbReferencedDomainName:=vlDomainSize, _ 
                      peUse:=veSnu                              '(C)

    If vlSidSize <= 0 Then Exit Function 

    'セキュリティID(SID)の領域確保
    ReDim argcSid(vlSidSize - 1) 

    'セキュリティID(SID)・ドメイン名の取得
    If LookupAccountName(lpSystemName:=0, _ 
                         lpAccountName:=vsUser, _ 
                         Sid:=argcSid(0), _ 
                         cbSid:=vlSidSize, _ 
                         ReferencedDomainName:=vsDomain, _ 
                         cbReferencedDomainName:=vlDomainSize, _ 
                         peUse:=veSnu) = 0 Then 
        Exit Function 
    End If 

    FnGetCurrentUserSid = True 
End Function 

'イベントログへ書込み
'引数 argsMsg イベントログに書き込むメッセージ 
'     arglData イベントログに書き込むバイナリデータ 
'戻値 アプリケーションログに書き込まれた時True
Public Function GFnReportEvent(argsMsg As String, _ 
                               arglData As Long) As Boolean 
    Dim vcSid()    As Byte 'SID
    Dim vlHandle   As Long 

    'セキュリティIDの取得
    If FnGetCurrentUserSid(argcSid:=vcSid) = False Then Exit Function 

    '登録済みイベントソースのハンドル取得
    '(レジストリにソースを作成してないのでアプリケーションログが使用される)
    vlHandle = RegisterEventSource(lpUNCServerName:=0, _ 
                                   lpSourceName:="MSACCESS")  '(A)イベントソース名(未登録)
    If vlHandle = 0 Then Exit Function 

    'イベントログに書込み
    If ReportEvent(hEventLog:=vlHandle, _ 
                   wType:=EVENTLOG_INFORMATION_TYPE, _ 
                   wCategory:=0, _ 
                   dwEventID:=0, _ 
                   lpUserSid:=vcSid(0), _ 
                   wNumStrings:=1, _ 
                   dwDataSize:=4, _ 
                   lpString:=argsMsg, _ 
                   lpRawData:=arglData) <> 0 Then              '(B)

        GFnReportEvent = True 
    End If 

End Function
  1. のRegisterEventSourceで任意のイベントソース名(この場合はMSACCESS)を指定してハンドルを 取得しています。もちろん未登録なので、アプリケーションログが使われますが、イベントビューワでは きちんとソース名は表示されています。(上記ダイアログ)
  2. でイベントログに記録していますが、wCategoryとdwEventID引数は対応する分類名、説明が記録してある DLLがないので、0にしてありますが、dwEventIDあたりでイベントを区分したほうがいいでしょう (説明文はありませんが)。
    ユーザ名はlpUserSidで指定しますが直接ユーザ名を指定するようになっていません。セキュリティID(SID)と 呼ばれる堅牢度が高いWindowsが管理するIDを指定しなければなりません。

現在のWindowsのログイン名(ユーザ名)からSIDを求めているのが、 FnGetCurrentUserSid関数です。SIDは可変長なので取得はちょっとややこしいですが、 ユーザ名をGetUserNameで得た後、一度目のLookupAccountNameの発行でSIDのサイズ(バイト単位) を求めます(C)。Byte型の配列であるargcSidにサイズ分の領域を確保して、 再度LookupAccountNameを発行するとargcSidにバイトシーケンスとしてSID収められ、 ReportEventに渡しています。

ReportEventのlpStringにはイベントIDに対応する説明文にマージする文字列を 指定できますが、その説明文はなく、この文字列はエラー文の最後に付け加えられます。 (下図の説明(D)参照)

ReportEventのarglDataは今回はLong型のデータを渡していますが、サイズさえ dwDataSize引数に渡せばどんな型でもいいです。エラー時にイベントログを使う場合、 エラー番号をここに渡すことが多いです。

以上でできたので、mdbが開かれたらイベントログに 書き込まれるようにAutoExecマクロのプロシージャの実行アクションにGFnReportEvent関数を 登録します。

データベースウィンドウのマクロから新規のマクロを作り、AutoExec という名前で保存して、上図のように「プロシージャの実行」アクションを加えてください。 「アクションの引数」に先ほど作成した関数を登録しますが、上図では途中で切れているので
GFnReportEvent ("EventLog.mdbを開きました", 1) 
と記述してください。

以上ですべて終わりましたのでいったんmdbを閉じてください。再びmdbを開いた後、 イベントビューワを開いて上図のようにアプリケーションログの 最初のログをダブルクリックしてください。下図のようなイベントが登録されているはずです。