認証を追加する

Pyramidauthenticationauthorization の ための機能を提供しています。アプリケーションにセキュリティを追加する ためにこれら両方の機能を利用します。私たちのアプリケーションは現在、 サーバーにアクセスできる誰もが wiki ページを見たり、編集したり、 追加したりすることが可能です。 wiki ページの追加や編集を group:editors という名前の グループ のメンバーである限られた人々だけに許可するように アプリケーションを変更してみましょう。しかし、依然としてサーバーに アクセスできる誰でもページを見ることは可能です。

さらに、ログインページとすべてのページにログアウトリンクを追加します。 ログインページは、ユーザがパーミッションを必要とする任意のビューへのアクセスを 拒否された場合に、デフォルトの “403 Forbidden” ページの代わりに表示されます。

次のステップでアクセス制御を実装していきます。

  • ユーザとグループを追加 (security.py という新しいモジュール)
  • ACL を追加 (models.py__init__.py)
  • authentication policyauthorization policy を追加 (__init__.py)
  • edit_page および add_page ビューに permission 宣言を 追加 (views.py)

次にログインとログアウトの機能を追加します:

  • /login と /logout に対する route を追加 (__init__.py)
  • login および logout ビューを追加 (views.py)
  • ログインテンプレートを追加 (login.pt)
  • 既存のビューがレンダラーに logged_in フラグを返すように修正 (views.py)
  • “Logout” リンクを追加して、ログイン中にページを閲覧または編集しているときに 表示されるようにする (view.pt, edit.pt)

このチュートリアルステージのソースコードを以下の場所で閲覧することができます。 http://github.com/Pylons/pyramid/tree/1.3-branch/docs/tutorials/wiki2/src/authorization/.

アクセス制御

ユーザとグループの追加

新しい tutorial/tutorial/security.py モジュールを以下の内容で作成します:

1
2
3
4
5
6
7
USERS = {'editor':'editor',
          'viewer':'viewer'}
GROUPS = {'editor':['group:editors']}

def groupfinder(userid, request):
    if userid in USERS:
        return GROUPS.get(userid, [])

groupfinder 関数は、 userid と request を受け取って、以下のどちらかを 返します:

  • userid がシステムに存在すれば、グループ識別子のシーケンス (ユーザがどのグループのメンバーでもなければ空のシーケンス) を返します。
  • userid がシステムに存在 しなければNone を返します。

例えば、 groupfinder('editor', request ) は [‘group:editor’] を返し、 groupfinder('viewer', request) は [] を、 groupfinder('admin', request)None を返します。 あとで、ユーザに principal (複数可) を提供する authentication policy “コールバック” として groupfinder() を使用します。

プロダクションシステムでは、ユーザとグループデータは、ほとんどの場合 データベースから取得されますが、ここではユーザとグループのソースを表わすために 「ダミーの」データを使用します。

ACL の追加

tutorial/tutorial/models.py を開いて先頭に以下のインポート文を 追加してください:

1
2
3
4
from pyramid.security import (
    Allow,
    Everyone,
    )

以下のクラス定義を追加してください:

1
2
3
4
5
class RootFactory(object):
    __acl__ = [ (Allow, Everyone, 'view'),
                (Allow, 'group:editors', 'edit') ]
    def __init__(self, request):
        pass

Allow (パーミッションが許可されることを意味す るアクション) と Everyone (すべてのリクエスト に関連付けられる特別な principal) をインポートします。両者は ACL を構成するために ACE エントリの中で使用されます。

ACL はリストで、 __acl__ という名前のクラス属性である必要があります。 ここでは2つの ACE エントリを持つ ACL を定義しています: 1番目のエントリは、あらゆるユーザに view パーミッションを与えます。 2番目のエントリは、 group:editors principal に edit パーミッション を与えます。

ACL を含む RootFactory クラスは root factory です。 それを Pyramid アプリケーションに関連付ける必要があります。 それにより ACL が各ビューに対してリクエストの context で (context 属性として) 提供されます。

