[IM]LDAP認証の実装がだいたいできてきました

INTER-Mediatorには、自分自身でユーザーテーブルを持つ認証(ビルトイン認証)が可能です。そして、データベースエンジンのユーザーによる認証(ネイティブ認証)もすでにサポートしています。次に取り組むのはやはりLDAPです。ただ、MySQLにはLDAP接続のプラグインがあり、そういう手法を使えば、ネイティブ認証の一環としてLDAP認証を実現することができます。しかし、通常とは異なるデータベース運用が必要になりますし、MySQLとPostgreSQLで動作に違いがあるとまた大変です。SQLiteはそういう統合はできません。LDAPについては、単に認証の確認だけだと、要するに認証バインドをするだけのことであり、PHPではさほど難しくはないので、データベースエンジンと独立してLDAP認証の仕組みを搭載しました。

LDAPサーバに対する設定

まず、LDAPサーバ情報は、params.phpファイルに、変数で記載する方法にしました。LDAPでの認証時は、ユーザーはユーザー名だけを与えるのではなく、ユーザーレコードのDN(Distinguished Name)を指定する必要があります。たとえば、OS X Serverで、ホスト名がhomeserver.msyk.netの場合、

uid=msyk,cn=users,dc=homeserver,dc=msyk,dc=net

というのがユーザーを特定するDNとなります。ここで、dc…以降は「検索ベース」、cn=usersは「コンテナ」とします。さらに、OS X Serverはuidという属性名でユーザー名を指定しますが、これもActive Directoryでは違うこともあって、結果的に上記のユーザー名「msyk」以外の部分をすべて、変数で与えて、DNを構成するようにしています。サーバーのURL、ポート番号も合わせて、結局、params.phpファイルに以下のように記載をすることにします。

$ldapServer = "ldap://homeserver.msyk.net";
$ldapPort = 389;
$ldapBase = "dc=homeserver,dc=msyk,dc=net";
$ldapContainer = "cn=users";
$ldapAccountKey = "uid";

なお、$ldapServer変数に設定されている文字列の長さが0以上なら、LDAP認証を試みるように動作します。これらの変数部分を全部コメントにすると、LDAP認証に関しては何も行わず、以前の通りの動作になるようにしました。

PHPのLDAPライブラリの機能を使って認証するとしたら、その認証のためのバインドはサーバーからLDAPサーバーに送られます。ということは、クライアントからサーバーに対して、ユーザー名とパスワードを送り届けないといけません。これは、ネイティブ認証の仕組みを利用しますが、そのために、鍵ペアを生成して、params.phpファイルに記述します。以下の部分です。もちろん、最初から入っている鍵ではなく、コメントに書いている方法で自分が使用する鍵に置き換えてください。もちろん、クライアントに送られるのは、公開鍵です。

$generatedPrivateKey = <<<EOL
-----BEGIN RSA PRIVATE KEY-----
MIIBOwIBAAJBAKihibtt92M6A/z49CqNcWugBd3sPrW3HF8TtKANZd1EWQ/agZ65
 :
jU6zr1wG9awuXj8j5x37eFXnfD/p92GpteyHuIDpog==
-----END RSA PRIVATE KEY-----
EOL;

ネイティブ認証との混合動作

そして、LDAPアカウントでのログインは簡単にできました。ただし、1回の認証バインドに0.2〜0.3秒ほどかかります。同一のサブネットです。たとえば、フォームのサンプルだと、1ページの合成にデータベースアクセスを15回ほど行うので、5秒くらいのアクセス時間の増加になります。これは、ちょっと長いです。また、単に認証ができたとしても、グループ設定との連動等といった問題があります。

