21:授权保护资源

将安全语句分配给描述执行操作所需权限的资源。

背景

我们的应用程序有允许人们通过Web浏览器添加/编辑/删除内容的URL。向应用程序添加安全性的时间。让我们保护添加/编辑视图以要求登录(用户名为 editor 密码 editor )我们将允许其他视图在没有密码的情况下继续工作。

目标

  • 介绍认证、授权、权限和访问控制列表(ACL)的Pyramid概念。

  • 做一个 root factory 返回应用程序顶部的类实例。

  • 将安全语句分配给根资源。

  • 在视图上添加权限谓词。

  • 提供一个 Forbidden view 处理访问没有足够权限的URL。

步骤

  1. 我们将使用验证步骤作为起点:

    cd ..; cp -r authentication authorization; cd authorization
    $VENV/bin/pip install -e .
    
  2. 从改变开始 authorization/tutorial/__init__.py 将根工厂指定给 configurator

     1from pyramid.config import Configurator
     2
     3from .security import SecurityPolicy
     4
     5
     6def main(global_config, **settings):
     7    config = Configurator(settings=settings,
     8                          root_factory='.resources.Root')
     9    config.include('pyramid_chameleon')
    10
    11    config.set_security_policy(
    12        SecurityPolicy(
    13            secret=settings['tutorial.secret'],
    14        ),
    15    )
    16
    17    config.add_route('home', '/')
    18    config.add_route('hello', '/howdy')
    19    config.add_route('login', '/login')
    20    config.add_route('logout', '/logout')
    21    config.scan('.views')
    22    return config.make_wsgi_app()
    
  3. 这意味着我们需要实施 authorization/tutorial/resources.py

    1from pyramid.authorization import Allow, Everyone
    2
    3
    4class Root:
    5    __acl__ = [(Allow, Everyone, 'view'),
    6               (Allow, 'group:editors', 'edit')]
    7
    8    def __init__(self, request):
    9        pass
    
  4. 定义一个 GROUPS 数据存储和 permits 我们的方法 SecurityPolicy

     1import bcrypt
     2from pyramid.authentication import AuthTktCookieHelper
     3from pyramid.authorization import (
     4    ACLHelper,
     5    Authenticated,
     6    Everyone,
     7)
     8
     9
    10def hash_password(pw):
    11    pwhash = bcrypt.hashpw(pw.encode('utf8'), bcrypt.gensalt())
    12    return pwhash.decode('utf8')
    13
    14def check_password(pw, hashed_pw):
    15    expected_hash = hashed_pw.encode('utf8')
    16    return bcrypt.checkpw(pw.encode('utf8'), expected_hash)
    17
    18
    19USERS = {'editor': hash_password('editor'),
    20         'viewer': hash_password('viewer')}
    21GROUPS = {'editor': ['group:editors']}
    22
    23
    24class SecurityPolicy:
    25    def __init__(self, secret):
    26        self.authtkt = AuthTktCookieHelper(secret=secret)
    27        self.acl = ACLHelper()
    28
    29    def identity(self, request):
    30        identity = self.authtkt.identify(request)
    31        if identity is not None and identity['userid'] in USERS:
    32            return identity
    33
    34    def authenticated_userid(self, request):
    35        identity = self.identity(request)
    36        if identity is not None:
    37            return identity['userid']
    38
    39    def remember(self, request, userid, **kw):
    40        return self.authtkt.remember(request, userid, **kw)
    41
    42    def forget(self, request, **kw):
    43        return self.authtkt.forget(request, **kw)
    44
    45    def permits(self, request, context, permission):
    46        principals = self.effective_principals(request)
    47        return self.acl.permits(context, principals, permission)
    48
    49    def effective_principals(self, request):
    50        principals = [Everyone]
    51        userid = self.authenticated_userid(request)
    52        if userid is not None:
    53            principals += [Authenticated, 'u:' + userid]
    54            principals += GROUPS.get(userid, [])
    55        return principals
    
  5. 变化 authorization/tutorial/views.py 要求 edit 许可 hello 查看和执行禁止的视图:

     1from pyramid.httpexceptions import HTTPFound
     2from pyramid.security import (
     3    remember,
     4    forget,
     5)
     6
     7from pyramid.view import (
     8    view_config,
     9    view_defaults,
    10    forbidden_view_config
    11)
    12
    13from .security import (
    14    USERS,
    15    check_password
    16)
    17
    18
    19@view_defaults(renderer='home.pt')
    20class TutorialViews:
    21    def __init__(self, request):
    22        self.request = request
    23        self.logged_in = request.authenticated_userid
    24
    25    @view_config(route_name='home')
    26    def home(self):
    27        return {'name': 'Home View'}
    28
    29    @view_config(route_name='hello', permission='edit')
    30    def hello(self):
    31        return {'name': 'Hello View'}
    32
    33    @view_config(route_name='login', renderer='login.pt')
    34    @forbidden_view_config(renderer='login.pt')
    35    def login(self):
    36        request = self.request
    37        login_url = request.route_url('login')
    38        referrer = request.url
    39        if referrer == login_url:
    40            referrer = '/'  # never use login form itself as came_from
    41        came_from = request.params.get('came_from', referrer)
    42        message = ''
    43        login = ''
    44        password = ''
    45        if 'form.submitted' in request.params:
    46            login = request.params['login']
    47            password = request.params['password']
    48            hashed_pw = USERS.get(login)
    49            if hashed_pw and check_password(password, hashed_pw):
    50                headers = remember(request, login)
    51                return HTTPFound(location=came_from,
    52                                 headers=headers)
    53            message = 'Failed login'
    54
    55        return dict(
    56            name='Login',
    57            message=message,
    58            url=request.application_url + '/login',
    59            came_from=came_from,
    60            login=login,
    61            password=password,
    62        )
    63
    64    @view_config(route_name='logout')
    65    def logout(self):
    66        request = self.request
    67        headers = forget(request)
    68        url = request.route_url('home')
    69        return HTTPFound(location=url,
    70                         headers=headers)
    
  6. 运行 Pyramid 应用程序时使用:

    $VENV/bin/pserve development.ini --reload
    
  7. 在浏览器中打开http://localhost:6543/。

  8. 如果您仍在登录,请单击“注销”链接。

  9. 在浏览器中访问http://localhost:6543/howdy。应该要求您登录。

