What's New in Pyramid 2.0

This article explains the new features in Pyramid version 2.0 as compared to its predecessor, Pyramid 1.10. It also documents backwards incompatibilities between the two versions and deprecations added to Pyramid 2.0, as well as software dependency changes and notable documentation additions.

注釈

This is the first release of Pyramid that does not support Python 2, which is now End-of-Life and no longer receiving critical security updates by the PSF.

Feature Additions

The feature additions in Pyramid 2.0 are as follows:

Deprecations

Upgrading Authentication/Authorization

注釈

It's important to note that the principal and ACL features within Pyramid are not going away, nor deprecated, nor removed. Most ACL features are deprecated in their current locations and moved into the pyramid.authorization module. The main change is that they are now more optional than before and modifications were made to make the top-level APIs less opinionated as well as simpler.

Pyramid provides a simple set of APIs for plugging in allowed/denied semantics in your application.

The authentication and authorization policies of Pyramid 1.x have been merged into a single security policy in Pyramid 2.0. Authentication and authorization policies can still be used and will continue to function normally, however they have been deprecated and support may be removed in upcoming versions.

The new security policy should implement pyramid.interfaces.ISecurityPolicy and can be set via the security_policy argument of pyramid.config.Configurator or pyramid.config.Configurator.set_security_policy().

The policy contains pyramid.interfaces.ISecurityPolicy.authenticated_userid() and pyramid.interfaces.ISecurityPolicy.remember(), with the same method signatures as in the legacy authentication policy. It also contains pyramid.interfaces.ISecurityPolicy.forget(), but now accepting keyword arguments in the method signature.

The new security policy adds the concept of an identity, which is an object representing the user associated with the current request. The identity can be accessed via pyramid.request.Request.identity. The object can be of any shape, such as a simple ID string or an ORM object.

The concept of principals has been removed from the request object, security policy, and view/route predicates. Principals are replaced by identity. The pyramid.interfaces.ISecurityPolicy.permits() method is provided the request, context, and permissions and may now use the identity object, or derive principals, in any way it deems necessary for the application without being restricted to a list of principals represented by strings. This change gives much more flexibility in authorization implementations, especially those that do not match the ACL pattern. If you were previously using pyramid.authorization.ACLAuthorizationPolicy, you can achieve the same results by writing your own permits method using pyramid.authorization.ACLHelper. For more details on implementing an ACL, see Implementing ACL Authorization.

Pyramid does not provide any built-in security policies. Similiar functionality of the authentication and authorization policies is now provided by helpers, which can be utilized to implement your own security policy. The functionality of the legacy authentication policies roughly correspond to the following helpers:

Authentication Policy

Security Policy Helper

pyramid.authentication.SessionAuthenticationPolicy

pyramid.authentication.SessionAuthenticationHelper

pyramid.authentication.AuthTktAuthenticationPolicy

pyramid.authentication.AuthTktCookieHelper

pyramid.authentication.BasicAuthAuthenticationPolicy

Use pyramid.authentication.extract_http_basic_credentials() to retrieve credentials.

pyramid.authentication.RemoteUserAuthenticationPolicy

REMOTE_USER can be accessed with request.environ.get('REMOTE_USER').

pyramid.authentication.RepozeWho1AuthenticationPolicy

No equivalent.

Upgrading from Built-in Policies

Let's assume your application is using the built-in authentication and authorization policies, like pyramid.authentication.AuthTktAuthenticationPolicy. For example:

 1def groupfinder(userid, request):
 2    # do some db lookups to verify userid, then return
 3    # None if not recognized, or a list of principals
 4    if userid == 'editor':
 5        return ['group:editor']
 6
 7authn_policy = AuthTktAuthenticationPolicy('seekrit', callback=groupfinder)
 8authz_policy = ACLAuthorizationPolicy()
 9config.set_authentication_policy(authn_policy)
10config.set_authorization_policy(authz_policy)

We can easily write our own pyramid.interfaces.ISecurityPolicy implementation:

 1from pyramid.authentication import AuthTktCookieHelper
 2from pyramid.authorization import ACLHelper, Authenticated, Everyone
 3
 4class MySecurityPolicy:
 5    def __init__(self, secret):
 6        self.helper = AuthTktCookieHelper(secret)
 7
 8    def identity(self, request):
 9        # define our simple identity as None or a dict with userid and principals keys
