Pyramid は authentication と authorization の ための機能を提供しています。アプリケーションにセキュリティを追加する ためにこれら両方の機能を利用します。私たちのアプリケーションは現在、 サーバーにアクセスできる誰もが wiki ページを見たり、編集したり、 追加したりすることが可能です。特定のユーザー名 (editor) を持つ限られた 人々だけに wiki ページの追加や編集を許可するようにアプリケーションを 変更してみましょう。しかし、依然としてサーバーにアクセスできる誰でも ページを見ることは可能です。
次のステップで行います:
このチュートリアルステージのソースコードを以下の場所で閲覧することができます。 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 ファイルを変更する必要があります。
宣言的なセキュリティ検査を実装するため、 AuthTktAuthenticationPolicy と ACLAuthorizationPolicy を有効にします。 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 つの引数を受け付けます: secret と callback です。 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__.py や views.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_config と exception 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 の値をテンプレートへ渡すために、これら のビューの返り値を変更します。例えば:
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 principal を edit パーミッションに与えたので、 group:editors という名前のグループの メンバーであるユーザだけが add_page または edit_page のルートに 関連したビューを起動することができます。
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"
>© Copyright 2008-2011, Agendaless Consulting.</div>
</div>
</body>
</html>
さらに、誰かがログインしていれば “Logout” リンクを表示するように edit.pt と view.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 モジュールはこのようになるでしょう:
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"
>© 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"
>© Copyright 2008-2011, Agendaless Consulting.</div>
</div>
</body>
</html>
(ハイライトされた行は変更が必要な箇所です)
最終的に、ブラウザの中でアプリケーションを実行することができます (アプリケーションの起動 参照)。 ブラウザを起動して以下の各 URL にアクセスして、結果が期待通りであることを 確認してください: