著者:高瀬一彰
編集:うえだ
本記事は「ActiveLdap を使ってみよう(前編)」の後編です。ActiveLdap を扱い始めたばかりの方やこれから使ってみようとお考えの方は、先に前編の記事をご覧頂くと理解しやすいと思います。
ActiveLdap は LDAP を検索・操作するためのライブラリで、クリアコードの須藤功平さんが開発者です。
ActiveRecord に着想を得たこのライブラリは、これまでの LDAP ライブラリと比較するとシンプルかつ判りやすい LDAP プログラミングを可能とします。Rubyist な皆さんなら、以下のサンプルコードを直感的に理解していただけるのではないでしょうか。
# エントリの作成
user = User.new
# 属性の設定
user.cn = "Ruby Taro"
# 保存
user.save
# 削除
user.destroy
前編の記事執筆時から現在までに ActiveLdap のバージョンが上がったため、先ずこの点について説明します。本稿は最新版である 1.2.1 を利用して執筆していますが、「バージョンアップで変更された点」で記載の項目以外問題なく利用できると思います。
次に前編で説明した機能を土台として、その応用技術を解説していきます。そのため、前編はチュートリアル的に記述しましたが、今回はリファレンス的な記述になっています。大きく分けて 3 つのパートに分かれています。必要に応じて各部を参照してください。
以下に該当する方は先に 前編 をご覧ください。
この記事で記載のコマンド例などは以下の環境で確認しています。皆さんの環境では適宜読み替えてください。
前回執筆時の ActiveLdap はバージョン 1.1.0 でしたが、現在は 1.2.1 に変わっています。バグフィクスが主でしたが、マイナー番号が変わっていることから前バージョン(1.1.x)とは非互換の変更があった事を示しています。
バージョン 1.1.x までは各エントリの DN を取得したり、クラスの BASE DN を取得すると文字列が返されていました。
これがバージョン 1.2.1 では以下のように ActiveLdap::DistinguishedName オブジェクトを返すようになっています。
DistinguishedName オブジェクトを返すようになった事で DN パスの編集などが容易になりました。
以前のように文字列で取得したい場合は to_s メソッドをご利用ください。
前回解説した機能の知識を土台として、これを知れば更に便利であろうと思う機能を紹介していきます。
どれも ActiveRecord の機能を参考に構成されているので類似のインターフェースを利用できますが、検索フィルタの構成や関連性の操作に関しては LDAP の特性上、それなりに差異もあります。本記事が理解の一助になれば幸いです。
validation は一般的に言えばビジネスロジックを記述するための機構です。この仕組みを利用すると、オブジェクトの状態が一定の条件を満たしていない場合、エラーとして保存しないようにすることができます。また、どの位置でエラーが発生しているかも簡単にチェックすることが可能です。例えばエントリが特定の属性を保持していなかったり、ある属性の値が想定した範囲外の値を持っていた場合、これをエラーにする事が可能です。
validation は save ないし save! メソッドの直前に実行され、保存するエントリが期待通りの状態であるかをチェックする機構です。チェックに弾かれた場合、save メソッドであれば保存に失敗して false が返り、save! の場合は例外 ActiveLdap::EntryInvalid が発生します。実際に弾かれる例を見てみましょう。
以降では、このような機構の実装方法と簡単な解説を行います。詳細については ActiveRecord::Validations や ActiveRecord::Validations::ClassMethods などのドキュメントをご覧ください。
冒頭の例では以下のような User クラスを利用しました。このクラスは validate というメソッドが定義されており、ここで状態のチェックが行われています。
class User < ActiveLdap::Base
ldap_mapping :prefix => "ou=Users",
:dn_attribute => "uid",
:classes => ["inetOrgPerson", "posixAccount"]
def validate
# uid が正規表現にマッチするか確認
if self.uid !~ /\A[a-zA-Z0-9]+\z/
# uid でエラーが起きた事を記録
errors.add :uid, "must match with '/\\A[a-zA-Z0-9]+\\z/'."
end
end
end
validation の機構はインスタンスメソッド validate を見つけると、save の直前にそれを実行します。上記のクラスでは self.uid が正規表現にマッチしなかった場合に errors.add によってエラーの発生を記録していますが、これが validation の正体と言っても差支えないでしょう。
save メソッドは errors.add によって追加されたエラーを発見すると保存を行わずに false を返す仕組みになっています。冒頭の例では uid が正規表現にマッチしなかった結果、保存ができなかったのです。
save メソッドが呼ばれると、エラー情報が記録されていた場合は全てクリアされてから状態チェックが行われます。従って、一度エラーになっても以下のようにその原因を修正すれば保存が可能となります。
インスタンスメソッド validate を定義する以外に、クラスメソッド validate によって妥当性チェックメソッドを指定することができます。先ほどの状態チェックをこの方法で実装してみましょう。
class User < ActiveLdap::Base
ldap_mapping :prefix => "ou=Users",
:dn_attribute => "uid",
:classes => ["inetOrgPerson", "posixAccount"]
validate :validates_uid
def validates_uid
if self.uid !~ /\A[a-zA-Z0-9]+\z/
errors.add :uid, "must match with '/\\A[a-zA-Z0-9]\\z/'."
end
end
end
validate クラスメソッドで :validates_uid を状態チェックのためのメソッドとして実行すると予約しています。validate インスタンスメソッドを利用した場合と違うのは、validate クラスメソッドを複数回指定するか引数に複数のメソッド名を指定することで、状態チェックメソッドを複数設定できることです。
以下のように指定すれば、指定した順番にメソッドが実行されます。
validate :validate_one, :validate_two
valid? メソッドを利用すると状態のチェックのみを行うことができます。この場合、値が保存される事はありません。
ActiveRecord::Validations::ClassMethods では、良く使う状態チェックを予めメソッド化して提供しています。先の例では validates_uid としてチェック用のメソッドを定義していましたが、予め用意されている状態チェックメソッド validates_format_of を用いると以下のように記述できます。
class User < ActiveLdap::Base
ldap_mapping :prefix => "ou=Users",
:dn_attribute => "uid",
:classes => ["inetOrgPerson", "posixAccount"]
validates_format_of :uid, :with => /\A[a-zA-Z0-9]+\z/,
:message => "must match with '/\\A[a-zA-Z0-9]+\\z/'."
end
callbacks は save などのメソッドが実行される前後に、予め特定の処理を自動的に実行するよう”予約”することができる機構です。上手く使えば便利な機能でしょう。
class User < ActiveLdap::Base
ldap_mapping :prefix => "ou=Users",
:dn_attribute => "uid",
:classes => ["inetOrgPerson", "posixAccount"]
def before_save
puts "before_save received"
end
def after_save
puts "after_save received"
end
end
その他多数の before_、after_ といったコールバックが用意されています。詳しくは ActiveRecord::Callbacks の説明をご覧ください。
実際に調べる方のために本題から逸れた話を少しだけ。RoR で Callbacks といった場合は以下の 2 種類があります。
混乱の種にもなるので1老婆心ながら違いについて触れておきますと、ActiveSuport::Callbacks は特定のメソッドが実行される際にその前後をフックして他のメソッドを実行し易くするための仕組みです。これを利用すると様々なコールバックを定義することができます。
上記で解説したのは ActiveRecord::Callbacks です。これは ActiveSupport::Callbacks を利用して各種 before_* や after_* を定義している、という実装になっています2。
LDAP エントリを検索する際は LDAP フィルタを利用しますが、ActiveLdap では ActiveLdap::Base#find メソッドの :filter オプションで指定します。最もシンプルなやり方では :filter オプションに LDAP フィルタ文字列を直接渡します。
この方法の欠点は、検索条件を動的に構成するには不向きだという事です。SQL を利用する場合もそうですが、検索条件を動的に生成したい場合に文字列(String)は不便で、フィルタの構文に合うように、かつ意図した検索条件に合うように文字列を結合させていくのは、思いのほか手間がかかります。
実はこの :filter オプション、文字列以外の Array や Hash を渡すこともできます。これを利用すると構文をあまり意識せずにフィルタを構成する事ができるのです。
先ずはどんな形で渡せるのか見てみましょう。この時点では細かく見る必要はありません。大体で見てください。
# "(uid=ruby_taro)" の別表現
User.find :first, :filter => [:uid, 'ruby_taro']
# "(|(uid=ruby_taro)(uid=ruby_hanako))" の別表現
User.find :all, :filter => [:or, {:uid => ['ruby_taro', 'ruby_hanako']}]
ちょっと見づらいですが、重要なのは文字列を編集する必要がないという事です。つまりフィルタ構文のカッコの開閉や、メタ文字列の扱いなど気にすることなくフィルタを構成できる点こそが重要です。
以下の例では、もう少し複雑なフィルタを記述してみます。フィルタを動的に構成し易い点が見て取れると思います。
# :filter オプションを組み立てて記述する例
users = ['ruby_taro', 'ruby_hanako']
exclude_uidnumber = 10000
user_filter = [:or, {:uid => users}]
exclude_filter = [:not, [:uidNumber, exclude_uidnumber]]
# "(&(|(uid=ruby_taro)(uid=ruby_hanako))(!(uidNumber=10000)))" が適用される
User.find :all, :filter => [:and, user_filter, exclude_filter]
仕組みを簡単に解説します。概念はそう難しくはありません。
LDAP フィルタは各カッコ内に記述された比較式を、論理演算子によって結合して記述します。このため最小限の構成単位は比較式です。
ActiveLdap のフィルタオプションも同様の構成を取っています。最も原理的で基本的な単位である比較式から解説しましょう。
最小限のフィルタ構成は、以下のようなものです。
# "(uid=ruby_taro)" が利用される
User.find :all, :filter => [:uid, 'ruby_taro']
# "(uidNumber>=100)" が利用される
User.find :all, :filter => [:uidNumber, '>=', 100]
どちらもおよそ見た目通りです。前者の例では、配列の最初の要素に検索対象の属性名を String か Symbol で指定します。そのまま比較演算子を指定しないで検索値を入れると、比較演算子 “=” で繋がったフィルタが構成されます。後者の例では比較演算子を指定しています。二番目の要素に有効な比較演算子を指定するとその演算子でフィルタが作成されます。
これらの例が最も基本的なフィルタ構成、つまり単一の比較式です。あとは比較式を論理演算子によって繋げれば、複雑なフィルタを容易に構成することができます。
# (|(uid=ruby_taro)(uid=ruby_hanako))
User.find :all, :filter => [:or, [:uid, 'ruby_taro'], [:uid, 'ruby_hanako']]
# こちらも同じ
User.find :all, :filter => [:or, [:uid, 'ruby_taro', 'ruby_hanako']]
前者の場合、実際の LDAP フィルタに近い書式なので判りやすいでしょう。後者の場合、値を示す配列の要素が更に配列になっていますが、これは ActiveLdap が備える省略形であり、このように検索値を複数指定することが可能です。上記二例ともに、結果的に生成される LDAP フィルタは同じものです。
次は LDAP フィルタ自身の実装でも浮いている否定形ですが、ActiveLdap でもやはり浮いています(笑)。LDAP フィルタでの検索条件の否定は比較式を否定することによって構成します。このため論理演算子に近い利用の仕方をします。
# (!(uid=ruby_taro))
User.find :all, :filter => [:not, [:uid, 'ruby_taro']]
フィルタは入れ子にできるため、幾らでも深く記述することができます(あまりそこまで複雑にはしませんが……)。
最後に、Hash を渡す場合について簡単に触れておきます。Hash を渡す場合と Array を渡す場合にそれほど差異はありません。以下の例では Hash を利用する利点が見え難いですが、動きを確認するために記述します。
# "(|(uid=ruby_taro)(uid=ruby_hanako))" が利用される
User.find :all, :filter => [:or, [:uid, ['ruby_taro', 'ruby_hanako']]]
# これも同じ
User.find :all, :filter => [:or, {:uid => ['ruby_taro', 'ruby_hanako']}]
どちらの場合も生成される LDAP フィルタは同一です。Hash を利用すると Hash のキーが属性名、その値が検索値として扱われます。
以下のように、複数の属性に対する検索を and または or で一括して繋ぐような場合があれば、Hash は便利でしょう。
hash = {}
hash[:ou] = 'rubyistMagazine'
hash[:sn] = 'Ruby'
# "(&(ou=rubyistMagazine)(sn=Ruby))" が利用される
User.find :all, :filter => [:and, hash]
Hash を利用する利点は、単にその方が見やすい場合もあるでしょうし、プログラム上扱いやすい場合もあるでしょう。場合に合わせて使い分けてください3。
前回は関連性の定義方法と関連先へのアクセス方法を紹介しましたが、関連性には他にも便利な機能があります。特定のオブジェクトを関連先から外す、追加する、といった処理を簡便に行うことができます。
利用例
これらの機能を正しく利用するためには、関連性の実装面についてもう少し解説する必要があると考えます。従って、先ず関連性がどのように実装されているかを解説した後、実際に利用できる関連先操作メソッドを紹介します。
さて、先ずは前回のおさらいの意味も含め、has_many を用いた関連性を見てみましょう。
class Group < ActiveLdap::Base
ldap_mapping :prefix => "ou=Groups",
:dn_attribute => "cn", :classes => ["posixGroup"]
has_many :primary_users, :primary_key => "gidNumber",
:class_name => "User", :foreign_key => "gidNumber"
has_many :users, :wrap => "memberUid",
:class_name => "User", :primary_key => "uid"
end
上記の例では、関連性メソッド Group#users が関連先オブジェクト群を返しているように見えますが、実際には __関連性オブジェクト__を返しており、関連先オブジェクト(群)に対する Proxy パターンの実装です。
関連先に対する操作は、この 関連性オブジェクト が責任をもって行います。上記の例では users に対して Array のような API を提供していますが、このように見せているのが関連性オブジェクトです。上記のコードで “users[0]” と記載されている部分は、実際には 関連性オブジェクトの [] メソッド をコールしています。関連先オブジェクト(群)に対して直接メソッド呼び出しをするのが適切でない場合があるからです。
関連性オブジェクトは、関連性の特性に応じたかたちで関連先オブジェクト(群)をロードし、自身のインスタンス変数に格納します。
関連先が単一の関連性(belongs_to)であればそのオブジェクトをインスタンス変数に格納し、関連先が複数の関連性(has_many など)であれば配列にまとめてインスタンス変数に格納します。基本的には、関連性オブジェクトに対するメソッド呼び出しは method_missing によって、関連先オブジェクト(群)に飛ばしています。
前出の例の users は has_many ですから、関連性オブジェクト内では関連先オブジェクト群が配列の形で格納されています。method_missing によって各種メソッドがこの配列に飛ばされているので、Array のようにアクセスできます。
となれば、 users に対して “<<” メソッドを使い、関連先オブジェクトを追加できると直感的で便利ですが、method_missing でそのまま “<<” メソッドを格納先インスタンス変数に飛ばしても意味がありません。
関連先を追加ないし削除したい場合、has_many(:wrap) ならば自身の属性値を編集しなければなりませんし、belongs_to(:many) ならば相手先の属性値を変えなければなりません。関連性オブジェクトのインスタンス変数に入っている配列にオブジェクトを追加しても、関連性に変化は無い訳です。
関連性オブジェクトはこれを解決するために存在します。関連性オブジェクト自身に “<<” メソッドが定義されており、ここをうまく吸収しているからです。
# group の memberUid に new_user の uid を追加して、group を自動で保存する
group.users << new_user
また関連性そのものの情報を得ることなどもでき、関連性オブジェクトを通じて、既に関連先のオブジェクトがロードされたか? 関連先は存在するか? などを問い合わせることが可能です。
まとめます。has_many などによって定義された関連性メソッドは、関連性オブジェクトを返します。関連性オブジェクトには、大別して二種類のメソッドがあります。
以降、利用できるメソッド群のうち、特に有用と思われるものについて説明します。
# 関連先オブジェクト(群)を取得
group.users.target
# 関連先オブジェクト(群)が存在するかを確認する
group.users.exists? #=> true
# 関連先オブジェクト(群)を再ロードする
group.users.reload
# 関連先を追加する
group.users << user
children については「LDAPの管理的操作を可能にしていくために」の「ツリーを辿る」で説明します。
前回の記事では関連性定義の際に :class_name によって関連先オブジェクト(群)のクラスを指定すると説明しましたが、以下のようにキー :class によってクラスを直接指定することもできます。
class Group < ActiveLdap::Base
ldap_mapping :prefix => "ou=Groups",
:dn_attribute => "cn", :classes => ["posixGroup"]
# User クラスを直接指定している
has_many :members, :wrap => "memberUid",
:class => User, :primary_key => "uid"
end
メタプログラミングを意識する際などに有用でしょう。
関連性は、可能な限り関連性オブジェクトを通して編集してください。
関連性オブジェクトを通して関連性を編集した場合、関連性オブジェクトに保持されている関連先オブジェクト(群)も変更されます。一方、関連性を表現する属性(例の Group クラスで言うと gidNumber や memberUid)の値を直接編集した場合、関連性オブジェクトはそれを検知できません。この場合、属性が示す関連と関連性メソッドから取得できるオブジェクト(群)が一致せず、混乱をきたします。
関連性を編集する場合、原則として関連性オブジェクトの API を利用してください。何らかの制約によりそれができない場合は reload を呼び出すことをお勧めします。
一方で、API を利用して関連先を追加あるいは削除した場合、自動的に関連先オブジェクトやレシーバが保存される場合があります。例えば、以下のような場合には新規の関連先(user)が保存されます。
# user が保存される
# (has_many を利用し、posixAccount と posixGroup を ユーザ側の gidNumber で 関連付けている場合の例)
group.primary_users << user
逆に、以下の場合には関連元(group)が保存されます
# group が保存される
# (has_many(:wrap) を利用して posixAccount と posixGroup をグループ側の memberUid で関連付けている場合)
group.users << user
以下に関連先の追加あるいは削除の際の挙動をまとめます。参考にしてください。
関連性 | 関連先を追加/削除した場合の挙動 |
belongs_to(:many) | 関連先がその属性を編集された上、保存される |
has_many | 関連先がその属性を編集された上、保存される |
has_many(:wrap) | 関連__元__がその属性を編集された上、保存される |
children | 追加の場合は子が保存される。削除の場合、削除対象の子エントリがエントリごと削除される |
ActiveLdap は RoR と統合し易いよう構成されています。説明することはそう多くありませんが、RoR で利用する場合の手順例と注意点をそれぞれ説明します。
ActiveRecord では config/database.yml を利用して設定を行いますが、ActiveLdap でも同じように config/ldap.yml というファイルを利用します。記載の仕方もほぼ同様です。実際の作成例を示します。
ActiveLdap をインストールしていれば、ActiveLdap 用のジェネレータが利用可能になっています。以下のコマンドで設定ファイル ldap.yml のテンプレートを作成できます。
ファイル名の通り YAML で設定が記述されています。RoR に合わせて test、development、production の各動作モード用の設定をそれぞれ定義でき、モードに合わせて自動的に設定が適用されます。それぞれの内容は ActiveLdap::Base.setup_connection に渡す内容です。
ActiveLdap 用のモデルジェネレータも用意されています。
アプリケーションの作り方によりますが、例えばユーザにログインさせる Web アプリケーションの場合、ログインしたユーザ毎に bind させるようなロジックでは注意が必要です。
ActiveLdap のコネクションを特定のユーザで bind させると、mongrel なり Passenger なりが保持している LDAP 接続がそのユーザで bind された状態になります。直後に他の人がアクセスした場合にも、先に bind されたユーザと関連付けられた状態でアプリケーションが動作します。更に mongrel あるいは Passenger などで複数のサーバインスタンスを動作させた状態では、インスタンス間で LDAP 接続の共有はできないため、ログインした直後にログアウトしてしまったり、いつの間にか別のユーザになってしまったり、といった問題が生じる可能性があります。
Web アプリケーションが共通して利用するアカウントで bind するか、別の対策をご検討ください。
これまで LDAPエントリ群の基本的操作を解説してきました。このセクションでは、より応用的・メタ操作的な話題を取り上げていきます。高度な LDAP 管理アプリケーションを作成する場合などに役立つことでしょう。
エントリに対応する各インスタンスには、ツリー構造を辿ったり、子を親に紐づけたりする API が備わっています。これらを利用すれば LDAP のツリー構造を意識したプログラミングが可能になります。
ツリー操作のための各インスタンスメソッドを以下で解説します。
ツリーを構成するためのメソッドは parent= です。特定のエントリを示すオブジェクトの parent= メソッドで親になるオブジェクトを指定します。
保存した時点で、指定したエントリを親に持つエントリが作成されます。
ツリー構造を辿ってエントリを取得するためのメソッドを紹介します。これには少々制限があり、ActiveLdap::Base を継承したクラスが担当するツリー以下のみを辿る事ができます。幾つかの制限もありますが、各メソッドに記載していきます。
各クラスが担当する範囲を超えてツリーを辿る事も可能です。これについては「全てのエントリを扱うクラス」で紹介します。用途に応じて使い分けてください。
以降の説明では、以下のようなツリーを利用して説明を行います
ツリーのサンプル:
コード例:
parent
親のエントリを直接取得します。親のエントリと対応づいた ActiveLdap オブジェクトを返します。
parent で取得できるエントリの上限は、オブジェクトのクラスが担当する DN の直下のエントリです。上記のツリーのサンプルでいえば parent は devel や manage までのオブジェクトを返します。そこから更に遡ろうとして parent を呼ぶと nil を返します。
使用例:
siblings
兄弟のエントリ群を配列で返します。
self_and_siblings
(自身を含めた)兄弟のエントリ群を配列で返します。
children
直接の子に当たるエントリ群にアクセスします。孫のエントリは返しません。
実際のところ、これは関係性オブジェクトを返すメソッドです。従って << などのメソッドを利用できます。
root
レシーバの「根」にあたるエントリのオブジェクトを返します。ここで言う根とは LDAP ツリーの根のことではありません。
レシーバのクラスが担当する DN 直下のエントリが根として扱われます。parent で遡れる最後のエントリとも換言できます。
さて、これまで特定のエントリ(DN)以下を扱うクラスを紹介してきました。これらのクラスは指定された DN 以下について、指定されたオブジェクトクラスの組みによって管理対象のエントリを検索したり、そのオブジェクトクラスの組みを持つエントリを作成していました。
では LDAPツリー 全体を管理対象としたい場合などはどうでしょう。これは少し特殊な設定のクラスを作れば対応できます。これを利用すれば全てのエントリを取得できますし、ツリー全体を辿る事も可能です。
class Entry < ActiveLdap::Base
ldap_mapping :prefix => "",
:classes => ["top"],
:scope => :sub
self.dn_attribute = nil
end
スキーマ情報には ActiveLdap::Base.schema を通してアクセス可能です。これはスキーマ定義情報を抽象化したオブジェクト(ActiveLdap::Schema のインスタンス)に対するアクセサです。
スキーマ定義情報が格納されているエントリをサブスキーマサブエントリと言います。先ずサブスキーマサブエントリについて説明した後に、情報の取得方法を説明していきます。
LDAP にはサブスキーマサブエントリというエントリがあり、ここにスキーマ定義情報が格納されています。即ち『スキーマ定義情報を参照する』とは、このサブスキーマサブエントリの情報を参照するということです。ActiveLdap::Schema がこれを担当するクラスです。ここでは、どのようにそれらが格納されているかを解説します。
このサブスキーマサブエントリについて言及されている RFC4512 4.2 の冒頭部分を紹介しましょう。
少々判りづらい表現ですが、私が確認した限りでは、このサブスキーマサブエントリにディレクトリツリーの全てのスキーマ定義が属性として記述されているようです4
このサブスキーマサブエントリを参照するため、各エントリは subschemaSubentry という属性を持っています5。この属性にはサブスキーマサブエントリの DN が格納され、DNが示す先のエントリに、各種スキーマ定義が記載されています。
前回の記事と同じ LDAP ツリーを利用し、o=rubyistMagazine,c=jp のエントリから subschemaSubentry 属性を見てみましょう。
いろいろと見慣れない属性が出てきました。ldapsearch コマンドの最後には、検索条件に合致したエントリのうち取得したい属性名を指定します。ここに “+” を指定すると LDAP 上の管理属性が表示されるようになります。subschemaSubentry 属性はこの管理属性にあたるため、普段は表示されていないのです。 “+” を指定した事で “subschemaSubentry” 属性が表示され、”cn=Subschema” という値を持っています。これがスキーマ情報を格納しているエントリの DN です。
では実際にサブスキーマサブエントリの中身を見てみましょう。
色々と出てきました(笑)。これがスキーマ定義です。スキーマ定義はオブジェクトクラスの定義、属性タイプの定義など各種あります。ここでは記事の範囲を逸脱するため、定義方法やその構文については詳しくは触れませんが、こういった情報に簡便にアクセスできるのが ActiveLdap::Schema です。
ActiveLdap::Schema がアクセスできるスキーマ定義情報は様々です。先ずは、基本的な使い方から解説していきましょう。
ActiveLdap::Schema のインスタンスがスキーマ定義情報へのアクセスを担当します。インスタンスを開発者が作成する必要はなく、ActiveLdap::Base.schema によってアクセスが可能です。
スキーマ定義にはオブジェクトクラス定義や属性タイプ定義など幾つかの種類があり、少なくともどの種類の定義情報が必要かを指定する必要があります。予め特定の種別の情報にアクセスするよう作られているメソッドもありますし、より柔軟に(そしてローレベルで)アクセスする方法もあります。
先ず、よく利用するオブジェクトクラス定義と属性タイプ定義へのアクセス方法を紹介します。その後でよりローレベルなアクセス方法について言及します。
オブジェクトクラスの定義情報は ActiveLdap::Schema::ObjectClass のインスタンスとして返されます。 object_class は引数に oid または オブジェクトクラスの名前を取り、該当する定義情報を抽象化したオブジェクトを返します。object_classes は定義されている全てのオブジェクトクラスのインスタンスを配列にして返します。
使い方はオブジェクトクラスの定義情報を得る場合とほぼ同じです。得られる定義情報は ActiveLdap::Schema::Attribute のインスタンスとして取得します。
ここで紹介するメソッドは、スキーマ種別の知識が多少なりとも必要になります。
各種の定義情報にアクセスする場合、先ずスキーマ定義の種類の名前を文字列で指定します。この名前の実体はサブスキーマサブエントリの属性名です。
これらは RFC4512 のサブスキーマサブエントリの記述(4.2)に記述されています。例えば、オブジェクトクラスであれば “objectClasses” という種別を指定して情報を引き出します。
他にも幾つかのメソッドがあります。ここでは名前の紹介のみに留めます。
ここまで、全体のスキーマ情報を参照する手段について解説してきましたが、実際にデータを保持する各エントリが、幾つかのオブジェクトクラスに属する場合も多々あるでしょう。その場合、エントリを保存するための制約は所属する各オブジェクトクラスが持つ制約の和になるため、総合的にどのような制約があるかなどが見えづらくなります。
これらの制約などは、エントリに対応するオブジェクトから直接引きだす事が可能です。
オブジェクトクラスのスキーマ定義上 MUST と指定された属性は、そのオブジェクトクラスに所属したエントリは必ず値を持たなければならない属性になります。
エントリは複数のオブジェクトクラスに所属し得るため、その場合には全てのオブジェクトクラスで MUST に指定されている属性について値を持たなければなりません。
これは valid? でも一応調べる事が可能ですが、全てのリストを綺麗に得るならば must メソッドを利用するとよいでしょう。
must メソッドはそのエントリが必須で値を持たなければならない属性を、ActiveLdap::Schema::Attribute のインスタンスのリストで返します。
オブジェクトクラスが保有する属性の定義では、MUST の他に MAY も指定できます。これは「値を持っても持たなくてもいい」属性です。これらは may メソッドで取得でき、値を設定しなくても構いません。
must でも may でも出てこない属性に値を設定しようとするとエラーになります。
非常にシンプルで、classes メソッドを呼べばオブジェクトクラスの名前のリストが出力されます。
二稿に渡り ActiveLdap の紹介をさせて頂きました。まだ紹介し切れていない機能もありますが、ひとまずここで筆を置かせていただきます。
記事執筆にあたり、編集のうえださん、開発者の須藤さんには大変お世話になりました。この場を借りてお礼させていただきます。有難うございました。
高瀬一彰。ActiveLdap のドキュメント係とかやってます。
http://d.hatena.ne.jp/tashen
というか私が混乱したんですが…… ↩
内容からお気づきの方もいらっしゃると思いますが、validations でも callbacks の仕組みが利用されています ↩
例では判り易さのために、各検索値などを直接記述していますが、実際のプログラムでは他のメソッドの戻り値などを利用するでしょう。その場合にこそ、この仕組みは威力を発揮すると思います ↩
RFC の書き方的にツリーの一部のスキーマ定義を格納するのか、各部を合計した全部のスキーマ定義を集約しているのかはちょっと判りません。一見した限りでは全部のスキーマ定義が入っているように見えます。予想にすぎませんが、ツリーの一部を別のサーバに委譲する場合などに全部のスキーマ情報を格納する訳ではないのかもしれません。 ↩
厳密に言えば、RFC はこの属性を持つ事を「強く推奨」しています ↩