Pyramid は authentication と authorization の ための機能を提供しています。アプリケーションにセキュリティを追加する ためにこれら両方の機能を利用します。私たちのアプリケーションは現在、 サーバーにアクセスできる誰もが wiki ページを見たり、編集したり、 追加したりすることが可能です。 wiki ページの追加や編集を group:editors という名前の グループ のメンバーである限られた人々だけに許可するように アプリケーションを変更してみましょう。しかし、依然としてサーバーに アクセスできる誰でもページを見ることは可能です。
さらに、ログインページとすべてのページにログアウトリンクを追加します。 ログインページは、ユーザがパーミッションを必要とする任意のビューへのアクセスを 拒否された場合に、デフォルトの “403 Forbidden” ページの代わりに表示されます。
次のステップでアクセス制御を実装していきます。
次にログインとログアウトの機能を追加します:
このチュートリアルステージのソースコードを以下の場所で閲覧することができます。 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 を受け取って、以下のどちらかを 返します:
例えば、 groupfinder('editor', request ) は [‘group:editor’] を返し、 groupfinder('viewer', request) は [] を、 groupfinder('admin', request) は None を返します。 あとで、ユーザに principal (複数可) を提供する authentication policy “コールバック” として groupfinder() を使用します。
プロダクションシステムでは、ユーザとグループデータは、ほとんどの場合 データベースから取得されますが、ここではユーザとグループのソースを表わすために 「ダミーの」データを使用します。
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 つの引数を受け付けます: secret と callback です。 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つのビューを起動できるようにします。
これでアクセスを制御するのに必要とされる変更が終わりました。 続いての変更は、ログインおよびログアウト機能を追加することです。
tutorial/tutorial/__init__.py に戻り、これら2つの route を追加してください:
1 2 | config.add_route('login', '/login')
config.add_route('logout', '/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 つのデコレータでデコレートされています:
これら2つの view configuration デコレータの順番は重要ではありません。
logout() は、 logout ルートに関連付ける @view_config デコレータでデコレートされます。これは、 /logout を訪れたときに起動されます。
以下の内容で 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"
>© Copyright 2008-2011, Agendaless Consulting.</div>
</div>
</body>
</html>
上記のテンプレートは、さきほど views.py に追加した login ビュー内で 参照されます。
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 を 返します。
tutorial/tutorial/templates/edit.pt と tutorial/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_in が None なら、上記の 要素は含まれません。
最終的に 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"
>© 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"
>© Copyright 2008-2011, Agendaless Consulting.</div>
</div>
</body>
</html>
|
(ハイライトされた行は変更が必要な箇所です)
最終的に、ブラウザの中でアプリケーションを実行することができます (アプリケーションの起動 参照)。 ブラウザを起動して以下の各 URL にアクセスして、結果が期待通りであることを 確認してください: