PASリファレンスマニュアル

このリファレンスマニュアルではPluggable Authentication Service (PAS)について説明します。これはPlone2.5以降でユーザ管理用に用いられるようになったものです。このマニュアルの対象読者は、Ploneサイトのユーザ管理をカスタマイズする必要がある管理者や、PASプラグインの開発に興味がある開発者です。

1. 導入

Pluggable Authentication Service (PAS)は、標準のZopeユーザフォルダやグループユーザフォルダ (GRUF) の代わりに使用するものです。PASは高度にモジュール化された非常に強力な仕組みなのですが、そのぶん習得しにくいものとなっています。

PASは、インターフェイスやプラグインという考え方を使用して作成されています。ユーザ管理やグループ管理に関する作業や認証処理は、それぞれ個別のインターフェイスに分けて設計されています。これらのインターフェイスをプラグインで実装することで、個々の処理をお好みのもので置き換えられるようになるのです。

PloneではPlonePASを使用しています。これはPASを継承したもので、プラグインの型をいくつか追加したりGRUFとの互換性を保つ機能を追加したりしています。PlonePASが必要になることはほとんどなく、しかもこれはPloneの次期バージョンでは変更されることになっているので、このチュートリアルではPASそのものの機能についてのみ取り上げます。

2. PASの使用法

Plone 2.5以降を使用しておられるのなら、あなたのサイトにはすでにPlonePASが組み込まれています。これはPASをもとにしたユーザフォルダであり、Zopeのルートユーザフォルダに存在します。Plone 2.1の場合は、PlonePASのバージョン1.x系を手動でインストールします。しかし、可能ならPlone 2.5以降を使用することをお勧めします。

2.1. 機能とインターフェイス

PASが提供するユーザフォルダにはさまざまな機能があります。ユーザの認証を行ったり、必要に応じてログイン画面を表示したり、ユーザやグループを検索したりすることが可能です。

設定や実装をよりシンプルかつパワフルなものとするために、これらの機能はすべて別々のインターフェイスに分けて管理しています。個々のインターフェイスは、たとえばユーザの認証などの特定の機能だけを担当し、開発者はその機能だけを実装することになります。

PASの内部では、これらの機能をプラグインによって実現しています。プラグインとは、特定の機能を実装した小さなロジックの塊で、これらのインターフェイスで定義されています。

このように機能を分割しておくことには、以下のような利点があります。

  • システムのさまざまな機能を個別に設定できるようになります。たとえば、ユーザ認証の手段(クッキーやフォームなど……)とユーザ情報の保存場所(ZODB、LDAP、RADIUS、SQLデータベースなど……)を独立して切り替えられるのです。これにより、システムのニーズに合わせた決め細やかな調整が可能となります。
  • 開発者が機能を追加する際には、特定のタスクを実現するためのコードを書くだけでよくなります。その結果、コードが読みやすくなり、テストや保守が楽になります。

2.2. 重要なインターフェイス

PAS では、さまざまなインターフェイスが定義されています。

その中でも最も重要なものたちを、以下にまとめます。

Authentication(認証)

認証プラグインは、入力された情報に基づく認証を行います。普通は、ユーザ名とパスワードを入力してもらい、それをZODBやSQLデータベースに格納された情報と照合するといった作業を行います。

Extraction(抽出)

抽出プラグインは、リクエストから証明情報(credential)を取り出します。証明情報はHTTPクッキーであったりフォームの入力内容であったり、あるいはユーザのIPアドレスであったりします。

Groups(グループ)

グループプラグインは、あるメンバーがどのグループに属しているかを返します。

Properties(プロパティ)

プロパティプラグインは、ユーザのすべてのプロパティを管理します。この中には、名前やメールアドレスといった標準的な情報だけでなく、その他なんでもお望みの情報を含めることができます。複数のプロパティプラグインを平行して使うこともできます。そうすると、たとえば「全体で管理する情報はActive Directoryで管理する」「このPloneサイトでのみ使用する情報はZODBで管理する」といったことができるようになります。

User Enumeration(ユーザ列挙)

ユーザ列挙プラグインは、ユーザの検索機能を実装します。

2.3. PASの設定

Ploneから直接PASの設定を変更することはできません。PASを設定するにはZope Management Interface (ZMI)を使用する必要があります。ZMIでサイトのルートフォルダを見ると*acl_users*というフォルダが見つかるでしょう。これがPASの正体です。

