Chapter 9: アクセス制御

アクセス制御

Auth
Access Control
RBAC
DAC
MAC

web2pyには、パワフルでカスタマイズ可能なロールベースのアクセス制御メカニズム(RBAC)が含まれています。

Wikipediaでの定義を以下に示します:

「ロールベースアクセス制御(英: Role-based access control, RBAC)は、権限のあるユーザに対してシステムアクセスを制限する手法の一種。強制アクセス制御(MAC)や任意アクセス制御(DAC)に対するより新しい代替手法である。RBACはロールベースのセキュリティとして参照されることもある。

RBACは、ポリシーが中立で、柔軟にアクセス制御できる技術であり、DACやMACもシミュレート可能である。逆にMACは、ロールの構造が半順序ロール階層(Partially ordered role Hierarchy)ではなく、ツリーに制限されている場合に限り、RBACをシミュレートできる。

RBACが開発されるまで、アクセス制御のモデルとして知られていたのはMACとDACしかなかった。従って、モデルがMACでないならDACを、DACでないならMACであると考えられていた。90年代後半の研究では、RBACはそのどちらにも分類されていない。

組織において、ロール(役割)は仕事上の機能のために作られる。ある操作を実行する許可(パーミッション)は、特定のロールに対して割り当てられる。従業員(またはシステムユーザ)には特定のロールが割り当てられ、そのロールの割り当てを通して特定のシステム機能を実行するパーミッションが与えられる。コンテキストベースアクセス制御(CBAC)とは異なり、RBAC はメッセージのコンテキスト(コネクションがどこから起動されているかなど)を考慮しない。

ユーザに対して直接パーミッションが与えられるわけではなく、ロールを通して与えられるため、各人のアクセス権の管理はユーザへのロールの適切な割り当てに単純化される。つまり、これによりユーザの追加やユーザの部門の異動などの共通の操作が単純化される。

RBAC は従来の任意アクセス制御システムで用いられているアクセス制御リスト(ACL)とも異なる。ACL は低レベルのデータオブジェクトに対してパーミッションを与えるが、RBAC は組織において意味のある特定の操作に対してパーミッションを与える。例えば、一つのアクセス制御リストは特定のシステムファイルへの書き込みを許可/拒否するために用いられるが、そのファイルがどのように変更できるかは規定しない。」

web2pyには、RBACを実装した Auth クラスがあります。

Auth は下記のテーブル(と定義)が必要です:

  • auth_user ユーザの名前、メールアドレス、パスワード、ステータス(登録、保留、許可、禁止)が保存されます。
  • auth_group 多対多の構造においてユーザに対するグループまたはロールが保存されます。デフォルトでは各ユーザは自分自身のグループに属しています。しかし、1人のユーザを複数のグループに所属させることも、各グループに複数のユーザを含めることもできます。グループは、ロールと説明文で識別されます。
  • auth_membership 多対多の構造で、ユーザとグループをリンクします。
  • auth_permission グループとパーミッションを関連付けます。パーミッションは名前(テーブルとレコードが含まれることもあります)で識別されます。例えば、特定のグループのメンバーは、特定のテーブルの特定のレコードの "更新" のパーミッションが与えられます。
  • auth_event これ以外の他のテーブルに対する変更や、RBACにより制御されたオブジェクトへのCRUDを介して成立したアクセスを記録します。
  • auth_cas Central Authentication Service (CAS) で使用されます。全てのweb2pyアプリケーションはCASプロバイダーで、オプションでCASコンシューマになることもできます。

下のイメージで、スキマーは図的に再現されています:

image

原則として、ロールの名前とパーミッションの名前に制限はありません。つまり開発者は、組織内のロールやパーミッションに合わせてそれらを作成することができます。一度これらのテーブルが作成されるとweb2pyは、ユーザがログインしているかどうか、グループに属しているかどうか、および/もしくは、必要なパーミッションを持つグループの一つに属しているかどうかを確認するAPIを提供します。

web2pyは、ログイン、メンバシップ、パーミッションに基づいて任意の関数へのアクセスを制限するデコレータも提供しています。

web2pyはまた、幾つかの特別なパーミッション、すなわち、CRUDメソッド(削除、読み込み、更新、作成)に対応した名前を持つパーミッションに対応しており、デコレータを使用しなくても自動的にそのパーミッションを強制することができます。

この章では、RBACのそれぞれの構成要素を一つ一つ解説していきます。

認証

RBACを使用するためには、ユーザが識別されている必要があります。これはユーザが登録を行い(あるいは事前に登録されている状態で)、ログインする必要があることを意味しています。

Auth は複数のログイン用メソッドを提供します。デフォルトのメソッドは、ローカルの auth_user テーブルに基づいてユーザを識別します。 あるいは、サードパーティの認証システムや、シングルサインオンを提供するプロバイダ(Google、PAM、LDAP、Facebook、LinkedIn、Dropbox、OpenID、OAuth、その他)に対して、ユーザをログインさせることができます。

Auth の使用を開始するには、少なくとも以下のコードがモデルファイルに必要です。このコードはまた、web2pyの "welcome" アプリケーションで提供され、db 接続オブジェクトを利用しています:

1
2
3
from gluon.tools import Auth
auth = Auth(db)
auth.define_tables()

Authにはオプションの secure=True 引数があり、これは認証されたページに対しはHTTPS経由でのアクセスを強制します。

https

db.auth_userpassword フィールドには CRYPT バリデータがデフォルト指定されており、hmac_key と共に必要な設定です。レガシーのweb2pyアプリケーションでは、Authコンストラクタに渡す追加の次の引数がある場合があります: hmac_key = Auth.get_or_create_key()。これはアプリケーションフォルダ内の "private/auth.key" ファイルから、HMACキーを読み出す関数です。もしファイルが存在しない場合は、ランダムな hmac_keyを生成します。また複数のアプリケーションで同じ auth(アクセス制御)データベースを共有する場合は、同一の hmac_key が使われているか確認してください。新しいアプリケーションでは、これはもはや必要ありません。なぜなら、独立したランダムのサルト(salt)により、パスワードはサルト化されるためです。

デフォルトでは web2pyはログインに、email(メールアドレス)を使用します。もし代わりに usename(ユーザ名)を使用したい場合は、auth.define_tables(username=True) と設定します。

もし複数のアプリケーションで auth データベースを共有する場合は、auth.define_tables(username=True) のようにマイグレーションを無効に設定にしてください。

Auth を公開するには、次のような関数をコントローラに用意する必要があります("default.py" にサンプルがあります)。

1
def user(): return dict(form=auth())
auth オブジェクトと user 関数の両方は、ひな形アプリケーションで既に定義されています。

web2pyにはまた、適切にこの関数をレンダリングするための次のようなサンプルビュー "welcome/views/default/user.html" が含まれています:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{{extend 'layout.html'}}
<h2>{{=T( request.args(0).replace('_',' ').capitalize() )}}</h2>
<div id="web2py_user_form">
  {{=form}}
  {{if request.args(0)=='login':}}
    {{if not 'register' in auth.settings.actions_disabled:}}
      <br/><a href="{{=URL(args='register')}}">register</a>
    {{pass}}
    {{if not 'request_reset_password' in auth.settings.actions_disabled:}}
      <br/>
      <a href="{{=URL(args='request_reset_password')}}">lost password</a>
    {{pass}}
  {{pass}}
</div>

この関数は form を単純に表示しています。これにより、標準のフォームカスタマイズ用の構文を使ってカスタマイズすることが可能です。唯一の注意点は form=auth() によって表示されているフォームが request.args(0) に依存していることです。つまりデフォルトの auth() ログインフォームをカスタムログインフォームに置き換える場合、次のビューのようにif文を利用する必要があります:

1
{{if request.args(0)=='login':}}...custom login form...{{pass}}

auth.impersonate
auth.is_impersonating