tutorial/tutorial/__init__.py を開いて、 Configurator コンストラクタに root_factory パラメータを追加してください。 それは上で作成したクラスを指します:

1
2
    config = Configurator(settings=settings,
                          root_factory='tutorial.models.RootFactory')

(ハイライトされた行は変更が必要な箇所です)

これでアプリケーションに ACL を渡せるようになりました。 ACL が 何を表わすかについての詳細は Assigning ACLs to your Resource Objects を参照してください。

認証と認可のポリシーを追加する

tutorial/__init__.py を開いて、これらのインポート文を追加してください:

1
2
3
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy
from tutorial.security import groupfinder

そうしたら、設定にこれらのポリシーを加えてください:

1
2
3
4
5
6
7
    authn_policy = AuthTktAuthenticationPolicy(
        'sosecret', callback=groupfinder, hashalg='sha512')
    authz_policy = ACLAuthorizationPolicy()
    config = Configurator(settings=settings,
                          root_factory='tutorial.models.RootFactory')
    config.set_authentication_policy(authn_policy)
    config.set_authorization_policy(authz_policy)

(ハイライトされた行は変更が必要な箇所です)

AuthTktAuthenticationPolicy を有効にしています。 それは、リクエストに含まれている可能性のある auth チケットと、 ACL を使用してビューに対する許可または禁止の結果を決定する ACLAuthorizationPolicy に基づきます。

pyramid.authentication.AuthTktAuthenticationPolicy コンストラクタ は 2 つの引数を受け付けます: secretcallback です。 secret は、 このポリシーによって表わされる「認証チケット」機構によって使用される 暗号鍵を表わす文字列です: これは必須です。 callback は、以前作成した groupfinder 関数です。

パーミッション宣言を追加する

permission='edit' パラメータを add_page()edit_page() に対する @view_config デコレータに追加してください。例えば:

1
2
@view_config(route_name='add_page', renderer='templates/edit.pt',
             permission='edit')

(ハイライトされた行は変更が必要な箇所です)

その結果は、リクエストの時点で edit パーミッションを所有するユーザ だけが、それら2つのビューを起動できるということです。

permission='view' パラメータを view_wiki()view_page() に対する @view_config デコレータに追加してください。例えば:

1
2
@view_config(route_name='view_page', renderer='templates/view.pt',
             permission='view')

(ハイライトされた行は変更が必要な箇所です)

これは、誰でもこれら2つのビューを起動できるようにします。

これでアクセスを制御するのに必要とされる変更が終わりました。 続いての変更は、ログインおよびログアウト機能を追加することです。

ログイン、ログアウト

/login と /logout に対する route を追加する

tutorial/tutorial/__init__.py に戻り、これら2つの route を追加してください:

1
2
    config.add_route('login', '/login')
    config.add_route('logout', '/logout')

Login と Logout ビューを追加する

ログインフォームをレンダリングしたり、ログインフォームから送信された データを処理して認証情報 (credentials) をチェックしたりする login ビューを追加します。

さらに、アプリケーションに logout ビュー callable を加えて、 それへのリンクを提供します。このビューは、ログインユーザの 資格をクリアして、フロントページにリダイレクトします。

tutorial/tutorial/views.py の先頭に、以下のインポート文を追加してください:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from pyramid.view import (
    view_config,
    forbidden_view_config,
    )

from pyramid.security import (
    remember,
    forget,
    )

from .security import USERS

(ハイライトされた行は変更が必要な箇所です)

forbidden_view_config() は、デフォルトの 403 Forbidden ページをカスタマイズするために使用されます。 remember()forget() は、 auth チケットのクッキーの作成と破棄をサポートします。

そして login および logout ビューを追加します:

 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
