認証と権限付与

特定のユーザにだけ公開したいウェブページでは、認証と権限付与が必要になります。 認証 Authentication とは、ある人物が主張するとおりに、確かにその当人であるかどうかを確かめることです。 多くの場合ユーザ名とパスワードを用いますが、スマートカードや指紋など他の身分証明方法でもかまいません。 権限付与 Authorization とは、いったんある人物が特定された (すなわち認証された) 後、特定のリソースの操作を許可されているかどうかを判断することです。 たいていこれはその人物がリソースにアクセスする特定のロールを持っているかどうかで決定されます。

Yii には組み込みの認証・権限付与 (ここでは両方をあわせて auth と呼びます) 機構が備わっており、簡単に利用でき、また、特別なニーズのためにカスタマイズすることも可能です。

auth 機構の中心部分は、IWebUser インタフェースを実装するあらかじめ宣言済みの user アプリケーションコンポーネント です。 この user コンポーネントが現在のユーザの持続的な個人情報を保持します。 Yii::app()->user という式を使うと、どこからでもこの情報にアクセスできます。

この user コンポーネントを用いると、以下のことが可能になります。 すなわち、ユーザがログインしているかどうかを確認すること CWebUser::isGuest。 ユーザを login したり、logout したりすること。 ある操作が許可されているかどうかをチェックすること CWebUser::checkAccess。 そして、ユーザの ユニークな識別子 やその他の情報にアクセスすることが可能になります。

1. Identity クラスを定義する

上で述べたように、認証はユーザの同一性の検証に関するものです。 典型的なウェブアプリケーションの認証の実装は、通常、一対のユーザ名とパスワードを使って、ユーザの同一性を検証するものです。 しかし、その他の手段を使う場合もあり、異なった実装方法が要求される場合もあります。 さまざまな認証手段に対応できるように、Yii の auth フレームワークは identity クラスを導入しています。

定義する identity クラスは、実際の認証ロジックを含まなければなりません。 そして identity クラスは IUserIdentity インタフェースを実装しなければなりません。 異なった認証方法 (例: OpenID, LDAP, Twitter OAuth, Facebook Connect など) に対して、それぞれ異なったクラスを実装することが出来ます。 自分自身の実装を書くときには、CUserIdentity を継承すると始めやすいでしょう。 このクラスはユーザ名とパスワードを使う認証手段のための基本クラスです。

identity クラス定義の主な作業は IUserIdentity::authenticate メソッドの実装です。 このメソッドが認証手段の主要な内容をカプセル化するために使われます。 また identity クラスでは、ユーザセッションの間保持される必要がある追加の個人情報も宣言することが出来ます。

以下において、認証にデータベースを用いる identity クラスを例示します。 ウェブアプリケーションで典型的に見られるもので、 ユーザがログインフォームにユーザ名とパスワードを入力すると、その認証情報を検証するために、アクティブレコード を使って、データベースにある user テーブルに問い合わせをする、というものです。 実際には、この単純な例の中で、いくつかの事柄が示されています。

  1. データベースを使って認証情報を検証するように authenticate() を実装している。
  2. デフォルトの実装ではユーザ名を ID として返すので、CUserIdentity::getId() メソッドをオーバーライドして、_id プロパティを返している。
  3. 後に続くリクエストにおいて簡単に引き出せるように、setState() (CBaseUserIdentity::setState) メソッドを使ってその他の情報を保存している。
class UserIdentity extends CUserIdentity
{
    private $_id;
    public function authenticate()
    {
        $record=User::model()->findByAttributes(array('username'=>$this->username));
        if($record===null)
            $this->errorCode=self::ERROR_USERNAME_INVALID;
        else if($record->password!==crypt($this->password,$record->password))
            $this->errorCode=self::ERROR_PASSWORD_INVALID;
        else
        {
            $this->_id=$record->id;
            $this->setState('title', $record->title);
            $this->errorCode=self::ERROR_NONE;
        }
        return !$this->errorCode;
    }
 
    public function getId()
    {
        return $this->_id;
    }
}