上記のコントローラは、複数のアクションを公開しています:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
http://.../[app]/default/user/register
http://.../[app]/default/user/login
http://.../[app]/default/user/logout
http://.../[app]/default/user/profile
http://.../[app]/default/user/change_password
http://.../[app]/default/user/verify_email
http://.../[app]/default/user/retrieve_username
http://.../[app]/default/user/request_reset_password
http://.../[app]/default/user/reset_password
http://.../[app]/default/user/impersonate
http://.../[app]/default/user/groups
http://.../[app]/default/user/not_authorized
  • register ユーザの登録を行います。CAPTCHAも統合されていますが、デフォルトでは無効になっています。これはまた "web2py.js" で定義された、クライアントサイドのエントロピー・カルキュレータも統合されています。カルキュレータは新しいパスワードの強さを示します。web2pyで弱いパスワードの受け入れを防ぐため、IS_STRONG バリデータを使用することができます。
  • login 事前に登録されたユーザのログインを許可します(ユーザ登録を確認もしくは確認が要求されていない、ユーザ登録が承認もしくは承認が要求されていない、ユーザがブロックされていない、場合に許可されます)。
  • logout は期待通りの動きをしますが、その他のメソッドのように、イベントのログを記録し、他のイベントのトリガーとして使用することもできます。
  • profile ユーザにプロファイルを編集することを許可します。すなわち、auth_user テーブルの内容です。このテーブルは固定された構造を持っておらず、カスタマイズができることに注意してください。
  • change_password ユーザがフェイルセーフな方法で、パスワードを変更することができます。
  • verify_email メール検証が有効な場合、訪問者は登録時に、メール情報を検証するためのリンクを含むメールを受け取ります。リンクはこのアクションを指しています。
  • retrieve_username デフォルトでは、 Auth はメールとパスワードを使用しログインしますが、メールの代わりにユーザ名を使うこともできます。後者のケースでは、もしユーザがユーザ名を忘れた場合、retrieve_username メソッドによってユーザがメールアドレスを入力し、そのアドレスに送られたメールからユーザ名を取得することが可能になります。
  • request_reset_password パスワードを忘れたユーザが、新しいパスワードを要求できます。ユーザは、reset_password を指し示ている確認メールを受け取ることになります。
  • impersonate あるユーザを別の "偽装" ユーザにすることができます。これは、デバッグと、サポートにとって重要な機能です。request.args[0] が偽装されたユーザIDになります。このメソッドは、has_permission('impersonate', db_auth_user, user_id) として指定されたユーザでログインした時のみ有効となります。auth.is_impersonating() を使用し現ユーザが、他の誰かの偽装ができるかチェックすることが可能です。
  • groups 現在ログインしているユーザの所属しているグループ一覧表示されます。
  • not_authorized ユーザが権限のないページを表示しようとした時に、エラーメッセージを表示します。
  • navbar ログインやユーザ登録リンクなどのバー(ナビゲーションバー)を生成するヘルパーです。

Logout、profile、change_password、impersonate、groups、はログインしている必要があります。

デフォルトではこれらは全て公開されますが、機能の一部のみにアクセスを制限することも可能です。

上記の全てのメソッドは、 Auth サブクラスを作成することで、拡張したり置き換えたりすることが可能です。

また次の例のように、全てのメソッドは別々の機能として使用することも可能です。

def mylogin(): return dict(form=auth.login())
def myregister(): return dict(form=auth.register())
def myprofile(): return dict(form=auth.profile())
...

ログインした訪問者のみ関数にアクセスできるよう制限するためには、以下のサンプルのように関数にデコレータを指定します。

1
2
3
@auth.requires_login()
def hello():
    return dict(message='hello %(first_name)s' % auth.user)

全ての関数はデコレータを指定できます。公開されているものだけではありません。もちろんこれは、アクセスコントロールの非常に単純な例です。より複雑な例については後述します。

auth.user
auth.user_id
auth.user_groups.

auth.user は、現在ログインしているユーザに該当する db.auth_user のレコードのコピーか、None を格納しています。また auth.user_id は、auth.user.id と同じ値(すなわち、現在ログインしているユーザのID)か、None になります。同様に auth.user_groups は、ログインした現ユーザがメンバーのそれぞれのグループidがキーとなり、対応するグループロールが値となる、辞書が含まれます。
otherwise

auth.requires_login() デコレータは他の auth.requires_* デコレータと同様に、オプションの otherwise 引数を取ります。これは、登録ファイル場合はユーザのリダイレクト先の文字列を、もしくは呼び出し可能オブジェクトを、セットできます。登録が失敗した場合に、呼び出されます。

登録の制限

訪問者が登録を行うことはできるが、管理者によって承認されるまでログインできないようにする場合:

1
auth.settings.registration_requires_approval = True

appadminインターフェイスを介して、登録を承認することができます。auth_user テーブルを見てください。保留中の登録情報は、registration_key フィールドに "pending" がセットされています。登録を承認するには、このフィールドを空白にセットします。

appadminインターフェイスを介して、ユーザをログインできないようにすることもできます。auth_user から該当のユーザを見つけ、registration_key を "blocked" にセットします。"blocked" となったユーザは、ログインができません。ただし、ログインしていないユーザからのログインを防ぐことははできますが、既にログインしているユーザを強制的にログアウトさせることはできません。"disabled" を "blocked" の代わりに使用することもできます。動作は全く同じです。

また以下のステートメントを使用し、完全に "登録" ページへのアクセスをブロックすることができます:

1
auth.settings.actions_disabled.append('register')

訪問者の登録とその完了後に、自動ログインを行うことも可能です。その場合に確認用メールを送信し、メールを使った確認を完了しない限り、ログアウト後のログインを禁止するには次のように設定します:

1
2
auth.settings.registration_requires_approval = True
auth.settings.login_after_registration = True

Auth の他のメソッドも同じ方法で制限することができます。

OpenID, Facebook などとの統合

Integration with OpenID, Facebook, etc.

Janrain
OpenID
Facebook
LinkedIn
Google
MySpace
Flickr

web2pyのロールベースのアクセス制御を使用し、OpenID、Facebook、LinkedIn、Google、Dropbox、MySpace、Flickr、などの外部サービスによる認証が可能です。 最も簡単な方法は、Janrain Engage(旧RPX)(Janrain.com)を使用することです。

Dropboxはログインしたユーザへの、ストレジサービスプロバイダです。14章(その他のレシピ)で、ログイン以外のケースについても特別に触れます。

Janrain Engageはミドルウェアの認証を提供するサービスです。Janrain.comへの登録、そして使用するドメイン(あなたのアプリケーションの名前)と利用するURLの登録を行うことができます。そしてAPIキーが提供されます。

では、web2pyアプリケーションのモデルを編集し、以下の行を auth オブジェクトの定義の後に追加してください:

1
2
3
4
5
6
from gluon.contrib.login_methods.rpx_account import RPXAccount
auth.settings.actions_disabled=['register','change_password','request_reset_password']
auth.settings.login_form = RPXAccount(request,
    api_key='...',
    domain='...',
    url = "http://your-external-address/%s/default/user/login" % request.application)

最初の行は新しいログインメソッドをインポートしています。2行目は、ローカルユーザの登録を無効にしています。3行目はweb2pyに対してRPXのログインメソッドを使用するように指示しています。Janrain.comによって提供される api_key、登録する際に選択したドメイン、そしてアプリケーションのログインページの外部 url、を挿入する必要があります。これらを入手するには、janrain.comにログイン後、[Deployment][Application Settings] に移動します。右側に "Application Info" と、api_key は "API Key (Secret)" と書かれています。

ドメインは、"Application Domain" の先頭の "https://" と最後の ".rpxnow.com/" を除いたものです。 例えば: もし "secure.mywebsite.org" のようなWebサイトを登録した場合、JanrainはApplication Domainとして、"https://secure-mywebsite.rpxnow.com" と変えます。

image

新しいユーザが最初にログインした時に、web2pyはそのユーザに関連づけられた新規の db.auth_user のレコードを作成します。registration_id フィールドが、ユーザを一意に識別するIDとして設定されます。ほとんどの認証方法は、ユーザ名、電子メール、名前と名字を提供しますが、必ず提供されるとは限りません。どのフィールドを提供しているかは、ユーザによって選択されたloginメソッドに依存します。もし同じユーザが異なる認証のメカニズムを使ってログインした場合(例えば、OpenIDでログインしたあとにFacebookでログインし直した)Janrainは、それらを同じユーザとしては認識せず、それぞれの registration_id を発行します。

Janrainによって提供されるデータと db.auth_user に保存されるデータとのマッピングは、カスタマイズすることができます。ここではFacebookを例として示します:

1
2
3
4
5
auth.settings.login_form.mappings.Facebook = lambda profile:            dict(registration_id = profile["identifier"],
                 username = profile["preferredUsername"],
                 email = profile["email"],
                 first_name = profile["name"]["givenName"],
                 last_name = profile["name"]["familyName"])

辞書のキーは、db.auth_user のフィールドです。そして値は、Janrainによって提供されるプロファイルオブジェクトのデータエントリです。後者の詳細については、オンラインJanrainのマニュアルをご覧ください。

