ファイル: チュートリアル — ActiveLdap - ActiveLdap

チュートリアル

はじめに

ActiveLdap は LDAP を操作する今までにない方法です。大半のLDAP操作は難しいLDIFを扱うか、ウェブインターフェースを利用するか、説明の省かれたリファレンスマニュアルを参照しながら、難解なAPIを利用しなければなりませんでした。 ActiveLdap はこの解決を目指します。ActiveRecord によって着想した ActiveLdap は、LDAP エントリに対するオブジェクト指向のインターフェースを提供します。

対象読者はシステム管理者やLDAPユーザであって、手軽かつきれいなLDAPアクセスの方法を求めるすべての人です。

LDAPとは何か

LDAP は”軽量ディレクトリアクセスプロトコル”のことです。これは基本的にLDAPサーバと対話するためのプロトコルを意味します。LDAPサーバは軽量のディレクトリサーバです。LDAPサーバはシンプルな電話帳からコンピュータ上のユーザアカウントのリストまで、幅広い情報を格納することができますが、大抵の場合後者のために使われます。 このテキストでの各例は、読者が LDAP サーバを Unix 系システムのための中央認証・認可サーバとして利用することを熟知しているものと仮定して記載しています。(残念なことに、私はまだMicrsoft ActiveDirectoryに対して ActiveLdap を試していません。名前に "Directory" が含まれていはいるのですが。)

より理解するために:

では何故 ActiveLdap なのか?

LDAPを直接扱うさいには(たとえ素晴らしい Ruby/LDAP ライブラリを用いても)、既存の LDAP API に拘束されることになります。このAPIは巨大な配列を生成し、コードを読みづらく、また楽しくないものにしてしまいます。もしあなたがあなたのコードにLDAPを統合するためのきれいな方法を欲するなら、ActiveLdap を使う理由になるでしょう。

利用準備

動作条件

  • Ruby実装: Ruby 1.8.x または 1.9.1 または JRuby
  • 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> require 'active_ldap'
=> true
irb> exit

もし 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 setup_connection

他にも :scope, :classes, :dn_attribute といったオプションの引数があります。:scope は ou=Groups 以下の検索について、より深い階層までを検索するかどうかを指示します(cn=develop,ou=DevGroups,ou=Groups,dc=dataspill,dc=org のような LDAP オブジェクトのこと)。

: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 => 'ou=People', :classes => ['top','account']
irb*   belongs_to :groups, :class_name => 'Group', :many => 'memberUid', :foreign_key => 'uid'
irb* end

これで User クラスは 'groups' メソッドを利用できるようになります。このメソッドはユーザが所属するすべての Group オブジェクトを取得することができます。

irb> me = User.find('drewry')
irb> me.groups
=>  #<ActiveLdap::Association::BelongsToMany...>    # Enumerable object
irb> me.groups.each { |group| p group.cn };nil
"cdrom"
"audio"
"develop"
=> nil
(Note: nil is just there to make the output cleaner...)

豆知識: 上記では 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',
end

belongs_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
=> #<ActiveLdap::Association::HasManyWrap:..> # Enumerable object
irb> develop.members.map{|member| member.id}
=> ["drewry", "builder"]

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オブジェクトを検索可能です。