分析

这个简单的教程步骤可以归结为以下几步:

  • 视图可能需要 许可 (edit

  • 我们观点的背景 Root )具有访问控制列表(ACL)。

  • 这个acl说 edit 权限可用于 Rootgroup:editors 主要的 .

  • 这个 SecurityPolicy.effective_principals 方法回答特定用户是否 (editor )是一个特定群体的成员 (group:editors

  • 这个 SecurityPolicy.permits 方法在Pyramid希望知道是否允许用户执行某些操作时调用。为此,它使用 pyramid.authorization.ACLHelper 检查ACL context 并确定请求是允许还是拒绝特定权限。

综上所述, hello 欲望 edit 许可, Rootgroup:editorsedit 许可。

当然,这只适用于 Root . 网站的其他部分(A.K.A. 语境 )可能有不同的ACL。

如果您没有登录并访问 /howdy ,您需要显示登录屏幕。Pyramid如何知道要使用的登录页面是什么?我们明确地告诉Pyramid login 视图应通过使用 @forbidden_view_config .

额外credit

  1. 用户和主体有什么区别?

  2. 我可以用数据库代替 GROUPS 数据存储以查找主体?

  3. 我必须放一个 renderer 在我的 @forbidden_view_config 装饰师?

  4. 也许您希望没有足够权限(禁止)的体验变得更丰富。你怎么能改变这个?

  5. 也许我们希望将安全语句存储在数据库中,并允许通过浏览器进行编辑。怎么办?

  6. 如果我们想要在不同类型的对象上使用不同的安全性语句呢?或者在同一类对象上,但在URL层次结构的不同部分?