次の節でログインとログアウトを扱うときに、この identity クラスが user の login メソッドに引き渡されることを見ます。 CBaseUserIdentity::setState を呼んで保存されるすべての情報が CWebUser に渡され、CWebUser がそれをセッションなどの持続的ストレージに保存します。 するとこの情報は CWebUser のプロパティのようにアクセスすることが出来るようになります。 上記の例では、ユーザの title 情報を $this->setState('title', $record->title); によって保存しましたが、ログインのプロセスを完了した後は、現在のユーザの title 情報を Yii::app()->user->title を使うだけで取得することが出来ます。

情報: デフォルトでは CWebUser はユーザを特定する情報を保存するのに、持続的ストレージとしてセッションを使います。 クッキーベースのログインが有効 (CWebUser::allowAutoLogin が true) になっていると、ユーザを特定する情報もクッキーに保存される可能性があります。 取り扱いに注意を要する情報(例えばパスワード)については、持続的ストレージに保存しないよう気をつけてください。

パスワードをデータベースに保存する

ユーザのパスワードをデータベースに安全に保存するためには、いくらか配慮が必要です。 ユーザのテーブル(またはそのバックアップ)を盗んだ攻撃者は、対策を施していない場合には、標準的な手法を使ってユーザのパスワードを回復することが出来ます。 具体的に言えば、パスワードをハッシュする前にソルトすべきであり、また、攻撃者が計算に長い時間を要するようなハッシュ関数を使うべきです。 上記のコード例では、PHP 組み込みの crypt() 関数を使っていますが、これは、正しく用いるなら、非常にクラックしにくいハッシュを生成するものです。

これらの話題についての更に詳しい情報は、下記を参照して下さい。

2. ログインとログアウト

ユーザの identity を作成する例が分りましたので、今度はこれを使って、必要になる login と logout のアクションを簡単に実装することが出来ます。 以下のコードが、それがどのように達成されるかを示します。

// 入力されたユーザ名とパスワードでユーザをログインする
$identity=new UserIdentity($username,$password);
if($identity->authenticate())
    Yii::app()->user->login($identity);
else
    echo $identity->errorMessage;
......
// 現在のユーザをログアウトする
Yii::app()->user->logout();

ここでは、まず、新しい UserIdentity オブジェクトを作成して、認証情報 (すなわち、ユーザによって送信された $username$password) をコンストラクタに渡しています。 そして、次に、ただ単に authenticate() メソッドを呼んでいます。 成功した場合は、identity 情報を CWebUser::login メソッドに渡します。 login メソッドは identity 情報を持続的ストレージ(デフォルトでは PHP セッション)に保存して、後のリクエストで参照できるようにします。 認証が失敗した場合は、失敗した理由の詳細について、errorMessage プロパティを取り調べることが出来ます。

ユーザが認証されたか否かは、Yii::app()->user->isGuest を使えば、アプリケーションから簡単にチェックできます。 identity 情報の保存に、セッション (これがデフォルトです) および/またはクッキー (後述します) のような持続的ストレージを使う場合は、ユーザは後続するリクエストにおいてログインされた状態を継続することが出来ます。 この場合は、すべてのリクエストに対して UserIdentity クラスやログインのプロセス全体を使用する必要はありません。 そうしなくても、CWebUser がこの持続的ストレージから自動的に identity 情報を読み込み、その情報に従って、Yii::app()->user->isGuest が true または false のどちらを返すべきかを決定します。

3. クッキーベースのログイン

デフォルトでは、セッションの構成 によって、アクティブでない時間が一定期間以上経過すると、ユーザはログアウトされます。 この仕様を変更するために、user コンポーネントの allowAutoLogin を true にセットし、持続期間のパラメータを CWebUser::login に渡すことができます。 こうすると、ブラウザを閉じてもユーザは設定した持続期間の間はログインされたままになります。 この機能はユーザのブラウザがクッキーを受け入れる設定になっている必要があることに注意してください。

// 7日間ログイン状態を保持する
// user コンポーネントの allowAutoLogin が true であることを確認
Yii::app()->user->login($identity,3600*24*7);

既に言及したように、クッキーベースのログインが有効な場合、CBaseUserIdentity::setState によって保存された情報は、クッキーにも保存されます。 ユーザが次回ログインされたときには、その情報がクッキーから読み出されて、Yii::app()->user によってアクセス可能になります。

Yii はこの情報のクッキーがクライアント側で改竄されるのを防止する手段を備えていますが、セキュリティ上重要な情報は CBaseUserIdentity::setState によって保存しないことを強く推奨します。 セキュリティ上重要な情報は、サーバ側の持続的ストレージ (例えばデータベース) から読み出して、サーバ側で復元するようにすべきです。

