添加授权

在上一章中,我们构建了 authentication 进入我们的维基。我们还更进一步,使用 request.identity 对象执行一些显式的 authorization 支票。这对于很多应用程序来说都很好,但是 Pyramid 提供了一些工具来清理这种情况,并将约束与视图函数本身解耦。

我们将通过以下步骤实现访问控制:

添加ACL支持

A principal 是原始数据之上的一个抽象级别。 identity 它根据用户的能力、角色或其他更容易概括的标识符来描述用户。然后根据主体写入权限,而不关注所涉及的确切用户。

Pyramid 定义每个应用程序中使用的两个内置主体: pyramid.authorization.Everyonepyramid.authorization.Authenticated . 除此之外,我们已经在原始设计中提到了此应用程序所需的原则。用户有两个可能的角色: editorbasic . 这些将以字符串作为前缀 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 定义 principalpermission 对于当前请求,通过 __acl__ 方法或属性。

这个 permits 方法完成了 pyramid.interfaces.ISecurityPolicy 接口,使我们的应用程序能够使用 pyramid.request.Request.has_permission 以及 permission= 视图约束。

添加资源和ACL

资源和环境是 Pyramid . 你成功了!

Web应用程序中的每个URL表示 resource (统一资源定位器中的“R”)。通常,资源是数据模型中的某种东西,但它也可能是许多模型的抽象。

我们的wiki有两种资源:

  1. A NewPage . 表示一个潜力 Page 那是不存在的。任何登录用户,具有以下任一角色 basiceditor ,可以创建页面。

  2. 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__ 在它上面返回映射列表 principalpermission . 这定义了 who 能做 what 用那个 resource . 在我们的示例中,我们只允许那些具有 role:editorrole: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 加载为 contextadd_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 加载为 contextview_pageedit_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_pageedit_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 页面对象的视图。它可以由 editorbasic 用户。如果匿名用户调用它,则会显示一个登录表单。

  • http://localhost:6543/SomePageName/edit_page invokes the edit_page 视图,或者在该页不存在时生成错误。它可以由 basic 用户(如果页面是由该用户在上一步中创建的)。如果该页是由 editor 用户,则应显示以下内容的登录页 basic 用户。