電柱日報

日々の由無し事

IronRubyでLDAP検索

なんだか,ゴソゴソと弄ってLDAPサーバからエントリの情報を拾えるようになりましたので,そこまでの流れを簡単にご紹介。
使用環境は32bit版Windows7上で,.Net Framework 4.0版のIronRuby 1.0v4です。
なにはともあれ,サンプルコード。

# ldap_test.rb
require 'mscorlib'
require 'System.Net, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
require 'System.DirectoryServices.Protocols, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'

# 名前空間のプレフィックス指定が面倒くさいものぐさ用
include System::Net
include System::DirectoryServices::Protocols

# LDAPサーバ情報
li = LdapDirectoryIdentifier.new('ldap.somewhere.com')
# LDAPSを使う場合(1)
# li = LdapDirectoryIdentifier.new('ldap.somewhere.com', 636)

# bind用認証情報(bind dnとパスフレーズ)
nc = NetworkCredential.new('uid=admin,dc=somewhere,dc=com', 'passphrase')

# LDAP接続オブジェクト
lc = LdapConnection.new(li, nc)
# パスフレーズ認証を使用
lc.AuthType = AuthType.Basic
# LDAP v3 を使用
lc.SessionOptions.ProtocolVersion = 3
# 自動再接続は切る(なんとなく)
lc.SessionOptions.AutoReconnect = false
# SSLを使う場合(2)
# lc.SessionOptions.SecureSocketLayer = true

# bind実行
lc.Bind()

# 検索リクエストオブジェクト
req = SearchRequest.new()

# ベースDNを指定
req.DistinguishedName = 'ou=member,dc=somewhere,dc=com'
# スコープをサブツリー全体に
req.Scope = SearchScope.Subtree
# 検索フィルタを指定(氏名がyamadaで始まる人)
req.Filter = System::String.new('(cn=yamada*)')
# 取得する属性を指定(指定しなければ全部取得)
req.Attributes.Add('cn')
req.Attributes.Add('mail')

# 検索実行
res = lc.SendRequest(req)

# 検出件数
puts "Hit #{res.Entries.Count} entry(ies)"

# 検出エントリー単位の処理
res.Entries.each do |entry|
	# DN表示
	puts "\n#{entry.DistinguishedName}"
	# Object Classの表示
	entry.Attributes.Item('objectclass').sort.each do |value|
		puts "+ #{name}: #{String.new(value)}"
	end
	# 属性の表示
	entry.Attributes.AttributeNames.sort.each do |name|
		next if name == 'objectclass'
		entry.Attributes.Item(name).sort.each do |value|
			puts "  #{name}: #{String.new(value)}"
		end
	end
end

# コネクション破棄
lc.Dispose()

objectClassだけ別口で分けて表示してるのは単に趣味の問題です。
上記のIronRubyスクリプトを実行すると,たぶんこんな感じの結果が表示されるはず。

C:\> ir -Ku ldap_test.rb
Hit 2 entry(ies)

uid=ichiro,ou=section1,ou=member,dc=somewhere,dc=com
  cn: YAMADA Taro
  mail: taro@somewhere.com

uid=hanako,ou=section2,ou=member,dc=somewhere,dc=com
  cn: Yamada Hanako
  mail: hanako@somewhere.com

余談ですが,コードに日本語を使う際の文字コードUTF-8をオススメ。
なぜかと言うと,検索結果が収まっているSearchResultEntryクラスは,属性値をUTF-8形式のバイト列で返してくるから。
コンソールアプリだとコマンドプロンプトShift_JISなため,出力が化けてしまいますが,GUIアプリの場合,irコマンドに-Kuオプションをつけてやれば,UTF文字列をそのまま表示できますので,LDAPの検索結果をコード変換せずにWindowsアプリ上に表示可能です。
その他,細かく躓いた点を幾つかあげると……。

IronRubyから.Net Frameworkクラスを使うには

.Net Frameworkクラスライブラリを見ると,LDAP周りのクラスは System.DirectoryServices.Protocols に含まれているようです。
ということで,GUIアプリを作る際に使った Form 関係が

require 'System.Windows.Forms'

でrequireできたので,同じ感じで

require 'System.DirectoryServices.Protocols'

としてみたんですが,

LoadError: no such file to load -- System.DirectoryServices.Protocols

と怒られてしまいました。
で,いろいろと調べてみると,IronRuby.Net Frameworkのクラスを使う場合

  1. dllファイル(名前空間)単位にrequireする
  2. requireでは,dllファイルのStrong Nameを指定する
    • 例:System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
    • Formsなんかは,よく使うので,ラッパファイルが用意されていただけっぽい
  3. Strong Nameはdllファイルを署名した際の公開キートークンがいるらしい

ということがわかってきました。
たまたま,以前使っていたVisual Studioに付いてきた.Net SDKの中に,Strong Name Tool(sn.exe)が見つかり,System.DirectoryServices.Protocols.dllも,C:\Windows\Microsoft.NET\Framework\v4.0.30319 フォルダの中に見つかりましたんで,

c:\> sn -T "C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.DirectoryServices.Protocols.dll"
Microsoft(R) .NET Framework Strong Name Utility  バージョン 3.5.30729.1
Copyright (c) Microsoft Corporation. All rights reserved.