それに加えて、すべての本格的なウェブアプリケーションでは、次のような方策を講じて、クッキーベースのログインのセキュリティを強化することを推奨します。

  • ユーザがログインフォームに入力してログインに成功したときに、ランダムなキーを生成して、クッキーとサーバ側の持続的ストレージ (例えばデータベース) の両方に保存します。

  • 後のリクエストにおいて、ユーザ認証がクッキー情報によって行われる場合は、ユーザをログインする前に、このランダムなキーの二つのコピーを比較して、一致することを確認します。

  • ユーザが再びログインフォームからログインした場合は、キーは再生成される必要がります。

上記の方策を使うことで、ユーザが期限切れになった情報を含む可能性のある古いクッキーを再使用する可能性を除去することが出来ます。

この方策を実装するためには、以下の二つのメソッドをオーバーライドする必要があります。

  • CUserIdentity::authenticate(): ここが本当の認証が実行される場所です。 ユーザが認証された場合は、新しいランダムなキーを生成して、CBaseUserIdentity::setState によって identity 情報として保存すると同時に、データベースにも保存します。

  • CWebUser::beforeLogin(): ユーザがログインされる前に呼び出されます。 ここでクッキーから取得したキーがデータベースから取得したキーと同じかどうかチェックします。

4. アクセスコントロールフィルタ

アクセスコントロールフィルタは予備的な権限付与スキームです。 現在のユーザが要求されたコントローラのアクションを実行できるかどうかをチェックします。 権限付与はユーザ名、クライアントのIPアドレス、リクエストタイプなどに基づいて行われます。 これは "accessControl" という名前のフィルタとして提供されています。

ヒント: アクセスコントロールフィルタは単純なケースでは十分に役に立ちます。 より複雑なアクセスコントロールには、次の節で紹介するロールベースアクセス (RBAC) が使えるでしょう。

コントローラのアクションへのアクセスを制御するために、CController::filters をオーバーライドして、アクセスコントロールフィルタをインストールします (フィルタのインストールの詳細については Filter を参照してください)。

class PostController extends CController
{
    ......
    public function filters()
    {
        return array(
            'accessControl',
        );
    }
}

上記の例では、access control フィルタが PostController のすべてのアクションに適用されるよう指定しています。 フィルタによって実行される実際の権限付与ルールは CController::accessRules をオーバーライドすることで指定します。 以下に例を示します。

class PostController extends CController
{
    ......
    public function accessRules()
    {
        return array(
            array('deny',
                'actions'=>array('create', 'edit'),
                'users'=>array('?'),
            ),
            array('allow',
                'actions'=>array('delete'),
                'roles'=>array('admin'),
            ),
            array('deny',
                'actions'=>array('delete'),
                'users'=>array('*'),
            ),
        );
    }
}

上記のコードでは、三つのルールが指定されています。 各ルールは配列で表されます。 配列の最初の要素は 'allow''deny' で、残りの "名前-値" のペアは、ルールのパターンを指定するパラメータです。 上記のルールは、次のように解釈されます … create アクションと edit アクションは、匿名ユーザは実行できない。 delete アクションは、admin ロールを持ったユーザは実行できる。 delete アクションは、誰にも実行できない。

アクセスルールは定義されている順番にひとつずつ評価されます。 現在のパターン (例:ユーザ名、ロール、IPアドレス) に一致した最初のルールが権限付与の結果を決定します。 マッチしたルールが allow なら、アクションが実行され、deny ならアクションは実行されません。 一致するルールが一つもなかった場合は、アクションが実行されます。

ヒント: 予期せぬアクションの実行を防ぐため、ルールセットの最後に常に deny をおくことが有効です。 以下に例を示します。

return array(
    // ... ルール...
    // 最後のルールで必ず実行を阻止する
    array('deny',
        'action'=>array('delete'),
    ),
);

こうする理由は、一致するルールが一つも無かった場合には、アクションが実行されるからです。