Janrainはまた、ユーザのログインに関する統計情報を保持します。

このログインフォームは、web2pyのロールベースのアクセス制御と完全に統合されており、グループの作成、ユーザのグループへの割り当て、権限の割り当て、ユーザのブロック等を行うことができます。

JanrainのフリーBasicサービスは、年間2500ユニークユーザのサインインが可能です。さらに多くのユーザの利用には、有料サービスのどれかにアップグレードする必要があります。 Janrainではなく、他のログインメソッド(LDAP、PAM、Google、OpenID、OAuth/Facebook、LinkedIn、など)を必要とする場合は、それを利用することもできます。そのためのAPIは、本章の後半で説明します。

CAPTCHA と reCAPTCHA

CAPTCHA
reCAPTCHA
PIL
スパマーやボットによるあなたのサイトへの不要な登録を避けるため、登録用のCAPTHCAが必要になる時があります。web2pyはすぐ使える reCAPTCHA[recaptcha] をサポートしています。reCAPTCHAは、よく設計されていて、無料で、利便性がよく(訪問者が簡単に単語を読むことができる)、簡単に導入できて、その他のサードパーティ・ライブラリを必要としません。

以下は、reCPATCHA使用時に必要となることです:

  • reCPATCHA[recaptcha] に登録を行い、登録アカウント用のパブリックキー、プライベートキーを入手してください。これらは単に二つの文字列です。
  • 次のコードを、auth オブジェクトを定義した後のモデルに追加してください:
1
2
3
from gluon.tools import Recaptcha
auth.settings.captcha = Recaptcha(request,
    'PUBLIC_KEY', 'PRIVATE_KEY')

reCAPTCHAは、'localhost' や '127.0.0.1' というアドレスのサイトにアクセスした場合は、上手く動かないかもしれません。外部に公開されたサイトでのみ、動作するように登録されているからです。

Recaptcha のコンストラクタは、いくつかの任意の引数をとります:

1
Recaptcha(..., use_ssl=True, error_message='invalid', label='Verify:', options='')

デフォルトでは、use_ssl=False なのを注意してください。

options は設定用文字列などです。例えば、options="theme:'white', lang:'fr'"

詳細は: reCAPTCHA[recaptchagoogle] 及び customizing を参照ください。

もしreCAPTCHAを使いたくない場合は、"gluon/tools.py" の Recaptcha クラスの定義を参照ください。他のCAPTCHAシステムでの利用も容易です。

Recaptcha は、DIV を拡張しただけのヘルパーなのを注意してください。これは reCaptcha や他のサービスを使うことのできる、バリデータのダミーのフィールドを生成します。このため、FORM定義を使用したものを含む、どのフォームでも使用することが可能です:

1
form = FORM(INPUT(...),Recaptcha(...),INPUT(_type='submit'))

SQLFORMの全てのタイプにも挿入可能です:

1
2
form = SQLFORM(...) or SQLFORM.factory(...)
form.element('table').insert(-1,TR('',Recaptcha(...),''))

Auth のカスタマイズ

次の呼び出しは、

1
auth.define_tables()

まだ登録されていない Auth テーブルの定義を行います。つまり、独自の auth_user テーブルを定義することができるのです。

auth のカスタマイズは幾つもの方法があります。最も簡単な方法は、拡張フィールドを追加することです:

## after auth = Auth(db)
auth.settings.extra_fields['auth_user']= [
  Field('address'),
  Field('city'),
  Field('zip'),
  Field('phone')]
## before auth.define_tables(username=True)

"auth_user" テーブルだけでなく、拡張フィールドは他の "auth_" テーブルでも宣言することが可能です。 拡張フィールドを使う方法は、内部メカニズムを停止させることがないため、推奨している方法です。

これを実行する他の方法は、本当はお勧めしませんが、auth テーブル自身を定義する方法です。もしテーブルを auth.define_tables() の前に定義すれば、デフォルトテーブルの代わりに使われます。以下方法を示します:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
## after auth = Auth(db)
db.define_table(
    auth.settings.table_user_name,
    Field('first_name', length=128, default=''),
    Field('last_name', length=128, default=''),
    Field('email', length=128, default='', unique=True), # required
    Field('password', 'password', length=512,            # required
          readable=False, label='Password'),
    Field('address'),
    Field('city'),
    Field('zip'),
    Field('phone'),
    Field('registration_key', length=512,                # required
          writable=False, readable=False, default=''),
    Field('reset_password_key', length=512,              # required
          writable=False, readable=False, default=''),
    Field('registration_id', length=512,                 # required
          writable=False, readable=False, default=''))

## do not forget validators
custom_auth_table = db[auth.settings.table_user_name] # get the custom_auth_table
custom_auth_table.first_name.requires =   IS_NOT_EMPTY(error_message=auth.messages.is_empty)
custom_auth_table.last_name.requires =   IS_NOT_EMPTY(error_message=auth.messages.is_empty)
custom_auth_table.password.requires = [IS_STRONG(), CRYPT()]
custom_auth_table.email.requires = [
  IS_EMAIL(error_message=auth.messages.invalid_email),
  IS_NOT_IN_DB(db, custom_auth_table.email)]

auth.settings.table_user = custom_auth_table # tell auth to use custom_auth_table

## before auth.define_tables()

フィールドは幾つでも追加することは可能です。またバリデータも変更可能です。しかし、この例の "required" とマークが入ったフィールドは削除することはできません。

"password"、 "registration_key"、 "reset_password_key"、 "registration_id" フィールドを、readable=Falsewritable=False に設定することは重要です。訪問者が不正にこれらの値を、改竄できるようなことがあってはならないからです。

もし "username" というフィールドを追加した場合、"email" フィールドの代わりにログイン時に利用することができます。設定した場合、以下のようなバリデータを追加する必要があります:

1
auth_table.username.requires = IS_NOT_IN_DB(db, auth_table.username)

Auth テーブルの名前変更

Authテーブルの名前は、次のように定義されています。

1
2
3
4
5
auth.settings.table_user_name = 'auth_user'
auth.settings.table_group_name = 'auth_group'
auth.settings.table_membership_name = 'auth_membership'
auth.settings.table_permission_name = 'auth_permission'
auth.settings.table_event_name = 'auth_event'

これらの名前は、auth オブジェクトの定義を行ってからAuthテーブルの定義を行うまでの間に、再定義することにより変更できます。例えば:

1
2
3
4
auth = Auth(db)
auth.settings.table_user_name = 'person'
#...
auth.define_tables()

テーブルは次の属性値により、その名前とは無関係に参照することが可能です。

1
2
3
4
5
auth.settings.table_user
auth.settings.table_group
auth.settings.table_membership
auth.settings.table_permission
auth.settings.table_event

その他のログインメソッドとログインフォーム

LDAP
PAM

Authは多数のログインメソッドの提供と、新しいログインメソッド作成のためのフックを提供します。サポートしている各ログインメソッドは、次のフォルダ内の各ファイルに対応しています。

1
gluon/contrib/login_methods/

各ログインメソッドに関しては、ファイル自身に記述された説明を参照してください。ここでは幾つかの例を説明します。

まず、以下の二つの異なるタイプのログインメソッドの違いを理解する必要があります。

  • web2pyのログインフォームを使ったログインメソッド(ただし認証は web2py の外で行われる)。例:LDAP。
  • 外部のシングルサインオン用フォームを必要とするログインメソッド(例:GoogleとFacebook)

後者の場合は、web2pyはログイン用の認証情報を取得せず、サービスプロバイダから発行されたログイン用のトークンのみを取得します。このトークンは、db.auth_user.registration_id に保存されます。

それでは、最初の例を見てみましょう:

ベーシック認証

認証サービスがあります。例えば次のURLが、

1
https://basic.example.com

ベーシック認証を実施するようにします。これはこのサーバが以下のようなヘッダ情報を持つフォームの、HTTPリクエストを受けとることを意味します:

1
2
3
GET /index.html HTTP/1.0
Host: basic.example.com
Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==

後半の文字列は base64 でエンコーディングされた、ユーザ名:パスワード の文字列です。このサービスはユーザが認証に成功した場合は 200 OK を、それ以外の場合は 400、401、402、403 あるいは 404 を返します。

Auth 標準のログインフォームを使用し、ユーザ名とパスワードを入力させ、そのサービスに対しての認証をしたいとします。この場合は、単にアプリケーションに以下のコードを挿入するだけで十分です。

1
2
3
from gluon.contrib.login_methods.basic_auth import basic_auth
auth.settings.login_methods.append(
    basic_auth('https://basic.example.com'))