10        identity = self.helper.identify(request)
11        if identity is None:
12            return None
13        userid = identity['userid']  # identical to the deprecated request.unauthenticated_userid
14
15        # verify the userid, just like we did before with groupfinder
16        principals = groupfinder(userid, request)
17
18        # assuming the userid is valid, return a map with userid and principals
19        if principals is not None:
20            return {
21                'userid': userid,
22                'principals': principals,
23            }
24
25    def authenticated_userid(self, request):
26        # defer to the identity logic to determine if the user id logged in
27        # and return None if they are not
28        identity = request.identity
29        if identity is not None:
30            return identity['userid']
31
32    def permits(self, request, context, permission):
33        # use the identity to build a list of principals, and pass them
34        # to the ACLHelper to determine allowed/denied
35        identity = request.identity
36        principals = set([Everyone])
37        if identity is not None:
38            principals.add(Authenticated)
39            principals.add(identity['userid'])
40            principals.update(identity['principals'])
41        return ACLHelper().permits(context, principals, permission)
42
43    def remember(self, request, userid, **kw):
44        return self.helper.remember(request, userid, **kw)
45
46    def forget(self, request, **kw):
47        return self.helper.forget(request, **kw)
48
49config.set_security_policy(MySecurityPolicy('seekrit'))

This is a little bit more verbose than before, but it is easy to write, and is significantly more extensible for more advanced applications.

For further documentation on implementing security policies, see Writing a Security Policy.

Upgrading from Third-Party Policies

A generic security policy can be written to work with legacy authentication and authorization policies. Note that some new features like the identity may not be as extensible and nice to use when taking this approach but it can be done to ease the transition:

 1class ShimSecurityPolicy:
 2    def __init__(self, authn_policy, authz_policy):
 3        self.authn_policy = authn_policy
 4        self.authz_policy = authz_policy
 5
 6    def authenticated_userid(self, request):
 7        return self.authn_policy.authenticated_userid(request)
 8
 9    def permits(self, request, context, permission):
10        principals = self.authn_policy.effective_principals(request)
11        return self.authz_policy.permits(context, principals, permission)
12
13    def remember(self, request, userid, **kw):
14        return self.authn_policy.remember(request, userid, **kw)
15
16    def forget(self, request, **kw):
17        return self.authz_policy.forget(request, **kw)

Compatibility with Legacy Authentication/Authorization Policies and APIs

If you are upgrading from an application that is using the legacy authentication and authorization policies and APIs, things will continue to function normally. The new system is backward-compatible and the APIs still exist. It is highly encouraged to upgrade in order to embrace the new features. The legacy APIs are deprecated and may be removed in the future.

The new pyramid.request.Request.identity property will output the same result as pyramid.request.Request.authenticated_userid.

If you try to use the new APIs with an application that is using the legacy authentication and authorization policies, then there are some issues to be aware of:

Upgrading Session Serialization

In Pyramid 2.0 the pyramid.interfaces.ISession interface was changed to require that session implementations only need to support JSON-serializable data types. This is a stricter contract than the previous requirement that all objects be pickleable and it is being done for security purposes. This is a backward-incompatible change. Previously, if a client-side session implementation was compromised, it left the application vulnerable to remote code execution attacks using specially-crafted sessions that execute code when deserialized.

Please reference the following tickets if detailed information on these changes is needed:

For users with compatibility concerns, it's possible to craft a serializer that can handle both formats until you are satisfied that clients have had time to reasonably upgrade. Remember that sessions should be short-lived and thus the number of clients affected should be small (no longer than an auth token, at a maximum). An example serializer:

 1import pickle
 2from pyramid.session import JSONSerializer
 3from pyramid.session import SignedCookieSessionFactory
 4
 5
 6class JSONSerializerWithPickleFallback(object):
 7    def __init__(self):
 8        self.json = JSONSerializer()
 9
10    def dumps(self, appstruct):
11        """
12        Accept a Python object and return bytes.
13
14        During a migration, you may want to catch serialization errors here,
15        and keep using pickle while finding spots in your app that are not
16        storing JSON-serializable objects. You may also want to integrate
17        a fall-back to pickle serialization here as well.
18        """
19        return self.json.dumps(appstruct)
20
21    def loads(self, bstruct):
22        """Accept bytes and return a Python object."""
23        try:
24            return self.json.loads(bstruct)
25        except ValueError:
26            try:
27                return pickle.loads(bstruct)
28            except Exception:
29                # this block should catch at least:
30                # ValueError, AttributeError, ImportError; but more to be safe
31                raise ValueError
32
33# somewhere in your configuration code
34serializer = JSONSerializerWithPickleFallback()
35session_factory = SignedCookieSessionFactory(..., serializer=serializer)
36config.set_session_factory(session_factory)