はじめに
ActiveLdap は LDAP を操作する今までにない方法です。大半のLDAP操作は難しい LDIFを扱うか、ウェブインターフェースを利用するか、説明の省かれたリファレンスマ ニュアルを参照しながら、難解なAPIを利用しなければなりませんでした。
ActiveLdap はこの解決を目指します。ActiveRecord によって着想した ActiveLdap は、LDAP エントリに対するオブジェクト指向のインターフェースを提供します。
対象読者はシステム管理者やLDAPユーザであって、手軽かつきれいなLDAPアクセスの方 法を求めるすべての人です。
LDAP とは何か
LDAPは”軽量ディレクトリアクセスプロトコル”のことです。これは基本的にLDAPサーバ と対話するためのプロトコルを意味します。LDAPサーバは軽量のディレクトリサーバで す。LDAPサーバはシンプルな電話帳からコンピュータ上のユーザアカウントのリストま で、幅広い情報を格納することができますが、大抵の場合後者のために使われます。
このテキストでの各例は、読者が LDAPサーバを Unix 系システムのための中央認証・認 可サーバとして利用することを熟知しているものと仮定して記載しています。(残念なこ とに、私はまだMicrsoft ActiveDirectoryに対して ActiveLdap を試していません。名 前に "Directory" が含まれていはいるのですが。)
より理解するために:
- RFC1777 - 軽量ディレクトリアクセスプロトコル
- OpenLDAP
では何故 ActiveLdap なのか?
LDAPを直接扱うさいには(たとえ素晴らしい Ruby/LDAP ライブラリを用いても)、既存 の LDAP API に拘束されることになります。このAPIは巨大な配列を生成し、コードを読 みづらく、また楽しくないものにしてしまいます。もしあなたがあなたのコードにLDAPを 統合するためのきれいな方法を欲するなら、ActiveLdap を使う理由になるでしょう。
利用準備
動作条件
- Ruby実装: Ruby 1.8.x または 1.9.1 または JRuby 1.1
- LDAPライブラリ: Ruby/LDAP (Ruby の場合) または Net::LDAP (Ruby か JRuby の場合) または JNDI (JRuby の場合)
- LDAP サーバ: OpenLDAP など
- 利用する LDAP サーバはスキーマクエリのために root_dse クエリを許可していなければなりません
インストール
インストールは gem で行えます。
gem install activeldap
インストールされたか確認するには、irb を利用できます。以下のように require して、 true が返ってくればインストール成功です。
$ irb -rubygems
irb(main):001:0> require 'active_ldap'
=> true
irb(main):002:0>
もし require が false を返したり例外を発生させたら、インストールに失敗していま す。動作条件を満たしているかなどを確認してみてください。
利用方法
この章では ActiveLdap 拡張クラスをコーディングし、アプリケーションを記述し、それ らを利用するまでを記載します。
何を得られるかざっくりつかむために、irb を利用して簡単な例を紹介しましょう。まず ActiveLdap を require します。
irb> require 'active_ldap'
LDAPサーバとの接続を確立します。ここでは最も基本的なメソッドである setup_connection を利用します。サンプルとして、接続先は localhost、base を "dc=dataspill,dc=org" とします。
irb> ActiveLdap::Base.setup_connection :host => 'localhost', :base => 'dc=dataspill,dc=org'
次に、ActiveLdap::Base のサブクラスを作成します。これを拡張クラスと呼びます。こ の拡張クラスを、 base 以下に存在する LDAP の Group オブジェクト群とマッピングし てみましょう。
irb> class Group < ActiveLdap::Base
irb* ldap_mapping
irb* end
簡単に説明しましょう。上記のコードでは Group クラスが setup_connection でした: base 以下の ou=Groups 以下のオブジェクト群全体を取り扱うようになります。Groupク ラスのインスタンスは、ou=Groups 以下にある個々の LDAP オブジェクトを表現します。
これで、グループクラスは以下のように利用できます
# 全てのグループ名を取得
irb> all_groups = Group.find(:all).collect {|group| group.cn}
=> ["root", "daemon", "bin", "sys", "adm", "tty", ..., "develop"]
# develop グループの LDAP オブジェクトを取得
irb> group = Group.find("develop")
=> #<Group objectClass:<...> ...>
# develop グループの cn を取得
irb> group.cn
=> "develop"
# develop グループの gid_number を取得
irb> group.gid_number
=> 1003 ほら! もう元には戻れないでしょう?
ActiveLdap 拡張クラス
ActiveLdap 拡張クラスは ActiveLdap::Base のサブクラスです。これらは LDAP サーバ 内のオブジェクトを抽象的に表現するために使われます。
拡張クラスは LDAP オブジェクト群の属性情報を Ruby クラスに自動的にマッピングする ことで、オブジェクトの取り扱いを容易かつ便利にします。
拡張クラス定義用メソッド
LDAP オブジェクトが正しく Ruby オブジェクトにマッピングされるために、ActiveLdap のクラスメソッドを利用してマッピングのための情報を定義します。上記の例ではそれら のうち、Group クラスを定義する中で ldap_mapping だけを利用していました。必要に応 じて更に多くのメソッドを利用することができます。
ldap_mapping
ldap_mapping は ActiveLdap とともに拡張クラスを利用するにあたって必須とされる唯 一のメソッドです。
以下は ldap_mapping を更に詳細に記述した Group クラスです
class Group < ActiveLdap::Base
ldap_mapping :dn_attribute => 'cn',
:prefix => 'ou=Groups',
:classes => ['top', 'posixGroup']
:scope => :one
end Group クラスをどのようにして LDAP とマッピングとするかを定義するために、 ldap_mapping メソッドが使われていることが見てとれるでしょう。
私たちが扱う LDAP ツリーが以下のようなものであるとします。
* dc=dataspill,dc=org
|- ou=People,dc=dataspill,dc=org
|+ ou=Groups,dc=dataspill,dc=org
|- cn=develop,ou=Groups,dc=dataspill,dc=org
|- cn=root,ou=Groups,dc=dataspill,dc=org
|- ... ou=People 以下にはユーザオブジェクトを格納し、ou=Groups 以下にはグループオブジェ クトを格納するものとします。
ldap_mapping は LDAP ツリーを抽象化して拡張クラスにマッピングします。以下では、 先の Group クラスの例をとって説明します。
:prefix によって、このクラスはou=Groups,dc=dataspill,dc=org 以下のみを扱うように なっています。
:dn_attributeにより 'cn' がこのクラスにとってのプライマリ属性となります。つまり、 Group クラスが取り扱う LDAPオブジェクト(ou=Groups,dc=dataspill,dc=org 以下のLDAP オブジェクト)について、DN はcn から始まるものとして扱われます。
より理解をはっきりさせるために、図にしてみましょう。各引数は図のようにマッピングされます。
cn=develop,ou=Groups,dc=dataspill,dc=org
^^ ^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^
:dn_attribute | |
:prefix |
:base from configuration(setup_connection,
define_configuration etc) 他にも :scope, :classes, :dn_attribute といったオプションの引数があります。
:scope は ou=Groups 以下の検索について、より深い階層までを検索するかどうかを指示 します(cn=develop,ou=DevGroups,ou=Groups,dc=dataspill,dc=org のような LDAP オブ ジェクトのこと)。 ここには、:base, :one, :sub といった引数を指定できます。
:classes は ActiveLdap に対して、新しいオブジェクトを生成するのに必要な最低 条件を指定します。LDAP はオブジェクトクラスを利用して、LDAPオブジェクトがどのよ うな属性を持ち得るかを定義します。ActiveLdap はこれを :classes 引数によって 知ることになります。デフォルトは 'top' のみです。このままにすることもできます し、add_class メソッドなどによって追加することも可能です。
デフォルトの :classes の値はとても重要です。ほとんどの LDAP サーバは一度オブジ ェクトが造られると structual な objectClass は削除(または変更)されることはない でしょう。健全なデフォルトを設定しておくことは、後々バグを作り込むことを回避する 助けになるかもしれません。
:classes だけがオプションの引数ではありません。:dn_attribute が無い場合、このデ フォルトはスーパークラスと同じ値か、cn に設定されます。
:prefixが無い場合、このデフォルトは'ou=クラス名の複数形’に設定されます。この場 合は'ou=Groups'になります。
:classes は配列でなくてはなりません。:dn_attribute と :prefix は文字列である必要 があります。
belongs_to
このメソッドはLDAPツリーを横断して他の拡張クラスとの関連付けを行い、所属の表現を 行います。しばしば User オブジェクトは Group オブジェクトのメンバになったり、所 属したりします。
* dc=dataspill,dc=org
|+ ou=People,dc=dataspill,dc=org
\
|- uid=drewry,ou=People,dc=dataspill,dc=org
|- ou=Groups,dc=dataspill,dc=org
上記のようなLDAPツリーでは、'drewry'ユーザは 'develop' グループの一員であるとし ます。この場合、'develop' グループの 'memberUid' フィールドを見ることで、それを 確認できます。
irb> develop = Group.find('develop')
=> ...
irb> develop.memberUid
=> ['drewry', 'builder'] しかし、'drewry' のエントリを見た時に 'develop' グループと関連している事は判らな いでしょう。これを解決するために belongs_to を利用します。
irb> class User < ActiveLdap::Base
irb* ldap_mapping :dn_attribute => 'uid', :prefix => 'People', :classes => ['top','account']
irb* belongs_to :groups, :foreign_key => 'uid',
irb* :class_name => 'Group', :many => 'memberUid'
irb* end
これで User クラスは 'groups' メソッドを利用できるようになります。このメソッドは ユーザが所属するすべての Group オブジェクトを取得することができます。
irb> me = User.find('drewry')
irb> me.groups
=> [#<Group ...>, #<Group ...>, ...]
irb> me.groups.each { |group| p group.cn };nil
"cdrom"
"audio"
"develop"
=> nil
(注:irb 上の見やすさのために nil を返すようにしています) TIP: 上記では Group の Distinguished name が cn であると仮定しています。もし Group の Distinguished name の属性名を知らなければ、以下のようにすれば (Distinguished name がなんであれ)Distinguished name の属性を取得できます。
irb> me.groups.each { |group| p group.id };nil
"cdrom"
"audio"
"develop"
=> nil belongs_to の引数について説明します。理 解の助けのため、先ほどの Group クラスを少し拡張した以下のコードを例にとって説明 します。
class User < ActiveLdap::Base
ldap_mapping :dn_attribute => 'uid', :prefix => 'People', :classes => ['top','account']
# プライマリで所属しているグループと関連付け
belongs_to :primary_group, :foreign_key => 'gidNumber',
:class_name => 'Group', :primary_key => 'gidNumber'
# 所属しているグループ全てと関連付け
belongs_to :groups, :foreign_key => 'uid',
:class_name => 'Group', :many => 'memberUid',
endbelongs_to の最初の引数は作りたいメソッドの名前を Symbol で指定します。ここでは、 primary_group メソッドと groups メソッドを作成しています。次以降の引数は実際には Hashです(ldap_mapping のように)。
:foreign_key には、関連のキー値を持つ、自身の属性名を指定します。:foreign_keyに 何も設定しないと、自動的に ldap_mapping で指定した :dn_attribute の属性名が利用 されます。
例では :foreign_key に uid を指定していますが、これに違和感を覚えたかもしれま
せん。
ActiveLdapでの :foreign_key には "自身の属性名を指定する”という規約がありま
す。このため、必ずしも名前通りの”外部キー”とはならない場合があります。
belongs_toにおける :foreign_key は、単に所属の関連性を示すためのキーにすぎない
と考えた方が良いかも知れません。
後述する :primary_key も同様に”相手先の属性名を指定する”という規約が適用され
ています。
:class_name キーには、所属先となるオブジェクトの拡張クラス名を、 String で指定し ます。もし拡張クラスがモジュールやクラスの中に定義されている場合、:class_name => "MyLdapModule::Group" のようにトップレベルから全ての名前を記載してください。この 例では "Group" クラスを指定しています。
:many と :primary_key は両方とも似た意味を持ちます。どちらも、:foreign_key の参 照先属性名を指定します。指定する属性名は :class_name で指定した拡張クラスのイン スタンスで利用できる属性名です。
所属の関連付けは、:foreign_key に指定した属性の値を用い、 :class_name に指定の拡 張クラス配下のオブジェクト群を検索することによって行われます。この際の検索対象属 性が :primary_key または :many に指定の属性です。例で定義した parimary_group メ ソッドで言えば、User オブジェクトの gidNumber の値で、 Groupオブジェクト群の属性 gidNumber を検索します。マッチした Group オブジェクトが所属先となります。
:parimary_key は所属先がただ一つの場合に利用します。検索の結果、最初にマッチした もののみが所属先として扱われます。
:many は所属先が複数の場合に利用します。検索の結果マッチしたすべてのオブジェクト が所属先として扱われます。
尚、:many を利用していると、メンバーシップテストも下記のように行えます。
irb> me.groups.member? 'root'
=> false
irb> me.groups.member? 'develop'
=> true
has_many
このメソッドは belongs_to と対のものです。特定のオブジェクトに所属しているエント リを、所属されているオブジェクトからリスト可能にします。これを行うにはbelongs_to とは逆のことをします。
class Group < ActiveLdap::Base
ldap_mapping :dn_attribute => 'cn', :prefix => 'ou=Groups', :classes => ['top', 'posixGroup']
# プライマリで所属しているユーザ群と関連付け
has_many :primary_members, :foreign_key => 'gidNumber',
:class_name => "User", :primary_key => 'gidNumber'
# 所属しているユーザ全てと関連付け
has_many :members, :wrap => "memberUid",
:class_name => "User", :primary_key => 'uid'
end これで develop グループが 'drewry' ユーザをメンバーとして見ることができます。 belongs_to のように、メンバーのリストを members メソッドによって取得することがで きます。
irb> develop = Group.find('develop')
=> ...
irb> develop.members
=> [#<User ...>, #<User ...>] has_many の引数は belongs_to とまったく同じ形式をとります。つまり第一引数に実装 するメソッド名を Symbol で渡し、続く引数は Hash です。
:class_name、:parimary_key の意味合いも belongs_to と同様で、相手先の拡張クラス 名と参照先の属性名を指定します。相手先が所有先になるだけです。尚、 has_many に :many キーはありません。
belonsg_to と違う点は、自身の関連性のキー属性を指定するオプションが二つあること です。:foreign_key か :wrap のいずれかで指定します。
:foreign_key で属性名を指定した場合、has_many は単純に :class_name のクラスに対 して検索を行い、マッチしたオブジェクト群を所有オブジェクト群として扱います。
:wrap で指定した場合は挙動が変わります。:wrap の概念は”:wrap に指定した属性に保 持する値をオブジェクト化する”ことです。
:wrap に指定する属性は、memberUid のように一つ以上の値を持つことを想定しています。 この属性に保持する全ての値で検索し、マッチする全てのオブジェクトを返します。マッ チしない値がある場合、:class_name に指定の拡張クラスで new され、所有対象のリス トに含まれます。
拡張クラスの利用法
作成した拡張クラスは多くのメソッドコールを持ちます。それらのうちの多数は LDAP オブジェクトに対するアクセスを提供するために自動的に作成されています。他のメソッ ドはクラス定義時、belongs_to のような特別なメソッドによって作成されます。以下で はそれら以外のメソッドについて記載します。
.find
.find は ldap_mapping をコール済みの拡張クラスのクラスメソッドです。ActiveRecord のようにLDAPオブジェクトを検索可能です。
文字列を与えると、dn_attribute をキーにして最初にマッチするオブジェクトを返しま す。
irb> Group.find(:first, 'develop')
=> #<Group ...>
irb> Group.find(:first, 'develop').cn
=> "develop"
irb> Group.find(:first, 'develop').members
=> ['drewry']
最初のキーに :all を与えると全てのオブジェクトを返します。
irb> Group.find :all
=> [#<Group ...>, #<Group ...>, #<Group ...>]
irb> Group.find :all, :filter => '(gidNumber=2*)'
=> [#<Group ...>, #<Group ...>]
:attribute と :value によって特定のキーについて検索することも可能です。 :attribute が指定されない場合、:dn_attribute が利用されます。
irb> Group.find(:all, :attribute => 'gidNumber', :value => '1003').collect{|g| g.cn}
=> ["develop"] :filter によってLDAPフィルタを直接指定することも可能です。
irb> Group.find(:all, :filter => '(gidNumber=1003)').collect{|g| g.cn}
=> ["develop"].search
.search は ActiveLdap::Base か、そのサブクラスから呼び出し可能なクラスメソッドで す。拡張クラスと LDAPツリーとのマッピングを無視して検索することができます。直接 Base.connection を利用してもよいですが、このメソッドを利用すれば8割の目的は達成 できるでしょう。
irb> Base.search(:base => 'dc=example,dc=com', :filter => '(uid=roo*)',
:scope => :sub, :attributes => ['uid', 'cn'])
=> [["uid=root,ou=People,dc=dataspill,dc=org",{"cn"=>["root"], "uidNumber"=>["0"]}] :filter, :base, :scope, :attributes の各キーを指定できます。それぞれにデフォルト の値を持っています。
- :filter のデフォルトは "objectClass=" です。たいていの場合、変更する必要があるでしょう
- :base のデフォルトはこのメソッドを実行したクラスの :base です。これは ldap_mapping によって設定されているものです
- :scope は :sub に設定されています。
- :attributes のデフォルトは です。これはあなたが欲しい属性名のリストを指定してください。空の場合はすべての属性を取得します
valid?
valid? はインスタンスメソッドです。オブジェクトクラスで必要とされる属性が備わっ ているか検査します。真偽値を返します。
save
save は LDAP オブジェクトの変更を保存するためのインスタンスメソッドです。実行す ると、レシーバのオブジェクトに加えられていた変更が LDAP サーバに反映されます。新 しいオブジェクトか、既存のオブジェクトかを自動判別し、新規オブジェクトならLDAPサ ーバに追加します。既存のものならば更新します。
.exists?
exists? はシンプルなクラスメソッドです。ある :dn_attribute を持つオブジェクトが 存在するか検査したい時に利用します。
irb> User.exists?("dshadsadsa")
=> falseActiveLdap::Base
ActiveLdap::Base はこれまでの例の中で何回か登場してきました。主な目的としてLDAP オブジェクトをラップするために拡張クラスのスーパークラスとして活用しましたが、も う少しその背景を記載します。
これは何か
ActiveLdap::Base は ActiveLdap の心臓部です。属性を setter/getter にマッピングし たり、バリデーションを行うためのスキーマ解析を行います。同時に、LDAPサーバとの接 続の管理も行います。
setup_connection
Base.setup_connection は LDAP サーバとの接続のために多くの引数をとります。多くは オプションの引数です。時に匿名接続を行いたいでしょうし、時にはユーザ証明とともに TLSを利用した接続をしたいでしょう。Base.setup_connection はそれらを実現します。
Group のような、Base を親に持つサブクラスをコールした際、まだ接続が確立していなけ れば、Base.setup_connection で設定した情報をもとに接続しようと試みます。あなたの サーバが匿名バインドを許していて、かつ読み取り専用でのアクセスを行いたいのな ら、多くを設定する必要はないでしょう。以下にパラメータつきの setup_connection の 例を示します。
Base.setup_connection(
:host => 'ldap.dataspill.org',
:port => 389,
:base => 'dc=dataspill,dc=org',
:logger => logger_object,
:bind_dn => "uid=drewry,ou=People,dc=dataspill,dc=org",
:password_block => Proc.new { 'password12345' },
:allow_anonymous => false,
:try_sasl => false
) 多くの引数がありますが、そのうちの多くには安全なデフォルト値が設定設定されていま す。
- :host のデフォルト値は '127.0.0.1' です
- :port は nil です。何も設定しない場合は 389 が利用されます
- :bind_dn は nil です。何も設定しない場合、匿名アクセスとなります。
- :logger はロガーオブジェクトを設定します。デフォルトでは Logger オブジェクトです。
- :password_block のデフォルトは nil です。
- :allow_anonymous のデフォルトは true です
- :try_sasl のデフォルトは false になっています。このオプションについては後述の "高度な話題" をご覧ください
実際に利用可能な引数のリストを示します。
- :host は接続先LDAPサーバのホスト名を指定します
- :port は接続先LDAPサーバのポートを指定します
- :method は接続方法を指定します。 :tls, :ssl, :plain が指定可能です
- :base は LDAPの検索開始位置を指定します。これは Base のサブクラスで :prefix とともに利用されます
- :bind_dn はユーザ認証を伴った接続をする際に指定します。バインドする dn を指定します。
- :logger はカスタマイズしたロガーオブジェクトを指定します。あなたが利用したいロガーを渡すことができます。
- :password_block には Proc オブジェクトを渡します。このブロックを実行した結果の戻り値がパスワードとして利用されます
- :password にはパスワードの文字列を指定します
- :store_password には真偽値を設定します。再接続の際に :password_block で指定したブロックを再実行するかどうかが決定されます。このキーが存在する場合、password_block の実行結果は保存され、ブロックが再実行されることはありません
- :allow_anonymous は接続の際、バインドに失敗した場合、匿名で接続することを許可します
- :try_sasl が true の場合、SASL-GSSAPI バインドを試みます
- :sasl_quiet が true の場合、SASL ライブラリが STDOUT にメッセージを吐かないよう指示します
- :method は接続方式の指定です。:ssl, :tls または :plain を指定します
- :retry_limit は接続失敗時に、何回再接続を試みるかを数値で指定します。-1 を指定すると無限に再接続を試みます
- :retry_on_timeout タイムアウトが発生した場合に再接続を行うかどうかを指定します。デフォルトは true です
- :retry_wait は再接続するまえに何秒待つかを指定します
- :scope はLDAPオブジェクトの検索方式を指定します。デフォルトは :one です
- :timeout は検索のタイムアウト秒数を指定します。デフォルトでは無効になっています。search() リクエストもインターラプトされるため、注意してください
各オプションのデフォルト値は ActiveLdap::Configuration::DEFAULT_CONFIG に設定されています。
Base.setup_connection は接続の設定のみを行います。実際にLDAPサーバに接続しバインドするプロセスは必要になったときに一度に実施されます。だいたい、以下のような アプローチを取ります
- host:port にたいして、:method での接続を行います
- もし bind_dn と password_block か password が与えられていれば、ユーザ認証を行いバインドしようとします
- もし認証が失敗するか、パスワードが与えられていない場合、匿名アクセスが許可されていれば、匿名バインドを行います
- 匿名バインドも失敗すれば、エラー終了します
接続の際、渡された設定オプションはクラス変数の中に格納 されます。このさい、指定されていないオプションはデフォルト値が適用されます。
connection
Base.connection は ActiveLdasp::Connection オブジェクトを返します
例外クラス
ActiveLdap は幾つかのカスタマイズした例外クラスを扱います。以下にそれを示します。
DeleteError
LDAPオブジェクトの削除に失敗した際、この例外が発生します。エラー発生時の LDAP エ ラーメッセージが含まれます。
SaveError
LDAP オブジェクトの追加または更新に問題があった場合に発生する例外です。LDAPサー バのログや Ethereal などによる通信解析によってより詳しい情報が得られるかもしれま せん。
AuthenticationError
この例外は setup_connection で :method による指定された認証が成功しなかった場合に発生します。
ConnectionError
この例外は setup_connection で指定された接続が確立できなかった場合に発生します。 setup_connection のパラメータや、ネットワークの導通を確認してみてください。ちゃ んとリクエストが投げられている場合、LDAPサーバのログも確認してみてください。
ObjectClassError
この例外は、LDAPサーバのスキーマ上で定義されていないオブジェクトクラスを利用した 場合に発生します。
その他の例外
その他の例外は Ruby/LDAP モジュールか、さらに別のサブシステムから発生します。も しあなたがそれらの例外を受け取り、ActiveLdap の例外によりラップされるべきだと考 えたら、あなたが何を期待しているかをメールで教えてください。早く結果を出したい場 合、メールにパッチを添付してください。
実際に動作するサンプルコード
ここまでで ActiveLdap のすべてのコンポーネントについて記述しました。さあ、実際に 動作するコードを配置するときです! 以降では、いままで例に挙げてきた LDAP ツリー 上の、ユーザとグループを管理するためのスクリプトを順番にセットアップしてきま す。
以下に示すサンプルコードは、ActiveLdap ライブラリの examples/ ディレクトリにも配 置されています。
準備
まず必要なディレクトリを作成します
mkdir -p ldapadmin/objects
次に、ldapadin/objects/user.rb を作成します。コードは以下のようにしてください。
require 'objects/group'
class User < ActiveLdap::Base
ldap_mapping :dn_attribute => 'uid', :prefix => 'ou=People',
:classes => ['person', 'posixAccount']
belongs_to :primary_group, :class_name => "Group",
:foreign_key => "gidNumber", :primary_key => "gidNumber"
belongs_to :groups, :many => 'memberUid'
# An example of using the old "return_objects" API with the
# new ActiveRecord-style API.
alias groups_mapping groups
def groups(return_objects=true)
return groups_mapping if return_objects
attr = 'cn'
Group.search(:attribute => 'memberUid',
:value => id,
:attributes => [attr]).map {|dn, attrs| attrs[attr]}.flatten
end
end 同様に、ldapadmin/objects/group.rb を作成します。
class Group < ActiveLdap::Base
ldap_mapping :dn_attribute => "cn",
:classes => ['posixGroup']
# Inspired by ActiveRecord, this tells ActiveLDAP that the
# LDAP entry has a attribute which contains one or more of
# some class |:class_name| where the attributes name is
# |:local_key|. This means that it will call
# :class_name.new(value_of(:local_key)) to create the objects.
has_many :members, :class => "User", :wrap => "memberUid"
has_many :primary_members, :class_name => 'User',
:foreign_key => 'gidNumber',
:primary_key => 'gidNumber'
end これでシンプルな管理タスクのためのスクリプトを書けるようになりました。
LDAP エントリの作成
早速ユーザを追加するための ldapadmin/useradd スクリプトを作成しましょう。
#!/usr/bin/ruby -W0
base = File.expand_path(File.join(File.dirname(__FILE__), ".."))
$LOAD_PATH << File.join(base, "lib")
$LOAD_PATH << File.join(base, "examples")
require 'rubygems'
require 'active_ldap'
require 'objects/user'
require 'objects/group'
argv, opts, options = ActiveLdap::Command.parse_options do |opts, options|
opts.banner += " USER_NAME CN UID"
end
if argv.size == 3
name, cn, uid = argv
else
$stderr.puts opts
exit 1
end
pwb = Proc.new do |user|
ActiveLdap::Command.read_password("[#{user}] Password: ")
end
ActiveLdap::Base.setup_connection(:password_block => pwb,
:allow_anonymous => false)
if User.exists?(name)
$stderr.puts("User #{name} already exists.")
exit 1
end
user = User.new(name)
user.add_class('shadowAccount')
user.cn = cn
user.uid_number = uid
user.gid_number = uid
user.home_directory = "/home/#{name}"
user.sn = "somesn"
unless user.save
puts "failed"
puts user.errors.full_messages
exit 1
endLDAP エントリの管理
次に、ユーザの属性変更スクリプト ldapadmin/usermod を作成します。
#!/usr/bin/ruby -W0
base = File.expand_path(File.join(File.dirname(__FILE__), ".."))
$LOAD_PATH << File.join(base, "lib")
$LOAD_PATH << File.join(base, "examples")
require 'rubygems'
require 'active_ldap'
require 'objects/user'
require 'objects/group'
argv, opts, options = ActiveLdap::Command.parse_options do |opts, options|
opts.banner += " USER_NAME CN UID"
end
if argv.size == 3
name, cn, uid = argv
else
$stderr.puts opts
exit 1
end
pwb = Proc.new do |user|
ActiveLdap::Command.read_password("[#{user}] Password: ")
end
ActiveLdap::Base.setup_connection(:password_block => pwb,
:allow_anonymous => false)
unless User.exists?(name)
$stderr.puts("User #{name} doesn't exist.")
exit 1
end
user = User.find(name)
user.cn = cn
user.uid_number = uid
user.gid_number = uid
unless user.save
puts "failed"
puts user.errors.full_messages
exit 1
endLDAP エントリの削除
最後に、ユーザ削除のためのスクリプト ldapadmin/userdel を作成します。
#!/usr/bin/ruby -W0
base = File.expand_path(File.join(File.dirname(__FILE__), ".."))
$LOAD_PATH << File.join(base, "lib")
$LOAD_PATH << File.join(base, "examples")
require 'rubygems'
require 'active_ldap'
require 'objects/user'
require 'objects/group'
argv, opts, options = ActiveLdap::Command.parse_options do |opts, options|
opts.banner += " USER_NAME"
end
if argv.size == 1
name = argv.shift
else
$stderr.puts opts
exit 1
end
pwb = Proc.new do |user|
ActiveLdap::Command.read_password("[#{user}] Password: ")
end
ActiveLdap::Base.setup_connection(:password_block => pwb,
:allow_anonymous => false)
unless User.exists?(name)
$stderr.puts("User #{name} doesn't exist.")
exit 1
end
User.destroy(name)
高度な話題
以降では、ActiveLdap を最大限に生かすために、さまざまなシチュエーションで役 立つテクニックを紹介していきます。
バイナリデータとサブタイプ
しばしば、あなたは属性に言語指定子を指定して値を格納したくなるでしょう。それ にバイナリデータ形式で投入したいかもしれません。これは十分にサポートされてい ます。例を見てみましょう。
# 新規ユーザの作成
irb> user = User.new('drewry')
=> ...
# cn をサーバデフォルト(この場合は英語)でエントリに追加
irb> user.cn = [ 'wad', {'lang-en' => ['wad', 'Will Drewry']} ]
=> ...
irb> user.cn
=> ["wad", {"lang-en-us" => ["wad", "Will Drewry"]}]
# X.509 証明書をバイナリ形式で追加(objectClass の整合性は解決しているものとします)
irb> user.user_certificate = File.read('example.der')
=> ...
# 保存
irb> user.save
この例には見るべき点が多くあります。順番に見てみましょう。例では、cn に"wad" と cn;lang-en-us に ["wad", "Will Drewry"] を追加しています。LDAP属性のサブ タイプが必要とされるとき、それらのデータは Hash で包む必要があります。
一方で、上記の例では Hash に包まずに X.509 証明書を格納しました。バイナリデータ を格納するいくつかのの属性は ;binary サブタイプが必要です。これらの属性に対して は、プログラマがそうしなくても、自動で {'binary' => value} の Hash で包まれます。 これはコーディングの助けになりますが、正確性のために明示的に Hash で包むことも可 能です。
irb> user.user_certificate = {'binary' => File.read('example.der')} バイナリデータを格納する場合であっても、必ずしも ;binary サブタイプを利用する訳 ではないことに注意してください。例としては jpegPhoto があります。あなたは jpegPhoto;binaryか jpegPhoto を利用することができます。スキーマがバイナリ値だと 指示するので ActiveLdap はバイナリ値として書き込むでしょうが、サブタイプは自動的 には付与されません。jpegPhoto のような属性にサブタイプを利用するかどうかは、LDAP のサイトポリシーによって決められることであって、プログラムが自動で判断できるもの ではありません。(訳注:userCertificate 属性については、RFC上で ;binary サブタイ プを利用するよう指示されているため、自動的に ;binary 属性を付与できる一方、 jpegPhoto についてそのような言及が RFC 上にないために、そのような対応を自動的に 行うことは難しい、ということのようです)
LDAPv3 で唯一定義されているサブタイプが lang- と binary の組み合わせです。 これらは以下のように Hash をネストさせることで実現できます。
irb> user.cn = [{'lang-ja' => {'binary' => 'some Japanese'}}] ネストされたサブタイプは OpenLDAP でサポートされていません。ですが、いくつかの文 書を確認したところ、ネットスケープの LDAP サーバは対応しているようです。私がアク セスしたのは OpenLDAP のみなので、どなたかこの機能をテストしたなら、どのように動 作したかを連絡してもらえると助かります。
このセクションの他の項目についても同様です。どのように動作したか連絡もらえると助 かります。
環境とのさらなる統合 - 別名 名前空間の構築
ActiveLdap を Ruby のインクルードパスに統合したいなら、拡張クラスをカスタムモジ ュールの中に統合するとよいでしょう。
例:
./myldap.rb:
require 'active_ldap'
require 'myldap/user'
require 'myldap/group'
module MyLDAP
end
./myldap/user.rb:
module MyLDAP
class User < ActiveLdap::Base
ldap_mapping :dn_attribute => 'uid', :prefix => 'ou=People', :classes => ['top', 'account', 'posixAccount']
belongs_to :groups, :class_name => 'MyLDAP::Group', :many => 'memberUid'
end
end./myldap/group.rb:
module MyLDAP
class Group < ActiveLdap::Base
ldap_mapping :classes => ['top', 'posixGroup'], :prefix => 'ou=Group'
has_many :members, :class_name => 'MyLDAP::User', :wrap => 'memberUid'
has_many :primary_members, :class_name => 'MyLDAP::User', :wrap => 'gidNumber', :primary_key => 'gidNumber'
end
end こうすれば、あなたのアプリケーションでは以下のように呼び出すことができます。
require 'myldap'
MyLDAP::Group.new('foo')
... すべてのクラスは正しく動作するでしょう。
単一内容の属性に対する getter アクセスであっても、配列で返すようにする
属性メソッドの引数に true を渡すことで、その属性の中身が単一の内容であっても配列 で返すように挙動を変更することができます。
irb> user = User.new('drewry')
=> ...
irb> user.cn(true)
=> ["Will Drewry"]
動的な属性のクローリング
IRBでタブ補完機能を有効にしている場合、属性アクセサメソッドをタブ補完で呼び出す ことができます。また、Base#attribute_names メソッドを利用すれば属性アクセサメソ ッドの一覧を得ることが可能です。
irb> d = Group.new('develop')
=> ...
irb> d.attribute_names
=> ["gidNumber", "cn", "memberUid", "commonName", "description", "userPassword", "objectClass"]
複数の LDAP 接続の切り替え
最後に、クラスごとに別々の LDAP コネクションを利用するには、以下のようにしてくだ さい。
irb> anon_class = Class.new(Base)
=> ...
irb> anon_class.setup_connection
=> ...
irb> auth_class = Class.new(Base)
=> ...
irb> auth_class.setup_connection(:password_block => lambda{'mypass'})
=> ... これは認証のテストなどに有効です。
:try_sasl
:try_sasl を利用することによって LDAP サーバとのバインドに Kerberos を利用するこ とができます。
またこの場合、OpenLDAP 2.1.29 かそれ以上が必要です。それ以前のバージョンには多数 のバグが残っています。
こわがらないでください!
更にメソッドを追加することや、拡張クラスを定義することや、実験することを恐れない でください。私にとってこのパッケージの作成は一区切りつきましたが、もしクールな何 かをあなたが見つけたなら、それを共有させてください!
ActiveLdap::Base やそのサブクラスの構造は、まだ不安定です。外側の API については 最小の変化で済ませてきましたが、内部についてはまだ荒削りです。
ldap_mapping データはどこに保存されていますか? それらはどうやって取得しますか?
ldap_mapping をコールしたとき、ActiveLdap::Base のいくつかのクラスメソッドがオー バーライドされます。以下のメソッドです。
- Base.base()
- Base.required_classes()
- Base.dn_attribute()
これらのクラスメソッドに MyClass.base() のようにアクセスすることができます。イン スタンスからもこれらの情報を知るために、拡張クラスのインスタンスメソッドに以下の ような定義済みメソッドが用意されています。
- Base#base()
- Base#required_classes()
- Base#dn_attribute()
その他の話題
もしあなたが何らかの理由で LDAP コネクションを扱いたいなら、それを取得するために ActiveLdap::Base.connection メソッドをコールすることを今のところ提案します。他の 内部機構で接続を扱うものは少ないです。スキーマの情報については ActiveLdap::Base.schema メソッドにより取得できます。
他の便利な唯一の手法は、格納されたデータをデリファレンスしてアクセスすることです (訳注:たぶん、Perl の derefence と同じ意味で言っていると思われる)。
従来、LDAP属性はcn / commonName のように複数の名前を持つことができ、あなたが書く どのメソッドもそれを考慮にいれておかなければならないかもしれません(訳注:cn を デリファレンスしても、commonName をデリファレンスしても同じ参照先オブジェクトを 得ることがあるから注意せよ、の意だと思われます)。
デリファレンスして値を得るには self['属性名'] とすることを提案しますが、上記を考 慮すると十分ではありません。実際に格納されている属性の一意な名前を得るためには to_real_attribute_name プライベートメソッドを利用することができます。
>> u = User.find :first
>> u.instance_eval do
?> to_real_attribute_name 'commonName'
>> end
=> "cn"
このメソッドは背後(@data)で属性データが格納されている名前をあなたに教えます。 再び、self[属性名] はほとんどの拡張に対して十分でなくてはなりませんが、そうでな くても、たぶんここでは問題にならないでしょう。
また例えば、ユーザクラスの属性名のエイリアスを見つけるためには以下のようにすると よいでしょう。
irb> User.schema.attribute_type 'cn', 'NAME'
=> ['cn','commonName']
これはLDAP サーバのスキーマから自動的に発見します。
制限
実行速度
今のところ、ActiveLdap はまだ高速にできます。いくつかの再帰的な型チェックをオブ ジェクト作成時に行っており、それらがオブジェクトの生成速度を落としています。ま た、他のところでも多数の最適化が可能であることも認識しています。最適化できていな い部分については私が最適化できるようになるまで我慢してもらうか、または気軽にパッ チを送ってください。
フィードバック
どんなものでも、またすべてのフィードバックやパッチを歓迎します。私はこのパッケー ジについて興奮しています。また、私以上に、人々の助けになることを見たいと思ってい ます。
> gem install active_ldap -- gem install activeldap
修正しました!
This tutorial in English is here, http://ruby-activeldap.rubyforge.org/doc/