安全性

Pyramid 提供可选的声明性安全系统。系统确定当前用户的身份(身份验证)以及用户是否有权访问某些资源(授权)。

这个 Pyramid 安全系统可以防止 view 从基于 security policy . 在调用视图之前,授权系统可以使用 request 随着 context 用于确定是否允许访问的资源。下面是它在高层次上的工作方式:

通过修改应用程序以包含 security policy . Pyramid 提供各种帮助程序来帮助创建此策略。

编写安全策略

Pyramid 默认情况下不启用任何安全策略。所有视图都可以由完全匿名的用户访问。为了开始基于安全设置保护视图不被执行,您需要编写一个安全策略。

安全策略是实现的简单类 pyramid.interfaces.ISecurityPolicy . 简单的安全策略可能如下所示:

 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
from pyramid.security import Allowed, Denied

class SessionSecurityPolicy:
    def identity(self, request):
        """ Return app-specific user object. """
        userid = request.session.get('userid')
        if userid is None:
            return None
        return load_identity_from_db(request, userid)

    def authenticated_userid(self, request):
        """ Return a string ID for the user. """
        identity = self.identity(request)
        if identity is None:
            return None
        return string(identity.id)

    def permits(self, request, context, permission):
        """ Allow access to everything if signed in. """
        identity = self.identity(request)
        if identity is not None:
            return Allowed('User is signed in.')
        else:
            return Denied('User is not signed in.')

    def remember(request, userid, **kw):
        request.session['userid'] = userid
        return []

    def forget(request, **kw):
        del request.session['userid']
        return []

使用 set_security_policy() 方法 Configurator 对应用程序强制执行安全策略。

参见

有关实现 permits 方法,见 使用安全策略允许和拒绝访问 .

使用助手编写安全策略

为了帮助编写通用安全策略,Pyramid提供了几个助手。以下身份验证帮助程序帮助实现 identityrememberforget .

用例

帮手

存储 useridsession .

pyramid.authentication.SessionAuthenticationHelper

存储 userid 用“授权票”饼干。

pyramid.authentication.AuthTktCookieHelper

使用HTTP基本身份验证检索用户凭据。

使用 pyramid.authentication.extract_http_basic_credentials() 检索凭据。

检索 useridREMOTE_USER 在wsgi环境中。

REMOTE_USER 可以通过 request.environ.get('REMOTE_USER') .

例如,我们的上述安全策略可以利用这些助手,如下所示:

 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
from pyramid.security import Allowed, Denied
from pyramid.authentication import SessionAuthenticationHelper

class SessionSecurityPolicy:
    def __init__(self):
        self.helper = SessionAuthenticationHelper()

    def identity(self, request):
        """ Return app-specific user object. """
        userid = self.helper.authenticated_userid(request)
        if userid is None:
            return None
        return load_identity_from_db(request, userid)

    def authenticated_userid(self, request):
        """ Return a string ID for the user. """
        identity = self.identity(request)
        if identity is None:
            return None
        return str(identity.id)

    def permits(self, request, context, permission):
        """ Allow access to everything if signed in. """
        identity = self.identity(request)
        if identity is not None:
            return Allowed('User is signed in.')
        else:
            return Denied('User is not signed in.')

    def remember(request, userid, **kw):
        return self.helper.remember(request, userid, **kw)

    def forget(request, **kw):
        return self.helper.forget(request, **kw)

Helpers用于特定于应用程序的代码。注意上面的代码是如何从helper获取userid并使用它来加载 identity 从数据库中。 authenticated_userid 拉动 userididentity 为了保证存储在会话中的用户ID存在于数据库中(“authenticated”)。

使用权限保护视图

保护一个 view callable 当特定类型的资源成为 context ,您必须通过 permissionview configuration . 权限通常只是字符串,它们没有必需的组合:您可以随意命名权限。