acl_usersというフォルダを開くと、その中にはいろいろなアイテムが格納されています。これらがPASプラグインで、それぞれ何らかのPASの機能を実装しています。

この中にひとつだけ特別なアイテムがあります。それがpluginsオブジェクトで、これはPASそのものの管理作業を担当しています。各プラグインでどのようなプラグインが有効になっているかや、どの順でプラグインを適用するかなどを、このオブジェクトで管理しています。

では、実際どのように動作しているのかを見ていきましょう。pluginsオブジェクトを開くと、その中にはすべてのPASインターフェイスの一覧があります。さらに、それが何をするものなのかについての簡単な説明があります。

抽出プラグイン群についてみてみましょう、これらのプラグインは、ユーザ名やパスワードといった証明情報をリクエストから取り出す処理を担当します。取り出した情報をもとに、ユーザの認証を行います。Extraction Pluginsと書かれたリンクをクリックすると、このインターフェイスを実装しているプラグインの一覧が表示されます。この中から、どれを使用するかやどの順番で適用するかを設定します。

Ploneのデフォルトの設定では、このインターフェイスを実装する2つのプラグインが有効になっています。

  • credentials_cookie_authプラグインは、HTTPクッキーあるいはHTTPフォーム(ログインフォーム、ログインポートレット)の内容からユーザ名とパスワードを取り出します。
  • credentials_basic_authプラグインは、標準のHTTP認証ヘッダからユーザ名とパスワードを取り出します。

デフォルトの設定では、クッキープラグインのほうがベーシック認証プラグインよりも優先順位が高くなっています。つまり、クッキーの情報とHTTP認証の情報を両方受け取った場合には、クッキーの内容を優先するということです。これを試してみるには、まず最初に標準のHTTP認証でZopeのルートにログインしてみましょう。それからPloneサイトを訪れて別のユーザでログインします。すると、新しいユーザがアクティブなユーザになっていることでしょう。

プラグインを適用する順序を変更するには、そのプラグインをクリックして矢印ボタンで上か下に移動します。左と右の矢印を使用すると、そのプラグインを有効にするか無効にするかを切り替えられるようになります。

2.4. 個別のPASプラグインの設定

個々のプラグインを有効にしたり無効にしたりするだけではなく、プラグイン自体も設定項目を持つことができます。この設定項目にアクセスするには、ZMIでプラグインを開きます。

先ほどの例のcredentials_cookie_authプラグインをもう一度見てみましょう。画面の上部にActivateというタブがあることがご確認いただけるでしょう。このタブは必須で、そのプラグインで有効あるいは無効にするPASインターフェイスをここで設定します。これは先ほど見たプラグインの設定に対応しますが、インターフェイス内でのプラグインの順序を変更することはできません。この画面で新しいインターフェイスを有効にすると、そのインターフェイスで有効なプラグイン一覧の末尾に追加されます。

propertiesタブを開くと、このプラグイン固有の設定を変更することができます。

何を設定できるのかは、プラグインによって異なります。中には設定項目を持たないプラグインもあるでしょうし、そうかと思えばやたら複雑な設定画面を持つプラグインもあるかもしれません。

3. PASを使用した開発

このセクションでは、PASプラグインを開発する方法を説明します。

3.1. 概念

PASがらみの開発を行う前に、まず知っておくべき概念について説明します。

PASで使われる基本概念は、以下のようになります。

証明情報(credentials)

証明情報とは、ユーザを認証する際に使用する情報群のことです。ユーザ名とパスワード、IPアドレス、セッションクッキー、あるいはその他の情報などを使用します。

ユーザ名(user name)

ユーザ名とは、ユーザがシステムにログインする際に使用する名前のことです。ユーザ名とユーザIDを混同してしまうといけないので、このチュートリアルでは「ユーザ名」の代わりに「ログイン名」という言葉を使うことにします。

ユーザID(user id)

すべてのユーザには、一意なユーザIDが割り当てられていなければなりません。ユーザIDは、ログイン名とは別のものにすることができます。

プリンシパル(principal)

プリンシパルとは、認証システムで任意のエンティティに対して使用する識別情報です。これはユーザかグループのどちらかとなります。つまり、ユーザとグループに同じIDを割り当てることはできないということです!

3.2. ユーザオブジェクト

