認証を追加する

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

次のステップで行います:

  • ACL を持つ root factory を追加 (models.py)
  • authentication policyauthorization policy を追加 (__init__.py)
  • 認証ポリシーコールバックを追加 (新しい security.py モジュール)
  • login および logout ビューを追加 (views.py)
  • edit_page および add_page ビューに permission 宣言を 追加 (views.py)
  • 既存のビューがレンダラーに logged_in フラグを返すように修正 (views.py)
  • ログインテンプレートを追加 (新しい login.pt)
  • “Logout” リンクを追加して、ログイン中にページを閲覧または編集している ときに表示されるようにする (view.pt, edit.pt)

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

ルートファクトリを追加する

models.py を開き、以下のような文を追加してください。

1
2
3
4
5
6
7
8
9
from pyramid.security import (
    Allow,
    Everyone,
    )
class RootFactory(object):
    __acl__ = [ (Allow, Everyone, 'view'),
                (Allow, 'group:editors', 'edit') ]
    def __init__(self, request):
        pass

__init__.py ファイル内でカスタム root factory を使用する ことにします。ルートファクトリによって生成されたオブジェクトは、 アプリケーションへの各リクエストの context として使用されます。 この context オブジェクトは、 セキュリティ宣言でデコレートされます。コンテキストを生成するカスタム ルートファクトリの使用すると、 Pyramid の宣言的な セキュリティ機能を利用することができるようになります。

Configurator コンストラクタに root factory を渡すように __init__.py を修正します。それを models.py ファイルの内部で 作成する新しいクラスに向けます。

たった今追加した RootFactory クラスは、 context オブジェクトを 構築するために Pyramid によって使用されます。コンテキストは、 ビュー callable に渡された request オブジェクトに context 属性として 取り付けられています。

ルートファクトリによって生成された context オブジェクトは、 pyramid.security.Everyone (特別の principal) がすべてのページを 閲覧することを許可する一方、 group:editors という名前の principal だけがページを編集したり追加したりすることを許可する __acl__ 属性を所有します。コンテキストに取り付けられた __acl__ 属性は、ビュー callable の実行中にアクセスコントロールリストとして Pyramid によって特別に解釈されます。 ACL が何を表わすかに ついての詳細は Assigning ACLs to your Resource Objects を参照してください。

上記のステップで作成した RootFactory は、 Configurator への root_factory 引数として渡します。

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

__init__.py ファイルに、認可ポリシーを設定するのを助けるいくつかの 変更を加えていきます。

Pyramid アプリケーションが認可を行なうために、 security.py モジュールを追加する必要があります (私たちもまもなく追加します)。また、 security.py ファイルを コールバック に使用する authentication policy および authorization policy を 追加するために __init__.py ファイルを変更する必要があります。

宣言的なセキュリティ検査を実装するため、 AuthTktAuthenticationPolicyACLAuthorizationPolicy を有効にします。 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)
    authz_policy = ACLAuthorizationPolicy()
    config = Configurator(settings=settings,
                          root_factory='tutorial.models.RootFactory')
    config.set_authentication_policy(authn_policy)
    config.set_authorization_policy(authz_policy)

pyramid.authentication.AuthTktAuthenticationPolicy コンストラクタ は 2 つの引数を受け付けます: secretcallback です。 secret は、 このポリシーによって表わされる「認証チケット」機構によって使用される 暗号鍵を表わす文字列です: これは必須です。 callback はカレントディレクトリ の security.py ファイル中の groupfinder 関数です。そのモジュールは まだ追加していませんが、それはこれからやります。

変更内容を確認する

ルートファクトリの設定、認証および認可ポリシーの追加、 /login および /logout のためのルート追加が終わると、アプリケーションの __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
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

def main(global_config, **settings):
    """ This function returns a Pyramid WSGI application.
    """
    engine = engine_from_config(settings, 'sqlalchemy.')
    DBSession.configure(bind=engine)
    authn_policy = AuthTktAuthenticationPolicy(
        'sosecret', callback=groupfinder)
    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()

認証ポリシーコールバックを追加する

パッケージ内 (__init__.pyviews.py などと同じディレクトリ 内) に 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 関数は、 authentication policy 「コールバック」です; それは userid と request を受け取る callable です。 userid がシステムに存在すれば、コールバックはグループ識別子のシーケンス (ユーザがどのグループのメンバーでもなければ空のシーケンス) を返します。 userid がシステムに存在しなければ、コールバックは None を返します。 プロダクションシステムでは、ユーザとグループデータは、ほとんどの場合 データベースから取得されますが、ここではユーザとグループのソースを表わすために 「ダミーの」データを使用します。 editor ユーザは、ダミーのグループ データ (GROUPS データ構造) 中の group:editors グループのメンバー であることに注目してください。