@view_config(route_name='login', renderer='templates/login.pt')
@forbidden_view_config(renderer='templates/login.pt')
def login(request):
    login_url = request.route_url('login')
    referrer = request.url
    if referrer == login_url:
        referrer = '/' # never use the login form itself as came_from
    came_from = request.params.get('came_from', referrer)
    message = ''
    login = ''
    password = ''
    if 'form.submitted' in request.params:
        login = request.params['login']
        password = request.params['password']
        if USERS.get(login) == password:
            headers = remember(request, login)
            return HTTPFound(location = came_from,
                             headers = headers)
        message = 'Failed login'

    return dict(
        message = message,
        url = request.application_url + '/login',
        came_from = came_from,
        login = login,
        password = password,
        )

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

login は 2 つのデコレータでデコレートされています:

  • ビュー関数を login ルートに関連付け、 /login を訪れたときに それが表示されるようにする @view_config
  • ビュー関数を exception view に変換する @forbidden_view_configlogin() は、ユーザが許可されないビュー callable を実行しようとする 時に起動されます。例えば、もしユーザがログインせずに Wiki ページを 追加または編集しようとすれば、その続きが許可される前にログインフォーム が表示されます。

これら2つの view configuration デコレータの順番は重要ではありません。

logout() は、 logout ルートに関連付ける @view_config デコレータでデコレートされます。これは、 /logout を訪れたときに起動されます。

login.pt テンプレートを追加する

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

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
      xmlns:tal="http://xml.zope.org/namespaces/tal">
<head>
  <title>Login - Pyramid tutorial wiki (based on TurboGears
    20-Minute Wiki)</title>
  <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
  <meta name="keywords" content="python web application" />
  <meta name="description" content="pyramid web application" />
  <link rel="shortcut icon"
        href="${request.static_url('tutorial:static/favicon.ico')}" />
  <link rel="stylesheet"
        href="${request.static_url('tutorial:static/pylons.css')}"
        type="text/css" media="screen" charset="utf-8" />
  <!--[if lte IE 6]>
  <link rel="stylesheet"
        href="${request.static_url('tutorial:static/ie6.css')}"
        type="text/css" media="screen" charset="utf-8" />
  <![endif]-->
</head>
<body>
  <div id="wrap">
    <div id="top-small">
      <div class="top-small align-center">
        <div>
          <img width="220" height="50" alt="pyramid"
        src="${request.static_url('tutorial:static/pyramid-small.png')}" />
        </div>
      </div>
    </div>
    <div id="middle">
      <div class="middle align-right">
        <div id="left" class="app-welcome align-left">
          <b>Login</b><br/>
          <span tal:replace="message"/>
        </div>
        <div id="right" class="app-welcome align-right"></div>
      </div>
    </div>
    <div id="bottom">
      <div class="bottom">
        <form action="${url}" method="post">
          <input type="hidden" name="came_from" value="${came_from}"/>
          <input type="text" name="login" value="${login}"/><br/>
          <input type="password" name="password"
                 value="${password}"/><br/>
          <input type="submit" name="form.submitted" value="Log In"/>
        </form>
      </div>
    </div>
  </div>
  <div id="footer">
    <div class="footer"
         >&copy; Copyright 2008-2011, Agendaless Consulting.</div>
  </div>
</body>
</html>

上記のテンプレートは、さきほど views.py に追加した login ビュー内で 参照されます。

logged_in フラグをレンダラーに返す

tutorial/tutorial/views.py の先頭のインポートに以下の行を追加してください:

1
2
3
4
5
from pyramid.security import (
    remember,
    forget,
    authenticated_userid,
    )

(ハイライトされた行は変更が必要な箇所です)

logged_in パラメータを以下のように view_page(), edit_page(), add_page() の戻り値に追加してください:

1
2
3
4
return dict(page = page,
            content = content,
            edit_url = edit_url,
            logged_in = authenticated_userid(request))

(ハイライトされた行は変更が必要な箇所です)

authenticated_userid() は、もしユーザが認証 されなければ None を返し、ユーザが認証されれば何らかのユーザ id を 返します。

ログインした時に “Logout” リンクを追加する

tutorial/tutorial/templates/edit.pttutorial/tutorial/templates/view.pt を開いて <div id="right" class="app-welcome align-right"> div の内側にこれを 追加してください:

<span tal:condition="logged_in">
   <a href="${request.application_url}/logout">Logout</a>
</span>

logged_in が任意のユーザ id である場合、属性 tal:condition="logged_in" が要素に含まれるようになります。このリンクはログアウトビューを起動します。 ユーザが認証されていない場合のように logged_inNone なら、上記の 要素は含まれません。

変更点を確認する

最終的に tutorial/tutorial/__init__.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
from pyramid.config import Configurator
from pyramid.authentication import AuthTktAuthenticationPolicy
from pyramid.authorization import ACLAuthorizationPolicy

from sqlalchemy import engine_from_config

from tutorial.security import groupfinder

from .models import (
    DBSession,
    Base,
    )


def main(global_config, **settings):
    """ This function returns a Pyramid WSGI application.
    """
    engine = engine_from_config(settings, 'sqlalchemy.')
    DBSession.configure(bind=engine)
    Base.metadata.bind = engine
    authn_policy = AuthTktAuthenticationPolicy(
        'sosecret', callback=groupfinder, hashalg='sha512')
    authz_policy = ACLAuthorizationPolicy()
    config = Configurator(settings=settings,
                          root_factory='tutorial.models.RootFactory')
    config.set_authentication_policy(authn_policy)
    config.set_authorization_policy(authz_policy)
    config.add_static_view('static', 'static', cache_max_age=3600)
    config.add_route('view_wiki', '/')
    config.add_route('login', '/login')
    config.add_route('logout', '/logout')
    config.add_route('view_page', '/{pagename}')
    config.add_route('add_page', '/add_page/{pagename}')
    config.add_route('edit_page', '/{pagename}/edit_page')
    config.scan()
    return config.make_wsgi_app()

(ハイライトされた行は変更が必要な箇所です)

最終的に tutorial/tutorial/models.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
from pyramid.security import (
    Allow,
    Everyone,
    )

from sqlalchemy import (
    Column,
    Integer,
    Text,
    )

from sqlalchemy.ext.declarative import declarative_base

from sqlalchemy.orm import (
    scoped_session,
    sessionmaker,
    )

from zope.sqlalchemy import ZopeTransactionExtension

DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))
Base = declarative_base()


class Page(Base):
    """ The SQLAlchemy declarative model class for a Page object. """
    __tablename__ = 'pages'
    id = Column(Integer, primary_key=True)
    name = Column(Text, unique=True)
    data = Column(Text)

    def __init__(self, name, data):
        self.name = name
        self.data = data

class RootFactory(object):
    __acl__ = [ (Allow, Everyone, 'view'),
                (Allow, 'group:editors', 'edit') ]
    def __init__(self, request):
        pass

(ハイライトされた行は変更が必要な箇所です)

最終的に tutorial/tutorial/views.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
 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
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
import re
from docutils.core import publish_parts

from pyramid.httpexceptions import (
    HTTPFound,
    HTTPNotFound,
    )

from pyramid.view import (
    view_config,
    forbidden_view_config,
    )

from pyramid.security import (
    remember,
    forget,
    authenticated_userid,
    )

from .models import (
    DBSession,
    Page,
    )

from .security import USERS

# regular expression used to find WikiWords
wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")

@view_config(route_name='view_wiki',
             permission='view')
def view_wiki(request):
    return HTTPFound(location = request.route_url('view_page',
                                                  pagename='FrontPage'))

@view_config(route_name='view_page', renderer='templates/view.pt',
             permission='view')
def view_page(request):
    pagename = request.matchdict['pagename']
    page = DBSession.query(Page).filter_by(name=pagename).first()
    if page is None:
        return HTTPNotFound('No such page')

    def check(match):
        word = match.group(1)
        exists = DBSession.query(Page).filter_by(name=word).all()
        if exists:
            view_url = request.route_url('view_page', pagename=word)
            return '<a href="%s">%s</a>' % (view_url, word)
        else:
            add_url = request.route_url('add_page', pagename=word)
            return '<a href="%s">%s</a>' % (add_url, word)

    content = publish_parts(page.data, writer_name='html')['html_body']
    content = wikiwords.sub(check, content)
    edit_url = request.route_url('edit_page', pagename=pagename)
    return dict(page=page, content=content, edit_url=edit_url,
                logged_in=authenticated_userid(request))

@view_config(route_name='add_page', renderer='templates/edit.pt',
             permission='edit')
def add_page(request):
    pagename = request.matchdict['pagename']
    if 'form.submitted' in request.params:
        body = request.params['body']
        page = Page(pagename, body)
        DBSession.add(page)
        return HTTPFound(location = request.route_url('view_page',
                                                      pagename=pagename))
    save_url = request.route_url('add_page', pagename=pagename)
    page = Page('', '')
    return dict(page=page, save_url=save_url,
                logged_in=authenticated_userid(request))

@view_config(route_name='edit_page', renderer='templates/edit.pt',
             permission='edit')
def edit_page(request):
    pagename = request.matchdict['pagename']
    page = DBSession.query(Page).filter_by(name=pagename).one()
    if 'form.submitted' in request.params:
        page.data = request.params['body']
        DBSession.add(page)
        return HTTPFound(location = request.route_url('view_page',
                                                      pagename=pagename))
    return dict(
        page=page,
        save_url = request.route_url('edit_page', pagename=pagename),
        logged_in=authenticated_userid(request),
        )

@view_config(route_name='login', renderer='templates/login.pt')
@forbidden_view_config(renderer='templates/login.pt')
def login(request):
    login_url = request.route_url('login')
    referrer = request.url
    if referrer == login_url:
        referrer = '/' # never use the login form itself as came_from
    came_from = request.params.get('came_from', referrer)
    message = ''
    login = ''
    password = ''
    if 'form.submitted' in request.params:
        login = request.params['login']
        password = request.params['password']
        if USERS.get(login) == password:
            headers = remember(request, login)
            return HTTPFound(location = came_from,
                             headers = headers)
        message = 'Failed login'

    return dict(
        message = message,
        url = request.application_url + '/login',
        came_from = came_from,
        login = login,
        password = password,
        )

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

(ハイライトされた行は変更が必要な箇所です)

最終的に tutorial/tutorial/templates/edit.pt テンプレートは このようになっているはずです:

 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
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
      xmlns:tal="http://xml.zope.org/namespaces/tal">
<head>
  <title>${page.name} - Pyramid tutorial wiki (based on
    TurboGears 20-Minute Wiki)</title>
  <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
  <meta name="keywords" content="python web application" />
  <meta name="description" content="pyramid web application" />
  <link rel="shortcut icon"
        href="${request.static_url('tutorial:static/favicon.ico')}" />
  <link rel="stylesheet"
        href="${request.static_url('tutorial:static/pylons.css')}"
        type="text/css" media="screen" charset="utf-8" />
  <!--[if lte IE 6]>
  <link rel="stylesheet"
        href="${request.static_url('tutorial:static/ie6.css')}"
        type="text/css" media="screen" charset="utf-8" />
  <![endif]-->
</head>
<body>
  <div id="wrap">
    <div id="top-small">
      <div class="top-small align-center">
        <div>
          <img width="220" height="50" alt="pyramid"
        src="${request.static_url('tutorial:static/pyramid-small.png')}" />
        </div>
      </div>
    </div>
    <div id="middle">
      <div class="middle align-right">
        <div id="left" class="app-welcome align-left">
          Editing <b><span tal:replace="page.name">Page Name
            Goes Here</span></b><br/>
          You can return to the
          <a href="${request.application_url}">FrontPage</a>.<br/>
        </div>
        <div id="right" class="app-welcome align-right">
          <span tal:condition="logged_in">
              <a href="${request.application_url}/logout">Logout</a>
          </span>
        </div>
      </div>
    </div>
    <div id="bottom">
      <div class="bottom">
        <form action="${save_url}" method="post">
          <textarea name="body" tal:content="page.data" rows="10"
                    cols="60"/><br/>
          <input type="submit" name="form.submitted" value="Save"/>
        </form>
      </div>
    </div>
  </div>
  <div id="footer">
    <div class="footer"
         >&copy; Copyright 2008-2011, Agendaless Consulting.</div>
  </div>