auth.settings.login_methods は、順番に実行される認証メソッドのリストです。 デフォルでは以下のように設定されています。

1
auth.settings.login_methods = [auth]

代替のメソッドがリストに追加された時、例えば basic_auth で考えてみます。Auth は最初に auth_user に登録された情報に基づいて訪問者のログインを試みます。失敗した時、リストに載っている次のメソッドを試みます。もし訪問者ログインでメソッドが成功し、さらに auth.settings.login_methods[0]==auth の時、 Auth は以下の処理を実行します:

  • auth_user にユーザが存在しない場合、新しいユーザが作成され、 username/email と password を保存します。
  • auth_user にユーザが存在するが、認証された新しいパスワードと登録された古いパスワードが一致しない場合、古いパスワードを新しいものに置き換えます(特に指定しない限り、パスワードは常にハッシュ化されたものを保存します)。

新しいパスワードを auth_user に保存したくない場合は、ログインメソッドの順序を変更すれば十分です。もしくは、auth をリストから削除します。例えば:

1
2
from gluon.contrib.login_methods.basic_auth import basic_auth
auth.settings.login_methods =     [basic_auth('https://basic.example.com')]

ここで記載する他のログイン方法についても同様です。

SMTP と Gmail

SMTP
Gmail

ログイン用の認証情報をリモートのSMTPサーバ、例えば Gmail、を使用し検証することができます。すなわちユーザの提供するemailとパスワードが、GmailのSMTP サーバ(smtp.gmail.com:587)に正常にアクセスできるものであればユーザをログインさせることができます。これに必要なのは以下のコードです:

1
2
3
from gluon.contrib.login_methods.email_auth import email_auth
auth.settings.login_methods.append(
    email_auth("smtp.gmail.com:587", "@gmail.com"))

email_auth の最初の引数は、SMTPサーバの アドレス:ポート です。二つ目の引数はメールのドメインです。

これは TLS 認証を必要とするSMTPサーバに対しても動作します。

TLS

PAM
PAM

Pluggable Authentication Modules(PAM)を使った認証は、前のケースと同様に機能します。これにより、オペレーティングシステムのアカウントを用いてユーザの認証ができるようになります:

1
2
from gluon.contrib.login_methods.pam_auth import pam_auth
auth.settings.login_methods.append(pam_auth())
LDAP
LDAP

LDAPを使った認証は、前のケースと全く同様に機能します。

LDAPを MS Active Directoryと共に使用します:

Active Directory

1
2
3
4
from gluon.contrib.login_methods.ldap_auth import ldap_auth
auth.settings.login_methods.append(ldap_auth(mode='ad',
   server='my.domain.controller',
   base_dn='ou=Users,dc=domain,dc=com'))

LDAPをLotus NotesとDominoと共に使用します:

Lotus Notes
Domino

1
2
auth.settings.login_methods.append(ldap_auth(mode='domino',
   server='my.domino.server'))

LDAPをOpenLDAP(UIDによる)と共に使用します:

OpenLDAP

1
2
auth.settings.login_methods.append(ldap_auth(server='my.ldap.server',
   base_dn='ou=Users,dc=domain,dc=com'))

LDAPをOpenLDAP(CNによる)と共に使用します:

1
2
auth.settings.login_methods.append(ldap_auth(mode='cn',
   server='my.ldap.server', base_dn='ou=Users,dc=domain,dc=com'))
Google App Engine
GAE login

Google App Engine上で実行する時、Googleを使用した認証は、web2pyログインフォームをスキップする必要があります。Googleログインページにリダイレクトさせ、そして成功したら元のページに戻ります。前述の例とは動作が異なるため、APIは少し異なります。

1
2
from gluon.contrib.login_methods.gae_google_login import GaeGoogleAccount
auth.settings.login_form = GaeGoogleAccount()
OpenID
OpenID

(OpenIDのサポートがある)Janrainの組み込みについて説明しましたが、これが最も簡単なOpenIDを使う方法です。しかしサードパーティー製のサービスに頼るのではなく、OpenIDプロバイダにその利用者(あなたのアプリケーション)から直接アクセスしたいこともあります。

これがその設定例です:

1
2
from gluon.contrib.login_methods.openid_auth import OpenIDAuth
auth.settings.login_form = OpenIDAuth(auth)

OpenIDAuthpython-openid モジュールを別途インストールする必要があります。内部で、このログイン方式は次のテーブルを定義します:

1
2
3
4
db.define_table('alt_logins',
    Field('username', length=512, default=''),
    Field('type', length =128, default='openid', readable=False),
    Field('user', self.table_user, readable=False))

このテーブルには、各ユーザのopenidのユーザ名が保存されます。現在ログインしているユーザのopenidを表示したい場合は、次のようにします:

1
{{=auth.settings.login_form.list_user_openids()}}
OAuth2.0 と Facebook

OAuth
Facebook

(Facebookのサポートがある)Janrainの組み込みについて説明しましたが、サードパーティー製のサービスに依存せずに、OAuth2.0プロバイダ、例えば Facebook、に直接アクセスしたいこともあります。方法は以下の通りです:

1
2
from gluon.contrib.login_methods.oauth20_account import OAuthAccount
auth.settings.login_form=OAuthAccount(YOUR_CLIENT_ID,YOUR_CLIENT_SECRET)

独自のアプリケーションの代わりに、特定のFacebookアプリケーションのAPIにアクセスするために Facebook OAuth2.0 を使用する場合は、もう少し複雑になります。Facebook Graph APIにアクセスする例を示します:

最初に、Facebook Python SDK をインストールする必要があります。

そして、モデルに以下のコードが必要です:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
## import required modules
from facebook import GraphAPI
from gluon.contrib.login_methods.oauth20_account import OAuthAccount
## extend the OAUthAccount class
class FaceBookAccount(OAuthAccount):
    """OAuth impl for Facebook"""
    AUTH_URL="https://graph.facebook.com/oauth/authorize"
    TOKEN_URL="https://graph.facebook.com/oauth/access_token"
    def __init__(self, g):
        OAuthAccount.__init__(self, g,
                              YOUR_CLIENT_ID,
                              YOUR_CLIENT_SECRET,
                              self.AUTH_URL,
                              self.TOKEN_URL)
        self.graph = None
    # override function that fetches user info
    def get_user(self):
        "Returns the user using the Graph API"
        if not self.accessToken():
            return None
        if not self.graph:
            self.graph = GraphAPI((self.accessToken()))
        try:
            user = self.graph.get_object("me")
            return dict(first_name = user['first_name'],
                        last_name = user['last_name'],
                        username = user['id'])
        except GraphAPIError:
            self.session.token = None
            self.graph = None
            return None
## use the above class to build a new login form
auth.settings.login_form=FaceBookAccount()
LinkedIn
LinkedIn

(LinkedInのサポートがある)Janrainの組み込みについて説明しましたが、これが最も簡単なOAuthを使った方法です。しかしサードパーティー製のサービスに頼るのではなく、Janrainプロバイダが提供するよりも、多くの情報を得たいためにLinkedInに直接アクセスしたい場合もあります。

例を示します:

1
2
from gluon.contrib.login_methods.linkedin_account import LinkedInAccount
auth.settings.login_form=LinkedInAccount(request,KEY,SECRET,RETURN_URL)

LinkedInAccount は、"python-linkedin" モジュールを別途インストールする必要があります。

X509

x509証明書と証明書から取得した認証をページに渡すことによって、ログインがすることが可能です。これには M2Crypto を、次のサイトからインストールする必要があります。

http://chandlerproject.org/bin/view/Projects/MeTooCrypto

一度、M2Cryption がインストールされたら、使用可能です:

1
2
3
from gluon.contrib.login_methods.x509_auth import X509Account
auth.settings.actions_disabled=['register','change_password','request_reset_password']
auth.settings.login_form = X509Account()

web2py内部で、x509証明書を渡して認証します。どのようにするかはブラウザによって違いますが、Webサービス用の証明書使用する場合がほとんどです。次の例は cURL を使って、認証を試したものです:

curl -d "firstName=John&lastName=Smith" -G -v --key private.key      --cert  server.crt https://example/app/default/user/profile

この機能は Rocket(web2py組み込みの Webサーバ)では、すぐ使えます。しかし違うWebサーバを使用する場合は、サーバ側で何らかの特別な設定が必要です。証明書をローカルホスト上のどこに置くか、クライアントから来る証明書を検証する必要があるかなど、個別にWebサーバに指定する必要があります。これらはWebサーバに依存するため、説明は省略します。

