ActiveLdap を使ってみよう(後編)

著者:高瀬一彰
編集:うえだ

はじめに

本記事は「ActiveLdap を使ってみよう(前編)」の後編です。ActiveLdap を扱い始めたばかりの方やこれから使ってみようとお考えの方は、先に前編の記事をご覧頂くと理解しやすいと思います。

公式サイト
RubyForge: Ruby/ActiveLdap: Project Info
ダウンロード
RubyForge: Ruby/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 を既に利用していて、更に知識を深めたい方
  • ActiveLdap と Ruby on Rails を連携させたい方
  • LDAP の構造を Ruby を用いて参照したい方

以下に該当する方は先に 前編 をご覧ください。

  • Ruby を用いて LDAP プログラミングをしようと考えている方
  • ActiveLdap に興味のある方、ActiveLdap をこれから利用したい方

検証環境

この記事で記載のコマンド例などは以下の環境で確認しています。皆さんの環境では適宜読み替えてください。

  • OS: CentOS 5
  • LDAP: openldap 2.3.43
  • ActiveLdap: gem 版 (version 1.2.1)

目次

バージョンアップで変更された点

前回執筆時の ActiveLdap はバージョン 1.1.0 でしたが、現在は 1.2.1 に変わっています。バグフィクスが主でしたが、マイナー番号が変わっていることから前バージョン(1.1.x)とは非互換の変更があった事を示しています。

バージョン 1.1.x までは各エントリの DN を取得したり、クラスの BASE DN を取得すると文字列が返されていました。

 irb> User.base
 => "ou=Users,o=rubyistMagazine,c=jp"
 irb> User.first.dn
 => "uid=ruby_jiro,ou=Users,o=rubyistMagazine,c=jp"

これがバージョン 1.2.1 では以下のように ActiveLdap::DistinguishedName オブジェクトを返すようになっています。

 irb> User.base
 => #<ActiveLdap::DistinguishedName:0xb78f0fa4 @rdns=[{"ou"=>"Users"}, {"o"=>"rubyistMagazine"}, {"c"=>"jp"}]>
 irb> User.first.dn
 => #<ActiveLdap::DistinguishedName:0xb7a8d04c @rdns=[{"uid"=>"ruby_jiro"}, {"ou"=>"Users"}, {"o"=>"rubyistMagazine"}, {"c"=>"jp"}]>

DistinguishedName オブジェクトを返すようになった事で DN パスの編集などが容易になりました。

以前のように文字列で取得したい場合は to_s メソッドをご利用ください。

 irb> User.first.dn.to_s
 => "uid=ruby_jiro,ou=Users,o=rubyistMagazine,c=jp"

より効率よく・堅牢に開発するための知識

前回解説した機能の知識を土台として、これを知れば更に便利であろうと思う機能を紹介していきます。

validation と callbacks の紹介
より堅牢な LDAP 操作が可能になります。名前から容易に想像できる方もいると思いますが、ActiveRecord の validation、callbacks と同一のものです。非常に強力な機能なので、改めて基本的な機能を紹介しておきます。
検索フィルタを動的に組み立てる方法
ActiveLdap::Base#find メソッドで LDAP エントリの検索を行う際、:filter オプションに LDAP フィルタを直接書いて検索条件を指定できますが、動的に検索条件を作成したい場合、文字列結合によって条件を組み立てるのはミスが発生し易い上に煩雑な作業です。文字列結合以外の方法によって検索条件を構成する方法を解説します。
関連性の詳細と操作方法
関連性について更に踏み込んだ説明を行います。関連性がどのように実装されているかを説明した上で、設定された関連性や関連先を操作する方法と注意点について説明します。

どれも ActiveRecord の機能を参考に構成されているので類似のインターフェースを利用できますが、検索フィルタの構成や関連性の操作に関しては LDAP の特性上、それなりに差異もあります。本記事が理解の一助になれば幸いです。

validation

validation は一般的に言えばビジネスロジックを記述するための機構です。この仕組みを利用すると、オブジェクトの状態が一定の条件を満たしていない場合、エラーとして保存しないようにすることができます。また、どの位置でエラーが発生しているかも簡単にチェックすることが可能です。例えばエントリが特定の属性を保持していなかったり、ある属性の値が想定した範囲外の値を持っていた場合、これをエラーにする事が可能です。

validation は save ないし save! メソッドの直前に実行され、保存するエントリが期待通りの状態であるかをチェックする機構です。チェックに弾かれた場合、save メソッドであれば保存に失敗して false が返り、save! の場合は例外 ActiveLdap::EntryInvalid が発生します。実際に弾かれる例を見てみましょう。

 irb> taro = User.new :uid => "ruby_taro",
 irb*               :uidNumber => 10000,
 irb*               :sn => "Ruby",
 irb*               :cn => "Ruby Taro",
 irb*               :gidNumber => 20000,
 irb*               :homeDirectory => "/home/ruby_taro"
 => #<User ...>

 # 状態が期待通りでなかったので保存に失敗
 irb> taro.save
 => false

 # 失敗した理由を確認する(uid が指定された正規表現にマッチしていない)
 irb> taro.errors.full_messages
 => ["uid must match with '/\\A[a-zA-Z0-9]+\\z/'."]

以降では、このような機構の実装方法と簡単な解説を行います。詳細については ActiveRecord::ValidationsActiveRecord::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 メソッドが呼ばれると、エラー情報が記録されていた場合は全てクリアされてから状態チェックが行われます。従って、一度エラーになっても以下のようにその原因を修正すれば保存が可能となります。

 irb> taro.uid = 'rubytaro'
 => "rubytaro"
 irb> taro.save
 => true

インスタンスメソッド 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? メソッドを利用すると状態のチェックのみを行うことができます。この場合、値が保存される事はありません。

 irb> taro.valid?
 => true

予め用意された状態チェックメソッド

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

 irb> taro = User.new :uid => "ruby_taro",
 irb*                :uidNumber => 10000,
 irb*                :sn => "Ruby",
 irb*                :cn => "Ruby Taro",
 irb*                :gidNumber => 20000,
 irb*                :homeDirectory => "/home/ruby_taro"
 irb> taro.valid?
 => false
 irb> taro.errors.full_messages
 => ["uid must match with '/\\A[a-zA-Z0-9]+\\z/'."]

callbacks

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

 irb> taro = User.new :uid => "ruby_taro",
 irb*                :uidNumber => 10000,
 irb*                :sn => "Ruby",
 irb*                :cn => "Ruby Taro",
 irb*                :gidNumber => 20000,
 irb*                :homeDirectory => "/home/ruby_taro"
 irb* taro.save
 before_save received
 after_save received
 => true

その他多数の before_、after_ といったコールバックが用意されています。詳しくは ActiveRecord::Callbacks の説明をご覧ください。

実際に調べる方のために本題から逸れた話を少しだけ。RoR で Callbacks といった場合は以下の 2 種類があります。

  1. ActiveSupport::Callbacks
  2. ActiveRecord::Callbacks

混乱の種にもなるので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

関連性の詳細と操作方法

前回は関連性の定義方法と関連先へのアクセス方法を紹介しましたが、関連性には他にも便利な機能があります。特定のオブジェクトを関連先から外す、追加する、といった処理を簡便に行うことができます。

利用例

 irb> group.users.loaded?
 => true
 irb> group.users << user
 => #<ActiveLdap...>

これらの機能を正しく利用するためには、関連性の実装面についてもう少し解説する必要があると考えます。従って、先ず関連性がどのように実装されているかを解説した後、実際に利用できる関連先操作メソッドを紹介します。

関連性の実装

さて、先ずは前回のおさらいの意味も含め、has_many を用いた関連性を見てみましょう。