例如,以下视图声明保护名为 add_entry.html 当上下文资源类型为 Blogadd 使用权限 pyramid.config.Configurator.add_view() 应用程序编程接口:

1
2
3
4
5
6
# config is an instance of pyramid.config.Configurator

config.add_view('mypackage.views.blog_entry_add_view',
                name='add_entry.html',
                context='mypackage.resources.Blog',
                permission='add')

等效视图注册包括 add 权限名称可以通过 @view_config 装饰者:

1
2
3
4
5
6
7
from pyramid.view import view_config
from resources import Blog

@view_config(context=Blog, name='add_entry.html', permission='add')
def blog_entry_add_view(request):
    """ Add blog entry code goes here """
    pass

由于这些不同的视图配置语句中的任何一个,如果在正常应用程序操作期间发现视图可调用时有安全策略,则将查询该安全策略以查看是否允许请求用户使用 add 当前权限内 context . 如果政策允许访问, blog_entry_add_view 将被调用。如果不是的话 Forbidden view 将被调用。

使用安全策略允许和拒绝访问

要确定是否允许使用附加权限访问视图,Pyramid调用 permits 安全策略的方法。 permits 应该返回 pyramid.security.Allowedpyramid.security.Denied . 两个类都接受一个字符串作为参数,这应该详细说明为什么允许或拒绝访问。

简单的 permits 基于用户角色授予访问权限的实现可能如下所示:

 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
from pyramid.security import Allowed, Denied

class SecurityPolicy:
    def permits(self, request, context, permission):
        identity = self.identity(request)

        if identity is None:
            return Denied('User is not signed in.')
        if identity.role == 'admin':
            allowed = ['read', 'write', 'delete']
        elif identity.role == 'editor':
            allowed = ['read', 'write']
        else:
            allowed = ['read']

        if permission in allowed:
            return Allowed(
                'Access granted for user %s with role %s.',
                identity,
                identity.role,
            )
        else:
            return Denied(
                'Access denied for user %s with role %s.',
                identity,
                identity.role,
            )

设置默认权限

如果没有向视图配置提供权限,则注册的视图将始终由完全匿名的用户执行:任何有效的安全策略都将被忽略。

为了便于配置“默认安全”的应用程序, Pyramid 允许您配置 违约 许可。如果提供了默认权限,则默认权限将用作所有视图注册的权限字符串,而这些注册不是以其他方式命名的 permission 参数。

这个 pyramid.config.Configurator.set_default_permission() 方法支持为应用程序配置默认权限。

注册默认权限时:

  • 如果视图配置将显式 permission ,将忽略该视图注册的默认权限,并使用名为权限的视图配置。

  • 如果视图配置命名权限 pyramid.security.NO_PERMISSION_REQUIRED ,将忽略默认权限,并注册视图 没有 一个权限(使其对所有呼叫者都可用,而不管其凭据如何)。

警告

注册默认权限时, all 观点(甚至) exception view 视图)受权限保护。对于所有真正打算匿名访问的视图,您需要将视图的配置与 pyramid.security.NO_PERMISSION_REQUIRED 许可。

实现ACL授权

实现授权的一种常见方法是使用 ACL . ACL是 context -访问控制项的特定列表,根据用户主体允许或拒绝访问权限。

金字塔提供 pyramid.authorization.ACLHelper 帮助实现基于ACL的 permits . 特定于应用程序的代码应该为用户和调用构造一个主体列表 pyramid.authorization.ACLHelper.permits() ,它将返回一个 pyramid.authorization.ACLAllowedpyramid.authorization.ACLDenied 对象。实现可能如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from pyramid.authorization import ACLHelper, Everyone, Authenticated

class SecurityPolicy:
    def permits(self, request, context, permission):
        principals = [Everyone]
        if identity is not None:
            principals.append(Authenticated)
            principals.append('user:' + identity.id)
            principals.append('group:' + identity.group)
        return ACLHelper().permits(context, principals, permission)