Multiple login フォーム

ログインメソッドの中には、login_formを変更するものとしないものがあります。変更する場合は、他と共存することができないかもしれません。ただし、同じページに複数のログインフォームを表示することで共存させることができます。web2pyは、そのための方法を提供しています。ここに、通常のログイン(auth)とRPXログイン(janrain.com)を共存させる例を示します:

1
2
3
from gluon.contrib.login_methods.extended_login_form import ExtendedLoginForm
other_form = RPXAccount(request, api_key='...', domain='...', url='...')
auth.settings.login_form = ExtendedLoginForm(auth, other_form, signals=['token'])

もしシグナルがセットされ、リクエストに含まれるパラメータがいずれかのシグナルにマッチした場合、代わりに other_form.login_form への呼び出しを返します。other_form は、個別の状態をハンドルできます。例えば、other_form.login_form 内での複数ステップのOpenIDログインです。

そうでない場合は、other_form と共に通常のログインフォームが表示されます。

レコードバージョン管理

完全なレコードバージョン管理を有効にするため、Authを利用することができます:

1
2
3
4
db.enable_record_versioning(db,
    archive_db=None,
    archive_names='%(tablename)s_archive',
    current_record='current_record'):

これは、db の各テーブルのためのアーカイブテーブルの作成と、修正時の各レコードのコピーの保存を、web2pyに指示します。古いコピーは保存されます。新しいコピー(訳注:更新されたレコードのこと)はされません。

最後の3つのパラメータはオプションです:

  • archive_db アーカイブテーブルを保存する、別のデータベースを指定することができます。None をセットすると、db をセットすることと同じになります。
  • archive_names 各アーカイブテーブルの命名パターンを用意します。
  • current_record 変更されたレコードのオリジナルを参照するアーカイブテーブルで、参照フィールドの名前を指定します。archive_db!=db の時は、参照フィールドは単なる整数フィールドのため、クロスデータベース参照は不可能なことに注意してください。

modified_bymodified_on フィールド(例えばauth.signatureによって作成)を持つテーブルのみが、アーカイブされます。

enable_record_versioning の時に、レコードが is_active フィールド(auth.signatureにより作成)を持つ場合、レコードは削除されることはなく、is_active=False でマークされます。

実際は enable_record_versioning は、全てのバージョンテーブルに common_filter を追加します。これにより、is_active=False のレコードをフィルタし、本質的に見えなくします。

enable_record_versioning の場合は、auth.archive もしくは crud.archive は使用すべきではありません。さもないと、レコードが重複する結果になってしまいます。

これらの関数は明白に、enable_record_versioning に自動的に代わり、そして元の関数は非推奨になる予定です。

MailAuth

メーラを次のように設定できます:

from gluon.tools import Mail
mail = Mail()
mail.settings.server = 'smtp.example.com:25'
mail.settings.sender = 'you@example.com'
mail.settings.login = 'username:password'

もしくは簡単に auth を使い、メールプロバイダの設定ができます:

mail = auth.settings.mailer
mail.settings.server = 'smtp.example.com:25'
mail.settings.sender = 'you@example.com'
mail.settings.login = 'username:password'

SMTPサーバのmail.settingsを、適切なパラメータに置き換える必要があります。mail.settings.login=None とセットすると、SMTPサーバは認証を要求しません。TLSを使用したくない場合、mail.settings.tls = False とセットします。

web2py API のメールとメール設定に関しては、8章でさらに触れています。ここでは MailAuth の相互作用に関して、限定的に説明します。

Auth でのメール検証は、デフォルトでは無効になっています。 メール検証を有効にするには、モデルの auth 定義の後に次のコードを追加します:

1
2
3
4
5
auth.settings.registration_requires_verification = True
auth.settings.registration_requires_approval = False
auth.settings.reset_password_requires_verification = True
auth.messages.verify_email = 'Click on the link http://' +     request.env.http_host +     URL(r=request,c='default',f='user',args=['verify_email']) +     '/%(key)s to verify your email'
auth.messages.reset_password = 'Click on the link http://' +     request.env.http_host +     URL(r=request,c='default',f='user',args=['reset_password']) +     '/%(key)s to reset your password'

上の2つの auth.messages は、アクションの適切で完全なURLで、URLの一部の文字列を置き換える必要があるかもしれません。web2pyはプロキシの後にインストールされるかもしれず、絶対に確実な公開URLは決定できないので、これは必要なことです。とはいえ上記の例(デフォルト値)は、ほとんどの場合に動作するはずです。

認証

一度、新規ユーザが登録されると、ユーザを含む新しいグループが作成されます。新規ユーザのロールは慣習的に "user_[id]" となり、[id]は新しく作成したユーザのidが入ります。このグループ作成は、次のように無効にすることが可能です:

1
auth.settings.create_user_groups = None

とはいえ、この設定はお勧めしません。create_user_groups はブール値ではないこと(False にはセット可能)に注意してください。デフォルトは次の通りです:

1
auth.settings.create_user_groups="user_%(id)s"

これは、id ユーザ用に作成したグループ名のための、テンプレートを保持しています。

ユーザはグループのメンバーを持っています。個々のグループは、名前/ロールによって識別されます。グループは権限を持っています。所属するグループにより、ユーザは権限を持っています。デフォルトでは個々のユーザは、独自のグループのメンバーとして作成されます。

また、次のように設定できます。

1
auth.settings.everybody_group_id = 5

どの新しいユーザも、自動的にグループ番号5のメンバーにします。ここで5は例として使用されており、グループ自体は既に作成されていると仮定しています。

グループの作成と、グループのメンバーとパーミッションの割り当ては、appadmin を利用するか、以下のメソッドを使ってプログラム的に行うことができます:

1
auth.add_group('role', 'description')

これは、新しく作成したグループのidを返します。

1
auth.del_group(group_id)

これは、group_idを持つグループを削除します。

1
auth.del_group(auth.id_group('user_7'))

これは、"user_7" のロールを持つグループ、つまり、7番のユーザに一意に関連付けられたグループを削除します。

1
auth.user_group(user_id)

これは、user_id で特定されるユーザに一意に関連付けられたグループのidを返します。

1
auth.add_membership(group_id, user_id)

これは、group_id グループのメンバーを user_id に与えます。 user_id が指定されない場合、現在のログインユーザのidが使用されます。

1
auth.del_membership(group_id, user_id)

これは、group_id グループへの user_id のメンバーシップを取り消します。 user_id が指定されない場合、現在のログインユーザのidが使用されます。

1
auth.has_membership(group_id, user_id, role)

これは user_id が、group_id グループか、もしくは特定のロールを持つグループに属するメンバーシップがどうかを確認します。group_idrole の両方ではなく、どちらかを指定してください。user_id が指定されない場合、現在のログインユーザのidが使用されます。

1
auth.add_permission(group_id, 'name', 'object', record_id)

これは、(ユーザ定義の) "object" というオブジェクトに対する(ユーーザ定義の) "name" というパーミンションを、group_id グループのメンバーに与えます。"object" がテーブル名の場合、record_id がゼロならばテーブル全体に対して、record_id がゼロより大きい場合は特定のレコードにだけにパーミッションを与えます。パーミッションをテーブルに与えた場合は、('create', 'read', 'update', 'delete', 'select') の中からパーミッション名を利用するのが一般的です。CRUD API はこれらのパーミッション名を元に、認可を制御します。

group_id の指定がゼロの場合、現在のログインユーザに関連付けられているユニークなグループが使用されます。

また、auth.id_group(role="...") を使用すると、指定したロール名からグループidを取得できます。

id_group

1
auth.del_permission(group_id, 'name', 'object', record_id)

これは、パーミッションを取り消します。

1
auth.has_permission('name', 'object', record_id, user_id)

これは、user_id で特定されるユーザが、指定したパーミッションを持つグループのメンバーかどうかを確認します。

1
rows = db(auth.accessible_query('read', db.mytable, user_id))    .select(db.mytable.ALL)

これは、"mytable" テーブルの中の user_id ユーザが"read"パーミッションを持つ行を、全て返します。user_id の指定がない場合、現在のログインユーザのidが使用されます。accessible_query(...) は他のクエリと組み合わて、より複雑なものを作ることができます。accessible_query(...) はJOINを使う唯一の Auth のメソッドです。そのため、Google App Engineでは動作しません。

以下はコード例で使用する定義です:

1
2
3
4
5
6
>>> from gluon.tools import Auth
>>> auth = Auth(db)
>>> auth.define_tables()
>>> secrets = db.define_table('document', Field('body'))
>>> james_bond = db.auth_user.insert(first_name='James',
                                     last_name='Bond')

