添加授权¶
在上一章中,我们构建了 authentication 进入我们的维基。我们还更进一步,使用 request.identity
对象执行一些显式的 authorization 支票。这对于很多应用程序来说都很好,但是 Pyramid 提供了一些工具来清理这种情况,并将约束与视图函数本身解耦。
我们将通过以下步骤实现访问控制:
Update the security policy to break down the identity into a list of principals (
security.py
).利用
pyramid.authorization.ACLHelper
支持按上下文将主体映射到权限 (security.py
)添加一个 ACL 到每个资源 (
routes.py
)将视图上的内联检查替换为 permission 声明 (
views/default.py
)
添加ACL支持¶
A principal 是原始数据之上的一个抽象级别。 identity 它根据用户的能力、角色或其他更容易概括的标识符来描述用户。然后根据主体写入权限,而不关注所涉及的确切用户。
Pyramid 定义每个应用程序中使用的两个内置主体: pyramid.authorization.Everyone
和 pyramid.authorization.Authenticated
. 除此之外,我们已经在原始设计中提到了此应用程序所需的原则。用户有两个可能的角色: editor
或 basic
. 这些将以字符串作为前缀 role:
以避免与任何其他类型的主体发生冲突。
打开文件 tutorial/security.py
编辑如下:
1from pyramid.authentication import AuthTktCookieHelper
2from pyramid.authorization import (
3 ACLHelper,
4 Authenticated,
5 Everyone,
6)
7from pyramid.csrf import CookieCSRFStoragePolicy
8from pyramid.request import RequestLocalCache
9
10from . import models
11
12
13class MySecurityPolicy:
14 def __init__(self, secret):
15 self.authtkt = AuthTktCookieHelper(secret)
16 self.identity_cache = RequestLocalCache(self.load_identity)
17 self.acl = ACLHelper()
18
19 def load_identity(self, request):
20 identity = self.authtkt.identify(request)
21 if identity is None:
22 return None
23
24 userid = identity['userid']
25 user = request.dbsession.query(models.User).get(userid)
26 return user
27
28 def identity(self, request):
29 return self.identity_cache.get_or_create(request)
30
31 def authenticated_userid(self, request):
32 user = self.identity(request)
33 if user is not None:
34 return user.id
35
36 def remember(self, request, userid, **kw):
37 return self.authtkt.remember(request, userid, **kw)
38
39 def forget(self, request, **kw):
40 return self.authtkt.forget(request, **kw)
41
42 def permits(self, request, context, permission):
43 principals = self.effective_principals(request)
44 return self.acl.permits(context, principals, permission)
45
46 def effective_principals(self, request):
47 principals = [Everyone]
48 user = self.identity(request)
49 if user is not None:
50 principals.append(Authenticated)
51 principals.append('u:' + str(user.id))
52 principals.append('role:' + user.role)
53 return principals
54
55def includeme(config):
56 settings = config.get_settings()
57
58 config.set_csrf_storage_policy(CookieCSRFStoragePolicy())
59 config.set_default_csrf_options(require_csrf=True)
60
61 config.set_security_policy(MySecurityPolicy(settings['auth.secret']))
只需添加突出显示的行。
请注意,该角色来自 User
对象。我们还添加了 user.id
当我们希望允许准确的用户编辑他们创建的页面时,作为一个主体。
我们正在使用 pyramid.authorization.ACLHelper
对于大多数应用来说,这就足够了。它使用 context 定义 principal 和 permission 对于当前请求,通过 __acl__
方法或属性。
这个 permits
方法完成了 pyramid.interfaces.ISecurityPolicy
接口,使我们的应用程序能够使用 pyramid.request.Request.has_permission
以及 permission=
视图约束。
添加资源和ACL¶
资源和环境是 Pyramid . 你成功了!
Web应用程序中的每个URL表示 resource (统一资源定位器中的“R”)。通常,资源是数据模型中的某种东西,但它也可能是许多模型的抽象。
我们的wiki有两种资源:
A
NewPage
. 表示一个潜力Page
那是不存在的。任何登录用户,具有以下任一角色basic
或editor
,可以创建页面。A
PageResource
. 代表一个Page
要查看或编辑的。editor
用户,以及Page
可以编辑PageResource
. 任何人都可以观看。
备注
wiki数据模型非常简单, PageResource
与我们的 models.Page
SqlAlchemy类。把它们组合成一个类是完全有效的。但是,对于本教程,它们是明确分开的,以明确区分哪些部分与哪些部分有关 Pyramid 关注与应用程序定义的对象。
定义这些资源有很多种方法,甚至可以用层次结构将它们分组到集合中。但是,我们在这里保持简单!
打开文件 tutorial/routes.py
并编辑以下行:
1from pyramid.authorization import (
2 Allow,
3 Everyone,
4)
5from pyramid.httpexceptions import (
6 HTTPNotFound,
7 HTTPSeeOther,
8)
9
10from . import models
11
12
13def includeme(config):
14 config.add_static_view('static', 'static', cache_max_age=3600)
15 config.add_route('view_wiki', '/')
16 config.add_route('login', '/login')
17 config.add_route('logout', '/logout')
18 config.add_route('view_page', '/{pagename}', factory=page_factory)
19 config.add_route('add_page', '/add_page/{pagename}',
20 factory=new_page_factory)
21 config.add_route('edit_page', '/{pagename}/edit_page',
22 factory=page_factory)
23
24def new_page_factory(request):
25 pagename = request.matchdict['pagename']
26 if request.dbsession.query(models.Page).filter_by(name=pagename).count() > 0:
27 next_url = request.route_url('edit_page', pagename=pagename)
28 raise HTTPSeeOther(location=next_url)
29 return NewPage(pagename)
30
31class NewPage:
32 def __init__(self, pagename):
33 self.pagename = pagename
34
35 def __acl__(self):
36 return [
37 (Allow, 'role:editor', 'create'),
38 (Allow, 'role:basic', 'create'),
39 ]
40
41def page_factory(request):
42 pagename = request.matchdict['pagename']
43 page = request.dbsession.query(models.Page).filter_by(name=pagename).first()
44 if page is None:
45 raise HTTPNotFound
46 return PageResource(page)
47
48class PageResource:
49 def __init__(self, page):
50 self.page = page
51
52 def __acl__(self):
53 return [
54 (Allow, Everyone, 'view'),
55 (Allow, 'role:editor', 'edit'),
56 (Allow, 'u:' + str(self.page.creator_id), 'edit'),
57 ]
突出显示的行需要编辑或添加。
这个 NewPage
类有一个 __acl__
在它上面返回映射列表 principal 到 permission . 这定义了 who 能做 what 用那个 resource . 在我们的示例中,我们只允许那些具有 role:editor
或 role:basic
拥有 create
许可:
31class NewPage:
32 def __init__(self, pagename):
33 self.pagename = pagename
34
35 def __acl__(self):
36 return [
37 (Allow, 'role:editor', 'create'),
38 (Allow, 'role:basic', 'create'),
39 ]
这个 NewPage
加载为 context 的 add_page
通过声明 factory
路线:
19 config.add_route('add_page', '/add_page/{pagename}',
20 factory=new_page_factory)
这个 PageResource
类定义 ACL 对于一个 Page
. 它使用的是 Page
要确定的对象 who 能做 what 到这一页。
48class PageResource:
49 def __init__(self, page):
50 self.page = page
51
52 def __acl__(self):
53 return [
54 (Allow, Everyone, 'view'),
55 (Allow, 'role:editor', 'edit'),
56 (Allow, 'u:' + str(self.page.creator_id), 'edit'),
57 ]
这个 PageResource
加载为 context 的 view_page
和 edit_page
通过声明 factory
路线:
18 config.add_route('view_page', '/{pagename}', factory=page_factory)
19 config.add_route('add_page', '/add_page/{pagename}',
20 factory=new_page_factory)
21 config.add_route('edit_page', '/{pagename}/edit_page',
22 factory=page_factory)
添加视图权限¶
此时,我们已经修改了应用程序以加载 PageResource
包括实际 Page
模型中 page_factory
. 这个 PageResource
现在是 context 为了所有 view_page
和 edit_page
意见。同样地 NewPage
将是 add_page
查看。
打开文件 tutorial/views/default.py
.
首先,您可以删除一些不再需要的导入:
3from pyramid.httpexceptions import HTTPSeeOther
4from pyramid.view import view_config
5import re
编辑 view_page
视图以声明 view
权限,并删除视图中的显式检查:
18@view_config(route_name='view_page', renderer='tutorial:templates/view.jinja2',
19 permission='view')
20def view_page(request):
21 page = request.context.page
22
23 def add_link(match):
加载页面的工作已经在工厂完成了,所以我们只需 page
对象超出 PageResource
加载为 request.context
. 我们的工厂还保证 Page
,因为它提高了 HTTPNotFound
例外如果没有 Page
存在,再次简化视图逻辑。
编辑 edit_page
视图以声明 edit
许可:
38@view_config(route_name='edit_page', renderer='tutorial:templates/edit.jinja2',
39 permission='edit')
40def edit_page(request):
41 page = request.context.page
42 if request.method == 'POST':
编辑 add_page
视图以声明 create
许可:
52@view_config(route_name='add_page', renderer='tutorial:templates/edit.jinja2',
53 permission='create')
54def add_page(request):
55 pagename = request.context.pagename
56 if request.method == 'POST':
注意 pagename
这里是脱离上下文而不是 request.matchdict
. 工厂为我们隐藏实际的路线图做了很多工作。
在每个 resource 被使用 security policy 确定是否有 principal 允许吃一些 permission . 如果此检查失败(例如,用户未登录),则 HTTPForbidden
将自动引发异常。因此,我们可以从视图本身中删除这些异常和检查。相反,我们已经根据对资源的操作来定义它们。
决赛 tutorial/views/default.py
应如下所示:
1from docutils.core import publish_parts
2from html import escape
3from pyramid.httpexceptions import HTTPSeeOther
4from pyramid.view import view_config
5import re
6
7from .. import models
8
9
10# regular expression used to find WikiWords
11wikiwords = re.compile(r"\b([A-Z]\w+[A-Z]+\w+)")
12
13@view_config(route_name='view_wiki')
14def view_wiki(request):
15 next_url = request.route_url('view_page', pagename='FrontPage')
16 return HTTPSeeOther(location=next_url)
17
18@view_config(route_name='view_page', renderer='tutorial:templates/view.jinja2',
19 permission='view')
20def view_page(request):
21 page = request.context.page
22
23 def add_link(match):
24 word = match.group(1)
25 exists = request.dbsession.query(models.Page).filter_by(name=word).all()
26 if exists:
27 view_url = request.route_url('view_page', pagename=word)
28 return '<a href="%s">%s</a>' % (view_url, escape(word))
29 else:
30 add_url = request.route_url('add_page', pagename=word)
31 return '<a href="%s">%s</a>' % (add_url, escape(word))
32
33 content = publish_parts(page.data, writer_name='html')['html_body']
34 content = wikiwords.sub(add_link, content)
35 edit_url = request.route_url('edit_page', pagename=page.name)
36 return dict(page=page, content=content, edit_url=edit_url)
37
38@view_config(route_name='edit_page', renderer='tutorial:templates/edit.jinja2',
39 permission='edit')
40def edit_page(request):
41 page = request.context.page
42 if request.method == 'POST':
43 page.data = request.params['body']
44 next_url = request.route_url('view_page', pagename=page.name)
45 return HTTPSeeOther(location=next_url)
46 return dict(
47 pagename=page.name,
48 pagedata=page.data,
49 save_url=request.route_url('edit_page', pagename=page.name),
50 )
51
52@view_config(route_name='add_page', renderer='tutorial:templates/edit.jinja2',
53 permission='create')
54def add_page(request):
55 pagename = request.context.pagename
56 if request.method == 'POST':
57 body = request.params['body']
58 page = models.Page(name=pagename, data=body)
59 page.creator = request.identity
60 request.dbsession.add(page)
61 next_url = request.route_url('view_page', pagename=pagename)
62 return HTTPSeeOther(location=next_url)
63 save_url = request.route_url('add_page', pagename=pagename)
64 return dict(pagename=pagename, pagedata='', save_url=save_url)
在浏览器中查看应用程序¶
我们最终可以在浏览器中检查我们的应用程序(请参见 启动应用程序 )启动浏览器并访问以下每个URL,检查结果是否符合预期:
http://localhost:6543/ invokes the
view_wiki
查看。这总是重定向到view_page
视图FrontPage
页面对象。它可由任何用户执行。http://localhost:6543/login invokes the
login
视图,将显示一个登录表单。在每个页面上,当用户未通过身份验证时,在右上角有一个“Login”链接,否则,当用户通过身份验证时,它是一个“Logout”链接。为凭据提供用户名
editor
和密码editor
或用户名basic
和密码basic
将对用户进行身份验证并授予该组访问权限。登录后(点击编辑或添加页面并提交有效凭据),我们将在右上角看到“注销”链接。当我们点击它时,我们被注销,重定向回首页,“登录”链接显示在右上角。
http://localhost:6543/FrontPage invokes the
view_page
视图FrontPage
页面对象。http://localhost:6543/FrontPage/edit_page invokes the
edit_page
的视图FrontPage
页面对象。它只能由editor
用户。如果不同的用户调用它,则会显示“403禁止”页面。如果匿名用户调用它,则会显示一个登录表单。http://localhost:6543/add_page/SomePageName invokes the
add_page
页面的视图。如果该页已经存在,则它会将用户重定向到edit_page
页面对象的视图。它可以由editor
或basic
用户。如果匿名用户调用它,则会显示一个登录表单。http://localhost:6543/SomePageName/edit_page invokes the
edit_page
视图,或者在该页不存在时生成错误。它可以由basic
用户(如果页面是由该用户在上一步中创建的)。如果该页是由editor
用户,则应显示以下内容的登录页basic
用户。