(機械翻訳)認証の追加

Pyramidauthenticationauthorization のための機能を提供します。このセクションでは、ログインAPIとログアウト機能をWikiに追加するための認証APIのみに焦点を当てます。

次の手順で認証を実装します。

  • 認証ポリシー`と ` request.user``の計算されたプロパティー( `` security.py``)を追加してください。
  • `` /login``と `` /logout``( `` routes.py``)のルートを追加します。
  • ログインビューとログアウトビューを追加します( `` views /auth.py``)。
  • ログインテンプレートを追加します( `` login.jinja2``)。
  • ユーザーの認証済み状態( `` layout.jinja2``)に基づいて、すべてのページに"ログイン"と"ログアウト"リンクを追加します。
  • 既存のビューがユーザの状態を確認するようにします( `` views /default.py``)。
  • デフォルトの "403 Forbidden"ページ( `` views /auth.py``)ではなく、アクセス権が必要なビューへのアクセスがユーザから拒否された場合、 `` /login``に??リダイレクトします。

リクエストの認証

The core of Pyramid authentication is an authentication policy which is used to identify authentication information from a request, as well as handling the low-level login and logout operations required to track users across requests (via cookies, headers, or whatever else you can imagine).

認証ポリシーを追加する

以下の内容の `` tutorial /security.py``ファイルを新規作成してください:

 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
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy

from .models import User


class MyAuthenticationPolicy(AuthTktAuthenticationPolicy):
    def authenticated_userid(self, request):
        user = request.user
        if user is not None:
            return user.id

def get_user(request):
    user_id = request.unauthenticated_userid
    if user_id is not None:
        user = request.dbsession.query(User).get(user_id)
        return user

def includeme(config):
    settings = config.get_settings()
    authn_policy = MyAuthenticationPolicy(
        settings['auth.secret'],
        hashalg='sha512',
    )
    config.set_authentication_policy(authn_policy)
    config.set_authorization_policy(ACLAuthorizationPolicy())
    config.add_request_method(get_user, 'user', reify=True)

ここでは、

  • Pyramidの pyramid.authentication.AuthTktAuthenticationPolicy からサブクラス化された` MyAuthenticationPolicy``という名前の新しい認証ポリシーは、署名されたクッキーを使って :term:`userid を追跡します(7-11行目)。 userid
  • `` unauthenticated_userid``をポリシーから `` User``オブジェクトに変換する `` get_user``関数です(13-17行目)。
  • `` get_user``は、ログインしたユーザのために認証された `` User``オブジェクトとしてアプリケーション全体で使用される `` request.user``としてリクエストに登録されます(27行目)。

このファイルのロジックは少し面白いので、ここで何が起こっているのか詳しく説明します:

最初に、デフォルトの認証ポリシーはすべて、 `` unauthenticated_userid``という名前のメソッドを提供します。このメソッドは、リクエスト内の情報の低レベル解析(クッキー、ヘッダーなど)を担当します。 `` userid``が見つかった場合、このメソッドから返されます。最も低いレベルでは、それはクッキー内のユーザIDの値を知っているので、実際には私たちのシステム内のユーザであるかどうかはわからないので、これは `` unauthenticated_userid``という名前です(覚えておいて、信頼できません)。

第二に、アプリケーションは `` authenticated_userid``と `` request.user``だけを気にする必要があります。これはユーザーがログインしていることを検証するアプリケーション固有のプロセスを経ています。

`` authenticated_userid``を提供するためには、検証のステップが必要です。これはどこでも発生する可能性があるので、キャッシュされた `` request.user``の計算されたプロパティの中で実行することにしました。これは、 `` request.user``をシステムの真理の源にする便利な機能です。私たちのデータベースの `` None``か `` User``オブジェクトです。これは、 `` get_user``関数が `` unauthenticated_userid``を使ってデータベースをチェックする理由です。

アプリを設定する

新しい `` tutorial /security.py``モジュールを追加したので、それをインクルードする必要があります。ファイル `` tutorial /__ init __。py``を開き、以下の行を編集してください:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from pyramid.config import Configurator


def main(global_config, **settings):
    """ This function returns a Pyramid WSGI application.
    """
    config = Configurator(settings=settings)
    config.include('pyramid_jinja2')
    config.include('.models')
    config.include('.routes')
    config.include('.security')
    config.scan()
    return config.make_wsgi_app()

私たちの認証方針は新しい設定 `` auth.secret``を期待しています。ファイル `` development.ini``を開き、以下の強調表示された行を追加してください:

19
20
21
retry.attempts = 3

auth.secret = seekrit

最後に、ベストプラクティスでは、生産のために別の秘密を使用するように指示するので、 `` production.ini``を開き、別の秘密を追加してください:

17
18
19
retry.attempts = 3

auth.secret = real-seekrit

権限チェックを追加する

Pyramid は宣言的承認を完全にサポートしています。これについては次の章で説明します。しかし、足を濡らすことを望んでいる多くの人々は、自家認証のいくつかの基本的な形で認証に興味を持っています。以下に、wikiの単純なセキュリティ目標を達成する方法を示します。これで、ユーザーのログイン状態を追跡できるようになります。

私たちの目標を思い出してください:

  • ログインしたユーザーが `` editor``と `` basic``のみ新しいページを作成できるようにしてください。
  • `` editor``ユーザとページ作成者(おそらく `` basic``ユーザ)のみがページを編集できるようにしてください。

`` tutorial/views/default.py``ファイルを開き、以下のインポートを修正してください:

 5
 6
 7
 8
 9
10
11
12
13
from pyramid.httpexceptions import (
    HTTPForbidden,
    HTTPFound,
    HTTPNotFound,
    )

from pyramid.view import view_config

from ..models import Page

強調表示されている2つの行を変更します。

同じファイル内で、 `` edit_page``ビュー関数を編集してください:

45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@view_config(route_name='edit_page', renderer='../templates/edit.jinja2')
def edit_page(request):
    pagename = request.matchdict['pagename']
    page = request.dbsession.query(Page).filter_by(name=pagename).one()
    user = request.user
    if user is None or (user.role != 'editor' and page.creator != user):
        raise HTTPForbidden
    if 'form.submitted' in request.params:
        page.data = request.params['body']
        next_url = request.route_url('view_page', pagename=page.name)
        return HTTPFound(location=next_url)
    return dict(
        pagename=page.name,
        pagedata=page.data,
        save_url=request.route_url('edit_page', pagename=page.name),
        )

強調表示されている行だけを変更する必要があります。

ユーザーがログインしていないか、ユーザーがページの作成者*ではなく、エディタではない場合は、 `` HTTPForbidden ''を生成します。

同じファイルで、 `` add_page``ビュー関数を編集してください:

62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
@view_config(route_name='add_page', renderer='../templates/edit.jinja2')
def add_page(request):
    user = request.user
    if user is None or user.role not in ('editor', 'basic'):
        raise HTTPForbidden
    pagename = request.matchdict['pagename']
    if request.dbsession.query(Page).filter_by(name=pagename).count() > 0:
        next_url = request.route_url('edit_page', pagename=pagename)
        return HTTPFound(location=next_url)
    if 'form.submitted' in request.params:
        body = request.params['body']
        page = Page(name=pagename, data=body)
        page.creator = request.user
        request.dbsession.add(page)
        next_url = request.route_url('view_page', pagename=pagename)

強調表示されている行だけを変更する必要があります。

ユーザーがログインしていないか、 `` basic``または `` editor``の役割にない場合、 `` HTTPForbidden`` を生成し、ユーザーに「403 Forbidden」応答を返します。しかし、これを後でフックしてログインページにリダイレクトします。また、 `` request.user`` ができたので、作成者を editor のユーザとしてハードコーディングする必要がなくなり、最終的にそのハックを取り除くことができます。

これらの単純なチェックは私たちの見解を保護するはずです。

ログイン、ログアウト

ログインユーザーを検出できるようになったので、実際にログインしてログアウトできるように、 `` /login``と `` /logout``ビューを追加する必要があります。

`` /login``と `` /logout``のルートを追加する

`` tutorial /routes.py``に戻り、次の2つのルートをハイライト表示します:

3
4
5
6
    config.add_route('view_wiki', '/')
    config.add_route('login', '/login')
    config.add_route('logout', '/logout')
    config.add_route('view_page', '/{pagename}')

注釈

上記の行は、以下の `` view_page``ルート定義の前に追加する必要があります:

6
    config.add_route('view_page', '/{pagename}')

これは、 `` view_page``のルート定義がcatch-all replacement marker /{pagename} ((参照 ルートパターンの構文 )を使用していて、その前に登録されているどのルートでも捕らえられます。したがって、 `` login``と `` logout`` ビューは一致する(またはキャッチされます)機会を持つためには、 `` /{pagename} `` の上になければなりません。

ログイン、ログアウト、禁止されたビューを追加する

新しいファイル `` tutorial /views /auth.py``を作成し、次のコードを追加します:

 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
from pyramid.httpexceptions import HTTPFound
from pyramid.security import (
    remember,
    forget,
    )
from pyramid.view import (
    forbidden_view_config,
    view_config,
)

from ..models import User


@view_config(route_name='login', renderer='../templates/login.jinja2')
def login(request):
    next_url = request.params.get('next', request.referrer)
    if not next_url:
        next_url = request.route_url('view_wiki')
    message = ''
    login = ''
    if 'form.submitted' in request.params:
        login = request.params['login']
        password = request.params['password']
        user = request.dbsession.query(User).filter_by(name=login).first()
        if user is not None and user.check_password(password):
            headers = remember(request, user.id)
            return HTTPFound(location=next_url, headers=headers)
        message = 'Failed login'

    return dict(
        message=message,
        url=request.route_url('login'),
        next_url=next_url,
        login=login,
        )

@view_config(route_name='logout')
def logout(request):
    headers = forget(request)
    next_url = request.route_url('view_wiki')
    return HTTPFound(location=next_url, headers=headers)

@forbidden_view_config()
def forbidden_view(request):
    next_url = request.route_url('login', _query={'next': request.url})
    return HTTPFound(location=next_url)

このコードは、アプリケーションに3つの新しいビューを追加します。

  • `` login`` ビューはログインフォームをレンダリングし、ログインフォームからの投稿を処理し、データベースの `` users`` テーブルに対して資格をチェックします。

    チェックは、まずデータベース内の `` User`` レコードを見つけてから、 `` user.check_password`` メソッドを使ってハッシュされたパスワードを比較して行います。

    クレデンシャルが有効な場合は、私たちの認証ポリシーを使用して pyramid.security.remember() を使用してユーザーのIDをレスポンスに格納します。

    最後に、ユーザーは、アクセスしようとしていたページ( `` next``)またはフォールバックとしてフロントページにリダイレクトされます。このパラメータは、以下に説明するように、ログインワークフローを終了するために、禁止されたビューで使用されます。

  • `` logout``ビューは pyramid.security.forget`を使ってクレデンシャルをクリアし、フロントページにリダイレクトすることで `()/logout`` へのリクエストを処理します。

  • forbidden_viewpyramid.httpexceptions.HTTPForbidden デコレータを使って登録されています。これは special exception view で、pyramid.httpexceptions.HTTPForbidden 例外が発生したときに呼び出されます。

    このビューは、ユーザを /login にリダイレクトすることによって禁じられたエラーを処理します。便宜上、 next= クエリ文字列を現在のURL(アクセスを禁止しているURL)に設定します。このようにして、ユーザーが正常にログインすると、アクセスしようとしていたページに戻ります。

`` login.jinja2``テンプレートを追加してください

以下の内容の `` tutorial /templates /login.jinja2`` を作成してください:

{% extends 'layout.jinja2' %}

{% block title %}Login - {% endblock title %}

{% block content %}
<p>
<strong>
    Login
</strong><br>
{{ message }}
</p>
<form action="{{ url }}" method="post">
<input type="hidden" name="next" value="{{ next_url }}">
<div class="form-group">
    <label for="login">Username</label>
    <input type="text" name="login" value="{{ login }}">
</div>
<div class="form-group">
    <label for="password">Password</label>
    <input type="password" name="password">
</div>
<div class="form-group">
    <button type="submit" name="form.submitted" value="Log In" class="btn btn-default">Log In</button>
</div>
</form>
{% endblock content %}

上記のテンプレートは、 `` tutorial /views /auth.py`` で追加したログインビューで参照されています。

ブラウザでのアプリケーションの表示

ブラウザでアプリケーションを調べることができます( アプリケーションを起動する(Start the application) を参照してください)。ブラウザを起動し、次の各URLにアクセスして、結果が期待どおりであることを確認します。

  • http://localhost:6543/ invokes the view_wiki view. This always redirects to the view_page view of the FrontPage page object. It is executable by any user.
  • http://localhost:6543/FrontPageは `` FrontPage``ページオブジェクトの `` view_page``ビューを呼び出します。ユーザーが認証されていないときは右上に&quot;Login &quot;リンクがあり、認証されていない場合は&quot;Logout &quot;リンクです。
  • http://localhost:6543/FrontPage/edit_pageは、 `` FrontPage``ページオブジェクトの `` edit_page``ビューを呼び出します。 `` editor``ユーザだけが実行可能です。別のユーザー(または匿名ユーザー)がそれを呼び出すと、ログインフォームが表示されます。ユーザー名 `` editor``とパスワード `` editor``を入力すると、編集ページのフォームが表示されます。
  • http://localhost:6543/add_page/SomePageNameはページの `` add_page``ビューを呼び出します。ページがすでに存在する場合は、ページオブジェクトの `` edit_page``ビューにユーザをリダイレクトします。これは、 `` editor``または `` basic``ユーザのいずれかによって実行可能です。別のユーザー(または匿名ユーザー)がそれを呼び出すと、ログインフォームが表示されます。ユーザ名 `` editor``とパスワード `` editor``、またはユーザ名 `` basic``とパスワード `` basic``のいずれかで証明書を入力すると、編集ページのフォームが表示されます。
  • http://localhost:6543/SomePageName/edit_pageは、既存のページの `` edit_page``ビューを呼び出し、ページが存在しない場合はエラーを生成します。これは、前の手順でそのユーザがページを作成した場合、 `` basic``ユーザによって編集可能です。代わりに、 `` editor``ユーザがページを作成した場合、 `` basic``ユーザのログインページが表示されます。
  • ログインした後(編集やページを追加し、 `` editor``の資格情報を使ってログインフォームを送信した結果)、右上にLogoutというリンクが表示されます。クリックするとログアウトされ、フロントページにリダイレクトされ、右上にLoginリンクが表示されます。