認可に関するコード例です:

1
2
3
4
5
6
7
8
>>> doc_id = db.document.insert(body = 'top secret')
>>> agents = auth.add_group(role = 'Secret Agent')
>>> auth.add_membership(agents, james_bond)
>>> auth.add_permission(agents, 'read', secrets)
>>> print auth.has_permission('read', secrets, doc_id, james_bond)
True
>>> print auth.has_permission('update', secrets, doc_id, james_bond)
False

デコレータ

パーミッションを確認する最も一般的な方法は、上記のメソッドを明示的に呼び出すのではありません。関数にデコレータを指定し、ログインしている訪問者に対してパーミッションのチェックをするようにします。以下、いくつか例を示します:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
def function_one():
    return 'this is a public function'

@auth.requires_login()
def function_two():
    return 'this requires login'

@auth.requires_membership('agents')
def function_three():
    return 'you are a secret agent'

@auth.requires_permission('read', secrets)
def function_four():
    return 'you can read secret documents'

@auth.requires_permission('delete', 'any file')
def function_five():
    import os
    for file in os.listdir('./'):
        os.unlink(file)
    return 'all files deleted'

@auth.requires(auth.user_id==1 or request.client=='127.0.0.1', requires_login=True)
def function_six():
    return 'you can read secret documents'

@auth.requires_permission('add', 'number')
def add(a, b):
    return a + b

def function_seven():
    return add(3, 4)

@auth.requires(condition) の condition引数は、呼び出し可能にすることができます。そして、conditionが単純である場合を除き、必要に応じて条件を評価するより早くなるため、条件を渡すより呼び出し可能を渡した方が良いです。例えば、

1
2
3
@auth.requires(lambda: check_condition())
def action():
    ....

@auth.requires は、デフォルト値が True のオプションの引数 requires_login を取ります。もし False を指定した場合、true/false のようにcondition引数を評価する前に、ログインを要求しません。condition引数はブール値か、ブール値を返す関数を指定します。

最初の一つ以外の全ての関数は、訪問者がパーミッションを持っているか持っていないかによってアクセスを制限することに、注意してください。

訪問者がログインしていない場合、パーミッションの確認はできません。このためログインページにリダイレクトされ、ログイン後にパーミッションが必要なページに戻ります。

権限要求の組み合わせ

権限要求を組み合わせることが必要になる場合があります。これは真か偽かの条件を一つの引数で設定する、汎用的な requires デコレータによって行うことができます。例えば、agentsにアクセス権限を与えるが、火曜日のみにする場合は次のようにします:

1
2
3
@auth.requires(auth.has_membership(group_id='agents')               and request.now.weekday()==1)
def function_seven():
    return 'Hello agent, it must be Tuesday!'

次の書き方もできます:

1
2
3
@auth.requires(auth.has_membership(role='Secret Agent')                and request.now.weekday()==1)
def function_seven():
    return 'Hello agent, it must be Tuesday!'

権限とCRUD

デコレータと and/or を使って明示的なチェックすることは、アクセス制御の一つの実装方法です。

もう一つの実装方法は、データベースアクセスに常にCRUDを使う(SQLFORM ではなく)ことによって、データベーステーブルやレコードに対するアクセス制御をCRUDから実行することです。これは次の命令文を用いて、Auth とCRUDを紐づけることで実施します。

1
crud.settings.auth = auth

これは、訪問者が明示的なアクセス権を持ってログインしていない限り、いかなるCRUD関数へのアクセスも防ぎます。例えば、訪問者はコメントを投稿できるが、自分自身のコメントしか更新できないようにするには、次のようにします(crud、authおよびdb.commentが定義されているものとします):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def give_create_permission(form):
    group_id = auth.id_group('user_%s' % auth.user.id)
    auth.add_permission(group_id, 'read', db.comment)
    auth.add_permission(group_id, 'create', db.comment)
    auth.add_permission(group_id, 'select', db.comment)

def give_update_permission(form):
    comment_id = form.vars.id
    group_id = auth.id_group('user_%s' % auth.user.id)
    auth.add_permission(group_id, 'update', db.comment, comment_id)
    auth.add_permission(group_id, 'delete', db.comment, comment_id)

auth.settings.register_onaccept = give_create_permission
crud.settings.auth = auth

def post_comment():
   form = crud.create(db.comment, onaccept=give_update_permission)
   comments = db(db.comment).select()
   return dict(form=form, comments=comments)

def update_comment():
   form = crud.update(db.comment, request.args(0))
   return dict(form=form)

('read' 権限を持つ)特定のレコードを選択することもできます:

1
2
3
4
5
def post_comment():
   form = crud.create(db.comment, onaccept=give_update_permission)
   query = auth.accessible_query('read', db.comment, auth.user.id)
   comments = db(query).select(db.comment.ALL)
   return dict(form=form, comments=comments)

次の設定で強制されるパーミッションの名前は:

1
crud.settings.auth = auth

"read"、"create"、"update"、"delete"、"select"、"impersonate" になります。

認可とダウンロード

デコレータや crud.settings.auth を使用しても、通常のダウンロード関数を使ったファイルダウンロードの認可は実施されません。

1
def download(): return response.download(request, db)

ファイルダウンロードでの認可を実施する場合、ダウンロードの際にアクセス制御が必要なファイルが含まれる "upload" フィールドに、明示的に宣言しなければなりません。

1
2
3
4
5
db.define_table('dog',
   Field('small_image', 'upload'),
   Field('large_image', 'upload'))

db.dog.large_image.authorization = lambda record:    auth.is_logged_in() and    auth.has_permission('read', db.dog, record.id, auth.user.id)

uploadフィールドの authorization 属性は、None(デフォルト)とするか、関数を指定することができます。関数はユーザがログインしているかどうか、そして現在の行に対する 'read' パーミッションを持っているかどうかを判断します。この例では、"small_image" フィールドにリンクされた画像のダウンロードには何の制限もかけられていません。しかし "large_image" フィールドにリンクされた画像には、アクセス制御が掛けられています。

アクセス制御とベーシック認証

アクセス制御を行うデコレータが設定されている機能を、サービスとして公開しなければならない場合があります。すなわち、プログラムやスクリプトから呼び出された場合でも、認証と権限チェックを行なえることが必要な場合です。

Auth は、ベーシック認証によるログインが可能になります:

1
auth.settings.allow_basic_login = True

上記の設定と共に、次のようなアクションを定義すると、

1
2
3
4
@auth.requires_login()
def give_me_time():
    import time
    return time.ctime()

例えばシェルコマンドから、機能を呼び出すことができます:

1
2
wget --user=[username] --password=[password]
    http://.../[app]/[controller]/give_me_time

@auth デコレータを使用するのではなく、auth.basic() を呼び出しログインすることも可能です:

1
2
3
4
5
6
7
def give_me_time():
    import time
    auth.basic()
    if auth.user:
        return time.ctime()
    else:
        return 'Not authorized'

ベーシック認証でのログインは、多くの場合、サービス(次章で説明します)のための唯一のオプションです。しかし、デフォルトは無効になっています。

マニュアル認証

独自ロジックの実装や "マニュアル" によるログインを行いたい場合が稀にあります。 このような時、次の関数を呼び出すことができます:

1
user = auth.login_bare(username,password)

login_bare はユーザが存在しパスワードが正しい時、ユーザ(レコード)を返します。そうでない場合、Falseを返します。また、"auth_user" テーブルに "username" フィールドがない場合、username はemailになります。

設定とメッセージ

ここでは Auth のカスタマイズが可能な、全てのパラメータを示します。

次のパラメタには、gluon.tools.Mail オブジェクトを指定してください。これにより、auth によるメールの送信が可能になります:

1
auth.settings.mailer = None

次のパラメタには、user の機能を定義するコントローラの名前を指定してください:

1
auth.settings.controller = 'default'

次のパラメタは、とても重要な設定です:

1
auth.settings.hmac_key = None

"sha512:a-pass-phrase" のような設定をしてください。これは、auth_user テーブルの "password" フィールドのCRYPTバリデータへ渡されます。パスワードをハッシュ化するために使われる、アルゴリズムとパスフレーズになります。

最小パスワード文字数のデフォルト値は4です。これは変更可能です:

1
auth.settings.password_min_length = 4

user関数の特定アクションを無効にするのは、アクション名を次のリストに追加します:

1
auth.settings.actions_disabled = []

例えば:

1
auth.settings.actions_disabled.append('register')