クラスと関連性(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

関連先へのアクセス例
 irb> group = Group.find(:first)
 => #<Group ... >
 irb> group.users[0]
 => #<User ...>
 irb> group.users[0].cn
 => "Ruby Taro"

上記の例では、関連性メソッド 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

また関連性そのものの情報を得ることなどもでき、関連性オブジェクトを通じて、既に関連先のオブジェクトがロードされたか? 関連先は存在するか? などを問い合わせることが可能です。

 # 関連先が一つ以上存在するか確認する
 irb> group.users.exists?
 => true

まとめます。has_many などによって定義された関連性メソッドは、関連性オブジェクトを返します。関連性オブジェクトには、大別して二種類のメソッドがあります。

  • 関連先の情報を取得するメソッド
  • 関連先を操作するメソッド

以降、利用できるメソッド群のうち、特に有用と思われるものについて説明します。

関連先の情報を取得する

# 関連先オブジェクト(群)を取得
group.users.target

# 関連先オブジェクト(群)が存在するかを確認する
group.users.exists?  #=> true

target
ロード済みの関連先オブジェクト(群)を返します。ロードされていない場合はロードを試みてから返します。
loaded?
関連先オブジェクト(群)が、既にロードされているかを真偽値で返します。なお関連先オブジェクトがロードされるタイミングは、関連先オブジェクトにアクセスしようとした最初のタイミングか exists? をコールした時です。そのためオブジェクトを find などによって取得した直後に loaded? をコールしても偽が返ることでしょう。
exists?
関連先が存在すれば真を、存在しなければ偽を返します。loaded? と違い、このメソッドをコールすると実際に関連先オブジェクトを収集した上で存在確認を行います。

関連先を操作する

# 関連先オブジェクト(群)を再ロードする
group.users.reload

# 関連先を追加する
group.users << user

reload
関連先オブジェクト(群)を再ロードします。関連先オブジェクトの最新の状態を得たい場合に利用します。
<<(*entries)
has_many、belongs_to(:many)、children で利用可能です。関連先オブジェクトを追加するのに利用します。別名のメソッドとして push、concat が定義されています。
delete(*entries)
has_many、belongs_to(:many)、children で利用可能です。渡したオブジェクト(群)から、関連性を取り除きます。

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 追加の場合は子が保存される。削除の場合、削除対象の子エントリがエントリごと削除される

Ruby on Rails との統合

ActiveLdap は RoR と統合し易いよう構成されています。説明することはそう多くありませんが、RoR で利用する場合の手順例と注意点をそれぞれ説明します。

設定方法

ActiveRecord では config/database.yml を利用して設定を行いますが、ActiveLdap でも同じように config/ldap.yml というファイルを利用します。記載の仕方もほぼ同様です。実際の作成例を示します。

ActiveLdap をインストールしていれば、ActiveLdap 用のジェネレータが利用可能になっています。以下のコマンドで設定ファイル ldap.yml のテンプレートを作成できます。

 $ script/generate scaffold_active_ldap
       create  config/ldap.yml
 $ cat config/ldap.yml
   development:
     host: 127.0.0.1
     base: dc=devel,dc=local,dc=net
     bind_dn: cn=admin,dc=local,dc=net
     password: secret

   test:
     host: 127.0.0.1
     base: dc=test,dc=local,dc=net
     bind_dn: cn=admin,dc=local,dc=net
     password: secret

   production:
     host: 127.0.0.1
     method: :tls
     base: dc=production,dc=local,dc=net
     bind_dn: cn=admin,dc=local,dc=net
     password: secret

ファイル名の通り YAML で設定が記述されています。RoR に合わせて test、development、production の各動作モード用の設定をそれぞれ定義でき、モードに合わせて自動的に設定が適用されます。それぞれの内容は ActiveLdap::Base.setup_connection に渡す内容です。

モデルジェネレータ

ActiveLdap 用のモデルジェネレータも用意されています。

 $ script/generate model_active_ldap user
       exists  app/models/
       exists  test/unit/
       create  app/models/user.rb
       create  test/unit/user_test.rb
 $ cat app/models/user.rb
 class User < ActiveLdap::Base
   ldap_mapping :dn_attribute => "cn",
                :prefix => "ou=users"
 end

利用するユーザ毎にコネクションを貼る場合の注意点

アプリケーションの作り方によりますが、例えばユーザにログインさせる Web アプリケーションの場合、ログインしたユーザ毎に bind させるようなロジックでは注意が必要です。

ActiveLdap のコネクションを特定のユーザで bind させると、mongrel なり Passenger なりが保持している LDAP 接続がそのユーザで bind された状態になります。直後に他の人がアクセスした場合にも、先に bind されたユーザと関連付けられた状態でアプリケーションが動作します。更に mongrel あるいは Passenger などで複数のサーバインスタンスを動作させた状態では、インスタンス間で LDAP 接続の共有はできないため、ログインした直後にログアウトしてしまったり、いつの間にか別のユーザになってしまったり、といった問題が生じる可能性があります。

Web アプリケーションが共通して利用するアカウントで bind するか、別の対策をご検討ください。

LDAP の管理的操作を可能にしていくために

これまで LDAPエントリ群の基本的操作を解説してきました。このセクションでは、より応用的・メタ操作的な話題を取り上げていきます。高度な LDAP 管理アプリケーションを作成する場合などに役立つことでしょう。

LDAP のスキーマ情報を ActiveLdap から参照する
LDAP を操作していると LDAP サーバの DIT や スキーマ情報を利用したプログラミングが必要になって来るかもしれません。ActiveLdap::Schema はこれに対応します。
ツリー構造に対する操作
LDAPはツリー構造ですから、ツリー構造を辿ったり操作したりする API が最初から含まれています。これについて解説します。
全てのエントリを参照するクラス
ActiveLdap は特定のツリー以下を特定のクラスに結びつけると解説しました。そうではなく、全てのエントリ扱えるクラスの作り方を解説します。

ツリー構造に対する操作

エントリに対応する各インスタンスには、ツリー構造を辿ったり、子を親に紐づけたりする API が備わっています。これらを利用すれば LDAP のツリー構造を意識したプログラミングが可能になります。

ツリー操作のための各インスタンスメソッドを以下で解説します。

ツリーを構成する

ツリーを構成するためのメソッドは parent= です。特定のエントリを示すオブジェクトの parent= メソッドで親になるオブジェクトを指定します。

 # 親になる devel グループを取得
 irb> devel = Group.find 'devel'
 => #<Group objectClass:<posixGroup>.. >

 # web グループを作成
 irb> web = Group.new :cn => 'web', :gidNumber => 10001
 => #<Group objectClass:<posixGroup>.. >

 # web グループの親に devel を設定
 irb> web.parent = devel

 # 保存
 irb> web.save
 => true

保存した時点で、指定したエントリを親に持つエントリが作成されます。

ツリーを辿る

ツリー構造を辿ってエントリを取得するためのメソッドを紹介します。これには少々制限があり、ActiveLdap::Base を継承したクラスが担当するツリー以下のみを辿る事ができます。幾つかの制限もありますが、各メソッドに記載していきます。

各クラスが担当する範囲を超えてツリーを辿る事も可能です。これについては「全てのエントリを扱うクラス」で紹介します。用途に応じて使い分けてください。

以降の説明では、以下のようなツリーを利用して説明を行います

ツリーのサンプル:

 c=jp
    |
    +--- o=rubyistMagazine
            |
            +--- ou=Groups   # => Groups クラスが担当
                     |
                     +--- cn=devel
                     |        |
                     |        +--- cn=web
                     |        |        |
                     |        |        +--- cn=design
                     |        |
                     |        +--- cn=sever
                     |
                     +--- cn=manage

コード例:

 # 親を取得する
 irb> web.parent.cn
 => "devel"

 # 子を検索する
 irb> devel.children[0].cn
 => "web"

 # 兄弟のエントリを羅列する
 irb> devel.children.map &:cn
 => ["web", "server"]

parent

親のエントリを直接取得します。親のエントリと対応づいた ActiveLdap オブジェクトを返します。

parent で取得できるエントリの上限は、オブジェクトのクラスが担当する DN の直下のエントリです。上記のツリーのサンプルでいえば parent は devel や manage までのオブジェクトを返します。そこから更に遡ろうとして parent を呼ぶと nil を返します。

使用例:

 irb> web = Group.find 'web'
 => #<Group ...>
 irb> web.parent.cn
 => "devel"
 irb> web.parent.parent
 => nil

siblings

兄弟のエントリ群を配列で返します。

 irb> web.siblings.map &:cn
 => ["server"]

self_and_siblings

(自身を含めた)兄弟のエントリ群を配列で返します。

 irb> web.self_and_siblings.map &:cn
 => ["web", "server"]

children

直接の子に当たるエントリ群にアクセスします。孫のエントリは返しません。

 irb> devel = Group.find 'devel'
 => #<Group ...>
 irb> devel.children
 => #<ActiveLdap::Association::Children:0xb7a496bc ...>
 irb> devel.children.map &:cn
 => ["web", "server"]

実際のところ、これは関係性オブジェクトを返すメソッドです。従って << などのメソッドを利用できます。

 irb> network = Group.new :cn => 'network', :gidNumber => 10005
 => #<Group ...>
 irb> devel.children << network
 => #<ActiveLdap::Association::Children ... >
 irb> devel.children.map &:cn
 => ["web", "server", "network"]

root

レシーバの「根」にあたるエントリのオブジェクトを返します。ここで言う根とは LDAP ツリーの根のことではありません。

レシーバのクラスが担当する DN 直下のエントリが根として扱われます。parent で遡れる最後のエントリとも換言できます。

 irb> design = Group.find 'design'
 => #<Group ...>
 irb> design.root.cn
 => "devel"

 # parent で最後まで辿れるエントリと同じ
 irb> design.parent.parent.cn
 => "devel"

全てのエントリを扱うクラス

さて、これまで特定のエントリ(DN)以下を扱うクラスを紹介してきました。これらのクラスは指定された DN 以下について、指定されたオブジェクトクラスの組みによって管理対象のエントリを検索したり、そのオブジェクトクラスの組みを持つエントリを作成していました。

では LDAPツリー 全体を管理対象としたい場合などはどうでしょう。これは少し特殊な設定のクラスを作れば対応できます。これを利用すれば全てのエントリを取得できますし、ツリー全体を辿る事も可能です。

class Entry < ActiveLdap::Base
  ldap_mapping :prefix => "",
               :classes => ["top"],
               :scope => :sub
  self.dn_attribute = nil
end

 # 全てのエントリを取得して DN を表示する
 irb> pp Entry.find(:all).map{|e| e.dn.to_s}
 ["o=rubyistMagazine,c=jp",
  "ou=Users,o=rubyistMagazine,c=jp",
  "ou=Groups,o=rubyistMagazine,c=jp",
  "cn=devel,ou=Groups,o=rubyistMagazine,c=jp",
  "cn=web,cn=devel,ou=Groups,o=rubyistMagazine,c=jp",
  "cn=design,cn=web,cn=devel,ou=Groups,o=rubyistMagazine,c=jp",
  "cn=server,cn=devel,ou=Groups,o=rubyistMagazine,c=jp",
  "cn=manage,ou=Groups,o=rubyistMagazine,c=jp",
  "cn=network,cn=devel,ou=Groups,o=rubyistMagazine,c=jp"]
 => nil

 # LDAP ツリーの根の直下のエントリ群を選択し、DN を表示する
 irb> pp Entry.first.children.map{|e| e.dn.to_s}
 ["ou=Users,o=rubyistMagazine,c=jp", "ou=Groups,o=rubyistMagazine,c=jp"]
 => nil

LDAP のスキーマ情報を ActiveLdap から参照する

スキーマ情報には ActiveLdap::Base.schema を通してアクセス可能です。これはスキーマ定義情報を抽象化したオブジェクト(ActiveLdap::Schema のインスタンス)に対するアクセサです。

スキーマ定義情報が格納されているエントリをサブスキーマサブエントリと言います。先ずサブスキーマサブエントリについて説明した後に、情報の取得方法を説明していきます。

サブスキーマサブエントリ

LDAP にはサブスキーマサブエントリというエントリがあり、ここにスキーマ定義情報が格納されています。即ち『スキーマ定義情報を参照する』とは、このサブスキーマサブエントリの情報を参照するということです。ActiveLdap::Schema がこれを担当するクラスです。ここでは、どのようにそれらが格納されているかを解説します。

このサブスキーマサブエントリについて言及されている RFC4512 4.2 の冒頭部分を紹介しましょう。

 サブスキーマ(サブ)エントリはディレクトリスキーマの管理管理情報のために使われます。
 単一のサブスキーマ(サブ)エントリは、ディレクトリツリーの特定の部分に存在するエン
 トリ群で利用される全てのスキーマ定義を格納しています。

少々判りづらい表現ですが、私が確認した限りでは、このサブスキーマサブエントリにディレクトリツリーの全てのスキーマ定義が属性として記述されているようです4

このサブスキーマサブエントリを参照するため、各エントリは subschemaSubentry という属性を持っています5。この属性にはサブスキーマサブエントリの DN が格納され、DNが示す先のエントリに、各種スキーマ定義が記載されています。

前回の記事と同じ LDAP ツリーを利用し、o=rubyistMagazine,c=jp のエントリから subschemaSubentry 属性を見てみましょう。

 $ ldapsearch -x -b o=rubyistMagazine,c=jp -s base -LLL +
 dn: o=rubyistMagazine,c=jp
 structuralObjectClass: organization
 entryUUID: a3f93e6a-2386-102e-9707-09f5f7ff8de6
 creatorsName: cn=Manager,o=rubyistMagazine,c=jp
 createTimestamp: 20090822164318Z
 entryCSN: 20090822164318Z#000000#00#000000
 modifiersName: cn=Manager,o=rubyistMagazine,c=jp
 modifyTimestamp: 20090822164318Z
 entryDN: o=rubyistMagazine,c=jp
 subschemaSubentry: cn=Subschema
 hasSubordinates: TRUE

いろいろと見慣れない属性が出てきました。ldapsearch コマンドの最後には、検索条件に合致したエントリのうち取得したい属性名を指定します。ここに “+” を指定すると LDAP 上の管理属性が表示されるようになります。subschemaSubentry 属性はこの管理属性にあたるため、普段は表示されていないのです。 “+” を指定した事で “subschemaSubentry” 属性が表示され、”cn=Subschema” という値を持っています。これがスキーマ情報を格納しているエントリの DN です。

では実際にサブスキーマサブエントリの中身を見てみましょう。

 $ ldapsearch -x -b cn=Subschema -s base -LLL +
 dn: cn=Subschema
 structuralObjectClass: subentry
 createTimestamp: 20100130124925Z
 modifyTimestamp: 20100130124925Z
 ldapSyntaxes: ( 1.3.6.1.1.16.1 DESC 'UUID' )
 ldapSyntaxes: ( 1.3.6.1.1.1.0.1 DESC 'RFC2307 Boot Parameter' )
 ldapSyntaxes: ( 1.3.6.1.1.1.0.0 DESC 'RFC2307 NIS Netgroup Triple' )
 ldapSyntaxes: ( 1.3.6.1.4.1.1466.115.121.1.52 DESC 'Telex Number' )
 ldapSyntaxes: ( 1.3.6.1.4.1.1466.115.121.1.50 DESC 'Telephone Number' )
 ldapSyntaxes: ( 1.3.6.1.4.1.1466.115.121.1.49 DESC 'Supported Algorithm' X-BIN
 (snip)

色々と出てきました(笑)。これがスキーマ定義です。スキーマ定義はオブジェクトクラスの定義、属性タイプの定義など各種あります。ここでは記事の範囲を逸脱するため、定義方法やその構文については詳しくは触れませんが、こういった情報に簡便にアクセスできるのが ActiveLdap::Schema です。

スキーマ情報にアクセスする ActiveLdap::Schema

ActiveLdap::Schema がアクセスできるスキーマ定義情報は様々です。先ずは、基本的な使い方から解説していきましょう。

ActiveLdap::Schema のインスタンスがスキーマ定義情報へのアクセスを担当します。インスタンスを開発者が作成する必要はなく、ActiveLdap::Base.schema によってアクセスが可能です。

 # posixAccount オブジェクトクラスの description を得る
 irb> ActiveLdap::Base.schema.object_class('posixAccount').description
 => "Abstraction of an account with POSIX attributes"

 # 全ての属性タイプの名前を得る
 irb> ActiveLdap::Base.schema.attributes.map {|a| a.name}
 => ["ipServiceProtocol", "shadowWarning", "preferredLanguage", "aliasedObjectName", ...]

 # 全ての属性タイプの名前を得るローレベルアクセス
 irb> ActiveLdap::Base.schema.names "attributeTypes"
 => ["ipserviceprotocol", "shadowwarning", "preferredlanguage", "aliasedobjectname", ...]

スキーマ定義にはオブジェクトクラス定義や属性タイプ定義など幾つかの種類があり、少なくともどの種類の定義情報が必要かを指定する必要があります。予め特定の種別の情報にアクセスするよう作られているメソッドもありますし、より柔軟に(そしてローレベルで)アクセスする方法もあります。

先ず、よく利用するオブジェクトクラス定義と属性タイプ定義へのアクセス方法を紹介します。その後でよりローレベルなアクセス方法について言及します。

オブジェクトクラスの定義情報を得る
 # posixAccount オブジェクトクラスの description を得る
 irb> ActiveLdap::Base.schema.object_class('posixAccount').description
 => "Abstraction of an account with POSIX attributes"

 # oid からオブジェクトクラスの情報を得る
 irb> ActiveLdap::Base.schema.object_class("1.3.6.1.1.1.2.0").must.map &:name
 => ["cn", "uid", "uidNumber", "gidNumber", "homeDirectory", "objectClass"]

 # オブジェクトクラスの定義情報を全て得る
 # (凄く長い出力になるので注意)
 irb> ActiveLdap::Base.schema.object_classes
 => [#<ActiveLdap::Schema::ObjectClass:0xb79f23a8 >, ...]

オブジェクトクラスの定義情報は ActiveLdap::Schema::ObjectClass のインスタンスとして返されます。 object_class は引数に oid または オブジェクトクラスの名前を取り、該当する定義情報を抽象化したオブジェクトを返します。object_classes は定義されている全てのオブジェクトクラスのインスタンスを配列にして返します。

属性タイプの定義情報を得る
 # 属性タイプ cn の 別名を得る
 irb> ActiveLdap::Base.schema.attribute("cn").aliases
 => ["commonName"]

 # 属性タイプ定義情報のオブジェクトを全て得る
 # (凄く長い出力になるので注意)
 irb> ActiveLdap::Base.schema.attributes
 => [#<ActiveLdap::Schema::Attribute ...>, ...]

使い方はオブジェクトクラスの定義情報を得る場合とほぼ同じです。得られる定義情報は ActiveLdap::Schema::Attribute のインスタンスとして取得します。

よりローレベルでのアクセス方法

ここで紹介するメソッドは、スキーマ種別の知識が多少なりとも必要になります。

各種の定義情報にアクセスする場合、先ずスキーマ定義の種類の名前を文字列で指定します。この名前の実体はサブスキーマサブエントリの属性名です。

これらは RFC4512 のサブスキーマサブエントリの記述(4.2)に記述されています。例えば、オブジェクトクラスであれば “objectClasses” という種別を指定して情報を引き出します。

 # 指定した定義情報の名前を全て取得する
 irb> ActiveLdap::Base.schema.names "objectClasses"
 => ["locality", "pilotdsa", "residentialperson", "applicationprocess", "dcobject", ...]

 # 指定した名前を持つ定義情報が存在するか確認する
 irb> ActiveLdap::Base.schema.exist_name? "objectClasses", "posixAccount"
 => true

 # 指定した定義情報をハッシュで取得する
 irb> ActiveLdap::Base.schema.entry "attributeTypes", "cn"
 => {"NAME"=>["cn", "commonName"], "SUP"=>["name"], "DESC"=>["RFC2256: common name(s) for which the entity is known by"]}

 # スキーマ定義情報の内、特定のフィールドの値だけを取得する
 irb> ActiveLdap::Base.schema.fetch "attributeTypes", "cn", "DESC"
 => ["RFC2256: common name(s) for which the entity is known by"]

 # オブジェクトクラス定義の内、MUSTフィールドの値だけを取得する
 irb> ActiveLdap::Base.schema.object_class_attribute "posixAccount", "MUST"
 => ["cn", "uid", "uidNumber", "gidNumber", "homeDirectory"]
ids
指定した種類のスキーマ定義情報から、全ての oid を返します。
names
指定した種類のスキーマ定義情報から、全ての名前を返します。
exist_name?
指定した種類のスキーマ定義情報の中に、特定の名前を持つスキーマ定義があるかを真偽で返します。
resolve_name
スキーマ定義の種別と名前を渡し、対応する oid を返します。
fetch
スキーマ定義種別、名前かoid、取得したいフィールドをそれぞれ指定します。指定したスキーマ定義尾特定のフィールドの値を返します。
entry
スキーマ定義種別と oid または名前を指定して、スキーマ定義情報をハッシュにして返します。
object_class_attribute
オブジェクトクラスの名前と、定義情報のフィールド名を指定して特定のフィールドの定義だけを取得します。
attribute_type
属性タイプの名前と、定義情報のフィールド名を指定して特定のフィールドの定義だけを取得します。
その他のメソッド

他にも幾つかのメソッドがあります。ここでは名前の紹介のみに留めます。

  • dit_content_rule_attribute
  • ldap_syntax
  • ldap_syntaxes
  • ldap_syntax_attribute

エントリから構造上の情報を参照する

ここまで、全体のスキーマ情報を参照する手段について解説してきましたが、実際にデータを保持する各エントリが、幾つかのオブジェクトクラスに属する場合も多々あるでしょう。その場合、エントリを保存するための制約は所属する各オブジェクトクラスが持つ制約の和になるため、総合的にどのような制約があるかなどが見えづらくなります。

これらの制約などは、エントリに対応するオブジェクトから直接引きだす事が可能です。

必須の属性を調べる
 irb> g = Group.find :first
 => #<Group objectClass:<posixGroup>, ...>
 irb> g.must.map &:name
 => ["cn", "gidNumber", "objectClass"]

オブジェクトクラスのスキーマ定義上 MUST と指定された属性は、そのオブジェクトクラスに所属したエントリは必ず値を持たなければならない属性になります。

エントリは複数のオブジェクトクラスに所属し得るため、その場合には全てのオブジェクトクラスで MUST に指定されている属性について値を持たなければなりません。

これは valid? でも一応調べる事が可能ですが、全てのリストを綺麗に得るならば must メソッドを利用するとよいでしょう。

must メソッドはそのエントリが必須で値を持たなければならない属性を、ActiveLdap::Schema::Attriubte のインスタンスのリストで返します。

必須の属性以外で、設定可能な属性が何かを調べる
 irb> g.may.map &:name
 => ["userPassword", "memberUid", "description"]

オブジェクトクラスが保有する属性の定義では、MUST の他に MAY も指定できます。これは「値を持っても持たなくてもいい」属性です。これらは may メソッドで取得でき、値を設定しなくても構いません。

must でも may でも出てこない属性に値を設定しようとするとエラーになります。

所属するオブジェクトクラスを調べる
 irb> g.classes
 => ["posixGroup"]

非常にシンプルで、classes メソッドを呼べばオブジェクトクラスの名前のリストが出力されます。

おわりに

二稿に渡り ActiveLdap の紹介をさせて頂きました。まだ紹介し切れていない機能もありますが、ひとまずここで筆を置かせていただきます。

記事執筆にあたり、編集のうえださん、開発者の須藤さんには大変お世話になりました。この場を借りてお礼させていただきます。有難うございました。

著者について

高瀬一彰。ActiveLdap のドキュメント係とかやってます。
http://d.hatena.ne.jp/tashen

バックナンバー



  1. というか私が混乱したんですが…… 

  2. 内容からお気づきの方もいらっしゃると思いますが、validations でも callbacks の仕組みが利用されています 

  3. 例では判り易さのために、各検索値などを直接記述していますが、実際のプログラムでは他のメソッドの戻り値などを利用するでしょう。その場合にこそ、この仕組みは威力を発揮すると思います 

  4. RFC の書き方的にツリーの一部のスキーマ定義を格納するのか、各部を合計した全部のスキーマ定義を集約しているのかはちょっと判りません。一見した限りでは全部のスキーマ定義が入っているように見えます。予想にすぎませんが、ツリーの一部を別のサーバに委譲する場合などに全部のスキーマ情報を格納する訳ではないのかもしれません。 

  5. 厳密に言えば、RFC はこの属性を持つ事を「強く推奨」しています