公開キー トークン b03f5f7f11d50a3a

とコマンドを叩き,無事に公開キートークンを入手。
VersionやCultureは結局良くわからないままなんですが,Formsの記述をそのまま移植して

require 'System.DirectoryServices.Protocols, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'

としたところ,エラーが出なくなったのでまぁ良し。
NetworkCredentialクラス用に,System.Netの公開キートークンも調べたんですが,System.DirectoryServices.Protocolsと同じでした。
System.Windows.Formsのとは違うので,新しいdllを使うときは,その都度調べた方が良さゲ?

System::DirectoryServices::Protocols::LdapConnectionクラス

LDAPサーバへの接続に使います。
今回は,コンストラクタに与えるSystem::DirectoryServices::Protocols::LdapDirectoryIdentifierオブジェクトや,System::Net::NetworkCredentialオブジェクトを予め作ってから渡しましたが,サーバ名だけ指定して,あとからプロパティ値として与えてやることもできます。
気を付ける場所があるとすると,AuthTypeプロパティのデフォルトがMicrosoftネゴシエーションを使う設定*1になってるっぽい点。
パスフレーズベースのBINDをするなら,AuthType.Basicを指定する必要があります。
SessionOptionsプロパティでも,いろいろと細かい設定ができるようなので,そのへんは.Net Frameworkクラスライブラリで確認してみてください。

System::DirectoryServices::Protocols::SearchRequestクラス

検索リクエストの設定をします。
予めBINDしておいたLdapConnectionオブジェクトのSendRequestメソッドに渡してやると,後述するSearchResponseオブジェクトが返ってきます。
ベースDNにスコープにフィルタと,ldapsearchなどを使ってればお馴染みのプロパティが揃っています。
ここで気を付ける必要があるとすると,Filterプロパティの指定方法。
上記の例で普通に

req.Filter = '(cn=yamada*)'

と指定すると,

System.DirectoryServices.Protocols:0:in `set_Filter': The "filter" parameter must be a string or DsmlDocument type.

と怒られてしまいます。
「ちゃんとstringやんけ!!」と思うわけですが,どうやらFilterプロパティ1つで,文字列によるLDAP検索フィルタとXMLデータとしてのDSML検索フィルタを同時に扱う必要上,Filterプロパティ自身は.Net Framework上のSystem.Objectクラスとして定義されているようです。後でキャストでもして使ってるんでしょうね。
つまり,.Net Frameworkで定義されたオブジェクトを待っているトコロに,Ruby生粋のStringオブジェクトを直接渡すと内部で消化不良を起こすみたい。
なので,ここでは

req.Filter = System::String.new('(cn=yamada*)')

と,一旦.Net Framework側のStringオブジェクトに変換して渡してやっています。
このRuby Stringと.Net String。
==では同値とみなされますし,System.Stringオブジェクトとして定義されているプロパティ(DistinguishedNameとか)であれば,直接渡して問題なく使えるんですが,やはり出自は別物ということなんですねぇ。
細かいネタですが,p メソッドの出力にも微妙に違いがあります。

C:\> iirb
irb(main):001:0> p String.new('AAA')
"AAA"
=> nil
irb(main):002:0> p System::String.new('AAA')
'AAA'
=> nil

Rubyの方は"AAA"とダブルクォートで括られているのに対して,.Netの方は'AAA'とシングルクォートで括られてますね。

System::DirectoryServices::Protocols::SearchResponseクラス

上記のSearchRequestに対する検索結果が格納されるオブジェクトなんですが,.Net Frameworkのオブジェクトらしく(?),コレクションのコレクションといった階層構造だったりしますので,普段Rubyのフランクな配列操作や,ハッシュ操作に慣れた身からすると,ドキュメントを読んでも判りづらくていけません。
幸いにして,.Netのコレクション系クラスは,Enumerableモジュールと同じような機能が実装されているのか,eachを使ったイテレータで個々の要素にアクセスできるので非常にありがたいです。
EntriesプロパティはSearchResultEntryCollectionオブジェクト。これはSearchResultEntryオブジェクトのコレクションで,個々のエントリであるSearchResultEntryオブジェクトには,LDAPエントリのDNであるDistinguishedNameプロパティや,属性値を格納したAttributesプロパティが含まれています。
EntryオブジェクトのAttributesプロパティはSystem.DirectoryServices.Protocols.SearchResultAttributeCollectionオブジェクトで,Attributes.Itemプロパティに属性名をキーとして渡してやると,個々の属性値であるSystem.DirectoryServices.Protocols.DirectoryAttributeオブジェクトを取得できます。ちなみに,エントリに含まれる属性名はAttributeNamesプロパティにコレクションされているので,そこから引っ張りましょう。
で,DirectoryAttributeオブジェクトもまた属性値のコレクションになっているのでeachで回してやると,ようやく属性値を取得することができます。
自分で書いといてなんですが,読んでもさっぱり判りませんなぁ。
この辺は.Netの流儀に慣れるしかない感じです。
いろいろ面倒な部分が無いではありませんが,特に拡張ライブラリを入れなくてもLDAPディレクトリの操作ができてしまうの大きなメリットだと思いました。

*1:System::DirectoryServices::Protocols::AuthType.Negotiate