irb> Group.find(:first, 'deve*").cn
=> "develop"

文字列を与えると、dn_attribute をキーにして最初にマッチするオブジェクトを返します。

irb> Group.find(:all).collect {|group| group.cn}
=> ["root", "daemon", "bin", "sys", "adm", "tty", ..., "develop"]

最初のキーに :all を与えると全てのオブジェクトを返します。

irb> Group.find(:all, :attribute => 'gidNumber', :value => '1003').collect {|group| group.cn}
=> ["develop"]

:attribute と :value によって特定のキーについて検索することも可能です。

:attribute が指定されない場合、:dn_attribute が利用されます。

:filter によってLDAPフィルタを直接指定することも可能です。

.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")
=> false

ActiveLdap::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 にメッセージを吐かないよう指示します
  • :sasl_options はHashで指定します。指定したオプションはそのままLDAPライブラリに渡されます。今のところ、このオプションはRuby/LDAPでしかサポートされていません。現在サポートされているオプションのキーは:realmと:authcidと:authzidです。
  • :retry_limit は接続失敗時に、何回再接続を試みるかを数値で指定します。-1 を指定すると無限に再接続を試みます
  • :retry_wait は再接続するまえに何秒待つかを指定します
  • :scope はLDAPオブジェクトの検索方式を指定します。デフォルトは :one です
  • :timeout は検索のタイムアウト秒数を指定します。デフォルトでは無効になっています。search() リクエストもインターラプトされるため、注意してください
  • :retry_on_timeout タイムアウトが発生した場合に再接続を行うかどうかを指定します。デフォルトは true です

Base.setup_connection は接続の設定のみを行います。実際にLDAPサーバに接続しバインドするプロセスは必要になったときに一度に実施されます。だいたい、以下のようなアプローチを取ります

  • host:port にたいして、:method での接続を行います
  • もし bind_dn と password_block か password が与えられていれば、ユーザ認証を行いバインドしようとします
  • もし認証が失敗するか、パスワードが与えられていない場合、匿名アクセスが許可されていれば、匿名バインドを行います
  • 匿名バインドも失敗すれば、エラー終了します

接続の際、渡された設定オプションはクラス変数の中に格納されます。このさい、指定されていないオプションはデフォルト値が適用されます。

connection

Base.connection は ActiveLdasp::Connection オブジェクトを返します

例外クラス

ActiveLdap は幾つかのカスタマイズした例外クラスを扱います。以下にそれを示します。

DeleteError

LDAPオブジェクトの削除に失敗した際、この例外が発生します。エラー発生時の LDAP エラーメッセージが含まれます。

SaveError

LDAP オブジェクトの追加または更新に問題があった場合に発生する例外です。LDAPサーバのログや Wireshark などによる通信解析によってより詳しい情報が得られるかもしれません。

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 :groups, :class_name => 'Group', :many => 'memberUid'
end

同様に、ldapadmin/objects/group.rb を作成します。

class Group < ActiveLdap::Base
  ldap_mapping :classes => ['top', 'posixGroup'], :prefix => 'ou=Groups'
  has_many :members, :class_name => "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. += " 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
end

LDAPエントリの管理

次に、ユーザの属性変更スクリプト 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. += " 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
end

LDAPエントリの削除

最後に、ユーザ削除のためのスクリプト 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. += " 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')
=> ...
# 新規ユーザの作成
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' => '日本語'}}]

ネストされたサブタイプは 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=Groups'
    has_many :members, :class_name => 'MyLDAP::User', :wrap => 'memberUid'
    has_many :primary_members, :class_name => 'MyLDAP::User', :foreign_key => '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 メソッドにより取得できます。

残りの便利な機能は、格納されたデータをいろんな名前でアクセスできることです。LDAP属性はcn / commonName のように複数の名前を持つことができるるので、どの名前でも同じ値にアクセスすることになります。値を取得するときは self['どれかの属性名'] とすることをオススメします。もし、これだけでは十分ではない場合は、本当の属性名を得るためにto_real_attribute_name プライベートメソッドを利用することができます。

irb> User.find(:first).instance_eval do
irb>   to_real_attribute_name('commonName')
irb> end
=> 'cn'

このメソッドは背後(@data)で属性データが格納されている名前をあなたに教えます。再び、self[属性名] はほとんどの拡張に対して十分でなくてはなりませんが、そうでなくても、たぶんここでは問題にならないでしょう。

また例えば、ユーザクラスの属性名のエイリアスを見つけるためには以下のようにするとよいでしょう。

irb> User.schema.attribute_type 'cn', 'NAME'
=> ["cn", "commonName"]

これはLDAP サーバのスキーマから自動的に発見します。

制限

実行速度

今のところ、ActiveLdap はまだ高速にできます。いくつかの再帰的な型チェックをオブジェクト作成時に行っており、それらがオブジェクトの生成速度を落としています。また、他のところでも多数の最適化が可能であることも認識しています。最適化できていない部分については私が最適化できるようになるまで我慢してもらうか、または気軽にパッチを送ってください。

フィードバック

どんなものでも、またすべてのフィードバックやパッチを歓迎します。私はこのパッケージについて興奮しています。また、私以上に、人々の助けになることを見たいと思っています。