アクセスルールは、以下のコンテキストパラメータと比較することが出来ます。

  • actions: どのアクションにルールが適用されるかを定義します。 アクション ID の配列です。比較は大文字小文字を区別しません。

  • controllers: どのコントローラにルールが適用されるかを定義します。 コントローラ ID の配列です。比較は大文字小文字を区別しません。

  • users: どのユーザにルールが適用されるかを定義します。 現在のユーザの name が判断基準に使われます。比較は大文字小文字を区別しません。 三種類の特別な意味を持つ文字を使うことが出来ます。

    • *: あらゆるユーザ。匿名ユーザも認証済みのユーザも含みます。
    • ?: 匿名ユーザ。
    • @: 認証済みのユーザ。
  • roles: どのロールにルールが適用されるかを定義します。 このパラメータは、は次の節で説明する ロールベースアクセスコントロール 機能を利用します。 具体的には、ロールに対して CWebUser::checkAccess が true を返したときにルールが適用されます。 ロールは主としてに allow のルールで使うべきであることに注意して下さい。 その定義からして、ロール (役割) とは何かをする許可のことだからです。 さらに、ここでは ロール (roles) という用語を使っていますが、その値は実際には、ロール (role)、タスク (task)、オペレーション (operation) など、どの権限アイテムでも構いません。

  • ips: どのクライアントIPアドレスにルールが適用されるかを定義します。

  • verbs: どのリクエストタイプ (例: GET, POST) にルールが適用されるかを定義します。 比較は大文字小文字を区別しません。

  • expression: PHP の式でルールが適用されるかどうかを定義します。 式の中で、$user という変数を使って Yii::app()->user を参照できます。

5. 権限付与結果の取り扱い

権限付与が失敗すると、すなわちユーザがアクションの実行を許可されないと、以下の二つのシナリオのうちどちらかが発生します。

  • ユーザがログインしておらず、ユーザコンポーネントの loginUrl プロパティがログインページの URL に設定されていると、ブラウザはそのページにリダイレクトされます。 loginUrl はデフォルトでは site/login ページを指していることに注意して下さい。

  • それ以外の場合は HTTP 例外がエラーコード 403 で表示されます。

loginUrl を設定する際に、相対 URL か絶対 URL のどちらかを指定できます。 あるいはまた、CWebApplication::createUrl によって URL を生成するための配列を指定しても構いません。 配列の最初の要素はログインコントローラアクションへの ルート (経路) であり、残りは GET パラメータとして渡される "名前-値" のペアです。 例えば、

array(
    ......
    'components'=>array(
        'user'=>array(
            // これが実際のデフォルト値です
            'loginUrl'=>array('site/login'),
        ),
    ),
)

ブラウザがログインページリダイレクトされ、ログインが成功したら、権限付与に失敗した元のページに戻りたいでしょう。 どうしたら元のページの URL がわかるでしょうか? ユーザコンポーネントの returnUrl プロパティからこの情報を得ることができます。 したがって、以下のようにしてリダイレクトを実行できます。

Yii::app()->request->redirect(Yii::app()->user->returnUrl);

6. ロールベースアクセスコントロール

ロールベースアクセスコントロール (RBAC) はシンプルで強力な集中的アクセスコントロールを提供します。 RBAC と他の伝統的なアクセスコントロールスキーマとの比較については Wiki の記事 を参照してください。

Yii は authManager アプリケーションコンポーネントで、階層的 RBAC スキームを実装しています。 以下でこのスキームで使われる主な概念を紹介します。 次に権限データをどう定義するかを説明します。 最後に権限データをアクセスチェックに利用する方法を説明します。

概要

Yii の RBAC の基本概念は、権限アイテム です。 権限アイテムとは、何かをする許可のことです (例: 新しいブログ記事を作る、ユーザを管理する)。 粒度と対象者によって、権限アイテムは オペレーションタスクロール に分類されます。 ロールは複数のタスクからなり、タスクは複数のオペレーションからなります。 そして、オペレーションが一番小さな許可単位です。 例えば、administrator ロールが post management タスクと user management タスクを含むようなシステムを作ることができます。 user management タスクは create user, update user, delete user などのオペレーションから構成されるでしょう。 更なる柔軟性のために、Yii ではロールに他のロールを含めたり、タスクに他のタスクを含めたりできます。 さらにオペレーションも他のオペレーションを含むことができます。

権限アイテムは名前によって一意に識別されます。