そこで、LDAPアカウントで認証が成功したら、ビルトイン認証で使うテーブル(authuserテーブル)に、そのLDAPのアカウントの情報をレコードとして追加することにしました。1回成功したら、ビルトイン認証テーブルにユーザーを追加し、パスワードもわかっているのでハッシュ化したパスワードを保存します。そのために、ビルトイン認証を行って失敗するとLDAP認証を行うという流れにしました。ビルトイン認証とLDAP認証は切り替えるのではなく、この順序で両方行います。
しかし、その追加したユーザーがいつまでも使えるのは問題で、タイムアウトさせないといけません。そこで、結果的にはビルトイン認証のテーブルにDateTime型のフィールド「limitdt」を追加する必要が発生しました。現在の実装では、そのフィールドの有無で落ちることはないようにしてありますが、LDAPを使う場合には追加したフィールドの定義がされていないといけません。

こうしてユーザーのレコードを作っておくと、グループへの登録ができます。ユーザー名で識別され、1度登録すると消されないので、主キー値は保持されるはずです。タイムアウトすると、LDAP認証を試みます。ただ、この部分、現在実装中で、うまくいったりいかなかったり、一進一退ですが、ある程度のところまで動くようになっています。ちなみに、MoodleもLDAP認証をサポートしますが、ログインを1度することで、ユーーザーレコードが作られる仕組みになっており、やはり同じような仕組みをINTER-Mediatorでも実装しています。

認証処理の流れ

初めて、LDAPアカウントでログインしようとするとき、ビルドイン認証のテーブルにはそのユーザーはありません。したがって、ビルトイン認証では失敗しますが、その後のLDAP認証で成功します。このとき、新たにビルトイン認証のテーブルに、そのユーザーのレコードを作りパスワードのハッシュと期限を記録します。クライアント側には認証のための情報が残されているので、この次の通信時からは、自動的にビルトイン認証が成功します。したがって、1画面作るのに20回の通信処理が必要でも、最初の1回だけがLDAP認証されます。

次の通信処理が、LDAP認証のタイムアウト以内の期間なら、ビルトイン認証が成功して終了します。そのとき、limitdtフィールドも現在の時刻に更新します。しばらくクライアントを利用しないなどでタイムアウトになると、ビルトイン認証は失敗しますが、引き続くLDAP認証が成功するように、クライアントでは、LDAP認証とネイティブ認証に必要な情報が残されています。そして、1度LDAP認証に成功すると、次回以降はまたネイティブ認証がタイムアウトまで成功します。

認証に必要な情報はクッキーに保存することもできるので、翌日にまた認証を継続させることもできます。

今現在、動作が怪しいのは、LDAP認証のパスワードを変えたときです。まず、この方法だと、LDAP認証のタイムアウトの時間が来るまでLDAPでの認証は行われないので、その期間は変更前のパスワードが通ります。これは、とりあえず、仕方ないことの1つとしておきます。そして、タイムアウトしてLDAP認証すると、当然ながら以前のパスワードでは認証が通らないので認証エラーとなり、ログインパネルが表示されます。ここで、新しくなったパスワードを入力すると、また同じように動作するというのが期待する動作です。このあたりの挙動が現状、今ひとつな感じです。

FileMakerはサポートせず

なお、LDAP認証は、PDOのみで、FileMaker Serverはサポートしません。どうしようかと思ったのですが、Admin Consoleでの設定で「外部サーバーアカウント」をクライアント認証で使えるようにしておけば、ネイティブ認証で事実上、Active Directory、Open Directoryは利用できます。LinuxのLDAPは使えないけど、FileMakerのクライアントで使えないで、Web側だけで使える仕組みまでのサポートは必要かどうか疑問ですので、FileMakerはINTER-Mediator組み込みのLDAP認証はサポートしないということにしたいと思います。

今現在、USB-C搭載の新しいMacBookはどう使うか?

USB-Cとどう過ごすのかということで、いくつかの記事をここに投稿しました。まず、分かりきった結論かもしれませんが、29WのApple純正のACアダプタが安定して利用するということです。12Wだと、電源供給の認識はしますが、ディスプレイアダプタが間に入ると、おそらくそのディスプレイアダプタやそこから接続されている機器に配給する電力が足りなくなるのだと思いますが、予想外なことが起こります。従って、ディスプレイアダプタを使うときには、29WのACアダプタを使うべきです。この容量のもので、USB出力のものはあるかもしれませんが、結局Apple純正品がいちばんコンパクトで持ち運びしやすいということになりますね。ただ、2mのUSB-C両端ケーブルはちょっと長いので、短いものが欲しくなります。

仮にiPadのアダプタが役に立つとしたら、外出時、それほどの時間は使わないけど、念のためアダプタを持っておきたいとか、ディスプレイアダプタを使わない場合ということに限られるかと思います。それでも、以前に書いたように場所によっては給電状態にならないのであれば、やはり持ち歩くだけ無駄なのかもしれません。そういうわけで、結局、確実に電源にするには、Apple純正のACアダプタということになります。

ディスプレイは、DELLの4Kの27インチでMac純正ではありませんし、HDMIでの接続です。外付けディスプレイの接続が安定しないのがちょっと困り者です。いちばん困るのは、「ディスプレイ」環境設定での設定が記録されていないのか、何かある時点の記録なのか、変更しても戻るということです。しかし、これは、毎回そうでもないです。シャットダウンしてもそうなるときは続けてなるのだけど、なぜか突然、「直前と同じ状態」になったりします。

そして、ディスプレイがフリーズすることが稀にありあす。ディスプレイの電源ボタンを長押ししても、電源が消えず、画面も真っ暗なので、ディスプレイの電源の抜き差しが必要です。ディスプレイアダプタがフリーズしたかのようなときもあります。DELLのディスプレイはスリープと認識しますが、ディスプレイアダプタのすべてのコネクタを抜いて、挿し直すと直ります。その間、Mac側の操作で復活はどうやっても出来ない状態になります。これらは、純正のACアダプタを使っていてもごくごく稀に発生します。もしかしたら、ディスプレイアダプタには常に電源が入っているわけですから、コンピュータのリセットと連動していないということが原因の1つではないのだろうかと考えますが、それは単に推測です。

そういうわけで、Mini Display Portのアダプタが、次の期待ということになります。

[IM]バリデーションの実装を再考する

INTER-Mediatorではかなり以前にバリデーションの実装はしたものの、完全ではない状態だったのですが、このところ、手を入れるに従って、いろいろ不具合…というか、「考慮が薄い」ポイントが目立ち始めましたので、改めて、議論を進めたいと考えています。

まず、バリデーションは、「入力チェック」とも言われます。いちばん、根本的なことは何かというと、「正しくない値をデータベースに保持しないようにしたい」という要求があると考えています。「正しくない」の基準は、アプリケーションによって変わりますが、「NULLである」ということかもしれませんし、「正しいメールアドレスのフォーマットではない」ということかもしれません。その場合に、データベースに記録しないようにするということがあります。データベース絡みとなると、いろいろ複雑な問題が出てきますので、これを後回しにして、まずは、アプリケーション利用者レベルからの見方を考えてみます。

開発者や管理者がバリデーションを必要とし、実装しますが、Webアプリケーションフレームワークは、バリデーションに関連したユーザーインタフェースを構築し、ユーザーを惑わせない仕組みの提供が望まれます。そもそも、アプリケーションで、どようにバリデーションが絡み合うのか、5W1H的にまず考えます。

  • When:正しくない値が入力されたとき
  • What:開発者や管理者が望ましくないと考えるデータを検知する仕組み
  • Where:入力可能なコンポーネント
  • Who:ユーザーが生成すると考える
  • Why:単純なミス、さまざまな誤認、テストあるいはインスペクション
  • How:キータイプ、あるいはコピー&ペーストなどのユーザの操作

以上の分析からは、バリデーションの検知と通知が大きな目的であることが出てきます。しかし、一部に例外があって、フィールドの初期値がバリデーション違反という場合もあります。そのとき、上記のWhoは「システム」ということになってしまいます。この初期値が違反している問題は、厳密にはデータベースの定義が正しくないで終わってしまいますが、非常に複雑な事情が絡むので、後ほど議論します。

バリデーションの検知は、INTER-Mediatorではすでに実装しています。onchangeイベントが発生したときに、フィールド単位でのチェックを行います。フィールドをまたがった判定用にも、コールバックされるメソッドの定義があります。最近になって、初期値が違反しているときでもバリデーションが働くように、onblurイベントでも判定をするようにしました。これら、さまざまなニーズはあると思われますが、タイミングと仕様が確定していれば、対処できる範囲かと思います。

一方、バリデーション違反の通知は、さまざまなバリエーションが考えられます。そういうニーズも状況も多様な状況では、プログラムを組んで対処というのはもちろん柔軟な対応ができていい部分ですが、一定の範囲を宣言的な記述でまかなうことで、プログラムを書くことを減らす意図のあるINTER-Mediatorではなんとかしたかったところです。そこで、フィールド単位のバリデーションが違反したら、「ダイアログボックスを表示して促す」「近辺に赤字等でメッセージを出す」という仕組みを定義ファイルの設定だけで実現しました。そして、イベント発生時に違反が検知されれば、雇用な動作をして、フィールドからフォーカスがはずれないようにしました。

しかし、ここまででも、すでに議論のポイントはいくつもあります。

  • バリデーションはいつ行うのか?
    • キータイプごと?
    • カット&ペーストするごと?
    • データベース更新前?
    • 複数のフィールドに対して「書き込み」ボタンがあれば、それを押したとき?
    • ポストOnlyモードとデータベース更新時は動作を違う必要があるのか?
    • 現状は、定義ファイルで指定可能なのはデータベース更新前のみ。
  • 違反通知をどのように行うのか?
    • 現在は、ダイアログボックスとページ上への文字の追加
    • 新しいページを表示したい?
    • 何が間違えたのかをもっと詳しく表示したい?
  • バリデーションに違反したら、その後にどのような操作を期待するのか?あるいは期待されるのか?
    • そのままでいいのか?
    • どこまでロールバックするのか?
    • どこまで既定値的な値を設定するのか?
    • 違反したレコードは削除しなくていいのか?
    • 違反してもレコードは作るのがいいのか?
    • 現在は、そのままにしつつ、正しい値を入力しないとそのページの他の作業をできなくしている。

あらゆるバリエーションに対する答えを用意するのか、それとも、主要な手法以外はプログラミングをしてもらうのか、その辺りが議論のポイントになると思います。いずれにしても、問題を書き出すことは必要でしょう。

データベースエンジンには通常バリデーションの仕組みは含まているので、本来はそちらを使うべきという議論があるでしょう。SQL言語での定義時に記述するため、「難しいから敬遠している」という向きもあるかもしれません。一方、なぜ、フレームワークがバリデーションをサポートするかというと、データベース側でのバリデーション処理は、違反時の状況の取得や、そこからの適切なユーザーインタフェースの構築、さらにやり直しなどの処理の組み立てなど、単純ではないプログラミングを要求されます。また、その対処方法も、データベースごとに違う可能性もあるので、むしろ、フレームワークの内部でバリデーションをサポートした方が、動作上作りやすいということもあるわけです。データベース側のエラー検知は、レイヤーを上下するワークフローをうまく組み立てるようなプログラミングを必要としますし、その結果、アプリケーションサーバーとデータベースということなるソフトウエア間の連携ということも必要になります。結果として、データベースに頼らない方が、フレームワーク内部で完結するため複雑さの一部を回避でき、加えてデータベースごとの事情に左右されないというメリットを生みます。

その結果、データベースのスキーマ定義にバリデーションルールが入らないことになり、それによる大きな問題は、初期値がバリデーション違反という状況で、ユーザーの操作に入ることがあります。これをどこの段階で、不正とみなし、どのような方法で排除するか、これが定まらないと、実装が揺らぎます。

ということで、とりあえず、頭にあることを書き出しておいて、議論を進める手掛かりにしたいと思います。