LDAPサーバーが稼動している状態で、Fedora10のOSのアカウントやグループをPAMで設定する例をあげます。
参考:Fedora Directory Server - PAM Configuration for LDAP Client Systems
LDAPサーバーには、Posixアカウントが設定されていることを前提とします。
また、LDAPサーバーはSolaris10 で稼動している、Sun Java System Directory Server 6を使用しています。サーバーはOpenLDAPなどのLDAPであれば基本的な動作は変わらないはずです。クライアントとなるOSがFedora10です。
エントリの例)
dn: uid=yyamazaki,ou=People,dc=example,dc=com ... objectclass: posixaccount ... cn: Yuki Yamazaki uid: yyamazaki uidnumber: 3000 gidnumber: 300 homedirectory: /home/yyamazaki userpassword:: e1NTSEF9QVF... loginshell: /bin/sh gecos: Yuki Yamazaki LDAP Account ... dn: cn=kdc,ou=Groups,dc=example,dc=com ... objectClass: groupofuniquenames objectClass: posixGroup ... uniquemember: uid=yyamazaki,ou=People,dc=example,dc=com uniquemember: uid=kwada,ou=People,dc=example,dc=com uniquemember: uid=ichiki,ou=People,dc=example,dc=com ...これを、Linuxの認証に使用するには、次の3つのファイルが関連します。 PAM : /etc/pam.d/system-auth Naming Service: /etc/nsswitch.conf LDAP Setting: /etc/ldap.conf
JavaSE環境で、LDAPサーバー(OpenDS, Sun Java System Directory Server等)にアクセスする場合、接続情報をhashtableにセットして、それをInitialDirContextに渡してコンテキストを生成しました。
Book: Javaを使ったLDAPアクセス -JNDI, Search-: http://www.knowd.co.jp/yamazaki/?q=node/139
JavaEE環境では、コード内に接続情報を埋め込むのは流儀に反するので、これをリソース情報としてコンテナに定義できないかどうかを検討しました。
GlassFishでは、JDBC, JMS, JavaMail等のリソースに関してはGUI管理ツールなどでしっかりとサポートされており、あまり迷うことなく定義できます。LDAPのようなその他のリソースはどのように設定するのか? という疑問がこの検証の発端です。
JavaSE環境で、LDAPに接続するためのJNDI設定は以下のようにします。
...
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:389");
env.put(Context.SECURITY_PRINCIPAL, "cn=Directory Manager");
env.put(Context.SECURITY_CREDENTIALS, "password");
DirContext context = null;
context = new InitialDirContext(env);
NamingEnumeration entries = context.search("dc=example,dc=com", "uid=test*", searchControls);
...
こういった環境依存の設定をコードに埋め込むのはJavaEEでは好ましくありませんので、接続設定をコンテナに登録してそれをJNDIで取得する方法をとりたいのです。
JDBCでは、(DataSource)context.lookup( "jdbc/sample" ) で、データソースを取得していました。
LDAPでは、(DirContext)context.lookup( "ldap/ldaphost" )のように取得できれば、便利です。
JavaでLDAPにアクセスする方式は大きく分けて、LDAP SDKを使う方法と、JNDIを使ってアクセスする方法2種類があり、LDAP SDKにはいろいろな実装(OpenLDAP -Novel JLDAP-, Netscape,Mozilla LDAP Java SDKなど )があるが、開発が若干スローダウン気味です。(枯れていて安定しているという見方もできます。)
一方、JNDIのほうは、JDKとともに保守されているので、最新性が保たれているように感じられます。今回はJNDIを使ってアクセスしてみたいと思います。JNDI(Java Naming Directory Interface)は、LDAPに特化した仕様ではなく、Service Providerを切り替えれば、DNSやNIS、RMI、ファイルシステムなどからも情報を汎用的に操作できるようになる抽象化されたAPIです(個人的には抽象化により理解を困難にしている面もあると感じていますが・・・JDBCのようにうまくいっていないように感じますが、大きい構想のフレームワークなのでしょうがないのでしょう。)。LDAPのJNDI Service Provider("com.sun.jndi.ldap.LdapCtxFactory")はSunのJDK1.4から標準で付属してきます。
以下の例は、スタンドアローン(J2EEコンテナなしでもという意味)で動作する"uid=test*"というエントリを検索し、属性を表示する例です。
package ldapclient; import java.util.Hashtable; import java.util.logging.Level; import java.util.logging.Logger; import javax.naming.Context; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.Attribute; import javax.naming.directory.DirContext; import javax.naming.directory.InitialDirContext; import javax.naming.directory.SearchControls; import javax.naming.directory.SearchResult; /** * @see http://java.sun.com/developer/technicalArticles/Programming/jndi/index.h... * @author yyamazaki */ public class Main { public static void main(String[] args) { Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, "ldap://localhost:389"); //env.put(Context.SECURITY_AUTHENTICATION, "simple"); env.put(Context.SECURITY_PRINCIPAL, "cn=Directory Manager"); env.put(Context.SECURITY_CREDENTIALS, "password"); DirContext context = null; try { context = new InitialDirContext(env); //search entry. SearchControls searchControls = new SearchControls(); searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); NamingEnumeration entries = context.search("dc=example,dc=com", "uid=test*", searchControls); while (entries.hasMore()) { SearchResult entry = (SearchResult) entries.next(); System.out.println("dn: " + entry.getNameInNamespace()); //Attributes in the entry. NamingEnumeration attributes = entry.getAttributes().getAll(); while (attributes.hasMore()) { Attribute attribute = (Attribute) attributes.next(); // Values in the attributes of the entry. NamingEnumeration attributeValues = attribute.getAll(); while ( attributeValues.hasMore() ) { System.out.print(attribute.getID() + ": "); System.out.println(attributeValues.next()); } } System.out.println(); } } catch (NamingException ex) { Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex); } finally { if( context != null ){ try { context.close(); } catch (NamingException ex) { Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex); } } } } }
エントリの操作(追加、更新、削除)は、Net::LDAPオブジェクトのadd, modify, deleteでも行えますが、いったん検索されたエントリNet::LDAP::Entryからも行えます。
エントリの検索:
以下のコードでは、検索を行い$entryという変数にエントリを代入しています。
use Net::LDAP;
#
# bind as priviledged user for modify.
#
$ldap = Net::LDAP->new( "localhost" );
$message = $ldap->bind( "cn=Directory Manager", password=>"password" );
if( $message->code == 0 ) {
print "bind success\n";
} else {
warn "error with bind: " . $message->error ."\n";
exit( 1 );
}
$dn ="uid=test.0,ou=People,dc=example,dc=com";
$newRDN = "cn=test";
$result = $ldap->search( base=>$dn, scope=>'base', filter=>'objectclass=inetOrgPerson' );
if( $result->code == 0 ) {
@entries=$result->entries;
#取得されたエントリー
$entry=$entries[0];
}
$ldap->unbind;
上記の、$entryという変数(オブジェクト)では、以下のようなメソッドが使えます。
エントリのダンプ: dump
属性の追加: add( attr => value [,...] )
属性の変更: replace( attr => value )
エントリの削除: delete()
属性の削除: delete( attr -> [value[,...]] [,...] )
属性の取得: attributes
属性値の取得: get_value ( attr )
LDAPのエントリーの操作のうち、検索、追加、削除は、比較的簡単でした。
サンプル:
Search and Add: http://www.knowd.co.jp/yamazaki/?q=node/134
Search and Delete: http://www.knowd.co.jp/yamazaki/?q=node/136
次は、エントリが持っている属性を操作するサンプルをあげます。
属性を操作するには、まず、対象のエントリを取得(検索)した後、対象のエントリに対して、属性の追加、更新、削除などの操作を行っていきます。
この際、Net::LDAP->modify を使用します。
属性の追加: Net::LDAP->modify( $dn, add... )
エントリに対して、"mail: xxxx" という属性を追加します。
test-ldap3.pl
use Net::LDAP;
#
# bind as priviledged user for modify.
#
$ldap = Net::LDAP->new( "localhost" );
$message = $ldap->bind( "cn=Directory Manager", password=>"password" );
if( $message->code == 0 ) {
print "bind success\n";
} else {
warn "error with bind: " . $message->error ."\n";
exit( 1 );
}
#
# search entry.
#
$uid = "test.0";
$dn = "uid=$uid,ou=People,dc=example,dc=com";
$result = $ldap->search( base=>$dn, scope=>"base", filter=>"objectclass=*" );
@entries = $result->entries;
$entry = $entries[0];
if( !defined( $entry ) ) {
$ldap->unbind;
warn "$user not found.\n";
exit( 1 );
}
#
# add mail attribute to the entry.
#
$result = $ldap->modify( $entry,
add => {
mail => [ $uid.'@example.com' ]
}
);
if( $result->code != 0 ) {
$ldap->unbind;
warn $result->error;
exit( $result->code );
}
$ldap->unbind;
もし、属性がマルチバリューを許可していれば、次のように複数の値をまとめて追加することもできます。
...
# add mail attribute to the entry.
#
$result = $ldap->modify( $entry,
add => {
mail => [ $uid.'@example.com', $uid.'@example.net' ]
}
);
...
もし、すでに、同一の属性に同一の値がある場合、つまり、属性に重複した値が存在するような追加を試みた場合、以下のようなエラーとなり、結果としてadd操作は実行されません。
# perl test-ldap3.pl bind success エントリ uid=test.0,ou=People,dc=example,dc=com を変更できません。変更により、属性 mail の重複する値が 1 つ以上生成されます: uid=test.0@example.com, uid=test.0@example.net at test-ldap3.pl line 45, line 466.一見して、コードが長くなってきているように感じられます。エントリの検索に加え、属性操作が加わるためでしょう。。 これまでのサンプルは、プログラムをサブルーチン化せず、べたーっと書いてきましたが、この辺から、Perlのテクニックが必要になってきそうです。
前回のNet::LDAP Addオペレーションに続いて、Deleteオペレーションの例を挙げます。
Net::LDAP->delete( dn ) を使用します。
bindし、uid=test*で検索し、検索されたエントリのDNを@dns配列にプッシュし、それをdeleteメソッドに渡し、削除・・・という流れです。
test-ldap2.pl
use Net::LDAP;
#
# bind as priviledged user for delete.
#
$ldap = Net::LDAP->new( "localhost" );
$message = $ldap->bind( "cn=Directory Manager", password=>"password" );
if( $message->code == 0 ) {
print "bind success\n";
} else {
warn "error with bind: " . $message->error;
exit( 1 );
}
#
# search with uid=test* and found entry push to array named @dns.
#
$result=$ldap->search( base=>"dc=example,dc=com", filter=>"uid=test*" );
@dns = ();
if( $result->code == 0 ) {
foreach $entry ($result->entries){
push @dns, $entry->dn;
}
} else {
$ldap->unbind;
warn "error with search: " . $message->error;
exit( 1 );
}
#
# delete found entry in @dns
#
$counter=0;
foreach $dn ( @dns ) {
$result = $ldap->delete( $dn );
if( $result != 0 ) {
$counter++;
print "deleted :" . $dn . "\n";
} else {
warn $result->error . "\n";
}
}
print $counter." entries deleted.\n";
$ldap->unbind;
OpenDS 1.2では、1.0と比べGUIのStatus Panel(Control Panel)がアップグレードされています。
これで、簡単なユーザー管理をはじめとしてバックアップ、リストア、スキーマの管理等をGUIから行えるようになり、非常に重宝します。
# bin/control-panel & (またはbin/status-panel)

Manage Entriesからのユーザー管理GUI

Manage Schemaからのスキーマ管理GUI

CUIも充実していますが、ユーザー管理のGUIは実践上でも非常に役立つと思います。シンプルでよくできていると思います。リモートサーバーには接続できないみたいですが・・・
前述の設定が完了したら、http://search.cpan.org/~gbarr/perl-ldap-0.39/lib/Net/LDAP.pod に上げられているサンプルやリファレンスと、Perlの知識があれば、LDAPの操作が可能です。
以下は、LDAPサーバーのdc=example,dc=com以下からuidがtestで始まるユーザーを検索し、検索された件数を数えた後、新しくユーザーエントリを追加する一連の処理を記述したものです。(何度も言いますが…私はperlに明るくありません…)
test-ldap1.pl
use Net::LDAP;
#
# bind to LDAP.
#
$ldap = Net::LDAP->new( "localhost" );
$mesg = $ldap->bind( "cn=Directory Manager", password=>"password" );
if( $mesg->code == 0 ){
print "bind success\n";
} else {
warn "bind fail: ". $mesg->error;
exit( $mesg->error );
}
#
# search from LDAP and count user=test* user entry.
#
$baseDN = "dc=example,dc=com";
$searchResult = $ldap->search( base=>$baseDN, filter=>"uid=test*" );
$count=0;
foreach $entry ( $searchResult->entries ){
$count++;
}
print "uid=test* $count entries\n";
#
# add to LDAP.
#
$uid = "test." . $count;
$suffix = ",ou=People,dc=example,dc=com";
$dn = "uid=" . $uid . $suffix;
$addResult = $ldap->add( $dn,
attr => [
'cn' => $uid,
'sn' => $uid,
'mail' => $uid . '@example.com',
'uid' => $uid,
'objectClass' => [
'top',
'person',
'organizationalPerson',
'inetOrgPerson'
],
]
);
if ( $addResult->code == 0 ) {
print "added $dn\n";
} else {
$ldap->unbind;
warn "failed to add entry: ", $addResult->error, "\n";
exit( $addResult->code );
}
#
# search and dump added user.
#
$searchResult = $ldap->search( base=>$baseDN, filter=>"uid=".$uid );
foreach $entry ( $searchResult->entries ) {
$entry->dump
};
$ldap->unbind;
LDAPサーバーにアクセス可能なAPIは色々ありそうですが、まずは、PerlからLDAPにアクセスするための方法を見ていきます。サーバーはOpenDSのみならず、Sun Java System Directory Server, OpenLDAP, Fedora Directory Server 等などLDAPに準拠したものであればすべて同じようにアクセスできるはずです。(できる点がRDBMSと違う点なはずです)
1998年頃、ずいぶん前になりますが、LDAPサーバー実装の先駆けであるNetscape Directory Serverがリリースされたころ、PerLDAPというPerlからLDAPを操作できるライブラリが提供されていました。当初、これを使ってみようと思ったのですが、Mozilla.orgに移管された後、開発のスピードがスローダウンしてしまったようです。
よって、PerlのCPAN ( Comprehensive Perl Archive Network http://www.cpan.org )にあるNet::LDAPモジュールを使用したいと思います。
Net::LDAPモジュール
http://search.cpan.org/~gbarr/perl-ldap-0.39/lib/Net/LDAP.pod
OpenDSのサイトで公開されているOpenDS-x.y.z.zipをダウンロードし、インストールしておきたいディレクトリ(例えば/optに・・・)展開する。
展開されたディレクトリにsetupスクリプトがあるので、実行する。GUIのインストールシールドがいらない場合は、-i (または --cli )オプションをつける。