GROUPS データ構造の中で editor ユーザを group:editors グルー プにマッピングする (GROUPS = {'editor':['group:editors']}) ことにより、 editor ユーザを group:editors に所属させました。 groupfinder 関数が GROUPS データ構造を参照するので、これが意味 するのは、ルートファクトリによって返された context オブジェクトに 取り付けられた ACL と add_page, edit_page ビューに関連した許可の 結果、 editor ユーザがページの追加や編集ができるようになるということです。

ログインとログアウトのビューを追加する

views.py に、ログインフォームをレンダリングして、ログインフォーム からのポストを処理して認証情報 (credential) をチェックする login ビュー callable を付け加えます。

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

login ビュー callable は次のようになります:

 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
@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,
        )

logout ビュー callable は次のようになります:

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

login ビュー callable は 2 つのデコレータ、 login ルートに関連 付ける @view_configexception view に変換する @forbidden_view_config でデコレートされています。 login ルートに 関連付けるものは、 /login を訪れたときにそれを目に見えるようにします。 他のものはそれを forbidden view にします。 forbidden view は、 Pyramid またはアプリケーションが pyramid.httpexceptions.HTTPForbidden 例外を上げる場合に常に 表示されます。この場合、誰かがまだ行なうことを許可されていない行為を実行 しようとした場合に常にログインフォームを表示するために、 forbidden view に依存します。

logout ビュー callable は、 logout ルートに関連付ける @view_config デコレータでデコレートされます。これは、 /logout を訪れたときにそれを目に見えるようにします。

これら 2 つの関数に必要なものを提供するために、いくつかの値をインポート する必要があるでしょう: pyramid.view.forbidden_view_config クラス、 pyramid.security モジュールからの多くの値、そして新しく追加した tutorial.security パッケージからの値です。次のインポート文を views.py ファイルの先頭に加えてください:

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

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

from .security import USERS

既存のビューを変更する

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

次に、 views.py の中の view_page, edit_page, add_page ビュー callable の各々を変更する必要があります。これらの各ビューの中では、 “logged in” パラメーターをテンプレートへ渡す必要があります。 各ビュー本体にこのようなものを加えます:

1
2
from pyramid.security import authenticated_userid
logged_in = authenticated_userid(request)

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

そして、その結果の logged_in の値をテンプレートへ渡すために、これら のビューの返り値を変更します。例えば:

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

さらに、 add_page および edit_page ビュー callable の各々のための @view_config デコレータに permission 値を加える必要があるでしょう。 各々について、例えば permission='edit' を加えます:

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

そこに加えられた permission='edit' を見てください。これは、これらの ビューが参照するビュー callable を、現在の context に関して edit パーミッションを所有する認証されたユーザなしでは起動することが できない、ということを示します。

これらの permission 引数を追加すると、 Pyramid はリクエストの時に 有効な edit パーミッションを所有するユーザだけがこれら 2 つのビューを 起動できるという検証を行うようになります。 root factory の中で その ACL によって group:editors principaledit パーミッションに与えたので、 group:editors という名前のグループの メンバーであるユーザだけが add_page または edit_page のルートに 関連したビューを起動することができます。

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

templates ディレクトリに login.pt テンプレートを追加してください。 それはたった今 views.py に加えた login ビュー内で参照されます。

<!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>

ログイン中に “Logout” リンクを追加する

さらに、誰かがログインしていれば “Logout” リンクを表示するように edit.ptview.pt テンプレートを変更する必要があります。 このリンクは logout ビューを起動するでしょう。

そのために両方のテンプレートの <div id="right" class="app-welcome align-right"> div 内にこれを追加します:

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

views.py およびテンプレートへの変更を確認する

変更後に 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
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')
def view_wiki(request):
    return HTTPFound(location = request.route_url('view_page',
                                                  pagename='FrontPage'))

@view_config(route_name='view_page', renderer='templates/view.pt')
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):
    name = request.matchdict['pagename']
    if 'form.submitted' in request.params:
        body = request.params['body']
        page = Page(name, body)
        DBSession.add(page)
        return HTTPFound(location = request.route_url('view_page',
                                                      pagename=name))
    save_url = request.route_url('add_page', pagename=name)
    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):
    name = request.matchdict['pagename']
    page = DBSession.query(Page).filter_by(name=name).one()
    if 'form.submitted' in request.params:
        page.data = request.params['body']
        DBSession.add(page)
        return HTTPFound(location = request.route_url('view_page',
                                                      pagename=name))
    return dict(
        page=page,
        save_url = request.route_url('edit_page', pagename=name),
        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)
    

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

変更後に edit.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>${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>

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

変更後に view.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>${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 リンクが表示 されているでしょう。それをクリックするとログアウトして、 フロントページにリダイレクトされます。