要将ACL与资源关联,请添加 __acl__ 属性设置为资源对象。可以在资源上定义此属性 实例 如果您需要实例级安全性,或者可以在资源上定义它 如果您只需要类型级别的安全性。

例如,acl可以通过其类附加到博客的资源:

1
2
3
4
5
6
7
8
9
from pyramid.authorization import Allow
from pyramid.authorization import Everyone

class Blog(object):
    __acl__ = [
        (Allow, Everyone, 'view'),
        (Allow, 'group:editors', 'add'),
        (Allow, 'group:editors', 'edit'),
    ]

或者,如果您的资源是持久的,则可以通过 __acl__ AN属性 实例 资源的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from pyramid.authorization import Allow
from pyramid.authorization import Everyone

class Blog(object):
    pass

blog = Blog()

blog.__acl__ = [
    (Allow, Everyone, 'view'),
    (Allow, 'group:editors', 'add'),
    (Allow, 'group:editors', 'edit'),
]

无论ACL是附加到资源的类还是资源本身的实例,效果都是相同的。在诸如内容管理系统之类的应用程序中,使用ACL(而不仅仅是修饰类)来修饰单个资源实例是很有用的,在这些应用程序中,需要逐对象进行细粒度访问。

动态ACL也可以通过将ACL转换为资源上的可调用文件来实现。这可能允许ACL根据实例的属性动态生成规则。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from pyramid.authorization import Allow
from pyramid.authorization import Everyone

class Blog(object):
    def __acl__(self):
        return [
            (Allow, Everyone, 'view'),
            (Allow, self.owner, 'edit'),
            (Allow, 'group:editors', 'edit'),
        ]

    def __init__(self, owner):
        self.owner = owner

警告

写作 __acl__ 因为不鼓励属性,因为 AttributeError 发生在 fgetfset 将被静默解除(这与python一致 getattrhasattr 行为)。对于动态ACL,只需使用可调用文件,如上所述。

ACL的元素

下面是一个示例acl:

1
2
3
4
5
6
7
8
from pyramid.authorization import Allow
from pyramid.authorization import Everyone

__acl__ = [
    (Allow, Everyone, 'view'),
    (Allow, 'group:editors', 'add'),
    (Allow, 'group:editors', 'edit'),
]

示例acl指示 pyramid.authorization.Everyone principal——一个特殊的系统定义的principal,字面意思是,每个人都可以查看博客,并且 group:editors 主体可以添加到日志并对其进行编辑。

acl的每个元素都是 ACE 或访问控制项。例如,在上面的代码块中,有三个ace: (Allow, Everyone, 'view')(Allow, 'group:editors', 'add')(Allow, 'group:editors', 'edit') .

任何ace的第一个元素都是 pyramid.authorization.Allowpyramid.authorization.Deny ,表示在ACE匹配时要采取的操作。第二个元素是 principal . 第三个参数是权限或权限名称序列。

主体通常是用户id,但是如果您的身份验证系统提供组信息,它也可能是组id。

ACL中的每个ACE都由ACL helper处理 按照acl指定的顺序 . 因此,如果您有这样的ACL:

1
2
3
4
5
6
7
8
from pyramid.authorization import Allow
from pyramid.authorization import Deny
from pyramid.authorization import Everyone

__acl__ = [
    (Allow, Everyone, 'view'),
    (Deny, Everyone, 'view'),
]

ACL帮助程序将 允许 每个人都有视图权限,即使稍后在ACL中,您有一个拒绝所有人视图权限的ACE。另一方面,如果您有这样的ACL:

1
2
3
4
5
6
7
8
from pyramid.authorization import Everyone
from pyramid.authorization import Allow
from pyramid.authorization import Deny

__acl__ = [
    (Deny, Everyone, 'view'),
    (Allow, Everyone, 'view'),
]