ユーザ登録を無効にします。

ユーザ登録確認のために、メール受信するには、次を True にします:

1
auth.settings.registration_requires_verification = False

ユーザ登録後、自動でログインするには、次を True にします。この場合、メールによるユーザ登録確認プロセス中であっても、自動ログインします:

1
auth.settings.login_after_registration = False

新規ユーザで、管理者の承認を待ってからログインする場合は、次を True にします:

1
auth.settings.registration_requires_approval = False

承認するにはappadminを使用するか、プログラムなどを使って、registration_key=='' に変更します。

新規のユーザに、ユーザ専用の新しいグループを作成しないようにするには、次を False にします:

1
auth.settings.create_user_groups = True

次の設定は前述のように、代替のログイン方法とログインフォームを決定します:

1
2
auth.settings.login_methods = [auth]
auth.settings.login_form = auth

ベーシック認証を使用するときは、次を True にします。

1
auth.settings.allows_basic_login = False

次の設定は、login アクションのURLです:

1
auth.settings.login_url = URL('user', args='login')

ログインしている状態で、ユーザ登録用のページにアクセスした場合は、次のURLにリダイレクトします:

1
auth.settings.logged_url = URL('user', args='profile')

プロファイルに画像がある場合に指定する、ダウンロード用のURLです:

1
auth.settings.download_url = URL('download')

以下は auth の様々なアクションが実行された後に、(参照先が無い場合の)リダイレクト先のURLを指定しています:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
auth.settings.login_next = URL('index')
auth.settings.logout_next = URL('index')
auth.settings.profile_next = URL('index')
auth.settings.register_next = URL('user', args='login')
auth.settings.retrieve_username_next = URL('index')
auth.settings.retrieve_password_next = URL('index')
auth.settings.change_password_next = URL('index')
auth.settings.request_reset_password_next = URL('user', args='login')
auth.settings.reset_password_next = URL('user', args='login')
auth.settings.verify_email_next = URL('user', args='login')

呼び出し関数では認証が必要なのに訪問者がログインしていない場合、auth.settings.login_url にリダイレクトします。このデフォルト値は URL('default','user/login') になります。再定義することにより、この動作を置き換えることが可能です: URL('default','user/login').

on_failed_authentication
1
auth.settings.on_failed_authentication = lambda url: redirect(url)

これはリダイレクトで、呼び出される関数です。url 引数は、この関数にログインページのURLを渡します。

訪問者がアクセスに必要な十分な権限を所持していない場合、次に指定しているURLにリダイレクトされます。

on_failed_authorization
1
auth.settings.on_failed_authorization =     URL('user',args='on_failed_authorization')

この変数を変更し、ユーザを他の場所にリダイレクトさせることも可能です。

多くの場合、on_failed_authorization はURLです。しかしアクセスが失敗した場合に呼び出される、URLを返す関数にすることも可能です。

以下は、バリデーション実施後かつデータベースIO処理前に実行される、それぞれのアクションのコールバック関数の一覧です:

1
2
3
4
5
auth.settings.login_onvalidation = []
auth.settings.register_onvalidation = []
auth.settings.profile_onvalidation = []
auth.settings.retrieve_password_onvalidation = []
auth.settings.reset_password_onvalidation = []

各コールバックは form オブジェクトを引数とすることが必要です。これにより、データベースのIO処理前に、そのフォームオブジェクトの属性を修正することができます。

以下は、データベースIO処理後かつリダイレクト前に実行する、コールバック関数の一覧です:

1
2
3
4
auth.settings.login_onaccept = []
auth.settings.register_onaccept = []
auth.settings.profile_onaccept = []
auth.settings.verify_email_onaccept = []

設定例です:

1
2
auth.settings.register_onaccept.append(lambda form:   mail.send(to='you@example.com',subject='new user',
             message='new user email is %s'%form.vars.email))

いくつかの auth 機能に対して、キャプチャを有効にできます:

1
2
3
4
5
auth.settings.captcha = None
auth.settings.login_captcha = None
auth.settings.register_captcha = None
auth.settings.retrieve_username_captcha = None
auth.settings.retrieve_password_captcha = None

.captchagluon.tools.Recaptcha に設定されている場合、(.login_captcha などの)個別オプションが None と指定しているフォームは全て、キャプチャが有効になります。もし個別オプションの値が、False だったら、キャプチャは有効になりません。.captchaNone にセットされている場合、個別オプションに gluon.tools.Recapcha を指定している機能のみがキャプチャが有効になり、それ以外の指定が無いフォームではキャプチャは有効にはなりません。

次は、ログインセッションの有効期間です:

1
auth.settings.expiration = 3600  # seconds

パスワードフィールド名は変更することができます(例えば、Firebirdでは "password" は予約語です。このため、フィールド名として利用できません)。

1
auth.settings.password_field = 'password'

通常、ログインフォームはメールアドレスでのバリデーションを試みます。次の設定を False にすれば無効にできます:

1
auth.settings.login_email_validate = True

ユーザプロファイルの編集ページでレコードIDを表示するには、次の設定を True にします。

1
auth.settings.showid = False

フォームのカスタマイズで、自動的でエラー通知を行いたくない場合は次の設定を False にします:

1
auth.settings.hideerror = False

フォームのスタイルを変更したい場合は、次のようにします:

1
auth.settings.formstyle = 'table3cols'

(この値は "table2cols"、"divs" そして "ul" を指定することが可能です) (it can be "table2cols", "divs" and "ul")

authが生成する、フォームのラベル区切り記号を設定できます:

1
auth.settings.label_separator =        ':'

"remember me" オプションによって、自動でログイン状態を維持することが可能です。この機能はデフォルトで有効になっています。次の設定によって、自動ログインの有効期限の変更や、自動ログインのオプション自体を無効にすることができます:

1
2
auth.settings.long_expiration = 3600*24*30 # one month
auth.settings.remember_me_form = True

次のメッセージをカスタマイズすることができます。その用途や場面は明らかなため、特に説明はしません:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
auth.messages.submit_button = 'Submit'
auth.messages.verify_password = 'Verify Password'
auth.messages.delete_label = 'Check to delete:'
auth.messages.function_disabled = 'Function disabled'
auth.messages.access_denied = 'Insufficient privileges'
auth.messages.registration_verifying = 'Registration needs verification'
auth.messages.registration_pending = 'Registration is pending approval'
auth.messages.login_disabled = 'Login disabled by administrator'
auth.messages.logged_in = 'Logged in'
auth.messages.email_sent = 'Email sent'
auth.messages.unable_to_send_email = 'Unable to send email'
auth.messages.email_verified = 'Email verified'
auth.messages.logged_out = 'Logged out'
auth.messages.registration_successful = 'Registration successful'
auth.messages.invalid_email = 'Invalid email'
auth.messages.unable_send_email = 'Unable to send email'
auth.messages.invalid_login = 'Invalid login'
auth.messages.invalid_user = 'Invalid user'
auth.messages.is_empty = "Cannot be empty"
auth.messages.mismatched_password = "Password fields don't match"
auth.messages.verify_email = ...
auth.messages.verify_email_subject = 'Password verify'
auth.messages.username_sent = 'Your username was emailed to you'
auth.messages.new_password_sent = 'A new password was emailed to you'
auth.messages.password_changed = 'Password changed'
auth.messages.retrieve_username = 'Your username is: %(username)s'
auth.messages.retrieve_username_subject = 'Username retrieve'
auth.messages.retrieve_password = 'Your password is: %(password)s'
auth.messages.retrieve_password_subject = 'Password retrieve'
auth.messages.reset_password = ...
auth.messages.reset_password_subject = 'Password reset'
auth.messages.invalid_reset_password = 'Invalid reset password'
auth.messages.profile_updated = 'Profile updated'
auth.messages.new_password = 'New password'
auth.messages.old_password = 'Old password'
auth.messages.group_description =     'Group uniquely assigned to user %(id)s'
auth.messages.register_log = 'User %(id)s Registered'
auth.messages.login_log = 'User %(id)s Logged-in'
auth.messages.logout_log = 'User %(id)s Logged-out'
auth.messages.profile_log = 'User %(id)s Profile updated'
auth.messages.verify_email_log = 'User %(id)s Verification email sent'
auth.messages.retrieve_username_log = 'User %(id)s Username retrieved'
auth.messages.retrieve_password_log = 'User %(id)s Password retrieved'
auth.messages.reset_password_log = 'User %(id)s Password reset'
auth.messages.change_password_log = 'User %(id)s Password changed'
auth.messages.add_group_log = 'Group %(group_id)s created'
auth.messages.del_group_log = 'Group %(group_id)s deleted'
auth.messages.add_membership_log = None
auth.messages.del_membership_log = None
auth.messages.has_membership_log = None
auth.messages.add_permission_log = None
auth.messages.del_permission_log = None
auth.messages.has_permission_log = None
auth.messages.label_first_name = 'First name'
auth.messages.label_last_name = 'Last name'
auth.messages.label_username = 'Username'
auth.messages.label_email = 'E-mail'
auth.messages.label_password = 'Password'
auth.messages.label_registration_key = 'Registration key'
auth.messages.label_reset_password_key = 'Reset Password key'
auth.messages.label_registration_id = 'Registration identifier'
auth.messages.label_role = 'Role'
auth.messages.label_description = 'Description'
auth.messages.label_user_id = 'User ID'
auth.messages.label_group_id = 'Group ID'
auth.messages.label_name = 'Name'
auth.messages.label_table_name = 'Table name'
auth.messages.label_record_id = 'Record ID'
auth.messages.label_time_stamp = 'Timestamp'
auth.messages.label_client_ip = 'Client IP'
auth.messages.label_origin = 'Origin'
auth.messages.label_remember_me = "Remember me (for 30 days)"