</body>
</html>

(ハイライトされた行は変更が必要な箇所です)

最終的に tutorial/tutorial/templates/view.pt テンプレートは このようになっているはずです:

 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
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"
      xmlns:tal="http://xml.zope.org/namespaces/tal">
<head>
  <title>${page.name} - Pyramid tutorial wiki (based on
    TurboGears 20-Minute Wiki)</title>
  <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"/>
  <meta name="keywords" content="python web application" />
  <meta name="description" content="pyramid web application" />
  <link rel="shortcut icon"
        href="${request.static_url('tutorial:static/favicon.ico')}" />
  <link rel="stylesheet"
        href="${request.static_url('tutorial:static/pylons.css')}"
        type="text/css" media="screen" charset="utf-8" />
  <!--[if lte IE 6]>
  <link rel="stylesheet"
        href="${request.static_url('tutorial:static/ie6.css')}"
        type="text/css" media="screen" charset="utf-8" />
  <![endif]-->
</head>
<body>
  <div id="wrap">
    <div id="top-small">
      <div class="top-small align-center">
        <div>
          <img width="220" height="50" alt="pyramid"
        src="${request.static_url('tutorial:static/pyramid-small.png')}" />
        </div>
      </div>
    </div>
    <div id="middle">
      <div class="middle align-right">
        <div id="left" class="app-welcome align-left">
          Viewing <b><span tal:replace="page.name">Page Name
            Goes Here</span></b><br/>
          You can return to the
          <a href="${request.application_url}">FrontPage</a>.<br/>
        </div>
        <div id="right" class="app-welcome align-right">
          <span tal:condition="logged_in">
            <a href="${request.application_url}/logout">Logout</a>
          </span>
        </div>
      </div>
    </div>
    <div id="bottom">
      <div class="bottom">
        <div tal:replace="structure content">
          Page text goes here.
        </div>
        <p>
          <a tal:attributes="href edit_url" href="">
            Edit this page
          </a>
        </p>
      </div>
    </div>
  </div>
  <div id="footer">
    <div class="footer"
         >&copy; Copyright 2008-2011, Agendaless Consulting.</div>
  </div>
</body>
</html>

(ハイライトされた行は変更が必要な箇所です)

ブラウザでアプリケーションを表示する

最終的に、ブラウザの中でアプリケーションを実行することができます (アプリケーションの起動 参照)。 ブラウザを起動して以下の各 URL にアクセスして、結果が期待通りであることを 確認してください:

  • http://localhost:6543/view_wiki ビューを起動します。 これは常に FrontPage page オブジェクトの view_page ビューに リダイレクトします。このビューは任意のユーザによって実行可能です。
  • http://localhost:6543/FrontPage は FrontPage page オブジェクトの view_page ビューを起動します。
  • http://localhost:6543/FrontPage/edit_page は FrontPage オブジェクト に対応する edit ビューを起動します。それは editor ユーザだけが 実行可能です。異なるユーザ (あるいは匿名ユーザ) が起動すると、ログイン フォームが表示されます。認証情報としてユーザー名 editor 、パスワード editor を入力すると編集ページフォームが表示されるでしょう。
  • http://localhost:6543/add_page/SomePageName は、ページに対する add ビューを起動します。それは editor ユーザだけが実行可能です。 異なるユーザ (あるいは匿名ユーザ) が起動すると、ログインフォームが 表示されます。認証情報としてユーザー名 editor 、パスワード editor を入力すると編集ページフォームが表示されるでしょう。
  • (edit または add ページにアクセスしてログインフォームに editor 認証を送信した結果として) ログイン後に、右上に Logout リンクが表示 されているでしょう。それをクリックするとログアウトして、 フロントページにリダイレクトされます。