他のユーザフォルダとは異なり、PAS環境の下ではユーザの元となるものはひとつではありません。さまざまな種類のユーザ(プロパティ、グループ、ロール、……)があり、さまざまなプラグインでそれを管理しています。このような状況に適合させるために、PASのユーザオブジェクトはひとつのインターフェイスでさまざまなユーザに対応させています。

3.2.1. ユーザオブジェクト

他のユーザフォルダとは異なり、PAS環境の下ではユーザの元となるものはひとつではありません。さまざまな種類のユーザ(プロパティ、グループ、ロール、……)があり、さまざまなプラグインでそれを管理しています。このような状況に適合させるために、PASのユーザオブジェクトはひとつのインターフェイスでさまざまなユーザに対応させています。

ユーザには、2つの基本的な型があります。通常のユーザ(IBasicUserインターフェイスで定義されています)とメンバープロパティを持つユーザ(IPropertiedUserインターフェイスで定義されています)です。通常のユーザはPloneでは用いられていないので、ここではIPropertiedUserのユーザについてのも説明します。

getId()

ユーザIDを返します。これが、そのユーザの一意な識別子となります。

getUserName()

そのユーザがシステムにログインする際に使用するログイン名を返します。

getRoles()

そのユーザに"グローバルに"割り当てられているロールを返します。

getRolesInContext(context)

そのユーザに、特定のコンテキストで割り当てられているロールを返します。これには、getRoles()が返すグローバルロールも含まれます。

3.2.2. ユーザの作成

PASは、以下の手順でユーザオブジェクトを作成します。

  1. IUserFactoryPluginプラグインを使用して新しいユーザオブジェクトを作成する。
  2. すべてのIPropertiesPluginプラグインからプロパティシートの情報を取得する。
  3. すべてのIGroupsPluginプラグインからグループの情報を取得する。
  4. すべてのIRolesPluginプラグインからグローバルロールの情報を取得する。

3.2.3. ユーザファクトリプラグイン

PASは複数のユーザ型をサポートしています。デフォルトで含まれているユーザ型はIBasicUserとIPropertiesUserの2つです。IBasicUserはシンプルなユーザ型で、ユーザIDとログイン名、ロール、そしてドメインの制約を管理します。IPropertiedUserは、この型を継承してユーザプロパティを追加したものです。

ユーザファクトリプラグインは、新しいユーザのインスタンスを作成します。PASは、作成したユーザに対してプロパティを追加したり、グループやロールを適用したりといった作業を行います。

使用できるユーザファクトリプラグインがない場合は、PASは標準のPropertiedUserのインスタンスを作成します。

IUserFactoryPluginインターフェイスで定義しているのは、次のメソッドひとつだけです。

def createUser( user_id, name ):

    """ Return a user, if possible.

    o Return None to allow another plugin, or the default, to fire.
    """

PASのデフォルトの処理は、次のようになります。

def createUser(self, user_id, name):
    return ProperiedUser(user_id, name)

3.2.4. プロパティプラグイン

プロパティは、プロパティシートに保存されます。これは、pythonの辞書オブジェクトのようなもので、プロパティとプリンシパルを関連付けて格納します。プロパティシートには順番があります。ひとつのプロパティが複数のプロパティシートにある場合は、いちばん優先度の高いシートにあるプロパティのみが表示されます。

プロパティシートを作成するには、IPropertiesPluginインターフェイスを実装したプラグインを作成します。このインターフェイスに含まれるメソッドは、次のひとつだけです。

def getPropertiesForUser( user, request=None ):

    """ user -> {}

    o User will implement IPropertiedUser.

    o Plugin may scribble on the user, if needed (but must still
      return a mapping, even if empty).

    o May assign properties based on values in the REQUEST object, if
      present
    """

簡単な例を、以下に示します。

def getPropertiesForUser(self, user, request=None):
    return { "email" : user.getId() + "@example.com" }

これは、ユーザオブジェクトにemailプロパティを追加します。その値は、ユーザIDの後に会社のドメインをつなげたもので固定されています。

3.2.5. グループプラグイン

グループプラグインは、指定したプリンシパルが属しているグループの識別子を返します。プリンシパルにはユーザあるいはグループのいずれかが指定されるので、PASはグループのネストをサポートしているということになります。しかし、デフォルトのPASの設定ではこれをサポートしていません。

他のPASインターフェイスと同様、IGroupsPluginインターフェイスも非常にシンプルなもので、定義されているメソッドはたったひとつだけです。

def getGroupsForPrincipal( principal, request=None ):

    """ principal -> ( group_1, ... group_N )

    o Return a sequence of group names to which the principal
      (either a user or another group) belongs.

    o May assign groups based on values in the REQUEST object, if present
    """

以下に簡単な例を示します。

def getGroupsForPrincipal(self, principal, request=None):
    # もともとManagerだった場合、自分自身には所属できません
    if principal=="Manager":
        return ()

    # 現在のユーザに対してのみ適用します
    if getSecurityManager().getUser().getId()!=principal:
        return ()

    # ローカルホストからのリクエストである場合にのみ処理対象となります
    if request is not None:
        ip=request.get("HTTP_X_FORWARDED_FOR", request.get("REMOTE_ADDR", ""))
        if ip!="127.0.0.1":
            return ()

    return ("Manager",)

これは、Zopeが稼動しているサーバ自身からアクセスした場合に、そのユーザをManagerグループに所属させる処理をしています。

3.2.6. ロールプラグイン

IRolesPluginプラグインは、プリンシパルに対応するグローバルロールを指定します。他のインターフェイスと同様、IRolesPluginにもメソッドはひとつだけしか定義されていません。

def getRolesForPrincipal( principal, request=None ):

    """ principal -> ( role_1, ... role_N )

    o Return a sequence of role names which the principal has.

    o May assign roles based on values in the REQUEST object, if present.
    """

以下に例を示します。

def getRolesForPrincipal(self, principal, request=None):
    # 現在のユーザに対してのみ適用します
    if getSecurityManager().getUser().getId()!=principal:
        return ()

    # ローカルホストからのリクエストである場合にのみ処理対象となります
    if request is not None:
        ip=request.get("HTTP_X_FORWARDED_FOR", request.get("REMOTE_ADDR", ""))
        if ip!="127.0.0.1":
            return ()

    return ("Manager",)

これは、Zopeが稼動しているサーバ自身からアクセスした場合に、そのユーザのロールをManagerとする処理をしています。

3.3. 認可処理

Zopeのセキュリティシステムは、保護されているリソースへのアクセスがあったらそのアクセスを検証しなければなりません。これは、ユーザフォルダの検証用メソッドによって行います。このメソッドは、現在どのユーザでログインしているのかを調べ、そのユーザのアクセスが許可されているかどうかを確認します。ログインしているユーザがアクセスを許可されている場合は、そのユーザをセキュリティシステムに返します。

3.3.1. 認可の仕組み

PASユーザフォルダは、次の手順でアクセスを検証します。

  1. すべての証明情報を取り出す。リクエストの中に含まれる、すべての証明情報(たとえばHTTPクッキーやフォームのパラメータ、HTTP認証ヘッダ、アクセス元のIPアドレスなど)を探します。ひとつのリクエストに複数の証明情報が含まれることもあれば、一切証明情報が含まれないこともあります。
  2. 見つかった証明情報のひとつひとつについて、次の処理を行う。
    1. 認可を試みる。その証明情報が既知のユーザであり、アクセスを許可されているかどうかを調べます。
    2. ユーザのインスタンスを作成する。
    3. リクエストの認可を試みる。正しく認可された場合はそのユーザを使用し、それ以降の認可処理を行いません。
  3. 匿名(anonymous)ユーザを作成する。
  4. 匿名ユーザによるリクエストの認可を試みます。成功したらこれを使用します。もし失敗したら……
  5. 再度証明情報の入力を促します。

3.3.2. 証明情報の取り出し

PASにおける証明情報とは、ユーザを識別したり認証に使用したりする際に使用する情報を集めたもののことです。たとえば、ユーザのログイン名とパスワードの組み合わせなどは、最も一般的な証明情報です。あるいは、HTTPクッキーを用いてユーザの操作をたどることもあります。この場合は、そのクッキーが証明情報となります。

PASの抽出プラグイン(user credential extraction plugins)は、リクエストからすべての証明情報を取り出します。取り出した証明情報を用いた認証処理については、次の段階で認証プラグインが行います。

プラグインの書き方

独自の抽出プラグインを作成したい場合は、IExtractionPluginインターフェイスを実装する必要があります。このインターフェイスで定義しているメソッドはひとつだけです。

def extractCredentials( request ):

    """ request -> {...}

    o Return a mapping of any derived credentials.

    o Return an empty mapping to indicate that the plugin found no
      appropriate credentials.
    """

以下に、簡単な例を示します。

def extractCredentials(self, request):
    login=request.get("login", None)

    if login is None:
        return {}

    password="request.get("password", None)

    return { "login" : login, "password" : password }

このプラグインは、ログイン名とパスワードをリクエストオブジェクトの同名のフィールドから取得します。

3.3.3. 取り出した情報による認証

抽出プラグインが取り出した証明情報は、単にユーザが入力した認証情報にすぎません。認証プラグインによる認証を行い、それが実在する正しいユーザであることを確認する必要があります。

IAuthenticationPluginインターフェイスは、このように単純なものです。

def authenticateCredentials( credentials ):

    """ credentials -> (userid, login)

    o 'credentials' will be a mapping, as returned by IExtractionPlugin.

    o Return a  tuple consisting of user ID (which may be different
      from the login name) and login

    o If the credentials cannot be authenticated, return None.
    """

以下に簡単な例を示します。

def authenticateCredentials(self, credentials):
    users={ "hanno" : "hannosch", "martin" : "optilude",
            "philipp" : "philiKON" }

    if "login" not in credentials or "password" not in credentials:
        return None

    login=credentials["login"]
    password=credentials["password"]
    if users.get(login, None)==password:
        return (login, login)

    return None

このプラグインは、ユーザhanno、martinおよびphilippによるログインを許可します。パスワードは彼らのニックネームとなっています。

3.3.4. チャレンジ

現在のユーザ(おそらく匿名ユーザ)がそのリソースへのアクセスを認可されていない場合、ZopeはPASに対して「チャレンジ」を行うよう指示します。この「チャレンジ」とは、通常はログインフォームを表示して正しいアカウントでログインしてもらうような処理になります。

IChallengeProtocolChooserプラグインとIChallengePluginsプラグインを組み合わせてチャレンジを行います。Zopeはさまざまなプロトコル(ブラウザから、WebDAV経由、XML-RPC経由など……)でアクセスできるので、PASはまずどのプロトコルを扱うのかを知る必要があります。この作業を行うには、すべてのIChallengeProtocolChooserプラグインに対する問い合わせを行います。デフォルトの実装はChallengeProtocolChooserで、これはすべてのIRequestTypeSnifferプラグインに対してそれぞれのプロトコルを問い合わせます。

プロトコルの一覧ができあがったら、次はすべてのアクティブなIChallengePluginsプラグインを探します。

プラグインの書き方

IChallengePluginインターフェイスは非常にシンプルで、次のひとつのメソッドだけしかありません。

def challenge( request, response ):

    """ Assert via the response that credentials will be gathered.

    Takes a REQUEST object and a RESPONSE object.

    Returns True if it fired, False otherwise.

    Two common ways to initiate a challenge:

      - Add a 'WWW-Authenticate' header to the response object.

        NOTE: add, since the HTTP spec specifically allows for
        more than one challenge in a given response.

      - Cause the response object to redirect to another URL (a
        login form page, for instance)
    """

このプラグインは、リクエストオブジェクトの内容を調べて何をすべきなのかを判断します。レスポンスオブジェクトを変更することで、ユーザに対して「チャレンジ」を行うことができます。たとえば、次の例を見てみましょう。

def challenge(self, request, response):
    response.redirect("http://www.disney.com/")
    return True

これは、許可されていない場所へのアクセスを試みたユーザを毎回ディズニーのホームページに飛ばすという処理をしています。

3.4. 注意

PASプラグインを開発する際に注意すべき点をまとめます。

3.4.1. PASが例外をもみつぶす

ユーザフォルダが壊れてしまうというのは、Zopeで発生しうる問題の中でも最も厄介なもののひとつです。こうなってしまうと、そのユーザフォルダの配下にあるオブジェクトには一切アクセスできなくなってしまいます。

プラグインで発生したエラーの影響を受けるのを避けるため、PASは一般的な型(NameError、AttributeError、KeyError、TypeErrorおよびValueError)の例外をすべて無視するようにしています。

そのせいで、プラグインのデバッグが大変になっています。プラグインで発生した例外が、すべてPASに飲み込まれてしまうからです。

添付ファイル