権限アイテムは ビジネスルール に関連付けられることがあります。 ビジネスルールは、アクセスチェックの際に実行される PHP コード断片です。 ビジネスルールの実行結果が true を返したときだけ、ユーザは権限アイテムが表す実行許可を持っていると考えられます。 例えば、updatePost というオペレーションを定義するときに、記事作成者本人だけに更新許可を与えるために、ユーザの ID が記事作成者の ID と同じであるかどうか確認するビジネスルールを付け加えたいことがあるでしょう。

権限アイテムを使って、権限階層 を構築することができます。 アイテム A がアイテム B を含むとき、AB の親になります (つまり、AB の権限をすべて継承します)。 アイテムは複数の子を持つことができ、また複数の親を持つこともできます。 したがって、権限階層はツリー構造ではなく半順序グラフになります。 階層構造の中で、ロールアイテムは最上位に位置し、オペレーションアイテムは最下層、タスクアイテムはそれらの中間に位置します。

権限階層を作った後、階層の中にあるロールをアプリケーションのユーザに割り当てることができます。 ユーザはいったんロールを割り当てられると、ロールによって表される権限を持つことになります。 例えば、administrator ロールをあるユーザに割り当てると、そのユーザは administrator の権限を持ちます。 そして、administrator の権限には、post managementuser management のタスク権限が含まれ、さらに create_user などの対応するオペレーション権限が含まれます。

ここからが面白いところです。 コントローラのアクションでユーザがある記事を削除できるかどうかチェックしたいとしましょう。 RBAC 階層と権限割り当てを使うと、これは以下のように簡単になります。

if(Yii::app()->user->checkAccess('deletePost'))
{
    // 記事の削除
}

7. 権限マネージャの設定

権限階層を定義してアクセスチェックを始める前に、authManager アプリケーションコンポーネントを設定する必要があります。 Yii は二つのタイプの権限マネージャを提供します。 CPhpAuthManagerCDbAuthManagerです。 前者は権限データを格納するのに PHP ファイルを使い、後者はデータベースを使います。 authManager アプリケーションコンポーネントを設定する際に、どちらのクラスを使い、初期値をどうするのか指定しなければなりません。 例えば、

return array(
    'components'=>array(
        'db'=>array(
            'class'=>'CDbConnection',
            'connectionString'=>'sqlite:path/to/file.db',
        ),
        'authManager'=>array(
            'class'=>'CDbAuthManager',
            'connectionID'=>'db',
        ),
    ),
);

この設定によって、Yii::app()->authManager として authManagerアプリケーションコンポーネントにアクセスできるようになります。

8. 権限階層を定義する

権限階層の定義には三つのステップがあります。 権限アイテムを定義し、アイテム同士の関係を設定し、最後にユーザにロールを割り当てます。 authManager アプリケーションコンポーネントが、これらのタスクを実行するための完全な API セットを提供します。

権限アイテムの定義には、アイテムの種類によって以下のメソッドのいずれかを使います。

権限アイテムのセットができたら、以下のメソッドでアイテム間の関係を設定します。

そして最後に、以下のメソッドでロールを個々のユーザに割り当てます。

以下にこれらの API を使った例を示します。

$auth=Yii::app()->authManager;
 
$auth->createOperation('createPost','create a post');
$auth->createOperation('readPost','read a post');
$auth->createOperation('updatePost','update a post');
$auth->createOperation('deletePost','delete a post');
 
$bizRule='return Yii::app()->user->id==$params["post"]->authID;';
$task=$auth->createTask('updateOwnPost','update a post by author himself',$bizRule);
$task->addChild('updatePost');
 
$role=$auth->createRole('reader');
$role->addChild('readPost');
 
$role=$auth->createRole('author');
$role->addChild('reader');
$role->addChild('createPost');
$role->addChild('updateOwnPost');
 
$role=$auth->createRole('editor');
$role->addChild('reader');
$role->addChild('updatePost');
 
$role=$auth->createRole('admin');
$role->addChild('editor');
$role->addChild('author');
$role->addChild('deletePost');
 
$auth->assign('reader','readerA');
$auth->assign('author','authorB');
$auth->assign('editor','editorC');
$auth->assign('admin','adminD');

一旦この階層を構築した後は、authManager コンポーネント (例えば CPhpAuthManagerCDbAuthManager) が自動的に権限アイテムを読み出します。 従って、上記のコードは一回だけ実行すればよく、リクエストごとに実行する必要はありません。