add|del|has membership のログは、"%(user_id)s" と "%(group_id)s" の使用が可能です。 add|del|has permission のログは、"%(user_id)s"、"%(name)s"、"%(table_name)s"、及び "%(record_id)s" の使用が可能です。

Central Authentication Service

CAS
authentication

web2pyは、サードパーティ認証やシングルサインオンのサポートをしています。 ここでは業界標準で、クライアント及びサーバ共に web2py に組み込まれている、Central Authentication Service (CAS) について取り上げます。

CASは分散認証を行うためのオープンなプロトコルです。次のように動作します。訪問者がWebサイトにアクセスした時、アプリケーションはユーザが既に認証されているかどうかをセッションを使ってチェックします(例、session.token オブジェクトを経由)。もし認証されていない場合、コントローラはCASアプライアンスからユーザをリダイレクトします。リダイレクト先は、ユーザがログイン、ユーザ登録、認証情報の管理(名前、メールアドレス、パスワード)ができるところです。 ユーザ登録をした場合は、そのユーザが登録確認メールに対応するまで登録を保留します。登録が完了しログインすると、CASアプライアンスはキーとともにユーザをアプリケーションへリダイレクトします。アプリケーションではこのキーを使って、ユーザの認証情報を取得します。これはバックグランドで、CASサーバとのHTTPリクエストを介して実行します。

この仕組みを使うと、複数のアプリケーションが一つのCASサーバを通してシングルサインオンを使用することができます。認証を提供するサーバは、サービスプロバイダと呼びます。訪問者を認証しようとするアプリケーションは、サービスコンシューマと呼びます。

CASはOpenIDと似ていますが、大きな違いが一つあります。OpenIDの場合、訪問者はサービスプロバイダを選択することができます。CASの場合は、アプリケーションがサービスプロバイダを選択します。そのため、CASはより安全です。

web2pyのCASプロバイダを実行するのは、ひな形アプリケーションをコピーするのと同じくらい簡単です。実際にアクションを公開したweb2pyアプリケーションを、次に示します。

## in provider app
def user(): return dict(form=auth())

CAS 2.0 プロバイダです。そのサービスはURLでアクセスできます。

1
2
3
http://.../provider/default/user/cas/login
http://.../provider/default/user/cas/validate
http://.../provider/default/user/cas/logout

(アプリケーションは "provider" と呼ばれると仮定しています)

プロバイダに対するシンプルな代理認証によって、他のWebアプリケーション(コンシューマ)からサービスにアクセス可能です:

1
2
## in consumer app
auth = Auth(db,cas_provider = 'http://127.0.0.1:8000/provider/default/user/cas')

コンシューマ・アプリケーションのログインURLにアクセスすると、認証の働きをするプロバイダ・アプリケーションにリダイレクトし、さらにコンシューマにリダイレクトで戻ります。ユーザ登録、ログアウト、パスワード変更、パスワードの再確認などのすべての処理は、プロバイダアプリケーション上で完結します。拡張フィールドやローカルプロファイルのために、コンシューマにログインユーザのエントリが作成されます。幸いのことに CAS 2.0 は、全てのフィールドはプロバイダ上で読み取り可能です。そして コンシューマの auth_user テーブルに相当するフィールドは、自動でコピーされます。

Auth(...,cas_provider='...') はサードパティのプロバイダと共に動作し、CAS 1.0 及び 2.0 をサポートします。バージョンの検出は自動で行います。デフォルトでは追加することにより、ベース(上記の cas_provider URL)からプロバイダのURLを生成します。

1
2
3
/login
/validate
/logout

次の設定は、コンシューマとプロバイダで変更できます。

## in consumer or provider app (must match)
auth.settings.cas_actions['login']='login'
auth.settings.cas_actions['validate']='validate'
auth.settings.cas_actions['logout']='logout'

別ドメインからweb2py CASプロバイダに接続する場合は、許可ドメインのリストに追加する必要があります:

1
2
## in provider app
auth.settings.cas_domains.append('example.com')

web2pyを使用した、非web2pyアプリケーションの認可

これは可能ですが、Webサーバに依存します。 前提として、2つのアプリケーションは同じ mod_wsgi を使った Apatchサーバで動作するとします。 一方のアプリケーションは Auth を使用したアクセス制御を行う web2py アプリケーションです。 もう一方は CGIスクリプトのPHPプログラムなどです。 後者のアプリケーションにアクセス要求があった時、Webサーバは前者にパーミッションを問い合わせるようにします。

最初にweb2pyアプリケーションを修正して、次のコントローラを追加する必要があります:

1
2
def check_access():
    return 'true' if auth.is_logged_in() else 'false'

このコントローラから true が返ってくる時はユーザはログインしています。false の時はそうでない時です。このweb2pyのプロセスはバックグランドで動作しています:

nohup python web2py.py -a '' -p 8002

ポート8002 は必須です。adminの有効化とadminパスワードは必要ありません。

ここで Apacheの設定ファイル(例 "/etc/apache2/sites-available/default")を修正します。非web2pyプログラムの動作時に上記のチェックプログラムを呼び出し、true だった場合はレクエストに対するレスポンスを返し、他の場合はアクセス拒否を行うようにします。

web2pyと非web2pyはアプリケーションは同一ドメインで実行されます。このためユーザがweb2pyアプリケーションにログインしている場合は、他のアプリケーションからの要求であってもweb2pyのセッションクッキーをApacheに渡すため、認証情報の確認が可能になります。

これを実施するためには、"web2py/scripts/access.wsgi" スクリプトが必要になります。 web2pyでは、このスクリプトが付属しています。アクセス制御が必要なアプリケーションのURLとスクリプトの位置をApacthに知らせて、このスクリプトを呼び出す必要があります:

<VirtualHost *:80>
   WSGIDaemonProcess web2py user=www-data group=www-data
   WSGIProcessGroup web2py
   WSGIScriptAlias / /home/www-data/web2py/wsgihandler.py

   AliasMatch ^myapp/path/needing/authentication/myfile /path/to/myfile
   <Directory /path/to/>
     WSGIAccessScript /path/to/web2py/scripts/access.wsgi
   </Directory>
</VirtualHost>

ここで "^myapp/path/needing/authentication/myfile" は、受信レクエストとweb2pyフォルダの絶対パス "/path/to/" をマッチさせる正規表現になっています。

"access.wsgi" スクリプトは次の行を含んでいます:

URL_CHECK_ACCESS = 'http://127.0.0.1:8002/%(app)s/default/check_access'

これは、web2pyアプリケーションを指定してます。しかし、8002以外のポートで実行している特定のアプリケーションを指定するよう、編集することが可能です。

また、check_access() 機能を変更し、ロジックをより複雑にすることができます。この機能は環境変数を使用し、最初のリクエストのURLを取得することができます

request.env.request_uri

より複雑なルールを実装できます:

1
2
3
4
5
6
7
def check_access():
    if not auth.is_logged_in():
       return 'false'
    elif not user_has_access(request.env.request_uri):
       return 'false'
    else:
       return 'true'
第3版 - 翻訳: 中垣健志 レビュー: 細田謙二
第4版 - 翻訳: Hitoshi Kato レビュー: Omi Chiba
第5版 - 翻訳: Hitoshi Kato レビュー: Omi Chiba
 top