ACL helper将拒绝每个人查看权限,即使稍后在ACL中,有一个ACE允许所有人。

ACE中的第三个参数也可以是权限名序列,而不是单个权限名。因此,不要创建多个ace来表示对单个ace的多个不同权限授予 group:editors 分组,我们可以将其折叠成一个ACE,如下所示。

1
2
3
4
5
6
7
from pyramid.authorization import Allow
from pyramid.authorization import Everyone

__acl__ = [
    (Allow, Everyone, 'view'),
    (Allow, 'group:editors', ('add', 'edit')),
]

特殊主体名称

特殊主体名称存在于 pyramid.authorization 模块。它们可以导入以在您自己的代码中用于填充ACL,例如, pyramid.authorization.Everyone .

pyramid.authorization.Everyone

从字面上说,每个人,无论什么。这个物体实际上是引擎盖下面的一根绳子 (system.Everyone )每个用户 is 在每个请求期间,即使安全策略不在使用中,主体也会将其命名为“Everyone”。

pyramid.authorization.Authenticated

具有由当前安全策略确定的凭据的任何用户。您可能会认为它是任何“登录”的用户。这个物体实际上是引擎盖下面的一根绳子 (system.Authenticated

特殊权限

特殊权限名称存在于 pyramid.authorization 模块。这些可以导入以在ACL中使用。

pyramid.authorization.ALL_PERMISSIONS

一个物体,字面上表示, all 权限。在像这样的ACL中很有用: (Allow, 'fred', ALL_PERMISSIONS) . 这个 ALL_PERMISSIONS 对象实际上是具有 __contains__ 始终返回的方法 True ,对于所有已知的授权策略,它具有指示给定主体具有系统请求的任何权限的效果。

特殊王牌

方便 ACE 定义为对中所有权限的所有人表示拒绝 pyramid.authorization.DENY_ALL . 此ace通常用作 last acl的ace显式地导致继承授权策略“停止查找遍历树”(有效地破坏任何继承)。例如,允许 only fred 特定资源的查看权限(不管继承的ACL可能会说什么)可能如下所示:

1
2
3
4
from pyramid.authorization import Allow
from pyramid.authorization import DENY_ALL

__acl__ = [ (Allow, 'fred', 'view'), DENY_ALL ]

在引擎盖下面, pyramid.authorization.DENY_ALL ace等于以下值:

1
2
from pyramid.authorization import ALL_PERMISSIONS
__acl__ = [ (Deny, Everyone, ALL_PERMISSIONS) ]

ACL继承和位置感知

当ACL helper就位时,如果资源对象作为上下文时没有ACL,则其 起源 为ACL咨询。如果该对象没有ACL, its 在我们到达根目录并且没有更多的父目录之前,将为acl(无限)咨询父目录。

为了允许安全机制执行ACL继承,资源对象必须提供 location-awareness . 提供 location-awareness 意味着两件事:资源树中的根对象必须具有 __name__ 属性与A __parent__ 属性。

1
2
3
class Blog(object):
    __name__ = ''
    __parent__ = None

带有 __parent__ 属性与A __name__ 属性被称为 location-aware . 位置感知对象定义 __parent__ 指向其父对象的属性。根对象的 __parent__None .

参见

也见 pyramid.location 用于记录使用位置感知的功能。

参见

也见 位置感知资源 .

更改禁止的视图

什么时候? Pyramid 由于授权拒绝,拒绝视图调用 forbidden 调用视图。开箱即用,这张禁止观看的照片非常清晰。见 更改禁止的视图 在内部 使用钩子 有关如何创建自定义禁止视图以及如何安排在拒绝视图授权时调用该视图的说明。

调试视图授权失败

如果您判断的应用程序不适当地允许或拒绝视图访问,请使用 PYRAMID_DEBUG_AUTHORIZATION 环境变量设置为 1 . 例如:

PYRAMID_DEBUG_AUTHORIZATION=1 $VENV/bin/pserve myproject.ini

当在顶级视图呈现期间发生任何授权时,将向控制台(stderr)记录一条消息,说明ACL在哪个ACE中根据身份验证信息允许或拒绝授权。

也可以在应用程序中启用此行为 .ini 通过设置 pyramid.debug_authorization 关键 true 在应用程序的配置部分中,例如:

1
2
3
[app:main]
use = egg:MyProject
pyramid.debug_authorization = true

打开此调试标志后,发送到浏览器的响应在其主体中还将包含安全调试信息。

调试强制授权失败

这个 pyramid.request.Request.has_permission() API用于强制检查视图函数中的安全性。它返回有效布尔值的对象实例。但是这些东西不是生的 TrueFalse 对象,并向其附加有关允许或拒绝权限的原因的信息。对象将是 pyramid.authorization.ACLAllowedpyramid.authorization.ACLDeniedpyramid.security.Allowedpyramid.security.Denied ,如中所述 pyramid.security . 至少,这些对象将具有 msg 属性,该字符串指示拒绝或允许权限的原因。当调用 has_permission() 失败往往是有用的。

反对秘密分享的告诫

金字塔的各个组成部分都需要一个“秘密”。例如,下面的helper可能用于安全策略并使用机密值 seekrit ::

helper = AuthTktCookieHelper('seekrit')

A session factory 还需要一个秘密:

my_session_factory = SignedCookieSessionFactory('itsaseekreet')

对于多个金字塔子系统,使用相同的秘密是很有诱惑力的。例如,您可能会尝试使用该值 seekrit 作为上面定义的helper和会话工厂的秘密。这是个坏主意,因为在这两种情况下,这些秘密都被用来签署数据的有效载荷。

如果您将同一个秘密用于应用程序的两个不同部分进行签名,则可能会允许攻击者对所选明文进行签名,从而允许攻击者控制有效负载的内容。在两个不同的子系统中重新使用秘密可能会使签名的安全性降低到零。在攻击者可能提供所选纯文本的不同上下文中,不应重复使用密钥。

防止跨站点请求伪造攻击

Cross-site request forgery 攻击是一种现象,登录到您的网站的用户可能会无意中加载一个URL,因为它是从攻击者的网站链接或嵌入的。如果URL是一个可以修改或删除数据的URL,其后果可能是可怕的。

通过向浏览器发出一个唯一的令牌,然后要求它出现在所有潜在的不安全请求中,可以避免大多数攻击。 Pyramid 提供创建和检查CSRF令牌的工具。

默认情况下 Pyramid 带有基于会话的CSRF实现 pyramid.csrf.SessionCSRFStoragePolicy . 要使用它,必须首先启用 session factory 如上所述 使用默认会话工厂使用备用会话工厂 . 或者,您可以使用基于cookie的实现 pyramid.csrf.CookieCSRFStoragePolicy 这给了一些额外的灵活性,因为它不需要为每个用户提供会话。您还可以定义自己的 pyramid.interfaces.ICSRFStoragePolicy 并将其注册到 pyramid.config.Configurator.set_csrf_storage_policy() 指令。

例如:

from pyramid.config import Configurator

config = Configurator()
config.set_csrf_storage_policy(MyCustomCSRFPolicy())

使用 csrf.get_csrf_token 方法

要获取当前的CSRF令牌,请使用 pyramid.csrf.get_csrf_token 方法。

from pyramid.csrf import get_csrf_token
token = get_csrf_token(request)

这个 get_csrf_token() 方法接受单个参数:请求。它返回一个CSRF 令牌 字符串。如果 get_csrf_token()new_csrf_token() 以前为此用户调用过,则将返回现有令牌。如果此用户以前没有CSRF令牌,那么将在会话中设置一个新令牌并返回。新创建的令牌将是不透明的和随机的。

使用 get_csrf_token 模板中的全局

模板具有 get_csrf_token() 方法插入到它们的全局中,这允许您在不修改视图代码的情况下获取当前标记。此方法不接受任何参数并返回CSRF令牌字符串。您可以使用返回的令牌作为表单中隐藏字段的值,该表单向需要提升权限的方法发送消息,或者在Ajax请求中将其作为请求头提供。

例如,将CSRF令牌包含为隐藏字段:

<form method="post" action="/myview">
  <input type="hidden" name="csrf_token" value="${get_csrf_token()}">
  <input type="submit" value="Delete Everything">
</form>

或者将其作为头包含在jquery ajax请求中:

var csrfToken = "${get_csrf_token()}";
$.ajax({
  type: "POST",
  url: "/myview",
  headers: { 'X-CSRF-Token': csrfToken }
}).done(function() {
  alert("Deleted");
});

然后,接收请求的URL的处理程序应要求提供正确的CSRF令牌。

使用 csrf.new_csrf_token 方法

要显式创建新的CSRF令牌,请使用 csrf.new_csrf_token() 方法。这与 csrf.get_csrf_token() 因为它清除任何现有的CSRF令牌,创建一个新的CSRF令牌,将令牌设置为用户,并返回令牌。

from pyramid.csrf import new_csrf_token
token = new_csrf_token(request)

注解

无法从模板强制新的CSRF令牌。如果要重新生成CSRF令牌,请在视图代码中执行该操作,并将新令牌作为上下文的一部分返回。

手动检查CSRF令牌

在请求处理代码中,您可以使用 pyramid.csrf.check_csrf_token() . 如果令牌有效,它将返回 True ,否则会升高 HTTPBadRequest . 或者,您可以指定 raises=False 把支票退了 False 而不是提出例外。

默认情况下,它检查名为 csrf_token 或一个名为 X-CSRF-Token .

from pyramid.csrf import check_csrf_token

def myview(request):
    # Require CSRF Token
    check_csrf_token(request)

    # ...

自动检查CSRF令牌

1.7 新版功能.

Pyramid 支持使用RFC2616定义的不安全方法对请求自动检查CSRF令牌。可以手动检查任何其他请求。对于使用 pyramid.config.Configurator.set_default_csrf_options() 指令。例如:

from pyramid.config import Configurator

config = Configurator()
config.set_default_csrf_options(require_csrf=True)

可以使用 require_csrf 查看选项。一个值 TrueFalse 将覆盖默认设置 set_default_csrf_options . 例如:

@view_config(route_name='hello', require_csrf=False)
def myview(request):
    # ...

当CSRF检查激活时,用于查找提供的CSRF令牌的令牌和头将 csrf_tokenX-CSRF-Token ,分别,除非另有规定 set_default_csrf_options . 将根据中的值检查令牌 request.POST 这是提交的表单主体。如果此值不存在,则将检查标题。

除了基于令牌的CSRF检查之外,如果请求使用HTTPS,那么自动CSRF检查还将检查请求的引用,以确保它与受信任的来源之一匹配。默认情况下,唯一受信任的源是当前主机,但是可以通过设置配置其他源 pyramid.csrf_trusted_origins to a list of domain names (and ports if they are non-standard). If a host in the list of domains starts with a . 这样就允许所有子域以及没有 . . 如果没有 RefererOrigin HTTPS请求中存在标头,则CSRF检查将失败,除非 allow_no_origin 已设置。特别的 Origin: null 可以通过添加 nullpyramid.csrf_trusted_origins 名单。

可以选择不检查原点 check_origin=False . 如果 CSRF storage policy 已知是安全的,因此攻击者无法轻易使用令牌。

如果CSRF检查失败,则 pyramid.exceptions.BadCSRFTokenpyramid.exceptions.BadCSRFOrigin 将引发异常。此异常可能由 exception view 但是,默认情况下,会导致 400 Bad Request 正在向客户端发送响应。