情報: この例は長くて退屈に見えますが、それは説明が目的だからです。 普通、開発者は管理者用のユーザインタフェースを作って、エンドユーザが権限階層をもっと直感的に構築できるようにする必要があるでしょう。

9. ビジネスルールを使う

権限階層を定義する際に、ロール・タスク・オペレーションにいわゆる ビジネスルール を関連付けることができます。 さらにまた、ユーザにロールを割り当てる際にも、ビジネスルールを関連付けることができます。 ビジネスルールとは、アクセスチェックが行われる際に実行される PHP コードの断片です。 ビジネスルールの戻り値が、ロールや割り当てが現在のユーザに適用されるかどうかの判断に使われます。 上記の例では updateOwnPost タスクにビジネスルールを関連付けています。 そのビジネスルールでは、現在のユーザ ID と記事作成者の ID が同じかどうかを単にチェックしています。 $params 配列の記事情報は、アクセスチェックを実行する際に開発者によって設定されます。

アクセスチェック

アクセスチェックを実行するため、まず権限アイテムの名前を知る必要があります。 たとえば、ユーザが記事の新規作成ができるかどうかをチェックするには、createPost オペレーションで表される権限を持っているかどうかを調べます。 次に CWebUser::checkAccess を呼ぶことでアクセスチェックを実行します。

if(Yii::app()->user->checkAccess('createPost'))
{
    // 記事を作成する
}

権限付与ルールが追加のパラメータを必要とするビジネスルールに関連付けられている場合には、パラメータを渡すことも出来ます。 例えば、ユーザが記事の更新が可能かどうかを調べるために、以下のように $params に記事データを入れて渡します。

$params=array('post'=>$post);
if(Yii::app()->user->checkAccess('updateOwnPost',$params))
{
    // 記事を更新する
}

デフォルトロールを使う

多くのウェブアプリケーションでは、全てまたは殆ど全てのユーザに割り当てる非常に特殊なロールがいくつか必要になります。 例えば、ある権限を認証済みのすべてのユーザに割り当てたいとします。 このロールの割り当てを明示的に指定し、保存するとなると、大変なメンテナンスの手間が発生します。 この問題を解決するために、デフォルトロール を活用することができます。

デフォルトロールは、すべてのユーザに黙示的に割り当てられるロールです。 デフォルトロールを明示的にユーザに割り当てる必要はありません。 CWebUser::checkAccess が呼び出されるとき、デフォルトロールはそのユーザに割り当てられているものとして、最初にチェックされます。

デフォルトロールは CAuthManager::defaultRoles プロパティで宣言されなければなりません。 例えば、以下の設定では authenticatedadmin という二つのロールをデフォルトロールとして宣言しています。

return array(
    'components'=>array(
        'authManager'=>array(
            'class'=>'CDbAuthManager',
            'defaultRoles'=>array('authenticated', 'admin'),
        ),
    ),
);

デフォルトロールはすべてのユーザに割り当てられるため、たいていは、ビジネスルールでロールが本当にユーザに適用されるかどうか決定する必要があります。 例えば、以下のコードでは authenticatedadmin という二つのロールを定義していますが、事実上は、それぞれを認証済みユーザと admin というユーザ名を持つユーザに割り当てています。

$bizRule='return !Yii::app()->user->isGuest;';
$auth->createRole('authenticated', 'authenticated user', $bizRule);
 
$bizRule='return Yii::app()->user->name === "admin";';
$auth->createRole('admin', 'admin user', $bizRule);

情報: バージョン 1.1.11 以降、ビジネスルールに引き渡される $params 配列に、userId という名前のキーが含まれるようになりました。 userId の値は、ビジネスルールが権限をチェックするユーザの id です。 CDbAuthManager::checkAccess() または CPhpAuthManager::checkAccess() を呼ぶときに、 Yii::app()->user が利用できない場合、あるいは、Yii::app()->user が権限をチェックする対象のユーザと違う場合は、この値を利用する必要があります。

$Id$

Total 1 comment

#5909 report it
morita at 2011/11/23 10:20pm
RBACのデータベース定義ファイル

CDbAuthManagerのAPIリファレンスの先頭にさらっと書かれてますが、yii/framework/web/auth/schema.sqlに定義ファイルがあります。 英語ページのコメントに書いてあったので、こっちにも書いておきます。

Leave a comment

Please to leave your comment.